分析 C++ 程序时,要是能从一个二进制文件中识别出 C++ 程序的结构,并且能标识出一些主要的类和它们的关系是非常有帮助的。而其中为了实现运行时类型识别而生成的运行时类型信息(Runtime type information,RTTI)也能在程序分析上提供方便。

注意,文中讨论的 C++ 可执行文件仅限于使用 MSVC 编译器编译出的 C++ 可执行文件。

●识别类及其构造函数

  1. 函数中多处使用 ecx 寄存器相对寻址(this 指针,也可能会有将 ecx 存至 ebx 等其他寄存器后再引用的,视具体情况),可能是在读写类的成员变量,不过这一点并不是特别准确,特别当读写栈上的局部类实例时,经过优化的代码不太容易直接看出几个内存变量之间的相关性。
  2. 通过 thiscall 的调用约定。thiscall 使用 ecx 传递 this 指针,因此碰到有为 ecx 赋值后紧接着调用函数的情况,基本确定是在调用某个类成员函数,当然也有可能是个 fastcall 的函数;特别的,如果发现 ecx 引用了一个未初始化过的地址,比如说栈上的,那调用的很有可能就是局部类变量的构造函数。同样,new 返回后的 eax 直接扔给 ecx 再调用函数,也极有可能是在调用构造函数。
  3. 通过间接函数调用,遇到形如
        mov  eax,dword ptr [ecx]
        mov  eax,dword ptr [eax+18h]
        call eax

    三连的话基本也可以确定是在调用类的虚函数。遇到虚函数的话,只能通过 this 指针找到虚表然后确定实际调用的函数了,显然无法进行静态分析。

  4. 多态类在调用了基类的构造函数后,执行后续代码前会设置该类自己的虚表,即引用一个位于 .rdata 段里的全局地址,常会有形如
    mov dword ptr [eax], offset 72C20C

    这样的代码出现,后者是一个只读的地址,就可能是在设置虚表。当然,查看该地址中的每个指针应当都是指向到代码段中的。析构函数中也会出现类似操作。

●类的内存布局

关于类继承布局在我之前的一篇文章里也提到过。

熟悉布局后可以方便我们确定类与类间的关系和类里的成员,使用 MSVC 的 -d1reportSingleClassLayoutXXX 或 -d1reportAllClassLayout 命令参数可以显示代码中单个或所有类的布局(将 XXX 替换成类名,不插入空格,编译器会显示出包含关键字 XXX 的所有类)。实际项目中虚继承我想应该不会太多(也可能只是我见的少……),但是接口多继承还是很常见的。贴几个不同类型的类布局信息。

class Animal_01
{
public:
    virtual void fff01() {}
};
class Animal_02
{
public:
    virtual void fff02() {}
};
class Animal_11
{
public:
    virtual void fff11() {}
};
class Animal_0 : public Animal_01, public Animal_02
{
public:
    virtual ~Animal_0() {}
};
class Animal_1 : public Animal_11
{
public:
    virtual void A1() {}
};
class Animal : virtual public Animal_1, virtual public Animal_0
{
public:
    virtual ~Animal(void) {}
};

单继承

class Animal_1 : public Animal_11
 class Animal_1	size(4):
	+---
 0	| +--- (base class Animal_11)
 0	| | {vfptr}
	| +---
	+---

Animal_1::$vftable@:
	| &Animal_1_meta
	|  0
 0	| &Animal_11::fff11
 1	| &Animal_1::A1

多继承

class Animal_0 : public Animal_01, public Animal_02
 class Animal_0	size(8):
	+---
 0	| +--- (base class Animal_01)
 0	| | {vfptr}
	| +---
 4	| +--- (base class Animal_02)
 4	| | {vfptr}
	| +---
	+---

Animal_0::$vftable@Animal_01@:
	| &Animal_0_meta
	|  0
 0	| &Animal_01::fff01
 1	| &Animal_0::{dtor}

