SafeSEH原理及绕过技术浅析

作者:magictong

时间:2012年3月16日星期五

摘要:主要介绍SafeSEH的基本原理和SafeSEH的绕过技术,重点在原理介绍。

关键词:SafeSEH;绕过技术;异常处理

目录

前言

SafeSEH的保护原理

(1)      二进制层面

(2)      系统层面

怎么关掉编译器的SafeSEH支持

怎样检测一个PE文件是否启用了SafeSEH

绕过方法简介

参考文献

前言

设计SafeSEH保护机制的目的,以为了防止那种攻击者通过覆盖堆栈上的异常处理函数句柄,从而控制程序执行流程的攻击。

自Windwos XP SP2之后,微软就已经引入了SafeSEH技术。不过由于SafeSEH需要编译器在编译PE文件时进行特殊支持才能发挥作用,而xpsp2下的系统文件基本都是不支持SafeSEH的编译器编译的,因此在xpsp2下,SafeSEH还没有发挥作用(VS2003及更高版本的编译器中已经开始支持)。

从Vista开始,由于系统PE文件基本都是由支持SafeSEH的编译器编译的,因此从Vista开始,SafeSEH开始发挥他强大的作用,对于以前那种简单的通过覆盖异常处理句柄的漏洞利用技术,也就基本失效了。

SafeSEH的保护原理

SafeSEH的基本原理很简单,即在调用异常处理函数之前,对要调用的异常处理函数进行一系列的有效性校验,如果发现异常处理函数不可靠(被覆盖了,被篡改了),立即终止异常处理函数的调用。不过SafeSEH需要编译器和系统双重支持,缺少一个则保护能力基本就丧失了。下面从两个方面来阐述怎样来实现SafeSEH。

(1)二进制层面

首先我们先看看编译器做了些什么事情(通过启用链接选项/SafeSEH即可使编译出来的二进制文件具备SafeSEH功能,微软VS2003及以后的编译器已经默认支持)。在编译器生成二进制IMAGE的时候,把所有合法的SEH函数的地址解析出来,在IMAGE里生成一张合法的SEH函数表,用于异常处理时候进行严格的匹配检查。可以使用VC下面的dumpbin工具查看一个二进制文件的config信息,这样调用dumpbin /loadconfig file_all_path_filename。

SafeSEH原理及绕过技术浅析-LMLPHP

输出的可能是下面这样(注:tttt.exe使用vs2005编译):

Dump of file H:\Prj_N\tttt\Release\tttt.exe

File Type: EXECUTABLE IMAGE

Section contains the following load config:

00000048 size

0 time date stamp

0.00 Version

0 GlobalFlags Clear

0 GlobalFlags Set

0 Critical Section Default Timeout

0 Decommit Free Block Threshold

0 Decommit Total Free Threshold

00000000 Lock Prefix Table

0 Maximum Allocation Size

0 Virtual Memory Threshold

0 Process Heap Flags

0 Process Affinity Mask

0 CSD Version

0000 Reserved

00000000 Edit list

00403018 Security Cookie

00402360 Safe Exception Handler Table

1 Safe Exception Handler Count

Safe Exception Handler Table

Address

--------

004018A1  __except_handler4

Summary

1000 .data

1000 .rdata

1000 .rsrc

1000 .text

注意里面加粗标红的部分,这就是该二进制文件里面的SEH异常处理函数地址表。上面的输出实际上涉及如下的一个结构,是保存在二进制文件里面的一份配置表:

#include <windows.h>

extern DWORD_PTR __security_cookie;  /* /GS security cookie */

/*

* The following two names are automatically created by the linker for any

* image that has the safe exception table present.

*/

extern PVOID __safe_se_handler_table[]; /* base of safe handler entry table */

extern BYTE  __safe_se_handler_count;  /* absolute symbol whose address is

the count of table entries */

typedef struct {

DWORD       Size;

DWORD       TimeDateStamp;

WORD        MajorVersion;

WORD        MinorVersion;

DWORD       GlobalFlagsClear;

DWORD       GlobalFlagsSet;

DWORD       CriticalSectionDefaultTimeout;

DWORD       DeCommitFreeBlockThreshold;

DWORD       DeCommitTotalFreeThreshold;

DWORD       LockPrefixTable;            // VA

DWORD       MaximumAllocationSize;

DWORD       VirtualMemoryThreshold;

DWORD       ProcessHeapFlags;

DWORD       ProcessAffinityMask;

WORD        CSDVersion;

WORD        Reserved1;

DWORD       EditList;                   // VA

DWORD_PTR   *SecurityCookie;

PVOID       *SEHandlerTable;

DWORD       SEHandlerCount;

} IMAGE_LOAD_CONFIG_DIRECTORY32_2;

const IMAGE_LOAD_CONFIG_DIRECTORY32_2 _load_config_used = {

sizeof(IMAGE_LOAD_CONFIG_DIRECTORY32_2),

0,

0,

0,

0,

0,

0,

0,

0,

0,

0,

0,

0,

0,

0,

0,

0,

&__security_cookie,

__safe_se_handler_table,

(DWORD)(DWORD_PTR) &__safe_se_handler_count

};

(2)系统层面

基本过程如下(XP SP2和VISTA一样)。

加载准备过程:

加载PE文件时,定位和读出合法SEH函数表的地址(如果该IMAGE是不支持SafeSEH的,则这个SEH函数表的地址为0),并使用共享内存中的一个随机数加密。将加密后的SEH函数表地址,IMAGE的开始地址,IMAGE的长度,合法SEH函数的个数,作为一条记录放入ntdll(ntdll模块是进行异常分发的模块)的加载模块数据内存中。

异常发生后,异常处理过程如下(RtlDispatchException框架伪码):

