【技术分享】逆向C++虚函数(一)

阅读量254646

|评论2

|

发布时间 : 2016-12-26 11:04:27

x
译文声明

本文是翻译文章,文章来源:alschwalm.com

原文地址:https://alschwalm.com/blog/static/2016/12/17/reversing-c-virtual-functions/

译文仅供参考,具体内容表达以及含义原文为准。

https://p2.ssl.qhimg.com/t019d4b63632d21830a.jpg

翻译:维一零

预估稿费:260RMB(不服你也来投稿啊!)

投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿

前言

在网上可以找到一些关于逆向C++的帖子,并且经常或多或少的涉及虚函数。然而,我想花一些时间详细的写点关于虚函数的处理,基于代码‘enterprisy’。这些代码通常可以包括成千上万的类和大量的类型层次结构,所以我认为一些逆向它们的技术值得好好描述。但是在那之前我想先通过一些简单的例子来进行。如果您已经熟悉虚函数逆向,我想可以直接进入第2部分。

值得注意的是以下几点:

代码编译没有RTTI和异常(稍后将讨论RTTI)

我使用的是32位x86平台

二进制文件已通过strip处理(无相关调试信息)

大多数虚函数的实现细节都没有标准化,并且不同编译器之间可能会有所不一样。出于这个原因,我们将专注于GCC的行为。

总之,我们将看到的二进制文件已经这样编译 g++ -m32 -fno-rtti -fnoexceptions -O1 file.cpp然后去除调试信息 strip.


我们的目标

在大多数情况下,我们无法“反虚拟化”一个虚函数调用,因为那些相关信息在运行时之前不会出现。相反,这个练习的目标是确定哪些函数可能在一个特定的地方被调用。在后面部分,我们将专注于缩小这些可能的范围。


基础

我假设您熟悉编写c++程序,但可能不清楚c++的内部实现细节。那么,让我们首先看看编译器如何实现虚函数。假设我们有以下类:

#include <cstdlib>
#include <iostream>
struct Mammal {
  Mammal() { std::cout << "Mammal::Mammaln"; }
  virtual ~Mammal() { std::cout << "Mammal::~Mammaln"; };
  virtual void run() = 0;
  virtual void walk() = 0;
  virtual void move() { walk(); }
};
struct Cat : Mammal {
  Cat() { std::cout << "Cat::Catn"; }
  virtual ~Cat() { std::cout << "Cat::~Catn"; }
  virtual void run() { std::cout << "Cat::runn"; }
  virtual void walk() { std::cout << "Cat::walkn"; }
};
struct Dog : Mammal {
  Dog() { std::cout << "Dog::Dogn"; }
  virtual ~Dog() { std::cout << "Dog::~Dogn"; }
  virtual void run() { std::cout << "Dog::runn"; }
  virtual void walk() { std::cout << "Dog::walkn"; }
};

并且我们有一些代码使用到它们:

int main() {
  Mammal *m;
  if (rand() % 2) {
    m = new Cat();
  } else {
    m = new Dog();
  }
  m->walk();
  delete m;
}

当然, m是否是一只猫或狗取决于 rand的输出。编译器无法提前知道这个,所以它是如何调用正确的函数呢?答案是,对于每种有虚函数的类型,编译器将一个被称为vtable的函数指针表插入生成的二进制文件。这种类型的每个实例对象将多出一个被称为vptr的额外成员指向正确的vtable。用来以正确值初始化这个指针的代码将被添加到构造函数里。

然后,当编译器需要调用一个虚函数,它可以通过对象的vtable来访问正确的入口地址并进一步调用。这意味着在每种相关类型里表中的入口地址有着相同的次序(如每个类的 run可以在索引1的入口地址,每一个 walk在索引2的入口地址,依此类推)。

于是我们希望在二进制文件里找到哺乳动物,猫和狗的三个vtable表。我们可以通过 .rodata段里相邻函数的偏移量快速的找到它们:

http://p3.qhimg.com/t01a1ad4c0d1e0132aa.png

IDA并不总是能很好地检测在rodata段的函数地址,你可能需要找一会才能看到第一个表。

程序的主函数是什么样的呢?IDA反编译看一下:

http://p0.qhimg.com/t014ea266fbe5cf9f17.png

我们可以看到每个分支分配了4字节内存。这是有理由的,因为在类型结构里唯一的数据是由编译器添加的vptr。我们还可以看到虚函数调用15行和17行。首先,编译器取值(获取vpt)并且通过加12来访问vtable表里的第四个入口地址。第17行获取了vtable表里的第二个入口地址,然后程序从vtable表里调用了这个函数指针。

http://p0.qhimg.com/t01720be05e40392416.png

http://p4.qhimg.com/t01d6dc8c79007add88.png

