• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Препарируем TypeLibrary. Реверсим код с ActiveX и OLE Automation

tabac

CPU register
Пользователь
Регистрация
30.09.2018
Сообщения
1 610
Решения
1
Реакции
3 332
Чего только не придумают хитрые кодеры, дабы осложнить работу бедным хакерам, ломающим их софт! Все уже привыкли к тому, что исполняемый модуль программы использует классы, функции и методы, содержащиеся в нем самом либо во внешних динамических библиотеках (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 очень похож на настоящий. Количество ссылок, во всяком случае, совпадает. Для примера берем код метода по произвольной ссылке: количество передаваемых параметров вроде как в норме. Что ж, можно нас поздравить, похоже, мы нашли таблицу виртуальных методов. Опираясь на которую, можно с определенной долей уверенности ставить точки останова в отладчике или реконструировать код. Вообще говоря, при определенной доле сноровки можно было бы обойтись и без скрипта.

Плагин СlassInformer для IDA

Плагин СlassInformer для IDA

К примеру, еще с конца 6-х версий IDA был создан плагин СlassInformer, помогающий в поиске и разборке RTTI (хотя, справедливости ради, у меня так толком и не получилось запустить его в полном объеме ни на одной версии IDA из имеющихся под рукой). Тем более седьмые версии и сами умеют искать и отображать RTTI Complete Object Locator, чего, в принципе, вполне достаточно для поиска vftable.

Ложка дегтя в том, что на один и тот же RTTI Complete Object Locator могут ссылаться несколько vftable, то есть полностью «однокнопочного» решения данный метод не дает и всегда есть место для хакерской интуиции.


Автор @МВК (Mikhail Kondakov)
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх