1、课程目标
- 掌握C++对象的内存布局
- 理解虚函数表的实现机制
- 学会识别成员函数调用
- 掌握对象布局的还原方法
2、名词解释
| 术语 |
英文 |
说明 |
| this指针 |
this Pointer |
指向当前对象的指针 |
| 虚函数表 |
Virtual Table (vtable) |
存储虚函数地址的表 |
| 虚表指针 |
vptr |
指向虚函数表的指针 |
| thiscall |
This Call |
C++成员函数调用约定 |
| RTTI |
Runtime Type Information |
运行时类型信息 |
3、使用工具
| 工具 |
用途 |
| IDA Pro |
识别类和虚函数表 |
| Class Informer |
IDA插件,分析C++类 |
4、技术原理
4.1、对象内存布局
无虚函数的类:
+0x00 成员变量1
+0x04 成员变量2
...
有虚函数的类:
+0x00 vptr (虚表指针)
+0x04 成员变量1
+0x08 成员变量2
...
4.2、虚函数表结构
vtable:
+0x00 &虚函数1
+0x04 &虚函数2
+0x08 &虚函数3
...
5、代码实现
5.1、示例1:简单类的汇编特征
class Point {
public:
int x, y;
void Set(int _x, int _y) {
x = _x;
y = _y;
}
int GetX() { return x; }
};
// Point::Set 反汇编:
// mov eax, ecx ; this在ECX
// mov edx, [esp+4] ; _x
// mov [eax], edx ; this->x = _x
// mov edx, [esp+8] ; _y
// mov [eax+4], edx ; this->y = _y
// ret 8
// Point::GetX 反汇编:
// mov eax, [ecx] ; return this->x
// ret
5.2、示例2:虚函数调用
class Animal {
public:
virtual void Speak() = 0;
virtual void Move() = 0;
};
class Dog : public Animal {
public:
void Speak() override { /* 汪汪 */ }
void Move() override { /* 跑 */ }
};
void CallSpeak(Animal* p) {
p->Speak(); // 虚函数调用
}
// CallSpeak 反汇编:
// mov eax, [ebp+8] ; p
// mov edx, [eax] ; vptr = *p
// mov ecx, eax ; this = p
// call dword ptr [edx] ; call vptr[0] (Speak)
// ret
// 虚函数调用特征:
// 1. 先读取对象第一个DWORD (vptr)
// 2. 再通过vptr间接调用
// 3. call [reg] 或 call [reg+offset]
5.3、示例3:识别虚函数表
class Base {
public:
int value;
virtual void Func1() { }
virtual void Func2() { }
virtual void Func3() { }
};
// 对象布局:
// +0x00 vptr -> [&Func1, &Func2, &Func3]
// +0x04 value
// 在IDA中识别vtable:
// 1. 找到.rdata段中的地址数组
// 2. 查看交叉引用,确认是否被构造函数引用
// 3. vtable之前可能有RTTI信息
/*
vtable_Base:
dd offset Base::Func1
dd offset Base::Func2
dd offset Base::Func3
*/
5.4、示例4:thiscall识别
class Calculator {
public:
int result;
int Add(int a, int b) {
result = a + b;
return result;
}
};
// thiscall特征:
// 1. ECX = this指针
// 2. 其他参数通过栈传递
// 3. 被调用者清栈 (ret N)
// Calculator::Add 反汇编:
// push ebp
// mov ebp, esp
// mov eax, [ebp+8] ; a
// add eax, [ebp+12] ; a + b
// mov [ecx], eax ; this->result = a + b
// pop ebp
// ret 8 ; 清理8字节参数
5.5、示例5:类继承的布局
class Base {
public:
int a; // +0x04
virtual void F() {}
};
class Derived : public Base {
public:
int b; // +0x08
void F() override {}
};
// Base布局:
// +0x00 vptr
// +0x04 a
// Derived布局:
// +0x00 vptr (Derived的vtable)
// +0x04 a (继承自Base)
// +0x08 b (Derived自己的)
// Derived::F 的vptr指向Derived的vtable
// 覆写后的函数地址不Base不同
5.6、示例6:还原类结构
/*
从反汇编还原类结构步骤:
1. 找到构造函数:
- 分配内存后的初始化
- 写入vptr
2. 分析成员访问:
- [ecx+offset]的访问模式
- 确定各成员偏移和类型
3. 分析vtable:
- 找到vtable地址
- 确定虚函数数量
4. 分析成员函数:
- thiscall调用约定
- this->member访问
示例还原:
反汇编看到:
mov dword ptr [ecx], offset vtable_xxx
mov dword ptr [ecx+4], 0
mov dword ptr [ecx+8], 0
可还原为:
class Unknown {
virtual void* vptr; // +0
int member1; // +4
int member2; // +8
};
*/
6、课后作业
- 基础练习:编写一个简单类,观察编译结果
- 虚函数分析:分析有虚函数的类的vtable
- 继承分析:分析继承关系的对象布局
- 实战还原:从反汇编还原一个完整的类定义