逆向 C++-- 2 识别类

翻译 2007年09月22日 20:03:00

逆向 C++

我们上面已经讨论了如何判断一个程序是不是用 C++写的,讨论了类的构造函数以及内存中类的实例的组织形式,这一节我们来讨论 C++的类在可执行文件中的使用情况。

我们先来讨论如何确定内存中哪些部分是类(或者称为对象)下一节再来讨论如何确定类之间的关系以及类中的成员。  

[1]识别构造函数和析构函数
为了能从二进制可执行文件中把类识别出来,我们必须先要理解这些类的实例——对象是怎样被创建的。因为这个创建过程在汇编级别上具体是怎样实现的会给我们在反汇编时如何识别这些类提供依据
1)全局对象。全局对象顾名思义就是那些被声明为全局变量的对象。这些对象的内存空间是在编译时就被分配好了的,它们位于可执行文件的数据段中。这些对象的构造函数是在这个程序启动之后,main()函数被调用之前被调用执行的,而它们的析构函数则是在程序退出(exit)时被调用的。

一般来讲,如果我们发现一个函数调用时,传入的 this 指针(一般是使用 ecx 寄存器)是指向一个全局变量的话,我们基本可以确定,这是一个全局对象,而要找到这个全局对象的构造函数和析构函数,我们一般要借助于
交叉引用(cross-references)的功能。我们观察所有使用指向这个全局对象的函数的位置,如果某个函数位于程序的入口点(entry point)和 main()
函数之间,那么它就很有可能就是这个对象的构造函数。

PPT 里的图很说明问题:
这是源码:

这是反汇编以后的代码:

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

3)动态分配的对象。这种对象是指哪些通过 new 操作符动态创建的对象。 实际上 new 操作符会转变成两个函数调用:一个 new() 函数的调用再紧接着一个构造函数 的 调用。new() 函数是用来在堆中为对象分配空间的 (对象的大小通过参数传递给 new()函数),然后把新分配的地址放在 EAX 寄存器中返回出来。然后这个地址就被当作 this 指针传递给构造函数。同样 delete 操作符也会转变成两个函数调用,先调用析构函数,然后接着调用 free()函数回收空间。

 如下面这个简单的例子:

 

[2] 利用 RT T I 识别多态类
如果 C++程序在编译时启用了 RTTI 功能,那么恭喜你!你又多了另一种识别类,特别是对多态类(即包含有虚函数的类),的方法——利用 RTTI(运行时类型信息 Run-time Type Information)。RTTI 是 C++中提供的一种在运 行时确定对象的类型的机制。在 C++中我们一般使用 typeid dynamic_cast  这两个操作符来实现这一机制。这两个操作符在实现时需要获得相关类的类名,类的层次等相关信息。在实际使用 VC 的过程中,如果你使用了 typeid 和 dynamic_cast 这两个操作符,却没有打开 RTTI 编译选项,编译器将会给你一个警告。在默认情况下 MSVC 6.0 是把 RTTI 给关闭掉的。

但是在 MSVC 2005 中,RTTI 默认是打开的。

为了实现 RTTI,编译器在编译完了的二进制可执行文件中加入一些结构体,这些结构体包含了代码中关于类(特别是多态类)的信息。这些结构体是:

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

怎么找到这个 RTTICompleteObjectLocator 结构体呢?我们先找虚函数表, 在内存中虚函数表上面一个 DWORD 就是指向 RTTICompleteObjectLocator 结构体的指针,不信?请看下面这两个例子,您上眼:

下面给出的是一个 RTTICompleteObjectLocator 结构体的实例:

2.TypeDescriptor
您想必已经看见了,在 RTTICompleteObjectLocator 结构体中,第四个 DWORD 域里是一个指向本类的 TypeDescriptor 结构体的指针。TypeDescriptor 这个结构体中记录了这个类的类名,我们逆向的时候一般可以根据类名大致猜出这个类是干什么的,这个结构体的结构如下图:

下面是 TypeDescriptor 的一个实例:

3.RTTIClassHierarchyDescriptor
RTTIClassHierarchyDescriptor 记录了类的继承信息,包括基类的数量, 以及一个 RTTIBaseClassDescriptor 数组,RTTIBaseClassDescriptor 我们下面详细讨论,现在我只先说一点,就是 RTTIBaseClassDescriptor 最终将指向当前各个基类的 TypeDescriptor。

比如说我们声明了一个类 ClassG,它虚继承了类 ClassA 和 ClassE:

那么 ClassG 的 RTTIClassHierarchyDescriptor 就应该是下面这个样子的:

 

