译者:firstrose
类的逆向工程是一项需要OOP相关知识以及特定编译器如何处理OOP部分的知识的复杂工作。 我们的任务是得到类、方法和成员。由于在用Delphi编写的程序里查找类相对容易,这里就用Delphi做示范。类的逆向先要从查找构造函数开始。因为类在这里被分配内存,而且我们可以从中得到构造函数的一些信息。在Delphi程序里找一个构造函数很简单,只需要查找类名出现的地方即可。例如,对于TList可以找到下面的结构: CODE:0040D598 TList dd offset TList_VTBLCODE:0040D59C dd 7 dup(0)CODE:0040D5B8 dd offset aTlist ; "TList"CODE:0040D5BC SizeOfObject dd 10hCODE:0040D5C0 dd offset off_4010C8CODE:0040D5C4 dd offset TObject::SafeCallExceptionCODE:0040D5C8 dd offset nullsub_8CODE:0040D5CC dd offset TObject::NewInstanceCODE:0040D5D0 dd offset TObject::FreeInstanceCODE:0040D5D4 dd offset sub_40EA08CODE:0040D5D8 TList_VTBL dd offset TList::GrowCODE:0040D5DC dd offset unknown_libname_107CODE:0040D5E0 aTlist db 5,'TList' 我们把这个结构称为“object descriptor”,即“对象描述符”。指向它的指针被传递给构造函数,构造函数则从中取得创建对象所需要的数据。通过查找对40D598的交叉引用,可以得到对构造函数的所有调用。 下面是其中的一个: CODE:0040E72E mov eax, ds:TListCODE:0040E733 call CreateClassCODE:0040E738 mov ds:dword_4A45F8, eax 这里的构造函数的名字是我们自己起的。通过查看函数,我们可以知道它是否真的是一个构造函数(CreateClass)CODE:00402F48 CreateClass proc near ; CODE XREF: @BeginGlobalLoading+17pCODE:00402F48 ; @CollectionsEqual+48p ...CODE:00402F48 test dl, dlCODE:00402F4A jz short loc_402F54CODE:00402F4C add esp, 0FFFFFFF0hCODE:00402F4F call __linkproc__ ClassCreateCODE:00402F54CODE:00402F54 loc_402F54: ; CODE XREF: CreateClass+2jCODE:00402F54 test dl, dlCODE:00402F56 jz short locret_402F62CODE:00402F58 pop large dword ptr fs:0CODE:00402F5F add esp, 0ChCODE:00402F62CODE:00402F62 locret_402F62: ; CODE XREF: CreateClass+EjCODE:00402F62 retnCODE:00402F62 CreateClass endp也就是说,如果函数里有 __linkproc__ ClassCreate ,它就是一个构造函数。下面让我们看看生成类实例的时候发生了什么特别的事。 CODE:00403200 __linkproc__ ClassCreate proc near ; CODE XREF: CreateClass+7pCODE:00403200 ; sub_40AA58+Ap ...CODE:00403200CODE:00403200 arg_0 = dword ptr 10hCODE:00403200CODE:00403200 push edxCODE:00403201 push ecxCODE:00403202 push ebxCODE:00403203 call dword ptr [eax-0Ch]CODE:00403206 xor edx, edxCODE:00403208 lea ecx, [esp+arg_0]CODE:0040320C mov ebx, fs:[edx]CODE:0040320F mov [ecx], ebxCODE:00403211 mov [ecx+8], ebpCODE:00403214 mov dword ptr [ecx+4], offset loc_403225CODE:0040321B mov [ecx+0Ch], eaxCODE:0040321E mov fs:[edx], ecxCODE:00403221 pop ebxCODE:00403222 pop ecxCODE:00403223 pop edxCODE:00403224 retnCODE: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:004010FCoCODE:00402F0C ; CODE:004011DCo ...CODE:00402F0C push eaxCODE:00402F0D mov eax, [eax-1Ch]CODE:00402F10 call __linkproc__ GetMemCODE:00402F15 mov edx, eaxCODE:00402F17 pop eaxCODE:00402F18 jmp TObject::InitInstanceCODE: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:TListCODE:0040E733 call CreateClassCODE: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:pTListCODE:004131A6 mov [eax+30h], edx ; 复制的指向对象的指针CODE:004131A9 jmp short loc_4131BD.............CODE:004131BDCODE:004131BD loc_4131BD: ; CODE XREF: sub_4130BC+EDjCODE:004131BD xor eax, eaxCODE:004131BF push ebpCODE:004131C0 push offset loc_413276CODE:004131C5 push dword ptr fs:[eax]CODE:004131C8 mov fs:[eax], espCODE: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 nearCODE:0040EA28 ; CODE XREF: @RegisterClass+9BpCODE:0040EA28 ; @RegisterIntegerConsts+20p ...CODE:0040EA28 push ebxCODE:0040EA29 push esiCODE:0040EA2A push ediCODE:0040EA2B mov edi, edxCODE:0040EA2D mov ebx, eax ;可以看作是This的另一种形式CODE:0040EA2F mov esi, [ebx+8] ; addressing to the object member №1CODE:0040EA32 cmp esi, [ebx+0Ch] ; addressing to the object member №3CODE:0040EA35 jnz short loc_40EA3DCODE:0040EA37 mov eax, ebxCODE:0040EA39 mov edx, [eax] ;addressing to TList->pVTBLCODE:0040EA3B call dword ptr [edx]CODE:0040EA3DCODE:0040EA3D loc_40EA3D: ; CODE XREF: Classes::TList::Add(void *)+DjCODE:0040EA3D mov eax, [ebx+4] ; addressing to the object member №2CODE:0040EA40 mov [eax+esi*4], ediCODE:0040EA43 inc dword ptr [ebx+8]CODE:0040EA46 mov eax, esiCODE:0040EA48 pop ediCODE:0040EA49 pop esiCODE:0040EA4A pop ebxCODE:0040EA4B retnCODE: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 nearCODE:0040EA28 ; CODE XREF: @RegisterClass+9BpCODE:0040EA28 ; @RegisterIntegerConsts+20p ...CODE:0040EA28 push ebxCODE:0040EA29 push esiCODE:0040EA2A push ediCODE:0040EA2B mov edi, edxCODE:0040EA2D mov ebx, eaxCODE:0040EA2F mov esi, [ebx+TList_obj.Property2]CODE:0040EA32 cmp esi, [ebx+TList_obj.Property3]CODE:0040EA35 jnz short loc_40EA3DCODE:0040EA37 mov eax, ebxCODE:0040EA39 mov edx, [eax+TList_obj.pVTBL]CODE:0040EA3B call dword ptr [edx] ;TList::GrowCODE:0040EA3DCODE:0040EA3D loc_40EA3D: ; CODE XREF: Classes::TList::Add(void *)+DjCODE:0040EA3D mov eax, [ebx+TList_obj.Property1]CODE:0040EA40 mov [eax+esi*4], ediCODE:0040EA43 inc [ebx+TList_obj.Property2]CODE:0040EA46 mov eax, esiCODE:0040EA48 pop ediCODE:0040EA49 pop esiCODE:0040EA4A pop ebxCODE:0040EA4B retnCODE: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], ediCODE: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 nearCODE:0040EA28 ; CODE XREF: @RegisterClass+9BpCODE:0040EA28 ; @RegisterIntegerConsts+20p ...CODE:0040EA28 push ebxCODE:0040EA29 push esiCODE:0040EA2A push ediCODE:0040EA2B mov edi, edxCODE:0040EA2D mov ebx, eaxCODE:0040EA2F mov esi, [ebx+TList_obj.Count]CODE:0040EA32 cmp esi, [ebx+TList_obj.Capacity]CODE:0040EA35 jnz short loc_40EA3DCODE:0040EA37 mov eax, ebxCODE:0040EA39 mov edx, [eax+TList_obj.pVTBL]CODE:0040EA3B call dword ptr [edx]CODE:0040EA3DCODE:0040EA3D loc_40EA3D: ; CODE XREF: Classes::TList::Add(void *)+DjCODE:0040EA3D mov eax, [ebx+TList_obj.Items]CODE:0040EA40 mov [eax+esi*4], ediCODE:0040EA43 inc [ebx+TList_obj.Count]CODE:0040EA46 mov eax, esiCODE:0040EA48 pop ediCODE:0040EA49 pop esiCODE:0040EA4A pop ebxCODE:0040EA4B retnCODE: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::`...'+9EpCODE:0040F020 ; Graphics::_16725+C4pCODE:0040F020CODE:0040F020 var_4 = dword ptr -4CODE:0040F020CODE:0040F020 push ebpCODE:0040F021 mov ebp, espCODE:0040F023 push ecxCODE:0040F024 push ebxCODE:0040F025 mov ebx, edxCODE:0040F027 mov [ebp+var_4], eaxCODE:0040F02A mov eax, [ebp+var_4]CODE:0040F02D call TThreadList::LockListCODE:0040F032 xor eax, eaxCODE:0040F034 push ebpCODE:0040F035 push offset loc_40F073CODE:0040F03A push dword ptr fs:[eax]CODE:0040F03D mov fs:[eax], espCODE:0040F040 mov eax, [ebp+var_4]CODE:0040F043 mov eax, [eax+4]CODE:0040F046 mov edx, ebxCODE:0040F048 call TList::IndexOfCODE:0040F04D inc eaxCODE:0040F04E jnz short loc_40F05DCODE:0040F050 mov eax, [ebp+var_4]CODE:0040F053 mov eax, [eax+4]CODE:0040F056 mov edx, ebxCODE: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+28pCODE:0040EE38 ; TCollection::RemoveItem+Bp ...CODE:0040EE38 push ebxCODE:0040EE39 push esiCODE:0040EE3A mov ebx, eaxCODE:0040EE3C mov eax, ebxCODE:0040EE3E call TList::IndexOfCODE:0040EE43 mov esi, eaxCODE:0040EE45 cmp esi, 0FFFFFFFFhCODE:0040EE48 jz short loc_40EE53CODE:0040EE4A mov edx, esiCODE:0040EE4C mov eax, ebxCODE:0040EE4E call TList::DeleteCODE:0040EE53CODE:0040EE53 loc_40EE53: ; CODE XREF: TList::Remove+10jCODE:0040EE53 mov eax, esiCODE:0040EE55 pop esiCODE:0040EE56 pop ebxCODE:0040EE57 retnCODE:0040EE57 TList::Remove endp 这样,TList::Delete和TList::Remove就有了。 下面就是所有对象指针的交叉引用和相关变量。这里是查找变量的例子:CODE:0041319D mov eax, [ebp+var_4]CODE:004131A0 mov edx, ds:pTListCODE: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+1DpCODE:0040EBE0 ; @UnRegisterModuleClasses+24p ...CODE:0040EBE0CODE:0040EBE0 var_4 = dword ptr -4CODE:0040EBE0CODE:0040EBE0 push ebpCODE:0040EBE1 mov ebp, espCODE:0040EBE3 push 0CODE:0040EBE5 push ebxCODE:0040EBE6 push esiCODE:0040EBE7 mov esi, edxCODE:0040EBE9 mov ebx, eaxCODE:0040EBEB xor eax, eax 一共2个参数,其中一个是This指针。那么TList::Get有1个参数。CODE:004198CC push ebpCODE:004198CD mov ebp, espCODE:004198CF add esp, 0FFFFFF8ChCODE:004198D2 push ebxCODE:004198D3 push esiCODE:004198D4 push ediCODE:004198D5 mov [ebp+var_C], ecxCODE:004198D8 mov [ebp+var_8], edxCODE:004198DB mov [ebp+var_4], eax 一共3个参数,其中一个是This指针。那么真正的参数是2个。值得指出的是,由于我们是在用IDA分析Delphi程序,基于上面的原因,写函数头时一定要考虑到对象指针This。参数的类型就要靠你去分析了。