Ты наверняка слышал о майкрософтовской технологии OLE Automation, которая позволяет связывать друг с другом приложения, написанные на совершенно разных языках, в том числе скрипты. Про нее сказано очень много (на страницах твоего любимого журнала тоже), поэтому не буду углубляться в тонкости ее реализации. Остановлюсь лишь на нескольких моментах, которые помогут в разборке и реконструкции кода, использующего OLE Automation.
Суть в том, что в операционной системе регистрируется некий набор управляющих элементов ActiveX, содержащих методы и классы, доступ к которым из любого приложения можно получить при помощи этой технологии. Такой элемент с иерархическим описанием содержащихся в нем классов и методов называется библиотекой типов (TypeLibrary). К примеру, другая известная майкрософтовская технология .NET поддерживает тесное взаимодействие с такими библиотеками. Настолько тесное, что может отдельные классы и методы в своих сборках выносить в эти библиотеки, а при загрузке сборки OLE Automation стыкует их как родные. В таких сборках напрочь отсутствует IL-код, а тела методов в самой библиотеке пустые. В сегодняшней статье я расскажу, как бороться с подобными явлениями и реконструировать такой запутанный код.
В одной из своих предыдущих статей я рассказывал о подмене IL-кода при JIT-компиляции на лету. Однако бывают случаи, когда IL-код в сборке отсутствует. К примеру, разбираешь ты себе спокойно некий дотнетовский проект в каком‑нибудь dnSpy, все замечательно, ни тебе обфускации, ни защиты от отладки. Трассируешь проверку лицензии, и р‑раз! — проваливаешься в функцию, в которой нет кода. Смотришь на библиотеку, а она вся такая: кода нет, одни заголовки.
Натравляем на нее деобфускаторы, в надежде, что код как‑то хитро спрятан. Но нет, код действительно отсутствует, а при вдумчивом анализе библиотеки в IDA или CFF видно, что все тела методов пустые. И только сейчас мы обращаем внимание, что методы помечены атрибутом MethodImpl(
. В 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. Действительно, в ветке HKLMACHINE\
обнаруживается раздел {
, а в нем — целых три подраздела. В одном из них, озаглавленном TypeLib
, мы видим другой GUID {
. Теперь вобьем в поиск уже его, и наше терпение вознаграждается: мы находим это значение в параметре TypeLib
раздела HKEY_CLASSES_ROOT\
. Этот GUID нам до боли знаком, мы видели его в заголовке нашей многострадальной библиотеки:
[ClassInterface(0), ComSourceInterfaces("LICCHECKLib._ICheckerEvents\0\0"), Guid("67283557-1256-3349-A135-055B16327CED"), TypeLibType(2)]
Этот раздел содержит много интересного, но главное — в подразделе InprocServer32
мы находим полный путь к TypeLibrary
, который можно препарировать! Вообще говоря, тот же результат можно (и нужно) было получить гораздо проще. У Microsoft есть маленькая, но очень полезная утилита OLE/COM Object viewer (oleview.
). Она входит в пакет утилит, поставляющихся вместе с MSVC. Мы с самого начала знали имя класса, поэтому достаточно запустить ее и найти этот класс в упорядоченном по алфавиту разделе Controls
.
Еще можно было поискать по имени класса и в Regedit, но у Oleview есть существенное преимущество: в контекстном меню при выборе пункта View
программа выдает всю внутреннюю структуру нужной библиотеки типов, включая экспортируемые классы и методы. Того же эффекта можно было бы добиться, загрузив в него наш 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->
возвращает собственный указатель на указатель на таблицу виртуальных методов 1012E1DC
. И тут нас снова ждет облом: это явно не та таблица, которую мы ищем.
Продолжение доступно только участникам
Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
Вариант 2. Открой один материал
Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.
Я уже участник «Xakep.ru»