14. C++逆向基础:this指针、虚函数表、构造与析构、继承与多态的识别

各位好,我是老周。今天咱们聊聊C++逆向里最核心的几个概念。说实话,很多新手一看到C++逆向就头大,觉得比C语言复杂太多。其实不然,你只要抓住了几个关键点——this指针怎么传、虚函数表怎么布局、构造析构的顺序、继承多态在内存里长什么样——那C++逆向就跟看自家后院一样清楚。

我个人习惯把C++逆向比作「拆积木」。你看到的每个对象,在内存里都是一块块积木拼起来的。咱们今天就把这些积木一块块拆开看。

14.1 this指针:对象自己的「身份证」

先说说this指针。这东西说白了就是每个对象自己的地址。在C++里,每个成员函数调用时,编译器都会偷偷把对象的地址塞进去。怎么塞的?不同平台不一样。

x86平台(32位):this指针通常通过ecx寄存器传递。你反汇编看到函数开头有mov ecx, [esp+4]或者lea ecx, [ebp-8],那八成就是this指针。

x64平台(64位):this指针通过rcx寄存器传递。因为x64调用约定前四个参数都用寄存器传,this指针就占了第一个位置。

ARM平台:this指针通过r0寄存器传递。ARM的调用约定里,第一个参数就是r0。

关键识别点

  • 函数内部频繁使用ecx/rcx/r0做基址偏移访问
  • 函数开头没有明显的参数压栈(因为this是寄存器传的)
  • 看到mov eax, [ecx+offset]这种模式,基本就是访问成员变量

我在项目中遇到过这么个坑:有一次逆向一个游戏外挂,发现某个函数明明有5个参数,但反汇编里只看到4个压栈。我琢磨了半天,后来才意识到第一个参数是通过ecx传的this指针。嗯,这就是典型的C++成员函数特征。

14.2 虚函数表(vtable):多态的「调度中心」

虚函数表,简称vtable,是C++实现多态的核心机制。你想想看,如果基类指针调用一个虚函数,编译器怎么知道该调用哪个版本的函数?答案就是vtable。

vtable的内存布局

  • 每个包含虚函数的类,编译器会生成一个vtable
  • vtable是一个函数指针数组,按声明顺序排列
  • 每个对象的前4/8字节(32/64位)是一个指向vtable的指针,叫vptr
  • vtable的索引0通常不是第一个虚函数,而是typeinfo指针(用于RTTI)

看个例子,假设有这样一个类:

class Base {
public:
    virtual void func1() { ... }
    virtual void func2() { ... }
    virtual void func3() { ... }
};

在内存里,vtable长这样:

vtable_Base:
    [0] typeinfo指针(指向Base的typeinfo)
    [1] &Base::func1
    [2] &Base::func2
    [3] &Base::func3

对象的内存布局:

对象地址: [vptr] -> vtable_Base
           [成员变量1]
           [成员变量2]
           ...

逆向识别技巧

  • 看到函数开头有mov eax, [ecx]然后call [eax+offset],这就是典型的虚函数调用
  • vtable通常位于只读数据段(.rdata),里面全是函数地址
  • 构造函数里一定有mov [ecx], offset vtable_XXX这样的代码,这是设置vptr

我曾经逆向过一个大型MMO客户端,里面有个类有30多个虚函数。一开始我手动一个个数,后来发现IDA Pro的「Class Browser」插件可以直接解析vtable。嗯,工具要用好,能省不少力气。

14.3 构造函数与析构函数:对象的「生与死」

构造函数和析构函数,说白了就是对象的初始化和清理工作。逆向时识别它们有几个关键特征。

构造函数特征

  • 函数名通常被编译器修饰为??0ClassName@@QAE@XZ(MSVC)或_ZN8ClassNameC1Ev(GCC)
  • 函数内部一定会设置vptr:mov [ecx], offset vtable_XXX
  • 会初始化所有成员变量(至少是那些有构造函数的成员)
  • 如果有基类,会先调用基类构造函数

析构函数特征

  • 函数名修饰为??1ClassName@@QAE@XZ(MSVC)或_ZN8ClassNameD1Ev(GCC)
  • 函数内部会重置vptr(指向当前类的vtable,防止析构时调用到派生类的虚函数)
  • 会释放资源(delete、free等)
  • 如果有基类,析构函数执行完后会调用基类析构函数

注意:MSVC编译器会生成多个版本的构造函数和析构函数:

  • ??0ClassName@@QAE@XZ:普通构造函数
  • ??0ClassName@@QAE@ABV0@@Z:拷贝构造函数
  • ??1ClassName@@QAE@XZ:普通析构函数
  • ??_GClassName@@UAEPAXI@Z:删除析构函数(用于delete操作)

