【翻譯】Delphi中類的逆向工程

看雪資料發表於2015-11-15

標 題:【翻譯】Delphi中類的逆向工程

發信人:firstrose 

時 間:2004-12-09,18:37 

詳細資訊:



前段時間看到NB王(就是說nbw:D……喂,nbw你不生氣吧?)轉的文章,覺得很有用。於是想翻譯一下。無奈本人太懶,拖到現在。不過總算在今天晚飯以前趕完了:D

沒有完全按照原文翻譯,有些地方是意譯的。大家湊合看吧。原貼地址是http://www.apriorit.com/our-articles/classes-restoration.html

注意:原文版權歸原文作者所有,譯文版權由譯者所有。請尊重譯者的勞動,不得轉貼!但得到譯者明確同意的情況除外!

==============================================================

類的逆向工程

譯者:firstrose
 
類的逆向工程是一項需要OOP相關知識以及特定編譯器如何處理OOP部分的知識的複雜工作。
 
我們的任務是得到類、方法和成員。由於在用Delphi編寫的程式裡查詢類相對容易,這裡就用Delphi做示範。

類的逆向先要從查詢建構函式開始。因為類在這裡被分配記憶體,而且我們可以從中得到建構函式的一些資訊。

在Delphi程式裡找一個建構函式很簡單,只需要查詢類名出現的地方即可。

例如,對於TList可以找到下面的結構:
 
CODE:0040D598 TList           dd offset TList_VTBL
CODE:0040D59C                 dd 7 dup(0)
CODE:0040D5B8                 dd offset aTlist        ; "TList"
CODE:0040D5BC SizeOfObject    dd 10h
CODE:0040D5C0                 dd offset off_4010C8
CODE:0040D5C4                 dd offset TObject::SafeCallException
CODE:0040D5C8                 dd offset nullsub_8
CODE:0040D5CC                 dd offset TObject::NewInstance
CODE:0040D5D0                 dd offset TObject::FreeInstance
CODE:0040D5D4                 dd offset sub_40EA08
CODE:0040D5D8 TList_VTBL           dd offset TList::Grow
CODE:0040D5DC                 dd offset unknown_libname_107
CODE:0040D5E0 aTlist          db 5,'TList'
 
    我們把這個結構稱為“object descriptor”,即“物件描述符”。指向它的指標
被傳遞給建構函式,建構函式則從中取得建立物件所需要的資料。透過查詢對40D598
的交叉引用,可以得到對建構函式的所有呼叫。

    下面是其中的一個:
 
CODE:0040E72E                 mov     eaxds:TList
CODE:0040E733                 call    CreateClass
CODE:0040E738                 mov     ds:dword_4A45F8, eax
 
這裡的建構函式的名字是我們自己起的。透過檢視函式,我們可以知道它是否真的是一個建構函式(CreateClass)

CODE:00402F48 CreateClass     proc near               ; CODE XREF: @BeginGlobalLoading+17p
CODE:00402F48                                         ; @CollectionsEqual+48p ...
CODE:00402F48                 test    dldl
CODE:00402F4A                 jz      short loc_402F54
CODE:00402F4C                 add     esp, 0FFFFFFF0h
CODE:00402F4F                 call    __linkproc__ ClassCreate
CODE:00402F54
CODE:00402F54 loc_402F54:                             ; CODE XREF: CreateClass+2j
CODE:00402F54                 test    dldl
CODE:00402F56                 jz      short locret_402F62
CODE:00402F58                 pop     large dword ptr fs:0
CODE:00402F5F                 add     esp, 0Ch
CODE:00402F62
CODE:00402F62 locret_402F62:                          ; CODE XREF: CreateClass+Ej
CODE:00402F62                 retn
CODE:00402F62 CreateClass     endp

也就是說,如果函式里有 __linkproc__ ClassCreate ,它就是一個建構函式。
下面讓我們看看生成類例項的時候發生了什麼特別的事。
 
