Хорошего хакера от хорошего кодера отличает то, что первый не просто знаком со всеми необходимыми для выполнения текущей задачи инструментами и умеет ими пользоваться, но также понимает их внутреннее устройство и принципы работы. Чтобы при необходимости обойтись без них, просто руками выполнив действие, под которое инструмент по каким‑то причинам не был заточен.
Одна из самых благодатных тем для хакеров — реверс приложений на Delphi. Об этом написано множество статей (в том числе и в «Хакере»), а также создано множество удобных инструментов, облегчающих хакерам жизнь. Однако тема настолько обширна, что даже эти инструменты не охватывают ее полностью: всегда найдутся случаи, когда все тулзы бессильны и приходится проявлять хакерскую смекалку.
Итак, сформулируем задачу. Имеется 64-битное приложение, первичный анализ которого при помощи DIE выдает его родство с Delphi.
Вроде бы все хорошо, однако на приведенном выше скриншоте нетрудно заметить взаимоисключающие параметры: предлагаемый анализатором Delphi не может быть 64-битным. Переключив режим сканирования с автоматического на Nauz File Detector, проясняем возникшее недоразумение.
Конечно же, это гораздо более поздняя версия Embarcadero Delphi 35.0 (28.0.44500.8973). Естественно, перед нами не XE7+, а целый XE11+. Данное открытие совсем нас не радует — общеизвестные инструменты DeDe и IDR не то что столь свежую версию не понимают, они вообще не умеют работать с 64-битным Delphi.
Есть робкая надежда на зачаточную бета‑версию IDR64, однако и она не признает наш модуль родным. Не откладываем ее далеко, она нам еще пригодится, гуглим дальше.
Находим занятный проект питоновского скрипта под IDA. Его авторы обнаружили в Delphi и успешно эксплуатируют интересную фичу. При выходе из внутреннего метода (Event Constructor) регистр EDX
будет содержать ссылку на имя обработчика события. Адрес обоаботчика будет находиться в регистре EAX
, примерно так, как показано на следующем скриншоте.
Проанализировав код 32-битных хендлеров, они выделили общий паттерн для поиска 80
(соответствующие ему байты помечены плюсиком):
.text:00408DB8 loc_408DB8: ; CODE XREF: sub_408D68+67↓j
.text:00408DB8 8A 5C 31 06 mov bl, [ecx+esi+6]
.text:00408DBC F6 C3 80 test bl, 80h
.text:00408DBF 75 E1 jnz short loc_408DA2
.text:00408DC1 32 1C 11 xor bl, [ecx+edx]
.text:00408DC4 F6 C3 80 test bl, 80h
.text:00408DC7 75 D9 jnz short loc_408DA2
.text:00408DC9 80+ E3+ DF+ and bl, 0DFh
.text:00408DCC 75+ D0 jnz short loc_408D9E
.text:00408DCE 49+ dec ecx
.text:00408DCF 75+ E7 jnz short loc_408DB8
.text:00408DD1
.text:00408DD1 loc_408DD1: ; CODE XREF: sub_408D68+4C↑j
.text:00408DD1 8B+ 46+ 02+ mov eax, [esi+2]
.text:00408DD4
.text:00408DD4 loc_408DD4: ; CODE XREF: sub_408D68+34↑j
.text:00408DD4 5F pop edi
.text:00408DD5 5E pop esi
.text:00408DD6 5B+ pop ebx
.text:00408DD7 C3+ retn
Аналогичный код хендлера для 64-битной версии находится по паттерну 80
. Например, вот как это выглядит для версии 31 (10.1).
Код будет таким (соответствующие паттерну байты тоже помечены плюсиком):
55 push rbp
57 push rdi
56 push rsi
53 push rbx
48 83 EC 48 sub rsp, 48h
48 8B EC mov rbp, rsp
48 89 CB mov rbx, rcx
4C 89 C6 mov rsi, r8
48 8B 0A mov rcx, [rdx]
48 89 F2 mov rdx, rsi
E8 94 89 E5 FF call sub_40E690
48 89 45 38 mov [rbp+var_s38], rax
48 83 7D 38 00 cmp [rbp+var_s38], 0
0F 94 C0 setz al
88 45 37 mov [rbp+var_s37], al
48 83 7B 68 00 cmp qword ptr [rbx+68h], 0
74 1D jz short loc_5B5D2F
48 8D 7B 68 lea rdi, [rbx+68h]
48 8B 4F 08 mov rcx, [rdi+8]
48 89 DA mov rdx, rbx
49 89 F0 mov r8, rsi
4C 8D 4D 38 lea r9, [rbp+var_s38]
48 8D 45 37 lea rax, [rbp+var_s37]
48 89 44 24 20 mov [rsp+var_s20], rax
FF 17 call qword ptr [rdi]
80+ 7D 37 00+ cmp [rbp+var_s37], 0
74+ 05 jz short loc_5B5D3A
E8+ B6 F9 FF FF call sub_5B56F0
48+ 8B+ 45 38 mov rax, [rbp+var_s38]
48+ 8D+ 65 48 lea rsp, [rbp+48h]
5B pop rbx
5E pop rsi
5F pop rdi
5D pop rbp
C3+ retn
Легко убедиться, что при останове в конце этой функции адресу метода в регистре RAX
соответствует его имя в регистре RDI
(да‑да, в коде скрипта ошибка, не RDX
, а RDI
).
Но это мелочь, поскольку недостатки данного метода видны невооруженным глазом. Трассировать программу ради того, чтобы получить весьма ограниченный список методов, довольно тоскливо, да и вообще если программа подлежит запуску и трассировке, то это большое везение. И наконец, самая главная беда — в нашей целевой версии XE11 аналогичного кода нет и быть не может, поскольку там весь этот процесс, судя по всему, проходит в недрах библиотеки rtl280.
.
Подумаем, чем еще нам может пригодиться IDR64. Я немного покривил душой, упомянув ее отказ от родства с нашим модулем. Конечно, у нее отсутствует база знаний по XE11. По сути, она только три версии и знает. Поэтому при открытии в режиме автодетекта сомневается в «дельфовости» нашей программы. Однако если немного схитрить и попробовать открыть ее как Delphi XE4, а затем согласиться с native knowledge base, то IDR64 вовсе не противится этому и после очень долгих раздумий кое‑как загружает программу в себя.
Конечно, она полноценно не восстанавливает всю структуру модулей и классов, однако из восстановленного ею кода можно почерпнуть значительно больше информации, чем из IDA. В частности, мы получим список имен методов и классов, не говоря уже о том, что IDA чертовски не любит паскалевские строки со счетчиком.
И в заключение я, как и обещал, немного расскажу о примерной внутренней структуре дельфовского кода и принципах его анализа «голыми руками». Когда мы анализировали дельфовские приложения при помощи IDR, то заметили, что для каждого скомпилированного класса присутствует своя характерная структура данных, называемая VMT. Структура не документирована, однако, погуглив, можно найти старую статью с ее описанием. В ней эта структура описывается (как тип в синтаксисе дельфи) следующим образом:
type PClass = ^TClass; PSafeCallException = function (Self: TObject; ExceptObject: TObject; ExceptAddr: Pointer): HResult; PAfterConstruction = procedure (Self: TObject); PBeforeDestruction = procedure (Self: TObject); PDispatch = procedure (Self: TObject; var Message); PDefaultHandler = procedure (Self: TObject; var Message); PNewInstance = function (Self: TClass) : TObject; PFreeInstance = procedure (Self: TObject); PDestroy = procedure (Self: TObject; OuterMost: ShortInt); PVmt = ^TVmt; TVmt = packed record SelfPtr : TClass; IntfTable : Pointer; AutoTable : Pointer; InitTable : Pointer; TypeInfo : Pointer; FieldTable : Pointer; MethodTable : Pointer; DynamicTable : Pointer; ClassName : PShortString; InstanceSize : PLongint; Parent : PClass; SafeCallException : PSafeCallException; AfterConstruction : PAfterConstruction; BeforeDestruction : PBeforeDestruction; Dispatch : PDispatch; DefaultHandler : PDefaultHandler; NewInstance : PNewInstance; FreeInstance : PFreeInstance; Destroy : PDestroy; {UserDefinedVirtuals: array[0..999] of procedure;} end;
Как видишь, она содержит исчерпывающую информацию о классе, в которой нас в первую очередь интересуют его методы. Для понимания этого покурим немного исходники IDR64. Бегло просмотрев их, находим два основных модуля, ответственных за разбор кода на классы: Threads.
и Misc.
. Первый содержит методы для поиска таблиц VMT в скомпилированном коде, второй служит для извлечения информации из них. В двух словах коснусь реализации этих процессов. В модуле Threads.
есть метод с соответствующим названием:
// Collect information from VMT structurevoid __fastcall TAnalyzeThread::FindVMTs(){ ... if (Adj0Count > Adj24Count) Adjustment = 0; else { Adjustment = -24; Vmt.AdjustVmtConsts(Adjustment); } for (int i = 0; i < TotalSize && !Terminated; i += 8) { ... if (idr.IsFlagSet(cfCode | cfData, i)) continue; DWORD adr = *((ULONGLONG*)(Code + i)); // Points to vmt0 (VmtSelfPtr) if (IsValidImageAdr(adr) && Pos2Adr(i) == adr + Vmt.SelfPtr) { DWORD classVMT = adr;...
Фактически он перебирает все 64-битные слова в файле, проверяя, указывает ли каждое на самое себя (если его интерпретировать как адрес со смещением +
). Например, на рисунке выделенный красным длинный указатель по адресу 0x3A806F48
полностью проходит данный магический тест, поскольку его содержимое имеет вид 0x3A807010=0x3A806F48+0xB0+0x18
.
Этой особенностью, в частности, объясняется весьма неприятное торможение IDR при загрузке больших файлов.
Итак, найдя начало таблицы VMT, можно начинать разбирать ее содержимое. Модуль Misc.
содержит множество методов для этого: GetParentAdr
, GetChildAdr
, GetClassSize
, GetClsName
и так далее. Устроены они, в общем‑то, однотипно: в них выполняется обращение к соответствующим полям структуры VMT относительно указателя Vmt.
:
String __fastcall GetClsName(DWORD adr){ if (!IsValidImageAdr(adr)) return ""; DWORD vmtAdr = adr - Vmt.SelfPtr; DWORD pos = Adr2Pos(vmtAdr) + Vmt.ClassName;...
Vmt.
, Vmt.
— это константы относительных смещений до полей данной структуры, инициализирующихся в методах DelphiVmt::
и DelphiVmt::
. Они зависят от версии Delphi, и нам сильно повезло, что они не менялись с 2014 года. Проанализировав их, составим схему блока VMT для класса, приведенного на предыдущем скриншоте:
SelfPtr DQ 3A807010h
IntfTable DQ 3A806f10h
AutoTable DQ 0
InitTable DQ 3A807010h
TypeInfo DQ 3A807A10h ; TMkManager
FieldTable DQ 3A80702Bh
MethodTable DQ 3A80716Bh
DynamicTable DQ 0
ClassName DQ 3A8072D9h ; TMkManager
InstanceSize DQ 0B8h
Parent DQ 3A813848h
Equals DQ 3A801080h
GetHashCode DQ 3A801090h
ToString DQ 3A8010B0h
SafeCallException DQ 3A8010A0h
AfterConstruction DQ 3A807B50h
BeforeDestruction DQ 3A8012F0h
Dispatch DQ 3A8010D0h
DefaultHandler DQ 3A801300h
NewInstance DQ 3A801020h
FreeInstance DQ 3A807FE0h
Destroy DQ 3A801030h
Как видим, раскладка 64-битной структуры вполне соответствует приведенному выше описанию. Не буду утомлять читателей подробным разбором структуры всяких полезных таблиц, ссылки на которые можно получить из содержимого полей. При желании ты можешь разобраться в этом сам, изучив исходники IDR. Тем более что в реальной жизни столь подробный разбор кода обычно и не нужен, если только пользователь не желает написать свой собственный декомпилятор покруче IDR. Рассмотрим практический способ применения этой информации для анализа дельфовского кода без специальных инструментов, прямо во время отладки приложения в нашем любимом отладчике x64dbg.
Итак, ковыряя приложение в этом отладчике, мы набрели на некий класс TMkManager
, ответственный за связь приложения с ключом MetroKey
. Для наглядности: в classwiever IDR64 список методов данного класса выглядят примерно так.
Рассмотрим организацию этого класса в памяти загруженного в отладчик процесса на основе анализа таблицы MethodTable
.
Красным цветом выделено имя класса TMkManager
, синим — имена его методов и их адреса, оранжевым — указатели на тип возвращаемого значения, а фиолетовым — параметры каждого метода, их имена и типы. Как видишь, значения возвращают только два из семи присутствующих на экране методов, хотя параметры есть у всех (как минимум Self
).
Работает это так: допустим, в какой‑то момент мы при отладке процесса зашли в процедуру по адресу 5E4AB020
, и она нас так заинтересовала, что мы решили узнать о ней подробности. Для этого мы ищем ссылку на нее. Простой референс наверняка не найдется, а вот если искать его как последовательность байтов 20,
, то будет найден указатель по адресу 5E4A72E6
(выделено синим).
За ним следует строка со счетчиком, имя этого метода — GetKeyServerIP
. Если полученная информация заинтересовала нас еще сильнее, то следующий указатель 5E4B3958
(выделено оранжевым) ссылается на другой указатель (в нашем примере по стрелке 59913С0
), который, в свою очередь, указывает на имя типа возвращаемого значения — string
.
Двигаясь дальше, мы обнаруживаем выделенное фиолетовым имя первого параметра этого метода Self
и указатель на его тип — 5E4A7A08
. Так же как и в предыдущем случае, через два переименования мы выходим на имя типа, точнее, класса — искомый TMkManager
, который по понятным причинам является родительским классом и для нашего метода.
На скриншоте видно, что если продолжить изыскания, то можно пройти по цепочке порождающих классов, но я предоставлю читателю, воодушевленному полученной информацией, самому развить эту тему, вплоть до написания собственных скриптов или даже декомпиляторов.