c++逆向中类的识别(Reversing C++ 读书笔记)

3 篇文章 0 订阅

paper is here
文章全文讲解的都是MSVC编译器编译出的cpp函数,不同编译器的一些实现细节是不同的。
由于文章中样例都是MSVC编译的32位程序,而我自己写的都是64位的所以一些结构体的size可能有小的不同。

前置知识

一个类的对象的布局

成员变量按照声明的顺序放置在内存中,但是如果该类中含有虚函数,那么会有一个指向虚表的指针在成员变量的第一个。

class Ex2
{
	int var1;
	public:
	virtual int get_sum(int x, int y);
	virtual void reset_values();
};
/* 对象中的布局如下
class Ex2 size(8):
	+---
	0 | {vfptr}  指向虚表
	4 | var1
	+---

其中虚表布局为
Ex2::$vftable@:
	0 | &Ex2::get_sum
	4 | &Ex2::reset_values
*/

现在如果一个类继承自另一个类怎么办? 当一个类从一个类继承时会发生以下情况,即单一继承:

class Ex3: public Ex2
{
	int var1;
	public:
	void get_values();
};
/*对象中的布局如下
class Ex3 size(12):
	+---
		| +--- (base class Ex2)
			0 | | {vfptr} 
			4 | | var1
		| +---
		8 | var1
	+---
*/

可以看到派生类的布局只是简单地附加到基类的布局中。 而在多重继承的情况下,会发生以下情况:

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 | | {vfptr}
		4 | | var1
	| +---
	| +--- (base class Ex4)
		8 | | {vfptr}
		12 | | var1
		16 | | var2
	| +---
	20 | var1
+---


Ex5::$vftable@Ex2@:
0 | &Ex2::get_sum
1 | &Ex2::reset_values
2 | &Ex5::v_ex5   // 当前对象的虚函数将附加在第一个基类的虚函数列表的末尾


Ex5::$vftable@Ex4@:
	| -8
0   | &Ex5::func1
1   | &Ex4::func
*/

我们来写个代码放到IDA里看一下:

#include <iostream>
using namespace std;
class Ex2
{
	int var1;
	public:
	virtual int get_sum(int x, int y){return x+y;};
	virtual void reset_values(){printf("Ex2:reset_values\n");};
};
class Ex4
{
	int var1;
	int var2;
	public:
	virtual void func1(){printf("Ex4:func1\n");};
	virtual void func2(){printf("Ex4:func2\n");};
};
class Ex5: public Ex2, public Ex4
{
	int var1;
	public:
	void func1(){
		printf("inhert from Ex4:func1\n");
	}
	virtual void v_ex5(){
		printf(" Ex5:v_ex5\n");
	}
}a;


int main(){
	a.func1();
	a.func2();
	a.v_ex5();
}

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
我们关注这几个构造函数,Ex2和Ex4分别构造了两个虚表赋值给了Ex5的this和this+16的位置,第二个虚表位置为什么是this+16? 这是成员变量是4字节,vptr是8字节(64bits),而结构体又要8字节对齐,因此下一个虚表位置就是12向上取整得到的16了。
回到原来的话题,此时Ex5只得到了两个父类的虚表,但还没有进行自己实现函数的重载,我们看到下面两个偏移off_409970和off_4099A0, 这一部分就是Ex5对于虚表进行修改以后的再次赋值。细心的读者或许发现了off_409970和off_4099A0位置-8处有一个奇怪的指针,这是RTTI技术中使用的一个指针,将在下文中讲到.
在这里插入图片描述
这里我们发现和paper里的说法有一些小不同(不知道是不是因为g++的实现有了什么变化,毕竟也是一篇有点久远的paper了),Ex5中的第一个虚表里除了继承自Ex2的部分以外,所有自己定义的函数(包括重载Ex4的Ex5::func1)也会append到这里。 而第二张继承自Ex4的虚表里的func1函数里其实只有一个跳转指令。
MinGW的实现

补: 我知道为什么我的二进制文件的相关实现和paper里不一样了. 我自己编译用的是Windows下的MinGW, 但是paper里的样例全都是用的Microsoft VisualC编译器. 因此实现细节上会有小的不同, 不过大的理念上都差不多. 下图是VSC的编译实现, 可以看到这里的定义就是新的虚函数append到第一张虚表.
VSC编译的实现
关于虚继承的一些知识还可以看:https://blog.csdn.net/weixin_43891775/article/details/112003915

Identifying C++ Binaries and Constructs

  1. 大量使用ecx 作为this 指针进行函数调用(不过g++里并不是这么调用的,ecx作为调用其实也暗示了编译器是VSC)

    .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
    
  2. 调用约定。 与上一点还挺相关的,使用堆栈中的常用函数参数和指向类的对象(即 this 指针)的 ecx 调用类成员函数。
    下面是一个类构造函数的调用样例

    .text:00401994 push 0Ch
    .text:00401996 call ??2@YAPAXI@Z ; operator new(uint)
    .text:004019AB mov ecx, eax
    :::
    .text:004019AD call ClassA_ctor
    
  3. STL 代码和导入进来的DLL库
    这两个一出现就知道一定是cpp的二进制文件了
    在这里插入图片描述