CODE:00403200 __linkproc__ ClassCreate proc near      ; CODE XREF: CreateClass+7p
CODE:00403200                                         ; sub_40AA58+Ap ...
CODE:00403200
CODE:00403200 arg_0           = dword ptr  10h
CODE:00403200
CODE:00403200                 push    edx
CODE:00403201                 push    ecx
CODE:00403202                 push    ebx
CODE:00403203                 call    dword ptr [eax-0Ch]
CODE:00403206                 xor     edxedx
CODE:00403208                 lea     ecx, [esp+arg_0]
CODE:0040320C                 mov     ebxfs:[edx]
CODE:0040320F                 mov     [ecx], ebx
CODE:00403211                 mov     [ecx+8], ebp
CODE:00403214                 mov     dword ptr [ecx+4], offset loc_403225
CODE:0040321B                 mov     [ecx+0Ch], eax
CODE:0040321E                 mov     fs:[edx], ecx
CODE:00403221                 pop     ebx
CODE:00403222                 pop     ecx
CODE:00403223                 pop     edx
CODE:00403224                 retn
CODE:00403224 __linkproc__ ClassCreate endp
 
好的,指令
 
CODE:0040E72E mov eaxds:TList 
 
把TList結構(也就是TList_VTBL)的地址放到EAX裡。

由於我們使用的是Delphi,可以看到,這裡使用了Borland的fastcall呼叫模式(引數按照以下次序傳遞:EAX,ECX,EDX和堆疊)。這意味著,指向虛方法表的指標是作為CreateClass的第一個引數傳遞的。此外,EAX在__linkproc__ClassCreate裡沒有改變。我們可以看到:

CODE:00403203                 call    dword ptr [eax-0Ch]
 
它呼叫了什麼呢?指向TList_VTBL=0х40D5D8的指標依然在EAX裡,即
 
CODE:0040D5CC                 dd offset TObject::NewInstance
 
這是父類的建構函式。可以看到,TList繼承了TObject。進去看看:
 
CODE:00402F0C TObject::NewInstance proc near          ; DATA XREF: CODE:004010FCo
CODE:00402F0C                                         ; CODE:004011DCo ...
CODE:00402F0C                 push    eax
CODE:00402F0D                 mov     eax, [eax-1Ch]
CODE:00402F10                 call    __linkproc__ GetMem
CODE:00402F15                 mov     edxeax
CODE:00402F17                 pop     eax
CODE:00402F18                 jmp     TObject::InitInstance
CODE:00402F18 TObject::NewInstance endp
 
EAX的值還是一樣的:0x40D5D8-0x1C=0x40D5BC。這樣物件的大小被儲存在0x40D5BC裡並傳遞給GetMem。
 
CODE:0040D5BC SizeOfObject    dd 10h
 
可以看到,這裡物件的大小是0x10。
TObject::InitInstance只是將物件所在區域用0填充後,設定了剛建立的物件中指向VTBL的指標。並沒有做什麼特別的工作。

然後CreateClass就結束了。EAX中返回了指向剛剛建立的物件的指標。

這樣,對建構函式的呼叫看起來就象下面這樣:

CODE:0040E72E                 mov     eaxds:TList
CODE:0040E733                 call    CreateClass
CODE:0040E738                 mov     ds:dword_4A45F8, eax
 
分析物件的結構
 
我們現在知道物件所佔記憶體的大小是0x10,其中的4個位元組是VTBL的指標。但是還剩下0xC個包含了物件成員的位元組,我們必須找出它們。這裡就有點直覺的成分了。首先,物件從來不會無緣無故被建立。物件的成員,或者由建構函式賦值(可能是全部,也可能是一部分),或者由相應的設定方法來賦值。

由於TList在建構函式里被以0填充(具體在TObject::InitInstance中),在建構函式里就找不到類成員的有關資訊。Thus let’s trace life cycle after the creation.

在本例中,指向物件例項的指標被放在全域性變數dword_4A45F8裡,所以我們只需要在dword_4A45F8下個讀取記憶體斷點就可以看到類成員被呼叫了。

