42 lines
3.5 KiB
Markdown
42 lines
3.5 KiB
Markdown
### C++ 程序怎么去逆向找虚表
|
||
|
||
**虚表(vtable)的编译后形态**
|
||
|
||
在 C++ 中,当一个类包含虚函数时,编译器会做两件事:
|
||
|
||
1. 为该类生成一个**虚表**。这个虚表本质上是一个**函数指针数组**。数组中的每个元素都指向该类中一个虚函数的实际地址
|
||
2. 为该类的每个对象(实例)在内存布局的开头添加一个隐藏的**虚表指针(vptr)**。这个指针指向该类的虚表
|
||
|
||
因此,逆向寻找虚表的过程,就是找到这个隐藏的 `vptr`,并顺藤摸瓜找到它指向的虚表
|
||
|
||
**逆向寻找虚表的三种主要方法**
|
||
|
||
**方法一:寻找虚表指针(vptr)的初始化**
|
||
|
||
这是最直接也最常用的方法。虚表指针通常在对象的构造函数中被初始化
|
||
|
||
1. **定位构造函数**:在 C++ 程序中,当你使用 `new` 关键字创建一个对象时,编译器会调用该类的构造函数。在反汇编代码中,你会看到对 `new` 操作的封装,然后是对构造函数的调用
|
||
2. **查找虚表指针的赋值**:在构造函数的开头,通常会有类似 `mov [ecx], offset class_vtable` 或 `mov [this], offset class_vtable` 的指令(取决于调用约定和寄存器)
|
||
- `this` 指针(通常在 `ecx` 或 `rcx` 寄存器中)指向新创建的对象
|
||
- `offset class_vtable` 是虚表的地址,这是一个常量,通常由链接器确定
|
||
- 这条指令的含义是:将虚表的地址存入对象的第一个成员变量中,这个变量就是 `vptr`
|
||
3. **识别虚表**:一旦你找到了虚表的地址,你就可以跳转到这个地址,IDA Pro 或 Ghidra 通常会将其识别为数据段中的一个指针数组
|
||
|
||
**方法二:从虚函数的调用处反推**
|
||
|
||
如果你无法直接找到构造函数,可以从虚函数的调用点入手。虚函数的调用通常是通过 `vptr` 进行的间接调用
|
||
|
||
1. **识别间接调用**:寻找类似 `call [eax+offset]` 或 `call [vptr]` 的指令
|
||
- `eax` 通常包含 `this` 指针,指向对象实例
|
||
- `offset` 是一个数字,通常是 `vptr` 在对象内存布局中的偏移量(在单继承情况下通常是 `0`)
|
||
- 这条指令的含义是:从 `eax` 指向的内存位置(即 `vptr`)获取一个地址,然后再加上一个偏移量,最终跳转到那个地址执行代码
|
||
2. **分析偏移量**:通过观察偏移量,你可以判断这是虚表中的第几个虚函数。例如,`call [eax+8]` 意味着调用虚表中的第二个函数(因为每个函数指针通常是 4 或 8 字节)
|
||
3. **反推虚表**:找到 `vptr` 的地址,然后跳转到该地址。你可以向上或向下遍历这个指针数组,来识别其他的虚函数
|
||
|
||
**方法三:利用 IDA Pro 的自动化识别功能**
|
||
|
||
IDA Pro 和 Ghidra 这样的高级反汇编器拥有强大的自动化分析能力,可以极大地简化寻找虚表的过程
|
||
|
||
1. **启用 C++ RTTI 分析**:在 IDA 的 `Options -> General -> IDA` 窗口中,确保 C++ 的**RTTI (Run-Time Type Information)** 分析选项已启用。这能帮助 IDA 识别类和虚表结构
|
||
2. **函数识别**:让 IDA 自动分析程序,它通常会尝试识别标准库中的虚表
|
||
3. **数据段搜索**:在 IDA 的数据段(通常是 `.data` 或 `.rdata`)中搜索,寻找**指针数组**。如果一个数组中的元素都是函数地址,并且这些函数之间有逻辑关联,那它很可能就是一个虚表。IDA 通常会把这些识别出来的虚表标记为 `vftable` 或类似的名字 |