Хакер - Препарируем TypeLibrary. Реверсим код с ActiveX и OLE Automation
hacker_frei
МВК
Чего только не придумают хитрые кодеры, дабы осложнить работу бедным хакерам, ломающим их софт! Все уже привыкли к тому, что исполняемый модуль программы использует классы, функции и методы, содержащиеся в нем самом либо во внешних динамических библиотеках (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. Действительно, в ветке HKLMACHINE\SOFTWARE\Classes\Interface\ обнаруживается раздел {3F5942E1-108B-11d4-B050-000001260696}, а в нем — целых три подраздела. В одном из них, озаглавленном TypeLib, мы видим другой GUID {62C8FE65-4EBB-45E7-B440-6E39B2CDBF29}. Теперь вобьем в поиск уже его, и наше терпение вознаграждается: мы находим это значение в параметре TypeLib раздела HKEY_CLASSES_ROOT\CLSID\{67283557-1256-3349-A135-055B16327CED}. Этот GUID нам до боли знаком, мы видели его в заголовке нашей многострадальной библиотеки:
[ClassInterface(0), ComSourceInterfaces("LICCHECKLib._ICheckerEvents\0\0"), 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. И тут нас снова ждет облом: это явно не та таблица, которую мы ищем.
При дальнейшем изучении видно, что в найденной таблице всего пять методов, а у нас свыше полусотни, да и код этих методов по передаваемым параметрам явно не соответствует заголовкам. Немножко подумав, мы понимаем, в чем дело: предложенный пример реализовывался через фабрику классов. Собственно, CoGetClassObject и возвращает интерфейс этой самой фабрики, содержащейся в библиотеке типов. Однако нам нужна не она, нам нужен наш родной класс Checker, точнее, его таблица виртуальных методов.
Ясно, что в данной библиотеке таких таблиц как собак нерезаных и, чтобы получить доступ именно к нужной, надо долго и упорно изучать чудовищно запутанные и нелогичные майкрософтовские мануалы по COM, которые (что обиднее всего) нам, скорее всего, и не понадобятся вне решения данной задачи. Поэтому откладываем пока упомянутый способ как запасной (как только у нас появится адрес нужной таблицы, его, несмотря ни на что, вполне можно использовать для грязной отладки методов) и возвращаемся к IDA.
Как я уже говорил, на первый взгляд все выглядит так же уныло. Из библиотеки торчат уши четырех экспортируемых функций: DllCanUnloadNow, DllGetClassObject, DllRegisterServer и DllUnregisterServer, при помощи которых отыскать нужную таблицу весьма проблематично. Названия методов в коде (помимо встроенного ресурса TYPELIB) тоже отсутствуют, и вообще данный раздел весьма слабо задокументирован. По счастью, нашлись умные люди, которые расковыряли принцип размещения таблиц виртуальных адресов в библиотеке типов, скомпилированных компилятором C++.
Разумеется, эти данные получены чисто эмпирически и зависят от реализации компилятора, поэтому все гарантии, что так будет везде и всегда, весьма смутны и основаны на совместимости с соглашениями о вызовах COM, которые требуют последовательного назначения слотов для виртуальных функций. Базовая структура, которая описывает каждый класс, — так называемый RTTI Complete Object Locator — выглядит вот так:
typedef const struct _s__RTTICompleteObjectLocator {
unsigned long signature;
unsigned long offset;
unsigned long cdOffset;
_TypeDescriptor *pTypeDescriptor;
__RTTIClassHierarchyDescriptor *pClassDescriptor;
} __RTTICompleteObjectLocator;
Нам нужны в ней два поля (остальные пустые) — pTypeDescriptor, которое указывает на имя класса, и второе, указывающее на описатель иерархии базовых классов pClassDescriptor. Последний содержит количество базовых классов и указатель на их массив:
typedef const struct _s__RTTIClassHierarchyDescriptor {
unsigned long signature;
unsigned long attributes;
unsigned long numBaseClasses;
__RTTIBaseClassArray *pBaseClassArray;
} __RTTIClassHierarchyDescriptor;
Данный массив содержит указатели, указывающие, в свою очередь, на TypeDescriptor каждого базового класса, причем обычно первый из них — наш класс. Для наглядности приведу схему размещения данных структур и их связи друг c другом на примере конкретного класса ATL::CComClassFactory. Таблицу виртуальных методов которого, кстати сказать, возвращает код из описанного выше примера.

Как видно из рисунка, у него шесть базовых классов, vftable состоит из пяти методов. Исходя из сказанного выше, применяем эмпирический принцип поиска vftable для каждого класса. Поскольку компилятор обычно дает компилированным классам имена, начинающиеся с .?AV, то тупо ищем в коде TypeDescriptor по данной сигнатуре. По ссылке на него ищем RTTI Complete Object Locator, по ссылке на который, в свою очередь, vftable. Код питоновского скрипта для IDA, реализующего данный поиск, приведен ниже (код взят из статьи в блоге Quarkslab):
# IDA Python RTTI parser ~pod2g 06/2013
from idaapi import *
from idc import *
# TODO: test on 64bit !!!
addr_size = 4
first_seg = FirstSeg()
last_seg = FirstSeg()
for seg in Segments():
if seg > last_seg:
last_seg = seg
if seg < first_seg:
first_seg = seg
def get_pointer(ea):
if addr_size == 4:
return Dword(ea)
else:
return Qword(ea)
def in_image(ea):
return ea >= first_seg and ea <= SegEnd(last_seg)
def get_class_name(name_addr):
s = Demangle('??_7' + GetString(name_addr + 4) + '6B@', 8)
if s != None:
return s[0:len(s)-11]
else:
return GetString(name_addr)
start = first_seg
while True:
# Ищем в коде сигнатуру ".?AV" — обычно так начинаются классы C++
f = FindBinary(start, SEARCH_DOWN, "2E 3F 41 56")
start = f + addr_size
if f == BADADDR:
break
rtd = f - 8
# Преобразуем в нормальное имя и печатаем
print "Found class: %s (rtd=0x%X)" % (get_class_name(f), rtd)
# Ищем все ссылки на смещение начала класса — 8
for xref in XrefsTo(rtd):
# Следующее слово — смещение rchd
rchd = get_pointer(xref.frm + addr_size)
# На всякий случай, вдруг случайное левое смещение
if in_image(rchd):
# rcol — RTTI Complete Object Locator
rcol = xref.frm - 12
# rchd + 8 — количество базовых классов
rchd_numBaseClasses = Dword(rchd + 8)
# rchd + 12 — их массив, который по очереди перебираем
rchd_pBaseClassArray = get_pointer(rchd + 12)
for i in range(rchd_numBaseClasses):
rbcd = get_pointer(rchd_pBaseClassArray + addr_size * i)
# Каждый элемент массива — указатель на базовый класс
rbcd_pTypeDescriptor = get_pointer(rbcd)
rbcd_pTypeDescriptor_name = get_class_name(rbcd_pTypeDescriptor + 8)
print " - base class: %s" % rbcd_pTypeDescriptor_name
# Ссылка на RTTI Complete Object Locator — vtable
for xref in XrefsTo(rcol):
vtable = xref.frm + addr_size
break
print " - vtable: 0x%X" % vtable
Как видишь, принцип достаточно прост: обращаю внимание, что существующий код заточен под 32-битную архитектуру, под 64-битную его придется допиливать, как минимум поменяв размер слова addr_size = 8, хотя, на первый взгляд, данный принцип построения кода характерен и для нее.
Натравив данный скрипт на нашу библиотеку типов, получаем в логе IDA длиннющую простыню, ибо классов в библиотеке содержится великое множество. Однако в самом начале ее мы видим следующее:
Found class: ATL::CComObjectCached<ATL::CComClassFactory> (rtd=0x10155070)
- base class: ATL::CComObjectCached<ATL::CComClassFactory>
- base class: ATL::CComClassFactory
- base class: IClassFactory
- base class: IUnknown
- base class: ATL::CComObjectRootEx<ATL::CComMultiThreadModel>
- base class: ATL::CComObjectRootBase
- vtable: 0x1012E1DC
Found class: ATL::CComClassFactory (rtd=0x101550BC)
- base class: ATL::CComClassFactory
- base class: IClassFactory
- base class: IUnknown
- base class: ATL::CComObjectRootEx<ATL::CComMultiThreadModel>
- base class: ATL::CComObjectRootBase
- vtable: 0x1012E1F8
Found class: ATL::CComObjectRootEx<ATL::CComMultiThreadModel> (rtd=0x10155128)
Found class: ATL::CComObjectRootBase (rtd=0x10155178)
Found class: ATL::CComObject<CChecker> (rtd=0x101551A4)
- base class: ATL::CComObject<CChecker>
- base class: CChecker
- base class: ATL::CComObjectRootEx<ATL::CComSingleThreadModel>
- base class: ATL::CComObjectRootBase
- base class: ATL::IDispatchImpl<IChecker,&_GUID const IID_IChecker,&_GUID const LIBID_LICCHECKLib,1,0,ATL::CComTypeInfoHolder>
- base class: IChecker
- base class: IDispatch
- base class: IUnknown
- base class: ATL::CComControl<CChecker,ATL::CWindowImpl<CChecker,ATL::CWindow,ATL::CWinTraits<1442840576,0>>>
- base class: ATL::CComControlBase
- base class: ATL::CWindowImpl<CChecker,ATL::CWindow,ATL::CWinTraits<1442840576,0>>
- base class: ATL::CWindowImplBaseT<ATL::CWindow,ATL::CWinTraits<1442840576,0>>
- base class: ATL::CWindowImplRoot<ATL::CWindow>
- base class: ATL::CWindow
- base class: ATL::CMessageMap
- base class: ATL::IPersistStreamInitImpl<CChecker>
- base class: IPersistStreamInit
- base class: IPersist
- base class: IUnknown
- base class: ATL::IOleControlImpl<CChecker>
- base class: IOleControl
- base class: IUnknown
- base class: ATL::IOleObjectImpl<CChecker>
- base class: IOleObject
- base class: IUnknown
- base class: ATL::IOleInPlaceActiveObjectImpl<CChecker>
- base class: IOleInPlaceActiveObject
- base class: IOleWindow
- base class: IUnknown
- base class: ATL::IViewObjectExImpl<CChecker>
- base class: IViewObjectEx
- base class: IViewObject2
- base class: IViewObject
- base class: IUnknown
- base class: ATL::IOleInPlaceObjectWindowlessImpl<CChecker>
- base class: IOleInPlaceObjectWindowless
- base class: IOleInPlaceObject
- base class: IOleWindow
- base class: IUnknown
- base class: ISupportErrorInfo
- base class: IUnknown
- base class: ATL::IConnectionPointContainerImpl<CChecker>
- base class: IConnectionPointContainer
- base class: IUnknown
- base class: ATL::IPersistStorageImpl<CChecker>
- base class: IPersistStorage
- base class: IPersist
- base class: IUnknown
- base class: ATL::ISpecifyPropertyPagesImpl<CChecker>
- base class: ISpecifyPropertyPages
- base class: IUnknown
- base class: ATL::IQuickActivateImpl<CChecker>
- base class: IQuickActivate
- base class: IUnknown
- base class: ATL::IDataObjectImpl<CChecker>
- base class: IDataObject
- base class: IUnknown
- base class: ATL::IProvideClassInfo2Impl<&_GUID const CLSID_Checker,&_GUID const DIID__ICheckerEvents,&_GUID const LIBID_LICCHECKLib,1,0,ATL::CComTypeInfoHolder>
- base class: IProvideClassInfo2
- base class: IProvideClassInfo
- base class: IUnknown
- base class: ATL::IPropertyNotifySinkCP<CChecker,ATL::CComDynamicUnkArray>
- base class: ATL::IConnectionPointImpl<CChecker,&_GUID const IID_IPropertyNotifySink,ATL::CComDynamicUnkArray>
- base class: ATL::_ICPLocator<&_GUID const IID_IPropertyNotifySink>
- base class: ATL::CComCoClass<CChecker,&_GUID const CLSID_Checker>
- vtable: 0x1012E6A4
То есть адрес таблицы нашего класса Checker — 1012E6A4 очень похож на настоящий. Количество ссылок, во всяком случае, совпадает. Для примера берем код метода по произвольной ссылке: количество передаваемых параметров вроде как в норме. Что ж, можно нас поздравить, похоже, мы нашли таблицу виртуальных методов. Опираясь на которую, можно с определенной долей уверенности ставить точки останова в отладчике или реконструировать код. Вообще говоря, при определенной доле сноровки можно было бы обойтись и без скрипта.

К примеру, еще с конца 6-х версий IDA был создан плагин СlassInformer, помогающий в поиске и разборке RTTI (хотя, справедливости ради, у меня так толком и не получилось запустить его в полном объеме ни на одной версии IDA из имеющихся под рукой). Тем более седьмые версии и сами умеют искать и отображать RTTI Complete Object Locator, чего, в принципе, вполне достаточно для поиска vftable.
Ложка дегтя в том, что на один и тот же RTTI Complete Object Locator могут ссылаться несколько vftable, то есть полностью «однокнопочного» решения данный метод не дает и всегда есть место для хакерской интуиции.
Читайте ещё больше платных статей бесплатно: https://t.me/hacker_frei