Animal_0::$vftable@Animal_02@:
	| -4
 0	| &Animal_02::fff02

虚继承

class Animal : virtual public Animal_1, virtual public Animal_0
 class Animal	size(16):
	+---
 0	| {vbptr}
	+---
	+--- (virtual base Animal_1)
 4	| +--- (base class Animal_11)
 4	| | {vfptr}
	| +---
	+---
	+--- (virtual base Animal_0)
 8	| +--- (base class Animal_01)
 8	| | {vfptr}
	| +---
12	| +--- (base class Animal_02)
12	| | {vfptr}
	| +---
	+---

Animal::$vbtable@:
 0	| 0
 1	| 4 (Animald(Animal+0)Animal_1)
 2	| 8 (Animald(Animal+0)Animal_0)

Animal::$vftable@:
	| -4
 0	| &Animal_11::fff11
 1	| &Animal_1::A1

Animal::$vftable@Animal_01@:
	| -8
 0	| &Animal_01::fff01
 1	| &Animal::{dtor}

Animal::$vftable@Animal_02@:
	| -12
 0	| &Animal_02::fff02

●利用 RTTI 信息识别 C++ 类

RTTI 对于识别多态类能提供很多帮助,可以了解到类名、类的继承关系等信息,当然非多态的类是没有这东西的。VS 编译 C++ 代码时默认启用 RTTI,其设置项在“项目属性-C++-语言-启用运行时类型识别”选项中,将其关闭后也就无法再使用 typeid 和 dynamic_cast 了。

下面介绍一下相关的结构。

  1. RTTICompleteObjectLocator
    该结构是从 this 指针找到的第一个结构,后续的结构均可从它引出。内部包含了两个指针,分别指向自身的类信息和类的继承信息。

    +0  unsigned long                 signature;       // 似乎都是 0
    +4  unsigned long                 offset;          // 虚表偏移
    +8  unsigned long                 cdOffset;        // ?
    +c  TypeDescriptor*               pTypeDescriptor; // 自身类信息
    +10 RTTIClassHierarchyDescriptor* pClassHierarchyDescriptor; // 类的继承信息

    查找的时候先通过 this 指针找到虚表,然后虚表往前移动一个 uint32_t 就是指向该结构的指针了。即:

    RTTICompleteObjectLocator* ptr = (RTTICompleteObjectLocator*)(*(uint32_t**)this)[-1];

    但是在复杂继承情况下该表达式中的 this 需要经过调整后才能正常工作,比如同时虚继承了两个类,那么此时 this 指向的第一个 uint32_t 是虚基类表指针。

  2. TypeDescriptor
    该结构包含了类自身的信息,最主要就是里面的字符串类名称

    +0  void*           pVFTable;   // 指向 type_info 的虚表
    +4  unsigned long   spare;      // 保留
    +8  char[1]         name;       // 类名
  3. RTTIClassHierarchyDescriptor
    该结构为类的继承信息,里面含有从当前类开始的其所有父类

    +0  unsigned long       signature;       // 似乎都是 0
    +4  unsigned long       attributes;      // 第0位置1表示多继承,
                                             // 第1位置1表示虚继承
    +8  unsigned long       numBaseClasses;  // 父类数量,包括自己在内,所以数量加1
    +c  RTTIBaseClassArray* pBaseClassArray; // 父类的数组

    这个数组中多继承的类也会一并列出,项的顺序是按继承树以深度优先的方式展示,如 C 同时继承了 A 和 B,AB 又分别继承自 A1、A2、B1、B2,那么数组中就会有 EAA1A2BB1B2 七项,后面的例子中也能看到。
    RTTIBaseClassArray 这个结构里面是单纯的包含一个 RTTIBaseClassDescriptor* 数组

    +0 RTTIBaseClassDescriptor* arrayOfBaseClassDescriptors[]
  4. RTTIBaseClassDescriptor
    该结构记录了相应父类的信息

    +0  TypeDescriptor *pTypeDescriptor; // 父类自身的信息
    +4  unsigned long numContainedBases; // 父类的父类数量
    +8  PMD where;
        +0  unsigned long mdisp;         // 成员偏移
        +4  unsigned long pdisp;         // 虚基类表在类中的偏移,
                                         // 如果是 -1 说明该类不是其虚基类
        +8  unsigned long vdisp;         // 基类在虚基类表中的偏移
    +14 unsigned long attributes;        // 
    +18 RTTIClassHierarchyDescriptor* pClassDescriptor; // 父类的继承信息

    PMD 中几个值主要用于计算该父类在其子类中的偏移位置,计算方式如下:

        size_t offset = 0;
        if (pmd.pdisp >= 0) {       // 为 -1 则跳过
            offset = pmd.pdisp;
            offset += *(__int32*)((char*)*(size_t*)((char*)pThis + offset)
                + pmd.vdisp);    // 在虚基类表中找到 vdisp 偏移值
        }
        offset += pmd.mdisp;

