反汇编C++
就在几年前,逆向工程师仅凭C和汇编的知识旧可以逆向大多数应用程序。现在由于在恶意软件中越来越多地使用C++,以及大多数用C++编写的现代应用程序,理解C++面向对象代码的反汇编是必须的。本文将试图通过讨论在逆向中手动识别C++概念的方法,如何自动化分析,以及我们开发的基于所做分析来增强逆向的工具来填补这一空白。
I.简介
作为逆向工程师,理解C++概念是很重要的,因为它们是在反汇编中识别的,当然,对C++目标的主要部分(类)以及这些部分如何关联在一起(类关系)有一个总体的想法。为了实现这一理解,逆向工程必须能够(1)识别类(2)识别类之间的关系(3)识别类成员。本文试图为读者提供如何实现这三个目标的信息。首先,本文讨论了手工分析C++目标以获取类信息的方法。接下来,讨论如何将这些手工方法自动化。
在反汇编中理解C++构造确实是一项很好的技能,但是我们学习这项技能和编写本文的动机是什么呢?以下是我们撰写这篇论文的动机。
(1)在恶意代码中增加C++代码使用
有经验的恶意代码分析师,有些情况下,我们试图理解的恶意diamagnetic是用C++编写的。在IDA中加载恶意代码并对虚函数调用进行静态分析有时很困难,因为简介调用,不容易理解确定这些调用的去向。一些著名的用C++编写的恶意代码的例子如Agobot,Mytob的一些变体,我们还看到一些从蜜罐中用C++开发的新恶意代码。
(2)大多数现代程序使用C++
对于大型和复杂的应用程序和系统,C++是首选语言之一。这意味着对于二级制审计,逆向者期望有用C++编写的目标。关于如何将C++概念转换为二进制以及能够获得诸如类关系之类的高级信息(如类关系)的信息是有益的。
(3)普遍缺乏关于C++逆向主题的公开信息
我们详细能够记录C++逆向的主题并将分享给其他逆向工程师是一件好事。收集关于这个主题的信息确实不容易,而且只有很少的信息专门关于它。
常用方法
本节介绍分析C++二进制文件的手工方法;它特别关注与识别/提取C++类及相应的成员(变量,函数,构造函数/析构函数)和关系。
A.识别C++的二进制和结构
1)大量使用ecx(this ptr)。调试器首先看到是ecx的大量使用(它被用作this指针)。分析员可能看到的是,它在函数即将被调用之前被赋值:
1 2 3 | .text: 004019E4 mov ecx,esi
.text: 004019E6 mov 0BBh
.text: 004019EB call sub_401120;Class member function
|
另一处,如果一个函数使用ecx而没有先初始化它,这表明这是一个可能的类成员函数:
1 2 3 4 5 6 7 8 9 10 | .text: 004010D0 sub_4010D0 proc near
.text: 004010D0 push esi
.text: 004010D1 mov esi,ecx
.text: 004010DD mov dword ptr [esi],offset off_40c0D0
.text: 00401101 mov dword ptr [esi + 4 ]. 0BBh
.text: 00401108 call sub_401EB0
.text: 0040110D add esp, 18h
.text: 00401110 pop esi
.text: 00401111 retn
.text: 00401111 sub_4010D0 endp
|
4)调用约定。与(1)相关,调用类成员函数时使用栈中常用的函数形参,并使用ecx指向类的对象(即this指针)。下面是一个类实例化的例子,其中分配的类对象(eax)最终被传递给ecx,然后调用构造函数。
1 2 3 4 5 | .text: 00401994 push 0Ch
.text: 00401996 call ?? 2 @YAPAXO@Z ;operator new(uint)
.text: 004019AB mov ecx,eax
:::
.text: 004019AD call ClassA_ctor
|
此外,分析人员会注意到间接函数调用,这更可能是虚函数;当然,如果不了解实际的类或在调试器下运行代码,就很难跟踪这些调用的去向。参考下面的虚函数调用示例:
1 2 3 4 5 6 7 8 9 10 | .text: 00401996 call ?? 2 @YAPAXI@Z ;operator new(uint)
:::
.text: 004019B2 mov esi,eax
:::
.text: 004019FF call ClassA_ctor
:::
.text: 00401A01 add esp, 8
.text: 00401A04 mov ecx,esi
.text: 00401A06 push 0CCh
.text: 00401A0B call dword ptr [eax]
|
在这种情况下,调试器必须首先知道ClassA的虚函数表(vfable)位于哪里。然后根据vfable中列出的函数链表确定函数的实际地址。
5)STL代码<STL(Standard Template Library),即标准模板库>和导入的dll文件。判断一个样本是否为C++二进制文件的另一种方法是,目标是否使用STL代码,这可以通过导入函数或库签名标识(如IDA的FLIRT)来确定:
对STL代码的调用:
1 2 3 4 | .text: 00401201 mov ecx,eax
.text: 00401203 call
ds:?sputc@?$basic_streambuf@DU?$char_traits@D@std@@@std@@QAEHD@Z;
std::basic_streambuf<char,std::char_traits<char>>::sputc(char)
|
类实例布局
在进一步深入之前,调试器还应该熟悉类是如何在内存中布局的。让我们从一个非常简单的类开始。
1 2 3 4 5 6 7 8 | class Ex1
{
int var1;
int var2;
char var3;
public:
int get_var1();
};
|
这个类的布局是这样的:
1 2 3 4 5 6 7 | class Ex1 size( 12 ):
+ - - -
0 | var1
4 | var2
8 | var3
| <alignment member>(size = 3 )
+ - - -
|
填充被添加到最后一个成员变量,因为它必须在4字节的边界上对齐。在Visual C++中,成员变量按照声明的顺序放置在内存中。
如果类包含虚函数怎么办?
1 2 3 4 5 6 7 | class Ex2
{
int var1;
public:
virtual int get_sum( int x, int y);
virtual void reset_values();
};
|
以下是类的布局:
1 2 3 4 5 | class Ex2 size( 8 ):
+ - - -
0 | {vtptr}
4 | var1
+ - - -
|
注意,在布局的开始部分添加了一个指向虚函数表的指针。该表按声明虚函数的顺序包含了虚函数的地址。类Ex2的函数表是这样的。
1 2 3 | Ex2::$vftable@:
0 | &Ex2::get_sum
4 | &Ex2::reset_values
|
现在,如果一个类继承自另一个类,该怎么办?当一个类从一个类继承时会发生什么?
1 2 3 4 5 6 | class Ex3: public Ex2
{
int var1;
public:
void get_values();
};
|
布局:
1 2 3 4 5 6 7 8 | class Ex3 size( 12 ):
+ - - -
| + - - - (base class Ex2)
0 | | {vtptr}
4 | | var1
| + - - -
8 | var1
+ - - -
|
派生类的布局只是附加到基类的布局中。在多重继承的情况下,会发生以下情况:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | class Ex4
{
int var1;
int var2;
public:
virtual void func1();
virtual void func2();
};
class Ex5: public Ex2,Ex4
{
int var1;
public:
void func1();
virtual void v_ex5();
};
class Ex5 size( 24 ):
+ - - -
| + - - - (base class Ex2)
0 | | {vtptr}
4 | | var1
| + - - -
| + - - - (base class Ex4)
8 | | {vfptr}
12 | | var1
16 | | var2
| + - - -
20 | var1
+ - - -
Ex5::$vfable@Ex2@:
0 | &Ex2::get_sum
1 | &Ex2::reset_values
2 | &Ex5::v_ex5
Ex5::$vfable@Ex4@:
| - 8
0 | &Ex5::func1
1 | &Ex4::func2
|
每个基类的实例数据将被嵌入到派生类的实例中,并且每个包含虚函数的基类将有自己的vfable。请注意,第一个基类与当前对象一个vfable。当前对象的虚函数将被追到到第一个基类的虚函数列表的末尾。
B.识别类
在确定了C++二进制文件、讨论了一些重要的c++构造以及类实例如何在内存中表示之后,这一部分现在介绍了确定目标中使用的C++类的方法。下面讨论的方法只是试图确定什么是类(目标有 ClassA、ClassB、ClassC 等)。本文接下来的部分将讨论如何推断这些类之间的关系并确定它们的成员。
1)识别构造函数/析构函数
要在二进制文件识别类,我们需要检查这些类的对象是如何创建的。如何在二进制中实现它们的创建可以为我们提供在反汇编的过程中去识别它们的提示。
1)全局对象。全局对象是声明为全局变量的对象。这些对象的内存空间在编译时分配,并放置在二进制数据段中。在C++启动期间,该构造函数在main()之前被隐式调用。在程序退出时调用析构函数。
要识别可能的全局对象,请查找调用的函数,该函数使用指向全局变量的指针作为this指针。要找到构造函数和析构函数,必须检查对这个全局变量的交叉引用。查找将此变量作为this指针传递给函数调用的位置。如果此调用位于程序入口点和main()之间的路径,它可能是构造函数。
2)局部对象(本地对象)。局部对象是被声明为局部变量的对象。这些对象的范围是从声明开始直到块退出,例如函数结束,右大括号。在栈中分配对象大小的空间。析构函数在对象声明时调用,析构函数在作用域结束时调用。
如果用指向未初始化堆栈变量的this指针调用,则可以识别局部对象的析构函数。析构函数是在调用构造函数的同一代码块(即声明对象的代码块)中使用this指针调用的最后一个函数。
demo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | .text: 00401060 sub_401060 proc near
.text: 00401060 var_C = dword ptr - 0Ch
.text: 00401060 var_8 = dword ptr - 8
.text: 00401060 var_4 = dword ptr - 4
.text: 00401060
...(some code)...
.text: 004010A4 add esp, 8
.text: 004010A7 cmp [ebp + var_4], 5
.text: 004010AB jle short loc_4010CE
.text: 004010AB {< - block begin
.text: 004010AD lea ecx,[ebp + var_8] ;var_8 is uninitialized
.text: 004010B0 call sub_401000 ;constructor
.text: 004010B5 mov edx,[ebp + var_8]
.text: 004010B8 push edx
.text: 004010B9 push offset str - >WithinIfX
.text: 004010BE call sub_4010E4
.text: 004010C3 add esp, 8
.text: 004010C6 lea ecx,[ebp + var_8]
.text: 004010C9 call sub_401020 ;destructor
.text: 004010CE }< - block end
.text: 004010CE
.text: 004010CE loc_4010CE: ;CODE XREF:sub_401060 + 4B
.text: 004010D5 lea ecx,[ebp + var_4]
.text: 004010D8 call sub_401020
|
3)动态分配(生成)对象。这些对象是通过new操作符动态创建的。new操作符实际上转换为对new()函数的调用,接着是对构造函数的调用。new()函数接收对象的大小作为参数(形参),在堆中分配该对象size的内存,然后返回该缓冲区的地址。然后将返回的地址作为this指针传送给构造函数。析构函数必须通过delete操作符显式调用。delete操作符转换为对析构函数的调用,然后调用free以释放堆中分配的内存。
demo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | .text: 0040103D _main proc near
.text: 0040103D argc = dword ptr 8
.text: 0040103D argv = dword ptr 0Ch
.text: 0040103D envp = dword ptr 10h
.text: 0040103D
.text: 0040103D push esi
.text: 0040103E push 4 ;size_t
.text: 00401040 call ?? 2 @YAPAXI@Z ;operator new(uint)
.text: 00401047 test eax,eax ;eax = address of allocated memory
.text: 00401048 pop ecx
.text: 0040104C jz short loc_401055
.text: 00401051 mov ecx,eax
.text: 00401053 jmp short loc_401057
.text: 00401055 loc_401055: ;CODE XREF: _main + B
.text: 00401055 xor esi,esi
.text: 00401057 loc_401057: ;CODE XREF: _main + 16
.text: 00401057 push 45h
.text: 00401059 mov ecx,esi
.text: 0040105B call sub_401027
.text: 00401060 test ecx,esi
.text: 00401062 jz short loc_401072
.text: 00401064 mov ecx,esi
.text: 00401066 call sub_40101B ;call to destrutor
.text: 0040106B push esi ;void *
.text: 0040106C call j_free; call to free thunk function
.text: 00401071 pop ecx
.text: 00401072 loc_401072: ;CODE XREF: _main + 25
.text: 00401072 xor eax,eax
.text: 00401074 pop esi
.text: 00401075 retn
.text: 00401075 _main endp
|
2)基于RTTI的多态类识别
识别类,特别是多态类(带有成员虚函数的类)的另一种方法是通过运行时类型信息(RTTI)。RTTI是一种可以在运行时确定对象类型的机制。这种机制正是typeid和dynamic_cast操作符所使用的机制。这两个操作符都需要传递给它们的类的信息,比如类名和类层次结构。事实上,如果在没有启用RTTI的情况下使用这些操作符,编译器将显示警告。默认情况下,msvc6.0禁用RTTI。
在MSVC 2005,RTTI是默认开启的。
注明,有一个编译器开关,使MSVC编译器能够生成类布局,该开关是 -dlreportAllClassLayout生成的一个 .layout文件, 其中包含有关类布局的大量信息,包括派生类中基类的偏移量、虚函数表(vftable)、虚基类表(vbtables,下面会进一步描述)以及成员变量等。
为了使RTTI成为可能,编译器在编译后的代码存储了几个数据结构,这些数据结构包含代码中关于类(特别是多态类)的信息。这些数据结构如下:
RTTICompleteObjectLocator
这个结构包含指向两个结构的指针,这两个结构分别是(1)识别的类信息(2)类层次结构:
OffSet |
Type |
Name |
Description |
0x00 |
DW |
signature |
Always 0? |
0x04 |
DW |
offset |
Offset of vftable within the class |
0x08 |
DW |
cdoffset |
|
0x0C |
DW |
pTypeDescriptor |
Class Information |
0x10 |
DW |
pClassHierarchyDescriptor |
Class Hierarchy Information |
下面是RTTICompleteObjectLocator指针布局的示例。指向这个函数的指针就在类的vftable的下面:
1 2 3 4 5 6 7 8 9 | .rdata: 00404128 dd offset ClassA_RTTICompleteObjectLocator
.rdata: 0040412C ClassA_vftable dd offset sub_401000 ; DATA XREF:...
.rdata: 00404130 dd offset sub_401050
.rdata: 00404134 dd offset sub_4010C0
.rdata: 00404138 dd offset ClassB_RTTICompleteObjectLocator
.rdata: 0040413C ClassB_vftable dd offset sub_4012B0 ;DATA XREF:...
.rdata: 00404140 dd offset sub_401300
.rdata: 00404144 dd offset sub_4010C0
|
一个实际的RTTICompleteobjectLocator结构的例子:
1 2 3 4 5 6 | .rdata: 004045A4 ClassB_RTTICompleteObjectLocator
dd 0 ;signature
.rdata: 004045A8 dd 0 ;offset
.rdata: 004045AC dd 0 ;cdoffset
.rdata: 004045B0 dd offset ClassB_TypeDescriptor
.rdata: 004045B4 dd offset ClassB_RTTIClassHierarchyDescriptor
|
TypeDescriptor
该结构是由RTTICompleteobjectLocator中的DWORD 4th字段指向的,它包含类名。如果获得该值,将使反调试器大致了解此类应该做什么。
Offset |
Type |
Name |
Description |
0x00 |
DW |
pVFTable |
Always point to type_info‘s vftable |
0x04 |
DW |
spare |
? |
0x08 |
SZ |
name |
Class Name |
一个实际的例子关于TypeDescriptor:
1 2 3 4 | .data: 0041A098 ClassA_TypeDescriptor ; DATA XREF: . . .
dd offset type_info_vftable ; TypeDescriptor.pVFTable
.data: 0041A09C dd 0 ; TypeDescriptor.spare
.data: 0041A0A0 db '.?AVClassA@@' , 0 ; TypeDescriptor.name
|
RTTIClassHierarchyDescriptor
该结构包含有关类层次结构的信息,包括基类的数量和一个RTTIBaseclassDescriptor的数组(稍后讨论),该数组最终将指向基类的TypeDescriptor。
下面是ClassG的类声明,它实际上继承自ClassA和ClassE。
1 2 3 | class ClassA {...}
class ClassE {...}
class ClassG: public virtual ClassA, public virtual ClassE {...}
|
以下是ClassG实际的RTTIClassHierarchyDescriptor的描述符:
1 2 3 4 5 6 7 8 9 10 | .rdata: 004178C8 ClassG_RTTIClassHierarchyDescriptor ; DATA XREF: ...
.rdata: 004178C8 dd 0 ; signature
.rdata: 004178CC dd 3 ; attributes
.rdata: 004178D0 dd 3 ; numBaseClasses
.rdata: 004178D4 dd offset ClassG_pBaseClassArray ; pBaseClassArray
.rdata: 004178D8 ClassG_pBaseClassArray
dd offset RTTIBaseClassDescriptor@ 4178e8
.rdata: 004178C8 dd offset RTTIBaseClassDescriptor@ 417904
.rdata: 004178C8 dd offset RTTIBaseClassDescriptor@ 417920
|
有3个基类(包括ClassG本身的计数),属性是3(multiple,virtual inheritance),最后,pBaseClassArray指向RTTIBaseClassDescriptors的指针数组。
RTTIBaseClassDescriptor
该结构包含关于基类的信息,其中包括指向基类的typeDescriptor和RTTIClassHierarchyDescriptor的指针,另外包含PDM结构,该结构包含关于基类如何在类中布局的信息。
为多重虚拟继承生成一个vbtable(虚拟基类表)。因为有时需要upclass(强制转换为基类),所以需要确定基类的确切位置。vbtable包含每个基类的位移,vftable实际上是基类在派生类中的开始。
考虑前面所示的clssG类声明;编译器将生成以下类结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | class ClassG size( 28 ):
+ - - -
0 | {vfptr}
4 | {vbptr}
+ - - -
+ - - - (virtual base ClassA)
8 | {vfptr}
12 | class_a_var01
16 | class_a_var02
| <alignment member> (size = 3 )
+ - - -
+ - - - (virtual base ClassE)
20 | {vfptr}
24 | class_e_var01
+ - - -
|
在这种情况下,vbtable位于类的偏移量为4。另一方面,vbtable包含派生类中每个基类的偏移:
1 2 3 4 | ClassG::$vbtable@:
0 | - 4
1 | 4 (ClassGd(ClassG + 4 )ClasA)
2 | 16 (ClassGd (ClassG + 4 )ClassE)
|
为了确定class在ClassG中的真实偏移量,需要取vbtable的偏移量(4),然后是classE从vbtable中的偏移(16),加起来等于20(4 + 16)。
ClassG中classE的实际BaseClassDescriptor(基类描述符)如下:
1 2 3 4 5 6 7 8 9 | .rdata: 00418AFC RTTIBaseClassDescriptor@ 418afc ; DATA XREF:...
dd offset oop_re$ClassE$TypeDescriptor
.rdata: 00418B00 dd 0 ; numContainedBases
.rdata: 00418B04 dd 0 ; PMD.mdisp
.rdata: 00418B08 dd 4 ; PMD.pdisp
.rdata: 00418B0C dd 8 ; PMD.vdisp
.rdata: 00418B10 dd 50h ; attributes
.rdata: 00418B14 dd offset oop_re$ClassE$RTTIClassE$RTTIClassHierarchyDescriptor ;
pClassDescriptor
|
PMD.pdisp为4,即vbtable在ClassG中的偏移量,而PMD.vdisp为8,即vbtable中的第3个DWORD。
下图显示了总体RTTI数据结构是如何连接和布局的。
D.识别类之间的关系
1.通过构造函数分析类之间的关系
构造函数包含初始化对象的代码,例如调用基类的构造函数和设置虚函数表。因此,分析构造函数可以让我们很好地了解这个类与其他类的关系。
单继承
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | .text: 00401010 sub_401010 proc near
.text: 00401010
.text: 00401010 var_4 = dword ptr - 4
.text: 00401010
.text: 00401010 push ebp
.text: 00401011 mov ebp,esp
.text: 00401013 push ecx
.text: 00401014 mov [ebp + var_4],ecx ;get this ptr to current object
.text: 00401017 mov ecx,[ebp + var_4] ;call class A constructor
.text: 0040101A call sub_401000; call class A constructor
.text: 0040101F mov eax,[ebp + var_4]
.text: 00401022 mov esp,ebp
.text: 00401024 pop ebp
.text: 00401025 retn
.text: 00401025 sub_401010 endp
|
让我们假设我们已经使用II-B节中提到的方法确定this is function是一个构造函数。现在,我们看到一个函数正在使用当前对象的this指针调用该函数。这可以是当前类的成员函数或基类的构造函数。
我们怎么知道是哪一个?实际上,仅通过查看生成的代码并不能完全区分这两者。然而,在现实的应用程序中,很有可能在此之前将构造函数识别如(第II-B节),因此必须将这些信息关联起来,以获得更准确的识别。换句话说,如果在另一个构造函数中调用一个预先确定为构造函数的函数,则使用当前对象的this指针,它可能是基类的构造函数。
手动识别此函数需要检查对该函数的其他交叉引用,并查看该函数是否是二进制文件中其他地方调用的构造函数。我们将在本文后面讨论自动识别方法。
多继承
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | .text: 00401020 sub_401020 proc near
.text: 00401020
.text: 00401020 var_4 = dword ptr - 4
.text: 00401020
.text: 00401020 push ebp
.text: 00401021 mov ebp,esp
.text: 00401023 push ecx
.text: 00401024 mov [ebp + var_4],ecx
.text: 00401027 mov ecx,[ebp + var_4] ;ptr to base class A
.text: 0040102A call sub_401000; call class A constructor
.text: 0040102A
.text: 0040102F mov ecx,[ebp + var_4]
.text: 00401032 add ecx, 4 ;ptr to base class C
.text: 00401035 call sub_401010 ; call class C constructor
.text: 00401035
.text: 0040103A mov eax,[ebp + var_4]
.text: 0040103D mov esp,ebp
.text: 0040103F pop ebp
.text: 00401040 retn
.text: 00401040
.text: 00401040 sub_401020 endp
|
多重继承实际上比单一继承更容易。与单个继承示例一,调用的第一个函数可以是成员函数,也可以是基类构造函数。请注意,在反汇编中,在调用第二个函数之前,向this指针添加了4个字节。这表明正在初始化一个不同的基类。
下面是这个类的布局,帮助大家形象化。上面的反汇编属于ClassD的构造函数。ClassD派生自另外两个ClassA和C:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | class A size( 4 ):
+ - - -
0 | a1
+ - - -
class C size( 4 ):
+ - - -
0 | c1
+ - - -
class D size( 12 ):
+ - - -
| + - - - (base class A)
0 | | a1
| + - - -
| + - - - (base class C)
4 | | c1
| + - - -
8 | d1
+ - - -
|
2.通过RTTI的多态类关系
正如II-B部分所讨论的,运行时类型信息(RTTI)可以用来识别多态类的类关系,用于确定它的相关数据结构是RTTIClassHierarchyDescriptor.下面是RTTIClassHierarchyDescriptor的字段
RTTIClassHierarchyDescriptor包含一个名为pBaseclassArray的字段,它是RTTIBaseClassDescriptor(BCD)的数组。这些BCD最终将指向实际基类的TypeDescriptor。
请参考下列类的布局:
下面是与上述类布局相关的实际类声明。
1 2 3 | class ClassA {...}
class ClassB: public ClassA {...}
class ClassC: public ClassB {...}
|
下面是RTTIClassHierarchyDescriptor、RTTIBaseClassDescriptor和代表ClassC的TypeDescriptor之间的关系布局。
需要注意的是pBaseClassArray也指向非直接基类的BCD。在本例中,classA的BaseClassDescriptor。一个解决方案是解析classB的ClaassHierararchyDescriptor,并确定classA是否是ClassB的基类,如果是,那么ClassA不是ClassC的直接基类,并且可以进行适当的继承推断。
E.识别类成员
识别类成员是一个直接的过程,虽然缓慢而乏味。可以通过查找相对于this指针的偏移量来识别类成员变量:
1 2 3 4 | .text: 00401003 push ecx
.text: 00401004 mov [ebp + var_4],ecx ; ecx = this pointer
.text: 00401007 mov eax,[ebp + var_4]
.text: 0040100A mov dword ptr [eax + 8 ], 12345h ;write to 3rd member variable
|
可以通过查找指向对象虚函数表的偏移处的指针的间接调用来识别虚函数成员:
1 2 3 4 5 6 | .text: 00401C21 mov ecx,[ebp + var_1C] ; ecx = this pointer
.text: 00401C24 mov edx,[ecx] ; ecx = ptr to vftable
.text: 00401C26 mov ecx,[ebp + var_1C]
.text: 00401C29 mov eax,[edx + 4 ] ;eax = address of 2nd virtual
;function in vftable
.text: 00401C2C call eax ; call virtual function
|
可以通过检查this指针是否作为隐藏参数传递给函数调用来识别非虚成员函数:
1 2 3 | .text: 00401AFC push 0CCh
.text: 00401B01 lea ecx,[ebp + var_C] ; ecx = this pointer
.text: 00401B04 call sub_401110
|
为了确保这确实是一个成员函数,可以检查被调用的函数是否使用ecx而没有先初始化它。参考sub_401110的汇编:
1 2 3 4 | .text: 00401110 push ebp
.text: 00401111 mov ebp,esp
.text: 00401113 push ecx
.text: 00401114 mov [ebp + var_4],ecx ;ecx used
|
III.自动化
· 本节将讨论用于自动提取类信息的方法。为此,我们将讨论为执行该任务而创建的工具,并提供关于如何实现该工具的信息。
A.OOP_RE
OOP_RE是我们内部创建的用于自动化提取类信息的工具的名称。提取的信息包含识别的类(如果RTTI可用,则包括类名、类关系和类成员)。它还通过注释确定的C++相关结构来增强反汇编。OOP_RE是用python开发的,运行在IDAPython平台上。IDAPython允许我们快速有效地编写和调试OOP_RE。
B.Why a Static Approach?
我们必须做的第一个决定是,我们是否要开发一个工具来执行静态或动态分析。我们之所以选择静态方法,是因为在一些大量使用C++的平台(Symbian)上很难进行运行时分析——如果该工具将被更新以处理已编译的Symbian应用程序的话。然而,混合方法(静态加动态分析)也更可取,因为它可以产生更准确的结果。
C.Automated Analysis Strategies
1.通过RTTI识别多态类
该工具第一步是收集可用的RTTI信息。利用RTTI数据,工具可以快速准确地提取以下内容:
1)多态类
2)多态类名
3)多态类层次结构
4)多态类虚表和虚函数
5)多态类构造函数或析构函数
为了搜索与RTTI相关的结构,该工具首先尝试识别虚函数表,因为结构RTTICompleteobjectLocator就在这些虚函数表的下面。为了识别虚函数表,工具执行以下检查:
1)如果数据类型是DWORD
2)如果是指向Code的指针
3)如果被代码引用,并且引用代码中的指令是mov指令(建议建立一个虚表分配)
一旦vftable被识别,该工具将验证vftable下面的DWORD是否是一个实际的RTTICompleteobjectLocator。这可以通过解析RTTICompleteObject来验证。并验证RTTICompleteObjectLocator.pTypeDescriptor是否是一个有效的类型描述符。验证TypeDescriptor的一种方法是检查TypeDescriptor.name是否以字符串".?AV",它用作类名的前缀。
如下所示,vftable在0x004165B4处:
1 2 3 4 5 6 | .rdata: 004165B0 dd offset ClassB_RTTICompleteObjectLocator@ 00
.rdata: 004165B4 ClassB_vftable
.rdata: 004165B4 dd offset sub_401410 ; DATA XREF : sub_401280 + 38
.rdata: 004165B4 ; sub_401280 + 29
.rdata: 004165B8 dd offset sub_401460
.rdata: 004165BC dd offset sub_401230
|
然后,该工具将通过检查由RTTICompleteobjectLocator所指向的TypeDescriptor来确定004165B0是否是有效的RTTICompeleteObjectLocator。
1 2 3 4 5 6 | .rdata: 00418A28 ClassB_RTTTICompleteObjectLocator@ 00
.rdata: 00418A28 dd 0 ; signature
.rdata: 00418A2C dd 0 ; offset
.rdata: 00418A30 dd 0 ; cdoffset
.rdata: 00418A34 dd offset ClassB_TypeDescriptor
.rdata: 00418A38 dd offset ClassB_RTTIClassHierarchyDescriptor
|
通过检查TypeDescriptor.name的".?AV"来验证TypeDescriptor。
1 2 3 4 | .data: 0041B01C ClassB_TypeDescriptor
dd offset type_info_vftable
.data: 0041B020 dd 0 ; spare
.data: 0041B024 a_?avclassb@@ db '.?AVClassB@@' , 0 ; name
|
一旦验证了所有的RTTICompleteObjectLocator,该工具将解析所有RTTI相关的数据结构,并从标识的类型描述符创建类。下面是使用RTTI数据提取的列表信息:
1 2 3 4 5 6 7 8 9 10 | new_class
- Identified from TypeDescriptors
new_class.class_name
- Identified from TypeDescriptor.name
new_class.vftable / vfuncs
- Identified from vftable - RTTICompleteObjectLocator relationship
new_class.ctors_dtors
- Identified from functions referencing the vftable
new_class.base_classes
- Identified from RTTICompleteObjectLocator.pClassHierarchyDescriptor
|
2.通过vftables识别多态类(w/o RTTI)
如果RTTI数据不可用,该工具将尝试通过搜索虚函数表来识别多态类(方法在C.1节中描述)。一旦识别了vftable,就会提取/生成以下类信息:
1 2 3 4 5 6 7 8 | new_class
- Identified from vftable
new_class.class_name
- Auto - generated (based from vftable address,etc)
new_class.vftable / vfuncs
- Identified from vftable
new_class.ctors_dtors
- Identified from functions referencing the vftable
|
尚未识别基类,已识别类的基类将通过后面描述的构造函数分析来识别。
3.通过构造函数/析构函数搜索来识别类
从这里开始讨论的自动化技术要求我们能够跟踪寄存器和变量中的值。为此,需要一个像样的数据流分析器。正如大多数以前解决过这个问题的研究人员所证明的那样,数据流分析是一个困难的问题。幸运的是,不必讨论一般情况,可以使用一个简单的数据流分析器,它将在特定情况下工作。至少,我们的数据流分析器应该能够做寄存器和指针的跟踪。
我们的工具将从特定的起点跟踪寄存器或变量。后续指令将被跟踪并拆分为块。每个块将分配一个跟踪变量,该变量指示在该特定块中跟踪哪个寄存器/指针。在跟踪期间,可能发生以下情况之一:
1)如果变量/寄存器被覆盖,停止跟踪
2)如果Eax正在被跟踪,并且遇到了一个call,请停止跟踪。(我们假设所有调用都在eax中返回值)
3)如果遇到一个调用,将下一条指令视为一个新的代码块
4)如果遇到条件跳转,则遵循彼此分支中的寄存器/变量
5)如果寄存器/变量被复制到另一个变量中,则启动一个新代码块并跟踪从该代码块开始的旧变量和新变量
6)否则,跟踪下一条指令
为了识别动态分配的对象的构造函数,可以应用以下算法:
1)寻找对new()的调用
2)跟踪EAX中返回的值
3)跟踪完成后,查找被跟踪寄存器/变量为ECX的最早调用。将此函数标记为构造函数。
对于局部对象,我们做同样的事情。而不是最初跟踪new()的返回值,而是首先找到将栈变量的地址写入ECX的指令,然后开始追踪ECX。
有一种可能性是,识别的一些构造函数是重载的,实际上属于一个类。我们可以通过检查传递给new()的值来过滤非重载的构造函数。如果对象的大小是唯一的,则相应的构造函数不会重载。然后,可以通过检查它们的特征是否与其他类相同(例如具有相同的vftable)来确定是否重载了其余的构造函数。具有相同的成员函数等。
4.Class Relationship Inferencing
如第II-D节所述,类之间的关系可以通过分析构造函数来确定。可以通过在构造函数中跟踪当前对象的this指针(ECX)来自动进行构造函数分析。跟踪完成后,检查以ECX作为跟踪变量的块,并查看是否存在对已被识别为构造函数的函数调用。如果存在,则此构造函数可能是基类的构造函数。如果存在,则此构造函数可能是基类的构造函数。为了处理多重继承,我们的工具还应该能够跟踪指向相对于类的地址。然后我们将使用上述过程跟踪这些指针以识别其他基类。
5.识别类成员
识别成员变量
要识别成员变量,必须从对象初始化处开始跟踪this指针。然后,可以访问相对于this指针的偏移。这些偏移将被记录为可能的成员变量。
识别非虚函数
该工具将跟踪初始寄存器或指针,在我们的示例中,它应该指向当前类的this指针。
一旦跟踪完成,注意ECX是被跟踪变量的所有代码块。然后再将该代码块中标记调用(如果有)标记为当前类的成员。
识别虚函数
要识别虚函数,只需首先通过构造函数分析来定位vftable。
完成所有这些工作之后,将使用这些分析的结果重新构造这个类。
D.增强反汇编
1.重建和注释结构
一旦提取类信息,OOP_RE将使用dDword()、make_ascii_string()和set_name()重构、命名和注释C++相关的数据结构。
对于RTTI数据,OOP_RE适当改变了数据结构成员的数据类型,并添加了注释来阐明反汇编。
下面是一个vftable和RTTICompleteObjectLocator指针的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | Original
.rdata: 004165A0 dd offset unk_4189E0
.rdata: 004165A5 off_4165A4
dd offset sub_401170 ; DATA XREF:...
.rdata: 004165A8 dd offset sub_4011C0
.rdata: 004165AC dd offset sub_401230
.rdata: 004165B0 dd offset unk_418A28
Processed
.rdata: 004165A0 dd offset oop_re$ClassA$ClassA$RTTICompleteObjectLocator@ 00
.rdata: 004165A4 oop_re$ClassA$vftable@ 00
dd offset sub_401170 ; DATA XREF:...
.rdata: 004165A0 dd offset sub_4011C0
.rdata: 004165A0 dd offset sub_401230
.rdata: 004165A0 dd offset oop_re$ClassB$RTTICompleteObjectLocator@ 00
|
另一个例子是实际的RTTICompleteobjectLocator结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | Original
.rdata: 004189E0 dword_4189E0 dd 0 ; DATA XREF:...
.rdata: 004189E4 dd 0
.rdata: 004189E8 dd 0
.rdata: 004189EC dd offset off_41B004
.rdata: 004189F0 dd offset unk_4189F4
Processed
.rdata: 004189E0 oop_re$ClassA$ClassA$RTTICompleteObjectLocator@ 00
dd 0 ; RTTICompleteObjectLocator.signature
.rdata: 004189E4 dd 0 ; RTTICompleteObjectLocator.offset
.rdata: 004189E8 dd 0 ; RTTICompleteObjectLocator.cdOffset
.rdata: 004189EC dd offset oop_re$ClassA$TypeDescriptor
.rdata: 004189F0 dd offset oop_re$ClassA$RTTIClassHierarchyDescriptor
|
2.Improving the Call Graph
完成的分析结果可以应用回IDA反汇编,例如,通过在虚函数调用上添加交叉引用。这将产生更准确的调用图,这反过来会导致二进制比较结果的改进BinDiff和DarunGrim。定位vtables也可以用于二进制比较技术,如Rafal Wojtczuk的博客所述。
E.Visualization:UML Diagrams
最后,最酷的部分——为类成员和类层次生成UML类图。为此,我们使用了pydot。OOP_RE基本上为每个类创建一个节点,然后从每个基类创建边。
下面是一个用OOP_RE生成的UML图的示例:
这个UML图表示了下面的类声明:
1 2 3 4 | class ClassA {...}
class ClassB : public ClassA {...}
class ClassC {...}
class ClassD : public ClassB,public ClassC {...}
|
当然,也会出现RTTI不可用的情况;在这种情况下,类名是自动生成的:
这些UML图提供了类的高级概览,以及它们如何相互关联。这为反汇编器提供了有关应用程序如何按类进行结构的重要信息,反汇编器随后可以记住这个结构,同时进一步细化反汇编。
IV.总结
在本文中,我们讨论了如何分析和理解C++编译的二进制文件。具体地说,它讨论了如何提取类信息和类关系地方法。希望本文能对C++逆向开发地研究起到一定的借鉴作用。
V.参考文献
Reversing Microsoft Visual C++ Part II:Classes, Methods and RTTI
https://www,openrce.org/articles/full_view/23
Igor Skochinsky
RE 2006:New Challenges Need Changing Tools
Defcon 14 talk
Halvar Flake
Inside the C++ Object Model
Stanley B.Lippman
C++:Under the Hood - Jan Gray
http://msdn.microsoft.com/archive/default.asp?url=/archive/en-us/dnarvc/html/jangrayhood.asp
Microsoft C++ Name Manging Scheme
http://sparcs.kaist.ac.kr/~tokigun/article/vcmangle.html#Microsoft
Binary code analysis:benefits of C++ virtual function tables detection
Rafal Wojtczuk
http://www.avertlabs.com/research/blog/?p=17
X86_RE_lib
http://www.sabre-security.com/x86_RE_lib.zip
Halvar Flake
IDAPython
http://d-dome.net/idapython
pydot
http://dkbza.org/pydot.html
Graphviz-Graph Visualization Software
http://www.graphviz.org/
[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法