Прог­раммы на Delphi пусть и нечас­то, но все же попада­ют в руки любите­лей поковы­рять­ся в чужом соф­те. Сегод­ня мы раз­берем методы ревер­са и отладки таких при­ложе­ний. А заод­но запасем­ся нуж­ными инс­тру­мен­тами.

Хо­роше­го хакера от хороше­го кодера отли­чает то, что пер­вый не прос­то зна­ком со все­ми необ­ходимы­ми для выпол­нения текущей задачи инс­тру­мен­тами и уме­ет ими поль­зовать­ся, но так­же понима­ет их внут­реннее устрой­ство и прин­ципы работы. Что­бы при необ­ходимос­ти обой­тись без них, прос­то руками выпол­нив дей­ствие, под которое инс­тру­мент по каким‑то при­чинам не был заточен.

Од­на из самых бла­годат­ных тем для хакеров — реверс при­ложе­ний на 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 E3 DF 75 ?? 49 75 ?? 8B 46 02 ?? ?? 5B C3 (соот­ветс­тву­ющие ему бай­ты помече­ны плю­сиком):

.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 ?? ?? 00 74 ?? E8 ?? ?? ?? ?? 48 8B ?? ?? 48 8D ?? ?? ?? ?? ?? ?? C3. Нап­ример, вот как это выг­лядит для вер­сии 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.dll.

По­дума­ем, чем еще нам может при­годить­ся 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.cpp и Misc.cpp. Пер­вый содер­жит методы для поис­ка таб­лиц VMT в ском­пилиро­ван­ном коде, вто­рой слу­жит для извле­чения информа­ции из них. В двух сло­вах кос­нусь реали­зации этих про­цес­сов. В модуле Threads.cpp есть метод с соот­ветс­тву­ющим наз­вани­ем:

// Collect information from VMT structure
void __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-бит­ные сло­ва в фай­ле, про­веряя, ука­зыва­ет ли каж­дое на самое себя (если его интер­пре­тиро­вать как адрес со сме­щени­ем + Vmt.SelfPtr (-0xBO)+ Adjustment (-24)). Нап­ример, на рисун­ке выделен­ный крас­ным длин­ный ука­затель по адре­су 0x3A806F48 пол­ностью про­ходит дан­ный магичес­кий тест, пос­коль­ку его содер­жимое име­ет вид 0x3A807010=0x3A806F48+0xB0+0x18.

Этой осо­бен­ностью, в час­тнос­ти, объ­ясня­ется весь­ма неп­рият­ное тор­можение IDR при заг­рузке боль­ших фай­лов.

Итак, най­дя начало таб­лицы VMT, мож­но начинать раз­бирать ее содер­жимое. Модуль Misc.cpp содер­жит мно­жес­тво методов для это­го: GetParentAdr, GetChildAdr, GetClassSize, GetClsName и так далее. Устро­ены они, в общем‑то, одно­тип­но: в них выпол­няет­ся обра­щение к соот­ветс­тву­ющим полям струк­туры VMT отно­ситель­но ука­зате­ля Vmt.SelfPtr:

String __fastcall GetClsName(DWORD adr)
{
if (!IsValidImageAdr(adr)) return "";
DWORD vmtAdr = adr - Vmt.SelfPtr;
DWORD pos = Adr2Pos(vmtAdr) + Vmt.ClassName;
...

Vmt.SelfPtr, Vmt.ClassName — это кон­стан­ты отно­ситель­ных сме­щений до полей дан­ной струк­туры, ини­циали­зиру­ющих­ся в методах DelphiVmt::SetVmtConsts и DelphiVmt::AdjustVmtConsts. Они зависят от вер­сии 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,B0,4A,5E,00,00,00,00, то будет най­ден ука­затель по адре­су 5E4A72E6 (выделе­но синим).

За ним сле­дует стро­ка со счет­чиком, имя это­го метода — GetKeyServerIP. Если получен­ная информа­ция заин­тересо­вала нас еще силь­нее, то сле­дующий ука­затель 5E4B3958 (выделе­но оран­жевым) ссы­лает­ся на дру­гой ука­затель (в нашем при­мере по стрел­ке 59913С0), который, в свою оче­редь, ука­зыва­ет на имя типа воз­вра­щаемо­го зна­чения — string.

Дви­гаясь даль­ше, мы обна­ружи­ваем выделен­ное фиоле­товым имя пер­вого парамет­ра это­го метода Self и ука­затель на его тип — 5E4A7A08. Так же как и в пре­дыду­щем слу­чае, через два пере­име­нова­ния мы выходим на имя типа, точ­нее, клас­са — иско­мый TMkManager, который по понят­ным при­чинам явля­ется родитель­ским клас­сом и для нашего метода.

На скрин­шоте вид­но, что если про­дол­жить изыс­кания, то мож­но прой­ти по цепоч­ке порож­дающих клас­сов, но я пре­дос­тавлю читате­лю, вооду­шев­ленно­му получен­ной информа­цией, самому раз­вить эту тему, вплоть до написа­ния собс­твен­ных скрип­тов или даже деком­пилято­ров.

  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    0 комментариев
    Межтекстовые Отзывы
    Посмотреть все комментарии