Identifying Classes

这一部分讨论的方法仅尝试确定类是什么(即目标具有 ClassA、ClassB、ClassC 等)。 本文的下一部分将讨论如何推断这些类之间的关系并确定它们的成员.

找到构造和析构函数

  1. 全局对象
    可以在.init 段找构造, 或者在main里看到有谁传this指针时候用的是全局变量
  2. 局部对象
    对象会被构造在栈上, 并且在函数退出时会有相应的传入this指针的析构函数执行
  3. allocated对象
    过于明显,有new就有构造函数,有delete就有析构函数

通过RTTI对多态(Polymorphic)类的识别

识别类,特别是多态类(具有成员虚函数的类)的另一种方法是通过运行时类型信息 (RTTI)。 RTTI 是一种机制,其中的类型对象可以在运行时确定。 该机制是 typeid 和 dynamic_cast 运算符所使用的机制。 这两个运算符都需要有关传递给它们的类的信息,例如类名和类层次结构。 事实上,如果在未启用 RTTI 的情况下使用这些运算符,编译器会显示警告。
As a side note, 有一个编译器开关使 MSVC 编译器能够生成类布局,开关是 -d1reportAllClassLayout 此开关生成一个 .layout 文件,其中包含有关类布局的大量信息,包括基类的偏移量 派生类内部、虚函数表(vftable)、虚基类表(vbtables,下文详述)、成员变量等(前置知识中代码下面的布局注释就是来自这个.layout文件)。
为了实现RTTI 的功能,编译器在编译后的代码中存储了几个数据结构,这些数据结构包含有关类(特别是多态类)的信息代码。 这些数据结构如下:

RTTICompleteObjectLocator

此结构包含指向两个结构的指针,这些结构标识 (1) 实际的类信息和 (2) 类层次结构:

OffsetTypeNameDescription
0x00DWsignatureAlways 0?
0x04DWoffsetOffset of vftable within the class
0x08DWcdOffset
0x0CDWpTypeDescriptorClass Information
0x10DWpClassHierarchyDescriptorClass Hierarchy Information

下面是 RTTICompleteObjectLocator 指针的布局示例。 指向这个数据结构的指针就在类的 vftable 下方:

.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结构体的样例:

.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 中的第 4 个 DWORD 字段指向,它包含类名,如果获得该类名,将使逆向者大致了解该类应该做什么.

OffsetTypeNameDescription
0x00DWpVFTableAlways point to type_info’s vftable
0x04DWspare?
0x08SZnameClass name

以及ida中的样子:

.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数组(稍后讨论),RTTIBaseClassDescriptor最终将指向基类的 TypeDescriptor。

OffsetTypeNameDescription
0x00DWsignatureAlways 0?
0x04DWattributesBit 0 – multiple inheritance
Bit 1 – virtual inheritance
0x08DWnumBaseClassesNumber of base classes Count
includes the classitself
0x0cDWpBaseClassArrayArray of RTTIBaseClassDescriptor
includes the classitself

作为例子,我们可以看一个虚继承的案例: ClassG 从 ClassA和ClassE两个类中虚继承。

class ClassA {}
class ClassE {}
class ClassG: public virtual ClassA, public virtual ClassE {}

在ida中可以看到ClassG的RTTIClassHierarchyDescriptor:

.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:004178DC dd offset RTTIBaseClassDescriptor@417904
.rdata:004178E0 dd offset RTTIBaseClassDescriptor@417920

可以看到ClassG的基类有3个(包括ClassG本身),attribute为3(既是多重又是虚继承),最后pBaseClassArray指向一个指向RTTIBaseClassDescriptors的指针数组。

RTTIBaseClassDescriptor

那接下来就要讲讲这个RTTIBaseClassDescriptor结构。此结构包含有关基类的信息,其中包括指向基类的 TypeDescriptor 和 RTTIClassHierarchyDescriptor 的指针,另外还包含 PDM 结构。PDM 结构包含有关基类在类内部如何布局的信息。

OffsetTypeNameDescription
0x00DWpTypeDescriptorTypeDescriptor of this base class
0x04DWnumContainedBasesNumber of direct bases of this base class
0x08DWPMD.mdispvftable offset (if PMD.pdisp is -1)
0x0cDWPMD.pdispvbtable offset (-1: if don’t have vbtable)
0x10DWPMD.vdispDisplacement of the base class vftable pointer inside the vbtable
0x14DWattributes?
0x18DWpClassDescriptorRTTIClassHierarchyDescriptor of this base class

多重虚继承会生成一个vbtable(虚基类表)。 因为有时程序需要将一个子类转换为基类,所以需要确定基类vftable的确切位置。 vbtable 包含每个基类的 vftable 的偏移,这个偏移实际上就是派生类中基类的开始地址,换言之,基类的第一个成员一定是vftable。
还是拿刚才的ClassG作为例子,编译器最终会生成下列的成员布局:

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
+---