第一次中斷:

CODE:0041319D mov     eax, [ebp+var_4]
CODE:004131A0 mov     edxds:pTList
CODE:004131A6 mov     [eax+30h], edx  ; 複製的指向物件的指標
CODE:004131A9 jmp     short loc_4131BD
.............
CODE:004131BD
CODE:004131BD loc_4131BD:                             ; CODE XREF: sub_4130BC+EDj
CODE:004131BD xor     eaxeax
CODE:004131BF push    ebp
CODE:004131C0 push    offset loc_413276
CODE:004131C5 push    dword ptr fs:[eax]
CODE:004131C8 mov     fs:[eax], esp
CODE:004131CB mov     eax, [ebp+var_4]
CODE:004131CE mov     edx, [eax+18h]
CODE:004131D1 mov     eax, [ebp+var_4]
CODE:004131D4 mov     eax, [eax+30h] ;隱含地傳遞了指向物件的指標
CODE:004131D7 call    Classes::TList::Add(void *)
 
現在看看Classes::TList::Add
 
CODE:0040EA28 __fastcall Classes::TList::Add(void *) proc near
CODE:0040EA28                                         ; CODE XREF: @RegisterClass+9Bp
CODE:0040EA28                                         ; @RegisterIntegerConsts+20p ...
CODE:0040EA28 push    ebx
CODE:0040EA29 push    esi
CODE:0040EA2A push    edi
CODE:0040EA2B mov     ediedx
CODE:0040EA2D mov     ebxeax ;可以看作是This的另一種形式
CODE:0040EA2F mov     esi, [ebx+8] ; addressing to the object member №1
CODE:0040EA32 cmp     esi, [ebx+0Ch] ; addressing to the object member №3
CODE:0040EA35 jnz     short loc_40EA3D
CODE:0040EA37 mov     eaxebx
CODE:0040EA39 mov     edx, [eax] ;addressing to TList->pVTBL
CODE:0040EA3B call    dword ptr [edx]
CODE:0040EA3D
CODE:0040EA3D loc_40EA3D:                             ; CODE XREF: Classes::TList::Add(void *)+Dj
CODE:0040EA3D mov     eax, [ebx+4] ; addressing to the object member №2
CODE:0040EA40 mov     [eax+esi*4], edi
CODE:0040EA43 inc     dword ptr [ebx+8]
CODE:0040EA46 mov     eaxesi
CODE:0040EA48 pop     edi
CODE:0040EA49 pop     esi
CODE:0040EA4A pop     ebx
CODE:0040EA4B retn
CODE:0040EA4B __fastcall Classes::TList::Add(void *) endp
 
好了,最後的3個成員找到了。它們都是4位元組長。

要使用IDA分析類的工作變得簡單一點,可以使用結構體功能。實際上,類和結構是一樣的:)))
用了下面的結構定義以後:
00000000 TList_obj struc ; (大小=0X10)
00000000 pVTBL dd ?
00000004 Property1 dd ?
00000008 Property2 dd ?
0000000C Property3 dd ?
00000010 TList_obj ends
 
程式碼清晰多了:
CODE:0040EA28 __fastcall Classes::TList::Add(void *) proc near
CODE:0040EA28                                         ; CODE XREF: @RegisterClass+9Bp
CODE:0040EA28                                         ; @RegisterIntegerConsts+20p ...
CODE:0040EA28 push    ebx
CODE:0040EA29 push    esi
CODE:0040EA2A push    edi
CODE:0040EA2B mov     ediedx
CODE:0040EA2D mov     ebxeax
CODE:0040EA2F mov     esi, [ebx+TList_obj.Property2]
CODE:0040EA32 cmp     esi, [ebx+TList_obj.Property3]
CODE:0040EA35 jnz     short loc_40EA3D
CODE:0040EA37 mov     eaxebx
CODE:0040EA39 mov     edx, [eax+TList_obj.pVTBL]
CODE:0040EA3B call    dword ptr [edx] ;TList::Grow
CODE:0040EA3D
CODE:0040EA3D loc_40EA3D:                             ; CODE XREF: Classes::TList::Add(void *)+Dj
CODE:0040EA3D mov     eax, [ebx+TList_obj.Property1]
CODE:0040EA40 mov     [eax+esi*4], edi
CODE:0040EA43 inc     [ebx+TList_obj.Property2]
CODE:0040EA46 mov     eaxesi
CODE:0040EA48 pop     edi
CODE:0040EA49 pop     esi
CODE:0040EA4A pop     ebx
CODE:0040EA4B retn
CODE:0040EA4B __fastcall Classes::TList::Add(void *) endp
 
