ExploitMe大赛:http://bbs.pediy.com/showthread.php?t=133692

通过ExploitMe第四题为引,详细分析了/GS和safeSEH在汇编代码中的具体实现过程,并在最后给出几种绕过的办法。

Pre1,之前的一些分析:
    程序会根据输入的block size来确定每一分割块所占用的字节数,进而将读取的文件数据分割成固定大小的分割块,而其在读取文件内容后,会在读取的内容末尾添加终止符NULL,然后将读取的总字节数除以block size,得到的就是分割块块数。
    我们可以搜索常见的溢出函数,比如strcpy,strcat之类的函数分析程序对它的调用过程,从而发现可能存在的栈溢出。通过IDA搜索,很容易就发现了这样的一个可能的溢出点:


Pre2,实时的调试:
    我们随意在1.txt中添加一些字符串,使用Immdbg来调试运行,随意输入一个非零的block size来使程序执行到strcpy的调用处。

    由于程序由/GS编译开关生成,很难用覆盖返回地址的方法。故而采用覆盖SEH的方法,必须在GS_cookie检测之前产生异常来调用SEH hanler。本人想到的是除零异常或者是把字符串长度超过0x194,拷贝越过栈底,产生访问异常。

下面对调用strcpy的函数体进行,探究微软/gs和safeSEH在汇编级别的实现细节。

1,/GS技术的实际操作办法
/GS编译选项会在函数的开头和结尾添加代码来阻止对典型的栈溢出漏洞(字符串缓冲区)的利用。 当应用程序启动时,程序的cookie(4字节(dword),无符号整型)被计算出来(伪随机数)并保存在 加载模块的.data节中,在函数的开头这个cookie被拷贝到栈中,位于EBP和返回地址的正前方(位于返回地址和局部变量的中间)。
[buffer][cookie][savedEBP][savedEIP]

其中Scope table 的结构是
    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)();
        };
    };
我们可以在内存窗口查看:

可以猜测,004010aa 正好是__except(EXCEPTION_EXECUTE_HANDLER)的指令地址,
而004010b0 正好是__except 开始的{}内的地址。

在函数的结尾处,程序会把这个cookie和保存在.data节中的cookie进行比较。
如果不相等,就说明进程栈被破坏,进程必须被终止。 

在cookie检测函数里面,其实就是一个简单的判断:


2,safeSEH技术的实际操作办法

在调试exploit.exe时,异常发生时,我们按shift+F7就能进入异常刚发生的系统代码处,KiUserExceptionDispatcher()函数的开始处。(类此的软件异常是通过直接或者间接调用内核服务NtRaiseException而产生的。而用户态中可以通过RaiseException API,或者Try-catch等高级语言来调用这个内核服务,而通过RaiseException来登记软件异常的过程可以简单表述如下:RaiseException在初始化一个EXCEPTION_RECORD结构体之后,开始调用NTDLL中的 RtlRaiseException; RtlRaiseException在初始化CONTEXT结构体之后,开始调用内核中NtRaiseException, NtRaiseException再调用另外一个内核函数KiRaiseException。接下来KiRaiseException会调用 KiDispatchException开始异常的分发)

这个时候其实系统已经帮你做了很多工作了;比如EXCEPTION_POINTERS就在栈顶,如下图:

这里我们给上KiUserExceptionDispatcher函数的伪代码:

KiUserExceptionDispatcher( PEXCEPTION_RECORD pExcptRec, CONTEXT * pContext )  
{  
    DWORD retValue;   
    // 注意:如果异常被处理,那么 RtlDispatchException 函数就不会返回  
    if ( RtlDispatchException( pExceptRec, pContext ) )  
        retValue = NtContinue( pContext, 0 );  
    else  
        retValue = NtRaiseException( pExceptRec, pContext, 0 );   
    EXCEPTION_RECORD excptRec2;  
    excptRec2.ExceptionCode = retValue;  
    excptRec2.ExceptionFlags = EXCEPTION_NONCONTINUABLE;  
    excptRec2.ExceptionRecord = pExcptRec;  
    excptRec2.NumberParameters = 0;   
    RtlRaiseException( &excptRec2 );  

}  

我们safeSEH检测的工作就在RtlDispatchException中完成。

当异常发生时,异常分发器创建自己的栈帧。它会把SEH Handler成员压入新创的栈帧中(作为函数起始的一部分)在SEH结构中有一个域是Establisher Frame 。这个域指向异常注册记录 (next  SEH )的地址并被压入栈中,当一个例程被调用的时候被压入的这个值都是位于ESP+8的地方。

我们经常用pop pop ret 串的地址覆盖SE Handler来达到跳转栈上的目的:
-第一个pop将弹出栈顶的4 bytes
-接下来的pop继续从栈中弹出4bytes
-最后的ret将把此时ESP所指栈顶中的值(next SEH的地址)放到EIP中。

3,绕过方法

绕过/GS的方法基本上都通过覆盖SEH来实现,在cookie未检测之前实现溢出。而覆盖SEH有两种,一种是2中所述;直接用pop|pop|retn指令地址来覆盖SEH Handler,从而跳到栈上执行。另外一种就是硬编码shellcode的开始地址,植入SEH Handler,在异常处理函数执行时,直接执行shellcode。

    a,可以用pvefindaddr插件跑,来获取加载模块之外的地址

    b,可以采用shellcode地址硬编码,当然不能用栈地址里面的shellcode。搜索我们发现ctype.nls里面有1.txt文件内容的复制品,我们硬编码地址设成这个模块里的shellcode的起始地址,就能绕过safeSEH。缺点就是通用性极差。

    c,在这里强调一点,插件搜索出来的结果可能是不全的,也可能存在其他的地址上有pop|pop|retn满足条件。比如本例可以发现:

 在高地址处,不受safeSEH的限制。

4,小结

     /GS和safeSEH都有较成熟的绕过机制,甚至有插件来进行傻瓜式的搜索。但是我们仍然需要详细了解微软设置这两个机制的详细实现过程,需要我们耐心的调试分析。最后,插件不是万能的。方法知道的越多才会越灵活。懂得原理才是王道!