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]
相关推荐
再吃一根胡萝卜2 分钟前
🔍 当 `<a-menu>` 遇上 `<template>`:一个容易忽视的菜单渲染陷阱
前端
Asort18 分钟前
JavaScript 从零开始(六):控制流语句详解——让代码拥有决策与重复能力
前端·javascript
无双_Joney37 分钟前
[更新迭代 - 1] Nestjs 在24年底更新了啥?(功能篇)
前端·后端·nestjs
在云端易逍遥39 分钟前
前端必学的 CSS Grid 布局体系
前端·css
ccnocare40 分钟前
选择文件夹路径
前端
艾小码40 分钟前
还在被超长列表卡到崩溃?3招搞定虚拟滚动,性能直接起飞!
前端·javascript·react.js
闰五月41 分钟前
JavaScript作用域与作用域链详解
前端·面试
泉城老铁1 小时前
idea 优化卡顿
前端·后端·敏捷开发
前端康师傅1 小时前
JavaScript 作用域常见问题及解决方案
前端·javascript