IDA PRO 09 - 反汇编基础03

本文主要讨论C++逆向的一些基础知识:this指针,虚函数与虚表。

C++ 代码非常复杂,我们不讨论其语法。但是有一点需要特别记住,牢固掌握 C++ 语言的基础知识,对于你理解已编译的 C++ 代码将大有裨益。

this指针

所有非静态 C++ 成员函数都使用 this 指针。以下面的函数调用为例:

scss 复制代码
//object1, object2, and *p_obj are all the same type.
object1.member_func();
object2.member_func();
p_obj->member_func();

在3 次调用member_func 的过程中,this 分别接受了&object1 、&object2 和p_obj 这3 个值。我们最好是把this 看成是传递到所有非静态成员函数的第一个隐藏参数。

Microsoft Visual C++利用thiscall 调用约定,并将 this 传递到ECX 寄存器中。GNU g++ 编译器则把this 看做是非静态成员函数的第一个(最左边)参数,并在调用该函数之前将用于调用函数的对象的地址作为最后一项压入栈中。

从逆向工程的角度看,在调用函数之前,将一个地址转移到 ECX 寄存器中可能意味着两件事情。首先,该文件使用 Visual C++编译;其次,该函数是一个成员函数。如果同一个地址被传递给两个或更多函数,我们可以得到结论,这些函数全都属于同一个类层次结构。

在一个函数中,在初始化之前使用 ECX 意味着调用方必定已经初始化了ECX ,并且该函数可能是一个成员函数(虽然该函数可能只是使用了fastcall 调用约定)。另外,如果发现一个函数向其他函数传递 this指针,则这些函数可能和传递 this 的函数属于同一个类。

使用g++ 编译的代码较少调用成员函数。但是,如果一个函数没有把指针作为它的第一个参数,则它肯定不属于成员函数。

虚函数和虚表

虚函数的作用可以简单理解成Java中的多态,纯虚函数即为抽象方法。

编译器会为每一个包含虚函数的类(或通过继承得到的子类)生成一个表,其中包含指向类中每一个 虚函数的指针,这样的表就叫做虚表 (vtable)。

此外,每个包含虚函数的类都获得额外一个数据成员,用于在运行时指向适当的虚表。这个成员通常叫做虚表指针 (vtable pointer),并且是类中的第一个数据成员。

如果对象调用一个虚函数,则通过在该对象的虚表中进行查询来选择正确的函数。因此,虚表是在运行时解析虚函数调用的基本机制。

下面我们举例说明虚表的作用。以下面的 C++ 类定义为例:

csharp 复制代码
class BaseClass {
public:
 BaseClass();
 virtual void vfunc1() = 0;
 virtual void vfunc2();
 virtual void vfunc3();
 virtual void vfunc4();
private:
 int x;
 int y;
};

class SubClass : public BaseClass {
public:
 SubClass();
 virtual void vfunc1();
 virtual void vfunc3();
 virtual void vfunc5();
private:
 int z;
};

在这个例子中,SubClass 是BaseClass 的一个子类。BaseClass 由4 个虚函数组成,而SubClass 则包含5 个虚函数(BaseClass 中的4 个函数加上一个新函数 vfunc5 )。

在 BaseClass 中,vfunc1 声明使用了=0 ,说明 vfunc1 是一个纯虚函数 。纯虚函数在它们的声明类中没有实现,并且必须在一个子类中被重写。SubClass 提供了这样一个实现,因此可以创建SubClass 的对象。

初看起来,BaseClass 似乎包含2 个数据成员,而 SubClass 则包含3个成员。但是,我们前面提到,任何包含虚函数(无论是本身包含还是继承得来)的类也包含一个虚表指针。因此,BaseClass 类型的实例化对象实际上有 3 个数据成员,而 SubClass 类型的实例化对象则有 4 个数据成员,且它们的第一个数据成员都是虚表指针。 在类SubClass 中,虚表指针实际上由类BaseClass 继承得来,而不是专门为类 SubClass 引入。下图是一个简化后的内存布局,它动态分配了一个 SubClass 类型的对象。在创建对象的过程中,编译器确保新对象的虚表指针指向正确的虚表(本例中为类 SubClass 的虚表):

值得注意的是,SubClass 中包含两个指向属于BaseClass的函数(BaseClass::vfunc2 和BaseClass::vfunc4 )的指针。这是因为SubClass 并没有重写任何一个函数,而是由 BaseClass继承得到这些函数。图中还显示了纯虚函数的典型处理方法。由于没有针对纯虚函数BaseClass::vfunc1 的实现,因此,在BaseClass的虚表中并没有存储vfunc1 的地址。这时,编译器会插入一个错误处理函数的地址,通常,该函数名为 purecall 。理论上,这个函数绝不会被调用,但万一被调用,它会令程序终止。

