Reference:

https://f002.backblazeb2.com/file/sec-news-backup/files/writeup/www.cmlab.csie.ntu.edu.tw/__cathyp_eBooks_C___Reverse_20c___pdf/index.pdf

https://www.blackhat.com/presentations/bh-dc-07/Sabanal_Yason/Paper/bh-dc-07-Sabanal_Yason-WP.pdf

judge c++

As a natural way to start, the reverser must first determine if a specific target is indeed a compiled C++ binary and is using C++ constructs. Below are some pertinent indications that the binary being analyzed is a C++ binary and is using C++ constructs.

  1. Heavy use of ECX(this ptr),One of the first things that a reverser may see is the heavy use of ecx (which is used as the this pointer). One place the reverser may see it is that it is being assigned a value just before a function is about to be called:

image-20220520195013869

Another place is if a function is using ecx without first initializing it, which suggests that this is a possible class member function:

image-20220520195000763

calling convention

Calling Convention. Related to (1), Class member functions are called with the usual function parameters in the stack and with ecx pointing to the class’s object (i.e. this pointer.). Here is an example of a class instantxiation, in which the allocated class object (eax) will eventually be passed to ecx and then invocation of the constructor follows.

image-20220520195810536

此外,reverser要注意到间接函数调用,这些调用更可能是虚拟函数;当然,如果不首先了解实际类或在调试器下运行代码,很难跟踪这些调用的去向。考虑以下虚拟函数调用示例:

image-20220520200722178

在这种情况下,reverser必须首先知道ClassA的虚拟函数表(vftable)的位置,然后根据vftable中列出的函数列表确定函数的实际地址。

类实例结构

普通类

在我们深入之前我们应该熟悉类在内存中的结构布局

image-20220520214836110

我们需要把padding增加到最后一个成员变量以确保size为4字节的倍数。

虚函数

image-20220520215713706

here’s the class layout

image-20220520215739428

请注意,在布局的开头添加了指向虚拟函数表的指针。此表按声明顺序包含虚拟函数的地址。Ex2类的虚拟函数表将像这样。

image-20220520220019231

现在,如果一个类从另一个类继承呢?以下是当一个类从单个类(即单个继承)继承时会发生什么:

image-20220520220112250

And the layout,我们可以看到存在两个var1在这个内存中。image-20220520220551662

如您所见,派生类的布局简单地附加到基类的布局中。在多次继承的情况下,情况如下:

image-20220520221648717

如您所见,如果每个基类的实例数据将嵌入派生类的实例中,并且每个包含虚拟函数的基类将有自己的vftable。请注意,fist base class与当前对象共享vftable。当前对象的虚拟函数将附加到第一个基类虚拟函数列表的末尾

类识别

我们上面已经讨论了如何判断一个程序是不是用 C++写的,讨论了类的构造函数以及内存中类的实例的组织形式,这一节我们来讨论 C++的类在可执行文件 中的使用情况。我们先来讨论如何确定内存中哪些部分是类(或者称为对象)下 一节再来讨论如何确定类之间的关系以及类中的成员。

  1. 识别构造函数和析构函数

    1. 全局对象。全局对象顾名思义就是那些被声明为全局变量的对象。这些对象的内存空间在编译时就已经被分配好了的, 它们位于可执行文件的数据段中。这些对象的构造函数是在这个程序启动以后,main调用之前被调用执行的,而它们的析构函数是在程序退出(exit)时被调用的。

      一般来说,如果我们发现一个函数调用时,传入的是this指针(一般是使用ecx寄存器)是指向一个全局变量的话,我们基本可以确定,这是一个全局变量,我们可以利用交叉引用找到该全局变量的构造函数和析构函数,如果该段代码位于entrypoint和main函数之间,那么很有可能这个为构造函数

      main函数

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      _main proc near

      argc= dword ptr 4
      argv= dword ptr 8
      envp= dword ptr 0Ch

      mov dword_403378, 3
      xor eax, eax
      retn
      _main endp

      通过交叉引用可以找到两处,也就是构造函数和析构函数

      image-20220521141409121

      跟进构造函数,_atexit注册了程序终止的函数为析构函数

      image-20220521141914948

      运行到返回后发现程序位于entrypoint和main函数之间,属于编译器生成的代码,通过call _initterm函数调用了构造函数

      image-20220521143307579

    2. 局部对象。这些对象的作用域起始于该对象被声明的地方,结束于声明该对象的模块退出之时(比如函数结尾或者分支结束的地方,下面例子里就是在一个 if 语句块 结束的地方调用析构函数的)。局部对象在内存中是位于栈(stack)里的。 它们的构造函数在该对象声明的地方被调用,而在对象离开其作用域时调用 对象的析构函数。
      局部对象的构造函数还是比较容易识别的,如果你发现一个函数调用, 传递过去的 this 指针竟然是指向了栈中一个未被初始化过的变量的话,你基 本上可以确定这个函数是一个对象的构造函数,同时也就发现了一个对象。 析构函数一般则是与构造函数位于同一个模块(也就是声明该对象的模块) 的最后一个使用指向该对象的 this 指针的函数。

      image-20220521150912632

    3. 动态分配的对象,这种对象是通过new操作符动态创建的对象。实际上,new操作符会变成两个符号调用:一个new()函数的调用再接着一个构造函数的调用。new()函数是用来在堆中分配空间的(对象的大小通过参数传给new函数),然后把心分配的地址存在EAX寄存器返回出来。同样delete也会变成析构函数和free()函数。

      image-20220521152155003

  2. 通过RTTI进行多态类识别(Run-Time-Type Information)