考慮到VBTL的結構,很容易想到:
 
CODE:0040EA3B call    dword ptr [edx]
 
就是TList::Grow, 

因為
CODE:0040D5D8 pVTBL dd offset TList::Grow  
 
現在我們可以對類的成員做一點深入的分析了。比方說,看到下面的程式碼: 
CODE:0040EA3D mov     eax, [ebx+TList_obj.Property1]
CODE:0040EA40 mov     [eax+esi*4], edi
CODE:0040EA43 inc     [ebx+TList_obj.Property2]
 
就可以知道Property2是TList中元素的計數器。因為增加一個元素時,它也被加一。Property1是指向元素陣列的指標。Property 2可以看作是陣列的索引。而Property 3則是一個list裡最多允許的元素數目。此外,只有當Property2等於Property3時,TList::Grow被呼叫。透過邏輯推理,我們知道了這些。現在,一切都清楚起來了。順便看看幫助文件,給這些成員命名吧:
 
CODE:0040EA28 __fastcall Classes::TList::Add(void *) proc near
CODE:0040EA28                                         ; CODE XREF: @RegisterClass+9Bp
CODE:0040EA28                                         ; @RegisterIntegerConsts+20p ...
CODE:0040EA28                 push    ebx
CODE:0040EA29                 push    esi
CODE:0040EA2A                 push    edi
CODE:0040EA2B                 mov     ediedx
CODE:0040EA2D                 mov     ebxeax
CODE:0040EA2F                 mov     esi, [ebx+TList_obj.Count]
CODE:0040EA32                 cmp     esi, [ebx+TList_obj.Capacity]
CODE:0040EA35                 jnz     short loc_40EA3D
CODE:0040EA37                 mov     eaxebx
CODE:0040EA39                 mov     edx, [eax+TList_obj.pVTBL]
CODE:0040EA3B                 call    dword ptr [edx]
CODE:0040EA3D
CODE:0040EA3D loc_40EA3D:                             ; CODE XREF: Classes::TList::Add(void *)+Dj
CODE:0040EA3D                 mov     eax, [ebx+TList_obj.Items]
CODE:0040EA40                 mov     [eax+esi*4], edi
CODE:0040EA43                 inc     [ebx+TList_obj.Count]
CODE:0040EA46                 mov     eaxesi
CODE:0040EA48                 pop     edi
CODE:0040EA49                 pop     esi
CODE:0040EA4A                 pop     ebx
CODE:0040EA4B                 retn
CODE:0040EA4B __fastcall Classes::TList::Add(void *) endp
 
物件的結構已經分析好了,下面是物件成員。
 
查詢物件方法
 
物件的方法可以是以下幾種:公開/私有(保護),虛方法/非虛方法以及靜態方法.

由於編譯後的靜態方法和普通的過程沒有什麼區別,所以靜態方法是無法被識別的。這些函式和某個特定的類之間的關係也是無法確定的。但是,應該指出的是,如果某個靜態方法在類的方法裡被呼叫,那麼,它是可見的。否則尋找靜態方法的企圖只是在浪費時間。

