C++虚表利用初探

面向对象多态

面向对象具有三大特性:继承、封装、多态。其中多态的前提是封装形成独立体,独立体之间存在继承关系,从而产生多态机制。多态是同一个行为具有多个不同表现形式或形态的能力。

多态体现为父类引用变量指向于子类对象,调用方法时实际调用子类方法。如以下C++代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include<cstdio>
class Fruit{
public:
virtual void printName(){
puts("I am a fruit.");
}
virtual void foo(){
puts("This function is only for base classes!");
}
};
class Apple: public Fruit{
public:
virtual void printName(){
puts("I am an apple.");
}
};
int main(){
Fruit* a = new Apple;
a->printName();
//output:I am an apple.
a->foo();
//output:This function is for base classes!
return 0;
}

此时,a的编译时类型为Fruit*,运行时a->printName()调用的却是Apple类的方法。对于Apple类没有实现的函数foo(),调用的则是基类的foo()。那么,运行时程序是如何实现这一点的呢?

虚表

对于多态的实现,C++标准并无规定。然而绝大多数编译器都选择了同一种(基本是最好的)实现方式——虚表。未避免不同编译器间的实现差别造成误解,以下内容均以Itanium C++ ABI(gcc, clang的参考标准)为例。

在编译时,对含有虚函数的类,编译器会生成一个虚表,虚表中存储了偏移值(用于实现多重继承)、RTTI(Run-Time Type Identification,用于唯一地标识该类型)、虚函数表(存放该类所有虚函数对应的函数指针)。当调用一个虚函数时,程序将通过查表找到对应的函数指针进行调用。

对含有虚函数的类的对象,其初始位置包含一个虚表指针(vptr),这个指针指向类对应的虚函数表。

通过g++ -fdump-lang-class [source code filename]我们可以查看代码中类的结构和类对应的虚表结构。对于本文起始处所示代码,结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Vtable for Fruit
Fruit::_ZTV5Fruit: 4 entries
0 (int (*)(...))0
8 (int (*)(...))(& _ZTI5Fruit)
16 (int (*)(...))Fruit::printName
24 (int (*)(...))Fruit::foo

Class Fruit
size=8 align=8
base size=8 base align=8
Fruit (0x0x7f97c22e3d20) 0 nearly-empty
vptr=((& Fruit::_ZTV5Fruit) + 16)

Vtable for Apple
Apple::_ZTV5Apple: 4 entries
0 (int (*)(...))0
8 (int (*)(...))(& _ZTI5Apple)
16 (int (*)(...))Apple::printName
24 (int (*)(...))Fruit::foo

Class Apple
size=8 align=8
base size=8 base align=8
Apple (0x0x7f97c2188270) 0 nearly-empty
vptr=((& Apple::_ZTV5Apple) + 16)
Fruit (0x0x7f97c22e3f00) 0 nearly-empty
primary-for Apple (0x0x7f97c2188270)

可见类的结构和对应的虚表与上文所述并无差别。

利用

虚表劫持

对于有虚函数的对象,运行时程序根据其虚表指针指向的虚表判断其类型,调用对应的虚函数,因此当对象的虚表指针可以被覆盖时,可以将其指向攻击者伪造的虚函数表,实现劫持。

demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include<cstdio>
#include<stdlib.h>
class Fruit{
public:
virtual void printName(){
puts("I am a fruit.");
}
};
class Apple: public Fruit{
public:
virtual void printName(){
puts("I am an apple.");
}
};
void backdoor(){
system("/bin/sh");
}
int main(){
Fruit* a = new Apple;
unsigned long long ptr=(unsigned long long)backdoor;
**((unsigned long long**)&a)=(unsigned long long)&ptr;
a->printName(); //getshell
return 0;
}

参考资料

https://www.zhihu.com/question/389546003/answer/1194780618