前段时间看到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 eax, ds: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 dl, dl
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 dl, dl
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 edx, edx
CODE:00403208 lea ecx, [esp+arg_0]
CODE:0040320C mov ebx, fs:[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 eax, ds: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 edx, eax
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 eax, ds: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 edx, ds: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 eax, eax
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 edi, edx
CODE:0040EA2D mov ebx, eax ;可以看作是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 eax, ebx
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 eax, esi
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 edi, edx
CODE:0040EA2D mov ebx, eax
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 eax, ebx
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 eax, esi
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 edi, edx
CODE:0040EA2D mov ebx, eax
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 eax, ebx
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 eax, esi
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 ebp, esp
CODE:0040F023 push ecx
CODE:0040F024 push ebx
CODE:0040F025 mov ebx, edx
CODE:0040F027 mov [ebp+var_4], eax
CODE:0040F02A mov eax, [ebp+var_4]
CODE:0040F02D call TThreadList::LockList
CODE:0040F032 xor eax, eax
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 edx, ebx
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 edx, ebx
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 ebx, eax
CODE:0040EE3C mov eax, ebx
CODE:0040EE3E call TList::IndexOf
CODE:0040EE43 mov esi, eax
CODE:0040EE45 cmp esi, 0FFFFFFFFh
CODE:0040EE48 jz short loc_40EE53
CODE:0040EE4A mov edx, esi
CODE:0040EE4C mov eax, ebx
CODE:0040EE4E call TList::Delete
CODE:0040EE53
CODE:0040EE53 loc_40EE53: ; CODE XREF: TList::Remove+10j
CODE:0040EE53 mov eax, esi
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 edx, ds: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 esi, edx ; 第一个参数
mov ebx, eax ; This指针
mov edi, ecx ; 第二个参数
根据被复制的寄存器个数就可以判断出参数的个数。比如:
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 ebp, esp
CODE:0040EBE3 push 0
CODE:0040EBE5 push ebx
CODE:0040EBE6 push esi
CODE:0040EBE7 mov esi, edx
CODE:0040EBE9 mov ebx, eax
CODE:0040EBEB xor eax, eax
一共2个参数,其中一个是This指针。那么TList::Get有1个参数。
CODE:004198CC push ebp
CODE:004198CD mov ebp, esp
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。
参数的类型就要靠你去分析了。
================================================================
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)