• 标 题:【翻译】Delphi中类的逆向工程
  • 作 者:firstrose
  • 时 间:2004-12-09,18:37
  • 链 接:http://bbs.pediy.com

前段时间看到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。

参数的类型就要靠你去分析了。
================================================================