虛方法很容易找到――它們都位於VTBL裡。但是我們應該如何查詢一般的方法呢?想想OOP:當物件方法被呼叫時,指向物件本身的指標被隱含地傳遞給該方法。實際上,這就意味著每個方法的第一個引數就是指向物件的指標。也就是說,如果該方法被宣告為fastcall型別,指向物件的指標是放在EAX裡的。而對於cdecl或stdcall型別的方法,首個引數是放在堆疊裡的。讓我們來看看指向物件的指標被放在什麼地方……好!在dword_4A45F8裡。透過查詢對4A45F8的交叉引用,我們可以找到很多非虛擬方法。我們還可以在4A45F8下一個斷點,追蹤對物件例項指標的複製以找出餘下的方法。

在本例中,由於使用了全域性變數,一切都很容易。但是如果使用的是區域性變數或者程式碼無法被執行(比如說,一個驅動程式。或者該程式碼不允許被執行),又應該怎麼做呢?這就需要一個特別的辦法。

一步一步來:
1)首先要找到所有呼叫建構函式的地方。
對每個呼叫重複以下步驟
2)跟去看看指向當前物件例項的指標被寫到哪裡了。
3)把所有呼叫了建構函式的函式作為物件方法。
4)如果沒有這樣的函式呼叫,就看建構函式下面的一個呼叫。否則就檢視所有對已經找到的方法的交叉引用。這樣就可以找到不在建構函式附近的呼叫。由於我們已經知道方法的首個引數是指向物件本身的指標,於是就可以查詢物件指標的交叉引用。用這樣的方法,我們可以一層一層地分析程式碼,直到出現僵局或者找到物件方法。
5)分析下一個已經找到的方法。
 
例如,我們已經找到了Classes::TList::Add,而且也找到了對Classes::TList::Add的一個引用:
 