使用虚表指针导致的一个后果是,在操纵 IDA 中的类时,你必须考虑到虚表指针。C++ 类是C 结构体的一种扩展。因此,我可以利用 IDA 的结构体定义来定义 C++ 类的布局。对于包含虚函数的类,你必须将一个虚表指针作为类中的第一个字段。在计算对象的总大小时,也必须考虑到虚表指针。这种情况在使用 new 操作符动态分配对象时最为明显,这时,传递给new 的大小值不仅包括类(以及任何超类)中的所有显式声明的字段占用的空间,而且包括虚表指针所需的任何空间。

下面的例子动态创建了 SubClass 的一个对象,它的地址保存在BaseClass 的一个指针中。然后,这个指针被传递给一个函数(call_vfunc ),它使用该指针来调用 vfunc3 :

scss 复制代码
void call_vfunc(BaseClass *b) {
 b->vfunc3();
}

int main() {
 BaseClass *bc = new SubClass();
 call_vfunc(bc);
}

由于 vfunc3 是一个虚函数,因此,在这个例子中,编译器必须确保调用SubClass::vfunc3 ,因为指针指向一个 SubClass 对象。下面call_vfunc 的反汇编版本说明了如何解析虚函数调用:

makefile 复制代码
.text:004010A0 call_vfunc proc near
.text:004010A0
.text:004010A0 b = dword ptr 8
.text:004010A0
.text:004010A0 push ebp
.text:004010A1 mov ebp, esp
.text:004010A3 mov eax, [ebp+b]
.text:004010A6 ➊ mov edx, [eax]
.text:004010A8 mov ecx, [ebp+b]
.text:004010AB ➋ mov eax, [edx+8]
.text:004010AE ➌ call eax
.text:004010B0 pop ebp
.text:004010B1 retn
.text:004010B1 call_vfunc endp

我们从函数栈可以知道,b是一个参数,拿到参数后读取里面的值,[]表示解析其内存引用,所以读取的是 b 指向的地址,就是读取了虚表指针的指向的地址,然后使用 [] 再读取其指向的地址,就是具体的虚表项了,+8 表示读取的是第3项。

在➊处,虚表指针从结构体中读取出来,保存在 EDX寄存器中。由于参数b 指向一个SubClass 对象,这里也将是 SubClass 的虚表的地址。

在➋处,虚表被编入索引,将第三个指针(在本例中为SubClass::vfunc3 的地址)读入 EAX寄存器。这里每个虚表指针占据4个字节。

最后,在➌处调用虚函数。值得注意的是,➋处的虚表索引操作非常类似于结构体引用操作。实际 上,它们之间并无区别。因此,我们可以定义一个结构体来表示一个类的虚表的布局,然后利用这个已定义的结构体来提高反汇编代码清单的可读性,如下所示:

bash 复制代码
00000000 SubClass_vtable struc ; (sizeof=0x14)
00000000 vfunc1 dd ?
00000004 vfunc2 dd ?
00000008 vfunc3 dd ?
0000000C vfunc4 dd ?
00000010 vfunc5 dd ?
00000014 SubClass_vtable ends

这个结构体将虚表引用操作重新格式化成以下形式:

arduino 复制代码
.text:004010AB mov eax, [edx+SubClass_vtable.vfunc3]
相关推荐
M_emory_24 分钟前
解决 git clone 出现:Failed to connect to 127.0.0.1 port 1080: Connection refused 错误
前端·vue.js·git
Ciito27 分钟前
vue项目使用eslint+prettier管理项目格式化
前端·javascript·vue.js
成都被卷死的程序员1 小时前
响应式网页设计--html
前端·html
mon_star°1 小时前
将答题成绩排行榜数据通过前端生成excel的方式实现导出下载功能
前端·excel
Zrf21913184551 小时前
前端笔试中oj算法题的解法模版
前端·readline·oj算法
文军的烹饪实验室3 小时前
ValueError: Circular reference detected
开发语言·前端·javascript
Martin -Tang4 小时前
vite和webpack的区别
前端·webpack·node.js·vite
迷途小码农零零发4 小时前
解锁微前端的优秀库
前端
王解4 小时前
webpack loader全解析,从入门到精通(10)
前端·webpack·node.js
我不当帕鲁谁当帕鲁5 小时前
arcgis for js实现FeatureLayer图层弹窗展示所有field字段
前端·javascript·arcgis