设为首页 加入收藏

TOP

C++虚函数逆向教程(1)(一)
2016-12-30 08:14:41 】 浏览:531
Tags:函数 逆向 教程

C++虚函数逆向教程(1):关于C++程序的逆向,网络上已经有很多文章了,这些文章也或多或少的提到了虚函数。然而,这篇文章中,我想着重介绍一下,在代码量比较大的程序中。

我们应该如何处理虚函数。这些程序里,通常存在着数以千计的类,类型之间的关系也很复杂,因此在我看来,分享处理这些类的经验是很有价值的。但在我介绍这些复杂的案例之前,我会先介绍一些简单的栗子。如果你已经对虚函数的逆向有了一些了解,那么可以直接去看本文的第二部分。(译者注:截止本文翻译结束前,作者尚未发布第二部分)
此外,注意以下几点:
示例代码编译时没有使用RTTI,也没有使用异常机制
下文中的样例在x86平台上测试
所有的二进制文件已经被strip了(剥离了符号)
大多数虚函数的实现细节是没有特定标准的,因此不同编译器对此的处理方法很可能不一致。因此,我们将着重讨论GCC编译器的行为
另外,我们的文件编译时的命令行参数为g++ -m32 -fno-rtti -fnoexceptions -O1 file.cpp,输出文件用strip处理过。
目标
大多数情况下,我们是没办法让一个对虚函数的调用,变换为一个对非虚函数的调用的。这是因为,我们需要的信息在静态编译中是不全面的,只有在运行时才会存在。因此,这段文章的目标,是判断哪些函数会在特定的情况下被调用。稍后我们会学习其他的技巧,来进一步缩小范围。
基本功
假设你已经比较熟悉C++了,但对它的具体实现还不太熟悉,那么就先来看一看编译器是如何实现虚函数的。现在有这么一段代码:
// file reversing-1.cpp
#include
#include
struct Mammal {
Mammal() { std::cout "Mammal::Mammal\n"; }
virtual ~Mammal() { std::cout "Mammal::~Mammal\n"; };
virtual void run() = 0;
virtual void walk() = 0;
virtual void move() { walk(); }
};
struct Cat : Mammal {
Cat() { std::cout "Cat::Cat\n"; }
virtual ~Cat() { std::cout "Cat::~Cat\n"; }
virtual void run() { std::cout "Cat::run\n"; }
virtual void walk() { std::cout "Cat::walk\n"; }
};
struct Dog : Mammal {
Dog() { std::cout "Dog::Dog\n"; }
virtual ~Dog() { std::cout "Dog::~Dog\n"; }
virtual void run() { std::cout "Dog::run\n"; }
virtual void walk() { std::cout "Dog::walk\n"; }
};
然后还有这么一段调用他们的代码:
// file reversing-2.cpp
int main() {
Mammal *m;
if (rand() % 2) {
m = new Cat();
} else {
m = new Dog();
}
m->walk();
delete m;
}
很显然,m是cat类还是dog类,取决于rand函数的输出。这是无法被编译器提前预测的,那么编译器是怎么调用合适的walk函数呢?
由于我们把walk函数声明为了虚函数,编译器会在程序所处的内存空间中,插入一张含有函数指针的表,称为“虚函数表”,也就是“虚表”(vtable);而在实例化类的时候,每个对象会多出一个称作“虚指针”(vptr)的成员,这个虚指针指向正确的虚表,初始化这个虚指针的代码会被添加到类的构造函数中。这样,当编译器需要调用虚函数的时候,就可以通过虚指针找到对应的虚表,从而找到合适的函数,进而调用他。这也意味着,具有同一个父类的子类,其虚表中函数的顺序也应该是一致的。比如,在上面的例子中,Dog和Cat类都是Mammal类的子类,那么Dog类的虚表中,第一项指向的是Dog::run,第二项指向的是Dog::walk,而Cat类的虚表中,第一项指向的是Cat::run,第二项则是Cat::walk。
通过在.rodata段中寻找指向函数的偏移量,我们可以在二进制文件中轻松地找到Mammal,Cat和Dog类的虚表,如下图所示。

主函数这时候被编译成了这个样子:

可以看到,程序在实例化类的时候,为每个对象申请了4字节的内存空间,这和我们预期是相符的(因为每个类中没有数据成员,而编译器为我们添加了vptr)。我们也可以在第15行和第17行中,看到对虚函数的调用过程。在第15行中,编译器先对指针解引用,从而获得vptr;接下来计算vptr+12,也就是访问虚表中的第四项,而第17行则是访问虚表中的第二项。之后,程序调用虚表中对应项目指向的函数。

我们再看一下,三张虚表的第四项分别是sub_80487AA、sub_804877E和___cxa_pure_virtual。前两个函数如上图所示,分别是Dog和Cat类中对walk函数的实现,那么最后一个函数一定是Mammal类中对应的实现了。这很正常,因为Mammal类中没有定义walk的具体实现,而是声明其为“纯虚函数”,那么就GCC就帮我们插入了一个默认的项目。那么到这里我们知道了,虚表1是属于Mammal类的,而2和3分别是属于Cats和Dogs类的。
但比较奇怪的是,每个vtable中含有五个项目,而在我们的程序中,每个类只有四个虚函数,他们分别是:
run
walk
move
析构器
实际上,多出来的项目是一个“额外的”析构器。这是因为,GCC会在不同的场景中,使用不同的析构函数。前者只是简单的把实例对应的所有成员都清理掉,而后者则会同时要求回收为这个实例分配的内存,这也就是在第17行调用的函数。在某些涉及到虚继承的情况下,还会有第三种析构器。
那么现在我们搞清楚了虚表的布局:

| Offset | Pointer to |
|--------+-------------|
| 0 | Destructor1 |
| 4 | Destructor2 |
| 8 | run |
| 12 | walk |
| 16 | move |
值得注意的是,虚表的前两个项目是空指针。这是新版本GCC的一个特征,当类具有纯虚函数时,编译器会将其析构器替换为空指针。
现在可以考虑给他们重命名,以方便 阅读

注意,由于Cat和Dog类都没有实现move方法,因此他们直接采用了Mammal类中的方法,虚表中的项目值也一样。
结构体
为了研究方便,我们定义几个结构体。刚才我们已经看到了Mammal、Cat和Dog类的唯一成员是他们的虚指针,因此做定义如下:

接下来就比较麻烦了:我们需要为每个虚表创建一个结构体。这是为了让反编译器能够更清楚的向我们展示,如果m具有一个特定类型的话,哪个函数应该被调用。这样,我们在阅读代码的时候就可以排除很多干扰了。那么为了达成这个目的,我们需要把结构体中的每个项目命名为对应的函数名:

接下来,把类中虚指针的类型调整为指向对应虚表的指针。比如,Cat类的vptr成员,应该使用CatVtable*类型。此外,我还把虚表中每个项目的类型都改为了函数指
首页 上一页 1 2 下一页 尾页 1/2/2
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇数字组合问题 下一篇雷德算法

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目