回头看看这些vtable表,第四个入口 sub_80487AA, sub_804877E,和 ___cxa_pure_virtual。如果我们看看两个“sub_”函数的代码我们知道它们定义了狗和猫的 walk(如上图所示)。经分析后, ___cxa_pure_virtual函数是属于哺乳动物的虚表。这是有理由的,因为哺乳动物没有定义 walk,当一个函数是纯虚函数时这些“纯虚”入口地址被GCC插入(毫不奇怪地)。因此,可以确定vtable表1是属于哺乳动物对象的,而表2是属于猫、表3是属于狗。

但似乎有点奇怪的是每个vtable表里有5个入口地址,而其中只有4个虚函数在使用:

run

walk

move

析构函数

这里多出了一个“额外”的析构函数。这是因为GCC会插入多个在不同的情况下使用的析构函数。这些析构函数中,第一个只会破坏对象的成员,第二个将删除对象的已分配内存(这是在上例第17行中被调用的版本)。在某些情况下可能有第三种版本,如在某些虚函数继承时。

通过回顾上述内容的“sub_”函数,我们发现虚表的布局如下:

https://p3.ssl.qhimg.com/t0199704637322b96ac.png

但是,注意到哺乳动物虚表中的前两项为零。这是新版本GCC的一个古怪现象。编译器会在有纯虚函数的类中将析构函数项替换为空指针(如抽象类)。

记住这些,然后让我们做一些重命名。之后就剩下:

http://p1.qhimg.com/t019329da6a52707f63.png

注意,因为既不是猫也不是狗实现的 move,这个是它们从哺乳动物那里继承的定义,所以在它们的虚表中这个move函数的入口地址是相同的。


结构

在这一点上开始定义一些结构是非常有用的。我们已经看到目前哺乳动物,猫和狗的数据结构里只有一个唯一的成员vptrs。所以我们可以快速地定义这些结构:

http://p6.qhimg.com/t01ebd88b6c6d0add56.png

下一步会更复杂一点。我们要为每个虚表创建一个结构。这里的目标是获取解码器的输出以告诉我们如果 m有一个特定的类型将会调用什么函数。我们可以通过这些可能性循环检查所有的选项。

为了实现这一点,这个结构的成员会有相应的函数名称将被指出,就像这样:

http://p6.qhimg.com/t018d4a74e6d62de579.png

您将需要为每个通过 Vtable类型通讯的结构设置对应vptr的类型。例如, Cat类vptr的类型应该是 CatVtable*。另外,我已经设置每个vtable入口的类型是一个函数指针。这将有助于IDA正确的显示内容。因此 Dog__run元素的类型应该是 void (*) (Dog*)(因为这是 Dog__run签名)。

如果回到main函数的反编译代码,我们现在可以重命名局部变量 m,并设置其类型 Cat*或 Dog*。之后就可以看到:

http://p6.qhimg.com/t01b6a89515662c6e7a.png

现在我们可以很容易地看到调用方可能的函数调用。如果 m是一个 Cat那么第15行就会调用 Cat__walk,如果是一个 Dog它就将调用 Dog__walk。显然这是一个简单的例子,但这是通用的思路。

我们也还可以设置 m的类型为 Mammal*,但如果这样做我们将会看到一些问题

http://p4.qhimg.com/t01009d7831ce1c9a45.png

注意到如果 m的真正类型是 Mammal然后在第15行的调用将是一个纯虚函数。这种事情不应该发生。并且在第17行这里还有一个调用到空指针显然页会导致异常问题。所以我们可以得出这样的结论: m不能一个 Mammal类型。

这可能看起来很奇怪,因为实际上 m是声明为 Mammal*。然而,那个类型是编译时类型(即静态类型)。我们感兴趣的是 m的动态类型(或运行时类型),因为那才能决定在一个虚拟函数调用里哪个函数将被调用。实际上,对象的动态类型不能是抽象类。因此如果一个给定的虚表包含一个 ___cxa_pure_virtual函数,那么它不是一个候选目标,你可以忽略它。我们可以不为哺乳动物创建一个虚表结构,因为它永远不会被使用(但我希望看到为什么它是有用的)。

因此猫或狗将成为动态类型,我们通过看它们的vtable条目也可以知道哪些函数将被调用。这是虚函数逆向相关的基础知识。在接下来的部分我们将会看到如何处理更大的代码和更复杂的场景。

本文翻译自alschwalm.com 原文链接。如若转载请注明出处。
分享到:微信
+12赞
收藏
维一零
分享到:微信

发表评论

内容需知
  • 投稿须知
  • 转载须知
  • 官网QQ群8:819797106
  • 官网QQ群3:830462644(已满)
  • 官网QQ群2:814450983(已满)
  • 官网QQ群1:702511263(已满)
合作单位
  • 安全客
  • 安全客
Copyright © 北京奇虎科技有限公司 360网络攻防实验室 安全客 All Rights Reserved 京ICP备08010314号-66