它里面有 3 个基类(包括了 ClassG 本身),attribute 是 3 表示这个类是多 继 承 加 上 虚 继 承 。 最 后 有 一 个  pBaseClassArrary  指 针 指 向 RTTIBaseClassDescriptor 指针数组.

4.RTTIBaseClassDescriptor
这 个 结 构 体 包 含 了 关 于 基 类 的 有 关 信 息 。 它包括一个指向基类的 TypeDescriptor 的指针和一个指向基类的RTTIClassHierarchyDescriptor 的指针,(译注:在 VC6.0 编译的结果中可能没有 pClassDescriptor)另外它还包含有 一 个 PMD 结 构 体 , 该 结 构 体 中 记 录 了 该 类 中 各 个 基 类 的 位 置 。 RTTIBaseClassDescriptor 的结构如下图所示:

虚基类表(virtual base class table,vbtable)只会在多重虚继承的情况下才会出现。因为在多重虚继承的情况下,有时会需要 upclass,(译注:比如这个 ClassG 这个例子中 ClassA 和 ClassE 都继承自 ClassX,《掀起你的盖头来——谈 VC++对象模型》一文中第五节虚继承中讲的比较细,我懒一下直接引用了,呵呵,http://dev.yesky.com/136/2317136_1.shtml)这时就需要精确定位基类。虚基类表包含了各个基类在派生类中的位置(或者也可以说是各个基类的虚函数表在派生类中的位置,因为虚函数表是位于类的起始位置的)。

虚函数表的指针被放在整个类偏移+4 这个位置上,而虚基类表中则记录了各个基类在派生类中的位置:

 

我们现在来试试通过虚基类表来确定 ClassE 在 ClassG 中的位置,我们先要知道虚基类表的偏移,嗯,它是 4,然后我们从虚基类表中读出 ClassE 的偏移,嗯,它是 16,16+4=20,所以 ClassE 在 ClassG 中位于偏移+20 这个位置上。

我们看到 PMD.pdisp 是 4,这个域表示的是 vbtable(虚函数表)在 ClassG中的偏移量,而 PMD.vdisp 是 8,表示 ClassE 在 ClassG 中的偏移量,是记录在虚函数表偏移+8 的位置上的。(也就是第三个 DWORD 域中)。
PPT 里的图实在是清楚啊:

下面这个图是对这一节的一个总结:

[3]. 判别类与类之间的关系
1.通过分析构造函数来分析类与类之间的关系

构造函数是用来初始化对象的(好像是废话啊,呵呵),所以在构造函数中,它会调用基类的构造函数(如果有的话)以及设置自己的虚函数表。因此分析类的构造函数是我们分析类与类之间关系的一个很好的突破口。
下面是一个简单单继承的例子:

假定我们已经知道上面这段代码是某个类的构造函数,我们发现红颜色标出的这个函数使用了一个由 ecx 传递进来的一个当前对象的 this 指针。问题来了:这个函数究竟是当前对象的一个成员函数呢,还是当前对象的基类的构造函数呢? 对不起,我不能 100%的确定。当然在实际的逆向工程里,很有可能这就是 一个基类的构造函数。当然有时我们干脆事先究已经知道了这个函数是另一个类的构造函数,问题也就迎刃而解了。 接下来说正事,如果我们发现类 A 的构造函数在类 B 的构造函数中出现,而且还把当前对象(类 B)的指针当成(类 A 的)this 指针来使用的话,我们基本上就可以确定,这是类 A 是类 B 的基类。 在进行人工判别时,我们应该多多利用交叉引用(cross-references)功能,看看红颜色标出的这个函数有没有被其他类当成构造函数使用。自动判别的有关 技巧我们稍后再进行讨论。

 下面我们再来看一个复杂一点的多继承的情况:


 
一开始还是和刚才那个单继承的情况一样,先有一个函数调用,使用了 ecx 把当前对象的指针当成 this 指针传给了这个函数。嗯,然后好像就有点不一样了,我们注意到当前对象的指针被加上 4,然后又被当成另一个函数的 this 指针呵呵,显然第二个函数是另一个基类的构造函数。 我们现在对这个类做一点解释,让你能比较直观的理解这一小节。上面这段 代码是类 D 的构造函数,类 D 继承了类 A 和 C,这三个类在内存中的布局如下:

我们现在知道了各个基类的构造函数所使用的 this 指针是怎么来的了。基类的 this 指针是与派生类的 this 指针戚戚相关的,具体说,就是类 A 和 C 的this 指针是类 D 的 this 指针加上类 A 和 C 各自在类 D 中的偏移量得出的。

2.通过 RTTI 分析类与类之间的关系
利用 RTTI 识别类我们在前面已经讨论过了,现在我们来讨论怎样利用 RTTI
来判别类与类之间的关系。现在我们要利用 RTTIClassHierarchyDescriptor 这
个结构体。为了便于大家参考,我把这个结构体的的结构在贴一遍:


我们现在注意最后一个域——pBaseClassArray。这个域里是一个指向
RTTIBaseClassDescriptor(BCD)组成的数组的指针。而数组中各个 BCD 则是指
向当前类的各个基类的 TypeDescriptor 结构体的指针(关于这一点我们前面已
经讨论过了)。 比如下面这个例子:

下 面是根据类C的 RTTIClassHierarchyDescriptor , RTTIBaseClassDescriptor 以及相关基类的 TypeDescriptor 画出的,A、B、C
三个类之间的关系:

仔细的看,可能已经发现一点问题了,在 pBaseClassArray 指向 的 BaseClassArray 数组中,甚至列出了类 C 的非直接基类——A。这样以来类 A 和 B  之 间 的 关 系 就 比 较 模 糊 了 。 当 然 你 可 以 再 去 分 析 类  B  的 RTTIClassHierarchyDescriptor,然后你就能知道类 A 实际上是类 B 的基类。所以类 A 就不可能再是类 C 的基类了。这样你就能正确推出 A、B、C 则三个类的关系了。
D.辨别类的成员
辨别类的成员的这一过程虽然有点枯燥乏味,但是相对而言技术难度却小的多。一般访问类的成员(读或者写)一般都会使用 this 指针加上该成员在类中的偏移量的方式实现。所以我们也利用这一特点来辨别类的成员,如下面这个例子:

一般调用虚函数都是使用读虚函数表中的偏移,然后进行间接调用的方式实
现的,我们也利用这一特点来辨别类的虚函数,比如下面这个例子:

那么类的非虚函数怎么来识别呢?我们可以利用 this 指针来做到这一点, 一般 this 指针要通过 ecx 寄存器来传递给函数,比如下面这个例子:

当然如果你觉得证据还不充分,你还可以进一步检查在被调用的函数中是不是没有初始化就直接使用了 ecx 寄存器,我们来具体看看 sub_401110 这个函数的实现代码:

C++中RTTI机制剖析

[置顶] C++中RTTI机制剖析 分类: C/C++2013-08-03 20:26 412人阅读 评论(0) 收藏 举报 C++编译器RTTI C++中要想在运行时获取类...
  • pi9nc
  • pi9nc
  • 2014年03月21日 20:34
  • 2839

C++ Internals: VC RTTI - 基本数据结构

相关资源: Reversing Microsoft Visual C++ Part II: Classes, Methods and RTTI Reversing C++ C++的RTTI一...

深度探索C++对象模型

20170514_深度探索C++对象模型 尊重知识产权: 转自:http://www.cnblogs.com/tgycoder/p/5426628.html   和   http://www.cn...
  • chrdww
  • chrdww
  • 2017年09月26日 14:08
  • 96

C++对象模型

何为C++对象模型? C++对象模型可以概括为以下2部分: 1.        语言中直接支持面向对象程序设计的部分 2.        对于各种支持的底层实现机制 语言中直接支持面向对象...

针对指定进程进行Hook

前言 昨天,写了一个Demo, 对指定进程进行Hook, 就是SetWindowsHookEx参数不同. 看msdn+自己做试验,试试就能写出来.demo中假定要Hook键盘和鼠标消息, 用Deb...

无法解析的外部符号 _main,该符号在函数 ___tmainCRTStartup 中被引用

无法解析的外部符号 _main,该符号在函数 ___tmainCRTStartup 中被引用 这个问题表明你新建的是一个main类型函数(控制台程序),而你的程序中有窗口程序,显然是...

c&c++反汇编与逆向分析学习笔记(2)--反汇编静态分析工具IDA

所谓“静态分析”,是相对于前面提到的“动态分析”而言的。在“动态分析”过程中,调试器加载程序,并以调试模式运行起来,分析者可以在程序的执行过程中观察程序的执行流程和计算记过。但是,在实际分析中,很多场...
  • kyt511
  • kyt511
  • 2015年04月05日 17:42
  • 2586

C++反汇编与逆向之识别main函数学习笔记

int main(int argc, char* argv[]) { // main(int 1, char * * 0x00380d70) line 7 // mainCRTStartup(...

逆向工程之表达式优化识别(2)-乘法

乘法优化说明:乘法和加减法相比,优化的可能性就高了很多了,主要是因为计算机对于乘法的运算速度不如加减法或者移位运算,所以如果可以转换成简单加减法和移位运算的乘法,编译器就可以选择不使用乘法,而用加减法...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:逆向 C++-- 2 识别类
举报原因:
原因补充:

(最多只允许输入30个字)