如果C++在编译的时候启用了RTTI功能,那么我们又会多一种识别类的方法。特别是对多态类(包含虚函数的类)。RTTI是C++中提供的一种在运行时确定对象类型的机制,在C++中一般时候typeid和dynamic_cast这两个操作符来实现这一机制。这两个操作符在实现时需要获得相关类的类名,类的层次等相关信息,在实际使用VC的时候,如果你用了以上两种操作符号但没有启用RTTI,编译器将会给你一个警告。在默认情况下MSVC6.0关闭了RTTI功能。而MSVC2005中默认开启了RTTI。

  1. RTTICompleteObjectLocator

    这个结构体包含了两个指针,一个指向实际的类信息,另一个指向类的继承关系。

    Offset Type Name Description
    0x00 DW signature Always 0 ?
    0x04 DW offset Offset of Vtable within the class
    0x08 DW cdoffset ?
    0x0c DW pTypeDescription Class Information
    0x10 DW pClassHierarchyDescription Class Hierarchy Information

    那么怎么找到这个结构体呢?我们只要找到虚函数表的上一个DWORD指向的即为RTTICompleteObjectLocator。

    image-20220521161903162

    这是RTTICompleteObjectLocator结构

    image-20220521162047050

  2. TypeDescriptor

    位于RTTICompleteObjectLocator结构的第四个DWORD是一个指向本类的TypeDescriptor结构体的指针,TypeDescriptor这个结构体中记录了这个类的类名、

    Offset Type Name Description
    0x00 DW pVFTable Always points to type_info’s vftable
    0x04 DW spare ?
    0x08 SZ name Class Name

    image-20220521190238726

  3. RTTIClassHierarchyDescriptor

    RTTIClassHierarchyDescriptor记录了类的继承信息、包括基类的数量以及一个RTTIBaseClassDescriptor数组,RTTIBaseClassDescriptor在下面详细讨论,RTTIBaseClassDescriptor最终将指向当前各个基类的TypeDescriptor。

    Offset Type Name Description
    0x00 DW signature Always 0 ?
    0x04 DW attributes Bit 0 - multiple inheritance;Bit 1 - virtual inheritance
    0x08 DW numBaseClasses number of base classes, Count includes the class itself
    0x0c DW pBaseClassArray Array of RTTIBaseClassDescriptor

    当classG 虚拟继承了classA和classE,那么结构如下。包括ClassG自身,我们可以看到numBaseClasses = 3attributes = 3 表示它既是多继承又是虚继承,最后一个BaseClassArray指向RTTIBaseClassDescriptor

    image-20220521191535688

  4. RTTIBaseClassDescriptor

这个结构体包含了基类的有关信息。它包括一个指向基类的TypeDescriptor的指针和一个指向基类的RTTIClassHierarchyDescriptor的指针,(VC6.0中可能没有pBaseClassArray)。另外,它还包含有一个PMD结构体,该结构体中记录了该类中的各个基类的位置。RTTIBaseClassDescriptor结构如下。

Offset Type Name Description
0x00 DW pTypeDescriptor TypeDescriptor of the base class
0x04 DW numContainedBases Number of direct bases of this base class
0x08 DW PMD.mdisp vftable offset
0x0c DW PMD.pdisp vbtable offset(-1:vftable is at displacement PMD.mdisp inside the class)
0x10 DW PMD.vdisp displacement of base class vftable pointer inside the vbtable

一个vbtable(Virtual base class table)是由多重虚拟继承生成的。因为在多重继承的情况下,有时候需要upclass。这时候就需要精确定位基类。虚基类表包含了各个基类在派生类中的位置(或者说各个基类的虚函数表在派生类中的位置,因为虚函数表是基于类的起始位置的)。

根据之前所说的ClassG类声明,编译器会生成以下类结构

image-20220521210502725

在如上这种情况下,vbcase存在于位移0x04处,另一方面,vbtable包含派生类内每个基类的位移:

image-20220521210907045

那么我们尝试利用vbtable来确定基类的真实地址。首先可以看到ClassE的偏移是4,然后我们从虚基类表中读取出classE的偏移为16,那么16 +4 = 20,ClassE位于ClassG +0x14 处,也就是下图的0x00418b14地址处。

以下为ClassG中ClassE的BaseClassDescriptor

image-20220521211249241

那么总结如下的关系图

image-20220522091012407

识别类关系

通过构造器识别类关系