其中vbptr在classG的offset为4,它指向的vbtable表如下:

ClassG::$vbtable@:
0 | -4
1 | 4 (ClassGd(ClassG+4)ClassA)
2 | 16 (ClassGd(ClassG+4)ClassE)

要确定 ClassE 在 ClassG 中的确切偏移量,需要获取 vbtable 的偏移量(4),然后 ClassE 从 vbtable 的位移(16),他们相加等于 20(4 + 16),这就是ClassG里的偏移了。

ClassG 中的基类ClassE 的实际 BaseClassDescriptor 如下:

.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$RTTIClassHierarchyDescriptor ;

PMD.mdisp由于PMD.pdisp的值是-1, 因此并没有用,PMD.pdisp 为 4,即 vbptr 在 ClassG 中的偏移量,而 PMD.vdisp 为 8,表示 vbtable 中的第 3 个 DWORD。
如果当前的这个类并不是虚继承,即类中并没有vbtable时,PMD.pdisp和PMD.vdisp事实上没有什么用,而PMD.mdisp表示了当前类中vfptr的偏移.
下图显示了整个 RTTI 数据结构是如何连接和布局的。
在这里插入图片描述

Identifying Class Relationship

Class Relationship via Constructor Analysis

构造函数包含初始化对象的代码,例如调用基类的构造函数和设置 vftable。 因此,分析构造函数可以让我们很好地了解这个类与其他类的关系。

Singer Inheritance
.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] ;
.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

假设我们已经确定sub_401000 是构造函数。现在,我们看到sub_401010 正在使用当前对象的 this 指针调用一个函数。这可以是当前类的成员函数,也可以是当前类的基类的构造函数。
我们怎么知道是哪一个?实际上,仅仅通过查看生成的代码是无法完美区分两者的。然而,在现实世界的应用程序中,构造函数很可能会在进入到sub_401010 函数之前就被识别,因此我们所要做的就是关联所有信息以提出更准确的识别。换句话说,如果一个预先确定为构造函数的函数在另一个构造函数中使用当前对象的 this 指针调用,它可能是基类的构造函数。

Multiple Inheritance
.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 个字节偏移(添加的偏移大小会受上一个初始化完成的基类大小影响)。 这表明正在初始化一个不同的基类。
下图是类的布局, 上面的反汇编属于D类的构造函数。D类是从另外两个类A和C派生的:

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
+---
Polymorphic Class Relationship via RTTI

运行时类型信息(RTTI)可以用来识别多态类的类关系,用来确定这一点的相关数据结构是RTTIClassHierarchyDescriptor。为了减少大家鼠标滑动的次数,我再抄一遍 RTTIClassHierarchyDescriptor 的字段:

OffsetTypeNameDescription
0x00DWsignatureAlways 0?
0x04DWattributesBit 0 – multiple inheritance
Bit 1 – virtual inheritance
0x08DWnumBaseClassesNumber of base classes Count
includes the classitself
0x0cDWpBaseClassArrayArray of RTTIBaseClassDescriptor
includes the classitself

RTTIClassHierarchyDescriptor 包含一个名为 pBaseClassArray 的字段,它指向一个 RTTIBaseClassDescriptor (BCD) 数组。 这些 BCD 最终将指向实际基类的 TypeDescriptor。

我们以如下所示的继承关系为例:

class ClassA {}
class ClassB : public ClassA {}
class ClassC : public ClassB {}

下图是表示 ClassC 中 RTTIClassHierarchyDescriptor、RTTIBaseClassDescriptor 和 TypeDescriptor 的关系之间的布局。
在这里插入图片描述
一个值得注意的点是 pBaseClassArray 也指向非直接继承的基类的 BCD(ClassA)。 那么我们要怎么在二进制文件中推断出ClassC究竟是继承自谁呢? 对此的一种解决方案是也解析 ClassB 的 ClassHierarchyDescriptor 并通过其中的numContainedBases等字段确定 ClassA 是否是 ClassB 的基类,如果是,则 ClassA 不是 ClassC 的直接基类,这样就可以推断出正确的继承关系。

Identifying Class Members

识别类成员是一个直截了当但是缓慢而乏味的过程。 我们可以通过查找对相对于 this 指针的偏移量的访问来识别类成员变量。
我们还可以通过查找相对于该对象虚函数表的偏移量的指针的间接调用来识别虚函数成员:

.text:00401C21 mov ecx, [ebp+var_1C] ; ecx = this pointer
.text:00401C24 mov edx, [ecx] ; edx = 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 指针是否作为隐藏参数传递给函数调用来识别非虚拟成员函数。

.text:00401AFC push 0CCh
.text:00401B01 lea ecx, [ebp+var_C] ; ecx = this pointer
.text:00401B04 call sub_401110

为了确保这确实是一个成员函数,我们可以检查被调用函数是否使用了 ecx 而无需先初始化它。 我们看一下sub_401110的代码

.text:00401110 push ebp
.text:00401111 mov ebp, esp
.text:00401113 push ecx
.text:00401114 mov [ebp+var_4], ecx ; ecx used
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值