举个例子,比如前面的

class Animal : virtual public Animal1, virtual public Animal2 

的继承布局,同时得到 Animal1 的 PMD 为 { mdisp:0, pdisp:0, vdisp:4 },那么通过 Animal 类的 this 得到 Animal1 的步骤为

  1. (size_t)this + pdisp 得到虚基类表地址 vbt
  2. *(size_t*)(*(size_t*)vbt + vdisp) 再获得虚基类表中第二项 Animal1 的偏移 offset
  3. 最后 (size_t)this + offset

得到最终 offset 的值为 4。下面的布局和前面展示的虚继承布局是一样的,这里重新贴一份。

class Animal	size(16):
    +---
 0  | {vbptr}
    +---
    +--- (virtual base Animal_1)
 4  | +--- (base class Animal_11)
 4  | | {vfptr}
    | +---
    +---
    +--- (virtual base Animal_0)
 8  | +--- (base class Animal_01)
 8  | | {vfptr}
    | +---
12  | +--- (base class Animal_02)
12  | | {vfptr}
    | +---
    +---

Animal::$vbtable@:
 0  | 0
 1  | 4 (Animald(Animal+0)Animal_1)
 2  | 8 (Animald(Animal+0)Animal_0)

●从可执行文件中提取 RTTI 信息

通过以上的 RTTI 解析,我写了个可以找出 exe 中多态类信息的程序,简单列出了类名、继承关系以及在构造函数中被引用的 RVA 地址。程序主要通过匹配字符串 “.?AV” 来找到类名,并识别出相应的 RTTI 信息,最后定位出引用了对应虚表的函数的 RVA 地址。代码放在这里
贴一部分输出结果(同样使用上面的 Animal* 继承结构)

---------------------------------
.?AVAnimal_11@@: 
base classes: .?AVAnimal_11@@, 

reference in ctors(next instruction): 13c9c, 

---------------------------------
.?AVAnimal_0@@: 
base classes: .?AVAnimal_0@@, .?AVAnimal_01@@, .?AVAnimal_02@@, 

reference in ctors(next instruction): 13c2f, 1436c, 13c39, 14376, 

---------------------------------
.?AVAnimal_1@@: 
base classes: .?AVAnimal_1@@, .?AVAnimal_11@@, 

reference in ctors(next instruction): 13cf4, 

---------------------------------
.?AVAnimal@@: 
base classes: .?AVAnimal@@, .?AVAnimal_1@@, .?AVAnimal_11@@, .?AVAnimal_0@@, .?AVAnimal_01@@, .?AVAnimal_02@@, 

reference in ctors(next instruction): 13adb, 142f7, 13aed, 1430b, 13b00, 1431f, 

看到 Animal 类的虚表有 6 处引用,而 Animal 中因为继承关系含有 3 个虚表,所以分别就是构造、析构函数各引用了 3 次。而 Animal_11 和 Animal_1 类没有定义虚基类,所以只有构造函数中引用的 1 次。