逆向 C++-- 2 识别类

逆向 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 这个函数的实现代码:

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值