void RtlDispatchException(...)

{

if (exception record is not on the stack)

goto corruption;

if (handler is on the stack)

goto corruption;

if (RtlIsValidHandler(handler, process_flags) == FALSE)

goto corruption;

// execute handler

RtlpExecuteHandlerForException(handler, ...)

...

}

RtlDispatchException()这个函数的检测主要分三步,首先检查异常处理节点是否在栈上,如果不在栈上程序将终止异常处理,其次检查异常处理句柄是否在栈上,如果在栈上程序将止异常处理,这两个检测可以防止那种在堆上伪造异常链和把shellcode放置在栈上的情况。最后检测handler的有效性,这才是SafeSEH的重点。

下面看一下RtlIsValidHandler的伪码(vista sp1):

BOOL RtlIsValidHandler(handler)

{

if (handler is in an image)

{

// 在加载模块的进程空间

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)

{

// 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)

{

// 在加载模块内存之外,并且是可执行页

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;

}

对上面的伪码的理解,请看代码注释和流程图:

SafeSEH原理及绕过技术浅析-LMLPHP

伪码里面的ExecuteDispatchEnable和ImageDispatchEnable位标志是内核KPROCESS结构的一部分,这两个位用来控制当异常处理函数在不可以执行内存或者不在异常模块的映像(IMAGE)内时,是否执行异常处理函数。这两个位的值可以在运行时修改,不过默认情况下如果进程的DEP被关闭,则这两个位置1,如果进程的DEP是开启状态,则这两个位被置0。

在进程的DEP是开启的情况,有两种异常处理函数被异常分发器认为是有效的:

(a)异常处理函数在进程映像的SafeSEH表中,并且没有NO_SEH标志。

(b)异常处理函数在进程映像的可执行页,并且没有NO_SEH标志,没有SafeSEH表,没有.NET的ILonly标志。

在进程的DEP关闭的情况下,有三种情况异常处理函数被异常分发器认为是有效的:

(a)异常处理函数在进程映像的SafeSEH表中,并且没有NO_SEH标志。

(b)异常处理函数在进程映像的可执行页,并且没有NO_SEH标志,没有SafeSEH表,没有.NET的ILonly标志。

(c)异常处理函数不在当前进程的映像里面,但是不在当前线程的堆栈上。

这里的伪码是非常简单的,还有很多值得探讨的问题,譬如ntdll里面,当前异常处理句柄是怎么和SEH表进行对比的等等,不过这暂时不列入讨论。

怎么关掉编译器的SafeSEH支持

虽然我不知道你为什么要这么做,而且我觉得很疯狂,但是方法还是有的,在编译器的属性框Liker|CommandLine的Additional options 加入/SAFESEH:NO即可,见下图。

SafeSEH原理及绕过技术浅析-LMLPHP

怎样检测一个PE文件是否启用了SafeSEH

前面介绍过数据目录里面的一个结构(IMAGE_LOAD_CONFIG_DIRECTORY),该结构的成员SEHandlerTable是指向合法SEH处理程序地址列表的指针,成员SEHandlerCount是数目。而IMAGE_LOAD_CONFIG_DIRECTORY这个结构体只有/SAFESEH选项设置了才存在,因此,就可以根据它来判断PE文件是否加了/SAFESEH链接选项。这个结构在PE中的偏移由PE附加头IMAGE_DATA_DIRECTORY 数组的第11项指定。

#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG  10  // Load Configuration Directory

绕过方法简介

(1)利用堆地址覆盖SEH结构绕过SafeSEH

上面讲过,在禁用DEP的进程中,异常分发器允许SEH handler位于除栈空间之外的非映像页面。也就是说我们可以把shellcode放置在堆中,然后通过覆盖SEH跳至堆空间以执行shellcode,这样即可绕过SafeSEH保护。

(2)利用没有启用SafeSEH保护的模块绕过SafeSEH

在介绍原理时讲过,在国内,目前大部分的PC都是安装的Windows XP,也就是说对于大部分Windows操作系统,其系统模块都没有受到SafeSEH保护,可以选用未开启SafeSEH保护的模块来利用,另外,现在还有很多VC6编译的软件,这些软件本身和自带的dll文件,都是可能没有SafeSEH保护的。这时就可以使用它里面的指令作为跳板来绕过SafeSEH。

(3)利用加载模块之外的地址绕过SafeSEH

同样是根据SafeSEH的原理可知,对于加载模块之外的地址,SafeSEH同样是不进行有效性检测的(当然假设是DEP是关闭的,或者DEP已经被绕过)。

注:绕过方法这里没有细讲,原因是没有找到很好的例子,在《0day安全:软件漏洞分析技术》上面有自己书籍作者自己写的例子。以后这块再详说。

参考文献

[1] Preventing the Exploitation of Structured Exception Handler (SEH) Overwrites with SEHOP

http://blogs.technet.com/b/srd/archive/2009/02/02/preventing-the-exploitation-of-seh-overwrites-with-sehop.aspx(可以列入翻译计划)

[2] SafeSEH笔记http://pstgroup.blogspot.com/2007/08/tipssafeseh.html

[3] /SAFESEH (Image has Safe Exception Handlers)

http://msdn.microsoft.com/en-us/library/9a89h429(VS.80).aspx

[4] 0day安全:软件漏洞分析技术(第二版)

[5] Bypassing Browser Memory Protections

[6] pecoff_v8

https://blog.csdn.net/magictong/article/details/7517630

写的挺好,但IMAGE_LOAD_CONFIG_DIRECTORY这个结构体并不是只有/SAFESEH选项设置了才存在,更准确的检查是检测这个结构不存在,或才存在时SEHandlerTable是否为0

04-14 12:05