Чего только не придумают хитрые кодеры, дабы осложнить работу бедным хакерам, ломающим их софт! Все уже привыкли к тому, что исполняемый модуль программы использует классы, функции и методы, содержащиеся в нем самом либо во внешних динамических библиотеках (DLL), стандартных или не очень. Однако нередко программа для получения данных или выполнения каких‑то действий обращается к системным службам или серверам ActiveX, и это очень неприятно. Первый случай гораздо более суровый, поэтому отложим его обсуждение на потом, а сегодня начнем с вещей попроще.
Ты наверняка слышал о майкрософтовской технологии OLE Automation, которая позволяет связывать друг с другом приложения, написанные на совершенно разных языках, в том числе скрипты. Про нее сказано очень много (на страницах твоего любимого журнала тоже), поэтому не буду углубляться в тонкости ее реализации. Остановлюсь лишь на нескольких моментах, которые помогут в разборке и реконструкции кода, использующего OLE Automation.
Суть в том, что в операционной системе регистрируется некий набор управляющих элементов ActiveX, содержащих методы и классы, доступ к которым из любого приложения можно получить при помощи этой технологии. Такой элемент с иерархическим описанием содержащихся в нем классов и методов называется библиотекой типов (TypeLibrary). К примеру, другая известная майкрософтовская технология .NET поддерживает тесное взаимодействие с такими библиотеками. Настолько тесное, что может отдельные классы и методы в своих сборках выносить в эти библиотеки, а при загрузке сборки OLE Automation стыкует их как родные. В таких сборках напрочь отсутствует IL-код, а тела методов в самой библиотеке пустые. В сегодняшней статье я расскажу, как бороться с подобными явлениями и реконструировать такой запутанный код.
В одной из своих предыдущих статей я рассказывал о подмене IL-кода при JIT-компиляции на лету. Однако бывают случаи, когда IL-код в сборке отсутствует. К примеру, разбираешь ты себе спокойно некий дотнетовский проект в каком‑нибудь dnSpy, все замечательно, ни тебе обфускации, ни защиты от отладки. Трассируешь проверку лицензии, и р‑раз! — проваливаешься в функцию, в которой нет кода. Смотришь на библиотеку, а она вся такая: кода нет, одни заголовки.
Натравляем на нее деобфускаторы, в надежде, что код как‑то хитро спрятан. Но нет, код действительно отсутствует, а при вдумчивом анализе библиотеки в IDA или CFF видно, что все тела методов пустые. И только сейчас мы обращаем внимание, что методы помечены атрибутом MethodImpl(MethodImplOptions.InternalCall)
. В CFFExplorer в окне Method ImplFlags тоже стоит галка напротив InternalCall
. Так что же это за неведома зверушка?
Немного покурив теорию, мы вспоминаем: этот атрибут указывает среде выполнения, что она имеет дело с вызовом нативного метода (не IL, а хардкорных платформенно зависимых машинных кодов) из связанной с исполняемым файлом библиотеки, которая может быть написана на C, C++ или даже на ASM. Подобным образом также реализуются внутренние вызовы исполняемого кода, например из mscorlib. Эту задачу можно реализовать, в частности, через атрибут DllImport
. В этом случае хотя бы ясно, в какой именно функции какой именно библиотеки следует искать нужный код реализации, но в нашем примере создатели проекта решили максимально испортить нам жизнь. Еще немного поковыряв куцый огрызок кода библиотеки, мы обнаруживаем в ее заголовке следующую конструкцию:
[CoClass(typeof(CheckerClass)), Guid("3F5942E1-108B-11d4-B050-000001260696")][ComImport]
Снова сверившись с документацией, мы приходим к выводу, что наша библиотека служит всего лишь переходным интерфейсом к COM-библиотеке типов с данным GUID. И все содержащиеся в ней функции автоматически перетранслируются в методы соответствующего класса. Благо в описании каждой функции есть ее индекс DispID
. Попробуем найти эту библиотеку типов среди зарегистрированных в системе.
Для начала просто запускаем regedit и ищем наш GUID. Действительно, в ветке HKLMACHINESOFTWAREClassesInterface
обнаруживается раздел {3F5942E1-108B-11d4-B050-000001260696}
, а в нем — целых три подраздела. В одном из них, озаглавленном TypeLib
, мы видим другой GUID {62C8FE65-4EBB-45E7-B440-6E39B2CDBF29}
. Теперь вобьем в поиск уже его, и наше терпение вознаграждается: мы находим это значение в параметре TypeLib
раздела HKEY_CLASSES_ROOTCLSID{67283557-1256-3349-A135-055B16327CED}
. Этот GUID нам до боли знаком, мы видели его в заголовке нашей многострадальной библиотеки:
[ClassInterface(0), ComSourceInterfaces("LICCHECKLib._ICheckerEvents "), Guid("67283557-1256-3349-A135-055B16327CED"), TypeLibType(2)]
Этот раздел содержит много интересного, но главное — в подразделе InprocServer32
мы находим полный путь к TypeLibrary
, который можно препарировать! Вообще говоря, тот же результат можно (и нужно) было получить гораздо проще. У Microsoft есть маленькая, но очень полезная утилита OLE/COM Object viewer (oleview.exe
). Она входит в пакет утилит, поставляющихся вместе с MSVC. Мы с самого начала знали имя класса, поэтому достаточно запустить ее и найти этот класс в упорядоченном по алфавиту разделе Controls
.
Еще можно было поискать по имени класса и в Regedit, но у Oleview есть существенное преимущество: в контекстном меню при выборе пункта View Type Information
программа выдает всю внутреннюю структуру нужной библиотеки типов, включая экспортируемые классы и методы. Того же эффекта можно было бы добиться, загрузив в него наш OCX через File → View TypeLib. По сути дела, он декомпилирует встроенный в библиотеку TLB, который можно самому вытащить оттуда редактором ресурсов (требуемый ресурс так и называется: TYPELIB).
Казалось бы, все у нас хорошо, да не очень. Мы, по сути, вернулись на исходную позицию: у нас есть список заголовков методов с параметрами, но как получить их код — неясно. Несмотря на то что TypeLibrary представляет собой стандартную библиотеку Windows, в отличие от экспортируемых функций DLL нельзя просто так взять и посмотреть список экспортируемых методов с их точками входа. Все потому, что COM-объекты внутренние и не раскрывают детали своей реализации путем экспорта функций. Вместо этого COM предоставляет интерфейс для создания экземпляров COM-класса через вызов CoCreateInstance
с использованием UUID (обычно известного CLSID) в качестве средства идентификации класса COM.
Возвращаемый объект — это объект C++, реализующий набор API-интерфейсов, которые представлены в виде таблицы виртуальных функций для этого COM-объекта. Поэтому нет необходимости экспортировать эти функции, и ты не можешь найти их с помощью представления экспорта IDA. Поскольку реализация данной выдачи может варьироваться разработчиком каждой конкретной TypeLibrary, не существует универсальных методов реверс‑инжиниринга для подобных библиотек. Хотя справедливости ради надо сказать, что начиная с конца шестых версий IDA сильно эволюционировала в данном вопросе.
Что ж, для начала попробуем смоделировать вызов метода из своей программы. Не буду вдаваться в непростые подробности программирования COM-клиента, они очень подробно и доходчиво расписаны на сайте «Первые шаги». Отсюда же берем и готовый код клиента:
#include "windows.h"#include "iostream.h"#include "initguid.h"DEFINE_GUID(IID_Step,0x3f5942e2, 0x108b, 0x11d4, 0xb0, 0x50, 0x0, 0x0, 0x1, 0x26, 0x6, 0x96);class IStep : public IUnknown { public: IStep(); virtual ~IStep(); STDMETHOD(MyComMessage) () PURE;};void main() { cout << "Initializing COM" << endl; if( FAILED( CoInitialize( NULL ) ) ) { cout << "Unable to initialize COM" << endl; return ; } CLSID clsid; HRESULT hr = ::CLSIDFromProgID( L"LicCheck.Checker.1", &clsid ); if( FAILED( hr ) ) { cout << "Unable to get CLSID " << endl; return ; } IClassFactory* pCF; hr = CoGetClassObject( clsid, CLSCTX_INPROC, NULL, IID_IClassFactory, (void**) &pCF ); if ( FAILED( hr ) ) { cout << "Failed to GetClassObject " << endl; return ; } IUnknown* pUnk; hr = pCF->CreateInstance( NULL, IID_IUnknown, (void**) &pUnk ); pCF->Release(); if( FAILED( hr ) ) { cout << "Failed to create server instance " << endl; return ; } cout << "Instance created" << endl; IStep* pStep = NULL; hr = pUnk->QueryInterface( IID_Step, (void**) &pStep ); pUnk->Release(); if( FAILED( hr ) ) { cout << "QueryInterface() for IStep failed" << endl; CoUninitialize(); return ; } pStep->MyComMessage(); pStep->Release(); cout << "Shuting down COM" << endl; CoUninitialize();}
В макросе DEFINE_GUID
мы поставили свой GUID, чтобы обращение велось именно к нашему классу. Не будем заморачиваться и менять объявление класса IStep
, в нем уже есть один метод. Нас, по сути, интересует реализация самой таблицы адресов. Мы даже не будем возиться с параметрами, хотя если мы начнем вдумчиво и полноценно копать конкретный метод в отладчике, то нам таки придется это делать. Однако в первом приближении для простоты примера опустим эти мелочи.
Итак, скомпилировав этот любезно предоставленный автором пример, загрузив его в отладчик и исполняя данный код пошагово, мы замечаем, что после вызова CoGetClassObject
наша библиотека типов загружается в память процесса и на нее уже можно ставить бряки. А pUnk->QueryInterface
возвращает собственный указатель на указатель на таблицу виртуальных методов 1012E1DC
. И тут нас снова ждет облом: это явно не та таблица, которую мы ищем.
Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
-35%
1 год9300 рублей 6040 р. |
1 месяц870 р. |
Я уже участник «Xakep.ru»
Читайте также
Последние новости