构造函数包含初始化对象的代码,例如调用基类的构造函数和设置vftables。因此,分析构造函数可以让我们很好地了解这个类与其他类的关系。

image-20220522091427369

让我们假设我们已经确定这个函数是通过之前所说方法识别的构造函数。现在,我们看到一个函数正在使用当前对象的这个指针调用。这可以是当前类的成员函数,也可以是基类的构造函数。

我们怎么知道是哪一个?事实上,仅仅通过查看生成的代码,就无法完美区分两者。然而,在现实世界的应用中,在此步骤之前,构造函数很有可能被识别在较前的位置,因此我们所要做的就是将这些信息关联起来,以得出更准确的标识。换句话说,如果使用当前对象的此指针在另一个构造函数中调用预先确定为构造函数的函数,它可能是基类的构造函数。

手动识别这一点需要检查对这个函数的其他交叉引用,看看这个函数是否是二进制文件中其他地方调用的构造函数。我们将在本文件后面讨论自动识别方法。

image-20220522091913348

多重继承实际上比单一继承更容易发现。与单个继承示例一样,第一个调用的函数可以是成员函数,也可以是基类构造函数。请注意,在反汇编中,调用第二个函数之前,在此指针中添加4个字节。这表明正在初始化另一个基类。

Here’s the layout for this class to help you visualize. The disassembly above belongs to the constructor of class D. Class D is derived from two other classes, A and C:

image-20220522092402038

image-20220522092414415

通过RTTI识别多态类关系

我们在之前的RTTI中讲到RTTIClassHierarchyDescriptor结构体如下

Offset Type Name Description
0x00 DW signature Always 0 ?
0x04 DW attributes Bit 0 - multiple inheritance;Bit 1 - virtual inheritance
0x08 DW numBaseClasses number of base classes, Count includes the class itself
0x0c DW pBaseClassArray Array of RTTIBaseClassDescriptor

我们可以通过这个结构体的pBaseClassArray数组来判断非直接基类,如:类A有基类类B,类C,但是类B中存在类C,那么类C就是类A的非直接基类。以下为关系图

image-20220522093145721

而其结构图如下

image-20220522093215268

识别类的成员

识别类成员是一个简单明了的过程,尽管缓慢而乏味。我们可以通过查找相对于此指针的偏移量访问来识别类成员变量:

image-20220522093643884

我们还可以通过查找对相对于此对象虚拟函数表偏移量的指针的间接调用来识别虚拟函数成员:

image-20220522093705706

通过检查此指针是否作为隐藏参数传递给函数调用,可以识别非虚拟成员函数。

image-20220522093758525

为了确保这确实是一个成员函数,我们可以检查被调用的函数是否使用ecx,而无需首先初始化它。让我们看看sub_401110的代码

image-20220522093849886

  • 通过指针偏移量来进行成员变量的赋值 mov dword ptr[eax+8], 12345h
  • 通过vftable指针来获取虚函数,关键为mov edx, [ecx],获取了虚函数指针指向的地址,然后通过mov eax, [edx+4]获取虚函数指针
  • 通过lea ecx,[ebp_var_c],隐藏传递参数给函数调用,注意此处不是64位二进制文件
    • 同时检查被调用的函数是否使用ecx而无需初始化,也就是直接使用之前隐藏传递进来给函数调用的对象指针

Automation

to be continuing…

STL

STL代码和导入的DLL。确定示例是否为C++二进制文件的另一种方法是目标是否使用STL代码,该代码可以通过导入函数或库签名标识(如IDA的FLIRT)确定:

to be continueing…

reverse c++ binary

  • 还原类的构造函数
  • 还原类的析构函数
  • 还原类的成员函数
  • 还原类的虚函数
  • 还原类的继承层次
  • 判断一个类是否是抽象类

References

Ready IDA

  • HexRaysPyTools: Extremely useful for quickly creating structures without having to find every offset that might be a field.
  • Classy: Makes working with vtables and child classes a lot easier.

other settings

  • Make sure to regularly create a snapshot of your database
  • Create/open database
  • make sure the compiler options are correct
  • (Optional)Always show demangled names (Options → Demangled names → Select Names)

theory

class layout in Memory

1
2
3
4
5
6
7
8
9
10
11
// Ususally stored in the data section
struct vtable{
void (*func1)();
void (*func2)();
};t

struct class{
vtable* vtbl;
int member1;
int member2;
}

decompiler

this patterns means below in decompiler

1
2
3
4
__int64 v1 = operater new(sizeof(class));
*v1 = gvtable; // stored someware in .data
*(v1+4) = 0;
*(v1+8) = 0;

call a vtable function

1
(*(void (*)())(*(_QWORD*)v1 + 8))();

Reconstruct Class

to be continue…

异常处理

to be continue…

符号恢复

to be continue…

https://github.com/push0ebp/sig-database

https://github.com/Maktm/FLIRTDB

https://github.com/maroueneboubakri/lscanDW

本文采用CC-BY-SA-3.0协议,转载请注明出处
Author: scr1pt