分析 C++ 程序时,要是能从一个二进制文件中识别出 C++ 程序的结构,并且能标识出一些主要的类和它们的关系是非常有帮助的。而其中为了实现运行时类型识别而生成的运行时类型信息(Runtime type information,RTTI)也能在程序分析上提供方便。
注意,文中讨论的 C++ 可执行文件仅限于使用 MSVC 编译器编译出的 C++ 可执行文件。
●识别类及其构造函数
- 函数中多处使用 ecx 寄存器相对寻址(this 指针,也可能会有将 ecx 存至 ebx 等其他寄存器后再引用的,视具体情况),可能是在读写类的成员变量,不过这一点并不是特别准确,特别当读写栈上的局部类实例时,经过优化的代码不太容易直接看出几个内存变量之间的相关性。
- 通过 thiscall 的调用约定。thiscall 使用 ecx 传递 this 指针,因此碰到有为 ecx 赋值后紧接着调用函数的情况,基本确定是在调用某个类成员函数,当然也有可能是个 fastcall 的函数;特别的,如果发现 ecx 引用了一个未初始化过的地址,比如说栈上的,那调用的很有可能就是局部类变量的构造函数。同样,new 返回后的 eax 直接扔给 ecx 再调用函数,也极有可能是在调用构造函数。
- 通过间接函数调用,遇到形如
mov eax,dword ptr [ecx] mov eax,dword ptr [eax+18h] call eax
三连的话基本也可以确定是在调用类的虚函数。遇到虚函数的话,只能通过 this 指针找到虚表然后确定实际调用的函数了,显然无法进行静态分析。
- 多态类在调用了基类的构造函数后,执行后续代码前会设置该类自己的虚表,即引用一个位于 .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 了。
下面介绍一下相关的结构。
- 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 是虚基类表指针。
- TypeDescriptor
该结构包含了类自身的信息,最主要就是里面的字符串类名称+0 void* pVFTable; // 指向 type_info 的虚表 +4 unsigned long spare; // 保留 +8 char[1] name; // 类名
- 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[]
- 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 的步骤为
- (size_t)this + pdisp 得到虚基类表地址 vbt
- *(size_t*)(*(size_t*)vbt + vdisp) 再获得虚基类表中第二项 Animal1 的偏移 offset
- 最后 (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 次。