CODE:0040F020 TThreadList::Add proc near              ; CODE XREF: TCanvas::`...'+9Ep
CODE:0040F020                                         ; Graphics::_16725+C4p
CODE:0040F020
CODE:0040F020 var_4           = dword ptr -4
CODE:0040F020
CODE:0040F020                 push    ebp
CODE:0040F021                 mov     ebpesp
CODE:0040F023                 push    ecx
CODE:0040F024                 push    ebx
CODE:0040F025                 mov     ebxedx
CODE:0040F027                 mov     [ebp+var_4], eax
CODE:0040F02A                 mov     eax, [ebp+var_4]
CODE:0040F02D                 call    TThreadList::LockList
CODE:0040F032                 xor     eaxeax
CODE:0040F034                 push    ebp
CODE:0040F035                 push    offset loc_40F073
CODE:0040F03A                 push    dword ptr fs:[eax]
CODE:0040F03D                 mov     fs:[eax], esp
CODE:0040F040                 mov     eax, [ebp+var_4]
CODE:0040F043                 mov     eax, [eax+4]
CODE:0040F046                 mov     edxebx
CODE:0040F048                 call    TList::IndexOf
CODE:0040F04D                 inc     eax
CODE:0040F04E                 jnz     short loc_40F05D
CODE:0040F050                 mov     eax, [ebp+var_4]
CODE:0040F053                 mov     eax, [eax+4]
CODE:0040F056                 mov     edxebx
CODE:0040F058                 call    Classes::TList::Add(void *)
 
就是說,我們找到了TList::IndexOf方法。
 
進一步分析發現,我們處在TthreadList物件的方法中,TList是它的成員之一。這裡沒有什麼可以看的東西。假定一下,沒有其他對Classes::TList::Add的引用。進到TList::IndexOf方法並且檢視對它的引用。下面是其中的一個:

CODE:0040EE38 TList::Remove   proc near               ; CODE XREF: TThreadList::Remove+28p
CODE:0040EE38                                         ; TCollection::RemoveItem+Bp ...
CODE:0040EE38                 push    ebx
CODE:0040EE39                 push    esi
CODE:0040EE3A                 mov     ebxeax
CODE:0040EE3C                 mov     eaxebx
CODE:0040EE3E                 call    TList::IndexOf
CODE:0040EE43                 mov     esieax
CODE:0040EE45                 cmp     esi, 0FFFFFFFFh
CODE:0040EE48                 jz      short loc_40EE53
CODE:0040EE4A                 mov     edxesi
CODE:0040EE4C                 mov     eaxebx
CODE:0040EE4E                 call    TList::Delete
CODE:0040EE53
CODE:0040EE53 loc_40EE53:                             ; CODE XREF: TList::Remove+10j
CODE:0040EE53                 mov     eaxesi
CODE:0040EE55                 pop     esi
CODE:0040EE56                 pop     ebx
CODE:0040EE57                 retn
CODE:0040EE57 TList::Remove   endp
 
這樣,TList::Delete和TList::Remove就有了。
 
下面就是所有物件指標的交叉引用和相關變數。

這裡是查詢變數的例子:
CODE:0041319D mov     eax, [ebp+var_4]
CODE:004131A0 mov     edxds:pTList
CODE:004131A6 mov     [eax+30h], edx  ;物件指標
CODE:004131A9 jmp     short loc_4131BD
 
下面可以看到:
CODE:00413236 mov     eax, [eax+30h]
CODE:00413239 mov     edx, [ebp+var_10]
CODE:0041323C call    TList::Get
 
如何分辨公開方法和私有方法呢?
只有當所有的方法全部找到以後才可以做這件事。私有方法只有在其它方法裡才有呼叫。就是說,必須檢視交叉引用了。查詢方法以前,建議先把它們編號。也即把你找到的方法依次命名為Object1::Method1,Object1::Method2……所有的方法全部出來以後,就可以開始分析它們的引數(主要是個數和型別)了。
 
確定方法引數的個數
 
關於cdecl和stdcall幾乎沒有什麼可說的。只要把IDA找到的引數個數減去1就可以了(還記得嗎?第一個引數是物件指標,其它的才是真正的引數)。

fastcall要複雜點兒。首先我們要記住引數的次序:EAX,EDX,ECX,堆疊。首先要看看IDA找到了幾個透過堆疊傳遞的引數。如果至少有一個,那麼引數的個數要加3(3個暫存器引數加上堆疊引數)。由於第一個引數是物件指標This,這個數目還要減去1才是真正的引數個數。如果沒有堆疊引數的話,就要看看函式的開頭了。由於Delphi試圖不去攪亂暫存器的值,結果每個fastcall函式的開頭都要儲存EAX,EDX和ECX:
 
mov esiedx ; 第一個引數
mov ebxeax ; This指標
mov ediecx ; 第二個引數

根據被複制的暫存器個數就可以判斷出引數的個數。比如:
 
CODE:0040EBE0 TList::Get      proc near               ; CODE XREF: @GetClass+1Dp
CODE:0040EBE0                                         ; @UnRegisterModuleClasses+24p ...
CODE:0040EBE0
CODE:0040EBE0 var_4           = dword ptr -4
CODE:0040EBE0
CODE:0040EBE0                 push    ebp
CODE:0040EBE1                 mov     ebpesp
CODE:0040EBE3                 push    0
CODE:0040EBE5                 push    ebx
CODE:0040EBE6                 push    esi
CODE:0040EBE7                 mov     esiedx
CODE:0040EBE9                 mov     ebxeax
CODE:0040EBEB                 xor     eaxeax
 
一共2個引數,其中一個是This指標。那麼TList::Get有1個引數。

CODE:004198CC                 push    ebp
CODE:004198CD                 mov     ebpesp
CODE:004198CF                 add     esp, 0FFFFFF8Ch
CODE:004198D2                 push    ebx
CODE:004198D3                 push    esi
CODE:004198D4                 push    edi
CODE:004198D5                 mov     [ebp+var_C], ecx
CODE:004198D8                 mov     [ebp+var_8], edx
CODE:004198DB                 mov     [ebp+var_4], eax
 
一共3個引數,其中一個是This指標。那麼真正的引數是2個。
值得指出的是,由於我們是在用IDA分析Delphi程式,基於上面的原因,寫函式頭時一定要考慮到物件指標This。

引數的型別就要靠你去分析了。
================================================================

相關文章