HITB GSEC CTF Windows Pwn: BABYSTACK
HITB GSEC CTF Windows Pwn: BABYSTACK
学习一下Windows下的pwn。
感谢aventador提供的建议:
- https://www.corelan.be/index.php/2009/07/19/exploit-writing-tutorial-part-1-stack-based-overflows/ windows系列的教程(英文)
Setup enviroment
这部分还不是很全面,工具可能不是很全,之后再补充。
python2 python3
windows 10虚拟机,Parallels Desktop用起来挺舒服的
winchecksec,如果报错就装一下vs,windows下的checksec
windbg,Windows下的gdb,命令需要重新学一下,我用的是windbg preview
关于起服务,有两个,一个是AppJailLauncher,不过有时候不太好用,推荐使用Ex师傅的win_server。
Debugging
记录一下windbg里面常用的命令。后续补充整理。
- lmf 列出当前进程中加载的所有dll文件和对应的路径
- r 命令显示和修改寄存器上的值,d 命令显示esp寄存器指向的内存(默认是db查看)
- dc 查看内存
数据查看指令 d{a|b|c|d|D|f|p|q|u|w|W}
d{b|c|d|D|f|p|q}分别是显示:
byte&ASCII, double-word&ASCII,double-word,double-precision,float,pointer-sized,quad-word数据
可以通过加L调整输出的长度
- e 命令可以用来修改内存地址。ed dd可以搭配使用。
- k 用于现实堆栈信息
栈指令k[b|p|P|v]
这四条指令显示的内容类似,但是每个指令都有特色;kb显示ebb, return address, arg;
kp显示所有的参数,但需要Full Symbols或Private PDBSymbols支持。KP与Kp相似,只是KP将参数换行显示了;
Kv用于显示FPO和调用约定;
KD,用于显示Stack的Dump,在跟踪栈时比较有用。
- u 查看反汇编
- x 查找符号的二进制地址,支持通配符
*
- dds 打印内存地址上的二进制值,类似于gdb里面的tele,处理虚函数表
- kn和.frame组合使用切换栈帧查看变量
- bp 设置断点
- 跟踪指令T,TA,TB,TC,WT,P,PA,PC
T 指令单步执行,在源码调试状态下,可指源码的一行,根据不同的选项也可以为一行ASM指令;
TA 单步跟踪到指定地址,如果没有参数将运行到断点处;TB 执行到分支指令,分支指令包括calls, returns, jumps, counted loops, and while loops;
TC 执行到Call指令;
WT 一条强大指令,Trace and Watch Data,对执行流程做Profile;
全一点的参考:https://blog.csdn.net/baidu_37503452/article/details/87874534
上面是一些指令,具体调试的方法(带脚本):
- 在windows中通过win_server起服务
- 在mac中运行pwntools脚本,在需要调试的前面加pause()暂停
- windbg attach process连接进程,通过lm指令拿到代码段基地址,计算出需要下断点的指令地址
- 下断点
g
继续运行,触发断点- 查看内存等信息
Windows Security Mitigations
- DEP
相当于linux的NX,可以通过ROP或者在JIT页绕过。
- ASLR
和Linux的PIE和ASLR不太一样。
- CFG
通过readonly的一个bitmap来判断call的函数是不是合法的。不合法就抛出异常。
基于栈的保护:
- GS
类似于canary。
- SEH
windows的异常处理机制。可以在__try
__except
__finally
中体现。
SEH以链的形式存在,第一个异常处理中未处理相关异常,它就会被传递到下一个异常处理器,直到得到处理。SEH是由_EXCEPTION_REGISTRATION_RECORD结构体组成的链表。
1 | ntdll!_EXCEPTION_REGISTRATION_RECORD |
Next成员指向下一个_EXCEPTION_REGISTRATION_RECORD结构体指针,handler成员是异常处理函数(异常处理器)。若Next成员的值为0xFFFFFFFF,则表示它是链表最后一个结点。
异常函数Handler定义:
1 | EXCEPTION_DISPOSITION __cdecl _except_handler ( |
EXCEPTION_RECORD的结构:
1 | typedef struct _EXCEPTION_RECORD { |
CONTEXT结构:
1 | typedef struct _CONTEXT { |
出现异常 ⇒ 运行SEH ⇒ 将CONTEXT指针传给Handler ⇒ 将CONTEXT.Eip赋值给当前的eip,返回到异常处理函数
如果能够伪造这个CONTEXT.Eip就可以劫持执行流了。
参考:https://bbs.pediy.com/thread-249592.htm
- SafeSEH
SEH加强版,加入了对Handler的合法性检查。
- SEHOP
检查了SEH Chain的终点是不是ntdll!FinalExceptionHandler。
可以泄露栈地址然后维护一下链来绕过。
基于堆的保护暂且不提,之后遇到题目再学习一下吧。
Reversing
建议IDA和Ghidra都看一下,因为有的时候IDA F5看不出来会被隐藏,尤其是system('cmd')
。
windows中后门函数就是
system('cmd')
,或者type flag.txt
直接显示flag文件。
这次的demo题目是HITB GSEC CTF 的BABYSTACK,下载在这里。babystack.7z
先winchecksec看一下保护:
查看main函数:
直接泄露栈地址和main函数地址,所以不用考虑ASLR了。之后有10次机会任意地址泄露。如果输入no,有一个栈溢出。在最后有一个异常处理和后门:
这里的后门直接用IDA F5是看不到的。
在任意地址读的功能中,通过atoi函数来读地址,需要用10进制来输入。否则触发异常,进入SEH。
既然有栈溢出,也有后门函数,代码段地址也知道,是不是可以直接溢出覆盖返回地址为后门呢?不行,因为都被exit函数堵上了。exit函数会切换栈帧,没用。
那如果我们直接将exception handler的值覆盖为后门函数的地址呢?也不行,因为程序有SafeSEH。合法的handler都存在一个表中,会进行校验,后门函数的地址肯定不在表中。
触发异常之后handler会调用scope table里的FilterFunc,将它覆盖为后门可不可以呢?答案是可以的。不过有一系列的检查需要绕过。
Hijack Scope Table
栈溢出直接覆盖返回地址不行,但是可以覆盖到scope table那里。
我们先整理一下异常处理的这些结构体都是拿来干啥的。
SEH布局:
1 | Scope Table |
Scope table结构:
1 | struct _EH4_SCOPETABLE { |
看一下题目中的:
出现exception之后,会调用scope table里的FilterFunc。如果我们将FilterFunc覆盖为后门函数地址似乎就可以getshell。需要注意的是scope table是在rdata上的,本身是写死的。所以我们只能在栈上伪造一个fake scope table,然后修改栈上的scope table指针。
但是我们是win10的环境,还有一些其他的检查。
SEHOP
- 对SEH链做检查,next指向的链末端需要是[0xffffffff, ntdll!FinalExceptionHandler],所以覆盖的时候需要泄露next指针,覆盖成原来一样的,链不能corrupt。
- 至于exception handler直接用程序原来的。
encode scope table
- 栈上的scope table最开始push之后会和cookie做一个异或,所以伪造scope table的话,填scope table要填cookie异或之后的,不过程序本身可以任意地址泄露,cookie位置固定,可以直接拿到
栈上的LocalCookie
触发一场后,还对栈上的值有一个校验
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
72void __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;
}
......
}
}
......
}会根据scope table中的GSCookieOffset和GSCookieXOROffset算一个值,需要等于ebp和cookie异或。
伪造的时候我们直接用原本的scope table,然后计算一下对应LocalCookie的位置和值就可以了。
Payload layout
画一个大概的图:
完整的payload:
1 | from pwn import * |
在调试的时候都是通过偏移来找值的。
在算LocalCookie的时候,有一个0x80是怎么来的:
- stack是ebp-0x9c, IDA里可以看到
- scope table里面GSCookieOffset是0x0FFFFFFE4,也就是-28(-0x1c),也就是LocalCookie是在ebp-0x1c的位置
- 所以填充到LocalCookie前先把0x9c-0x1c=0x80先填充了。
最开始填充0x10个a是给输入的”yes””no”留的空间。