我记得有一次分析一个恶意软件,它用C++写的,但把所有符号都strip掉了。我通过查找vptr的设置点,硬是定位到了它的构造函数,然后顺着构造函数找到了所有成员变量的初始化逻辑。这招在恶意软件分析里特别管用。

14.4 继承与多态的识别:看vtable怎么「串」起来的

继承和多态,是C++逆向里最精彩的部分。你想想看,一个派生类对象,它的vtable里既有自己的虚函数,也有从基类继承来的虚函数。怎么区分?看vtable的布局。

单继承

  • 派生类对象只有一个vptr
  • vtable里先放基类的虚函数,再放派生类新增的虚函数
  • 如果派生类重写了基类的虚函数,vtable里对应位置会被替换成派生类的函数地址

多继承

  • 派生类对象有多个vptr(每个基类一个)
  • 每个vptr指向不同的vtable
  • 派生类自己的虚函数通常放在第一个基类的vtable末尾
  • 调用第二个基类的虚函数时,需要调整this指针(thunk函数)

看个多继承的例子:

class Base1 {
public:
    virtual void f1() { ... }
    virtual void f2() { ... }
};

class Base2 {
public:
    virtual void g1() { ... }
    virtual void g2() { ... }
};

class Derived : public Base1, public Base2 {
public:
    virtual void f1() override { ... }  // 重写Base1的f1
    virtual void h1() { ... }           // 新增虚函数
};

内存布局:

Derived对象:
    [vptr1] -> vtable_Derived_Base1
    [Base1成员变量]
    [vptr2] -> vtable_Derived_Base2
    [Base2成员变量]
    [Derived新增成员变量]

vtable_Derived_Base1:
    [0] typeinfo指针
    [1] &Derived::f1        // 重写了Base1的f1
    [2] &Base1::f2          // 继承自Base1
    [3] &Derived::h1        // 新增虚函数

vtable_Derived_Base2:
    [0] typeinfo指针
    [1] &thunk: Derived::g1  // 需要调整this指针
    [2] &thunk: Derived::g2  // 需要调整this指针

多继承的逆向识别要点

  • 看到对象里有多个vptr(多个mov [ecx+offset], offset vtable
  • 看到thunk函数(一小段代码,作用是调整ecx/rcx的值,然后跳转到真正的函数)
  • thunk函数通常很短,就几条指令:sub ecx, offset; jmp real_function

我曾经在逆向一个商业软件时,遇到了一个极其复杂的多继承体系。那个类继承了6个基类,每个基类都有虚函数。我花了整整两天才理清楚它的vtable布局。后来我写了个IDA脚本,自动解析vtable并标注每个函数的作用。嗯,从那以后我就养成了「能自动化就自动化」的习惯。

14.5 实战技巧:快速识别C++对象的步骤

好了,理论讲完了,咱们来点实战的。如果你拿到一个二进制文件,怎么快速识别C++对象?我一般按这个步骤来:

  1. 找构造函数:搜索mov [ecx], offset vtable这种模式,找到所有vptr的设置点
  2. 分析vtable:跳转到vtable所在地址,把里面的函数指针都提取出来
  3. 确定继承关系:看vtable里有没有重复的函数地址,或者有没有thunk函数
  4. 识别成员变量:通过构造函数里对[ecx+offset]的访问,推断成员变量的偏移和类型
  5. 验证虚函数调用:找到调用虚函数的地方,看call [eax+offset]的偏移是否匹配vtable里的索引

工具推荐

  • IDA Pro + Hex-Rays:自动识别vtable和虚函数调用
  • Ghidra:开源,支持C++类分析
  • WinDbg:调试时查看vtable内容
  • 我自己写了个IDA脚本叫「vtable_analyzer.py」,可以自动标注vtable里的每个函数

最后说一句,C++逆向其实没那么可怕。你只要理解了this指针怎么传、vtable怎么布局、构造析构的顺序、继承多态在内存里的表现,那剩下的就是熟练度的问题了。多练,多看,慢慢就上手了。

C++对象逆向核心知识体系 C++对象逆向 this指针 x86: ecx寄存器 x64: rcx寄存器 ARM: r0寄存器 虚函数表vtable 函数指针数组 索引0: typeinfo 位于.rdata段 构造/析构函数 构造函数: 设置vptr 析构函数: 重置vptr MSVC有多个版本 继承与多态 单继承: 一个vptr 多继承: 多个vptr thunk函数调整this 逆向识别流程 ① 搜索 vptr 设置点 → 定位构造函数 ② 提取 vtable 函数指针 → 分析虚函数 ③ 检查 thunk 函数 → 判断多继承 ④ 分析成员变量偏移 → 还原类结构

公众号:蓝海资料掘金营,微信deep3321