第8章:Windows 下的异常处理——SafeSEH、SEHOP

异常处理程序的安全性
  SEH的结构是存储在栈中的,而栈中数据的安全性有时无法得到保证,例如程序接受恶意输入导致溢出攻击时,栈中的 SEHandler可能被覆盖为非法过程,从而执行攻击者预设的功能代码。为了防止此类攻击,微软提供了 SafeSEH 机制和 SEHOP 机制,以阻止那些非法的 SEHandler 程序被执行。


1. SafeSEH机制

这是微软从 Windows XP SP2 开始引人的一种安全机制,由操作系统和编译器联合提供,即由编译器提供 SEH 基础数据,由操作系统在产生异常时进行验证。

(1) 编译的工作

  从 Visual Studio .NET 2003 开始,在编译PE文件时加入了一个 SafeSEH 开关。如果编译时打开了这个开关,那么编译器会在PE头的 DllCharacteristics 中加人一个标志,并在编译阶段提取所有异常处理程序的相对虚拟地址(RVA),将它放入一个表。这个表的位置是由PE头部 IMACE_OPTIONAL_HEADER 结构中数据目录的第10项指定的(Load_Config Directory),相关定义如下。

第8章:Windows 下的异常处理——SafeSEH、SEHOP

目录实际指向一个结构体:

第8章:Windows 下的异常处理——SafeSEH、SEHOP

 第8章:Windows 下的异常处理——SafeSEH、SEHOP

  SEHandlerTable 是指向一个 SEH 处理函数 RVA 的表格,SEHandlerCount 是这个表格的项数,它指出了有几个有效的 SEHandler。当 PE 被载入时, PE 的基址、大小、SEHandlerTable (表格的地址)、SEHandlerCount (长度)会保存在 ntdll.dll 的一个表格中。当异常发生时,系统会根据每个 PE 的基址和大小检查当前 SEHandler 处理函数属于哪一个 PE 模块,然后取出相应的表格地址和长度。在载入时就已经取出,载入后 SEHandlerTable 和 SEHandlerCount 就没有用处了,所以对它进行修改不会影响系统对 SEHandler 的验证结果。

RtlIsValidHandler 函数:

第8章:Windows 下的异常处理——SafeSEH、SEHOP

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

以下的异常处理函数是有效的:

进程的 DEP 开启,异常处理函数在进程映像的 SafeSEH 表中,没有 NO_SEH 标志。

进程的 DEP 开启,异常处理函数在进程映像的可执行页中,没有 NO_SEH 标志,没有 SafeSEH 表,也没有 .NET 的 ILonly 标志。

进程的 DEP 关闭,异常处理函数在进程映像的 SafeSEH 表中,没有 NO_SEH 标志。

进程的 DEP 关闭, 异常处理函数不在当前进程的映像里,也不在当前线程的栈上。

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

  如果 SEHandler 处于动态申请的内存中,因为它不处于任何一个 PE Image 内,所以 SEH 是没有任何限制的;否则,不在相应的表格中,会导致 SEH 部分的异常处理被中止,即跳过后面所有 SEH 节点的遍历。在 Visual C++ 中使用的 C 异常处理 _try/_except (_finally) 及 C++ 异常处理 try/catch/finally 等SEH处理函数,都会被编译器自动放入该表格。但如果使用 inline asm 对 fs:[0] 进行操作,设置 SEH 就是无效的。

2.SEHOP

  SEHOP 是微软为了进一步增强 SEH 处理程序的安全性从 2009年开始在 Windows Server 2008SPO、Windows Vista SP1 和 Windows 7 及后续版本中加人的一种保护机制,它的全称是“StructuredException Handling Overwrite Protection”( SEH 覆写保护机制),可作为 SEH 的扩展,用于检测SEH是否被覆写。

SEHOP的核心检测主要包括如下两点。
① 检测 SEH链的完整性,即每一个节点都必须在栈中,并且都可以正常访问。
② 检测最后一个节点的异常处理函数是不是位于ntdll 中的ntdll!FinalExceptionHandler()。

  一般在使用 SEH 攻击执行 Shellcode 时,通常是用"jmp 06 pop pop ret"命令来覆盖 SEH 结构的,此时从当前SEH节点已经无法正确指向下一个 SEH 节点。只要SEH结构链表的完整性遭到了破坏,SEHOP就能检测到异常,从而阻止Shellcode的运行,

第8章:Windows 下的异常处理——SafeSEH、SEHOP

  开启 SEHOP 保护之后,在 SEH 链的最后增加了一个节点,此时与倒数第2个节点并不影响。执行到倒数第2个节点会执行 kernel32!UnhandledExceptionHandler 函数进行终结处理,此时最后一个节点的异常处理函数根本不会发挥作用,它只在对 SEHandler 进行验证时起辅助作用。即使基于某些原因或者在某些特殊的线程中,执行到了最后一个节点的异常处理函数ntdll!FinalExceptionHandler,该函数的内部仍然会调用 kernel32.dll 中的 UEF 函数或 ntdll.dll 自己的 UEF 函数,所以这与顶层异常处理的过程并不矛盾。

  SEHOP 的保护是系统级的,将更难绕过,而且不需要修改原有的应用程序,所以对性能的影响基本可以忽略(因为只有在触发异常时才会触发SEHOP保护逻辑)。但是,该方法并不是万无一失的,如果攻击者能够事先操纵栈中的数据,同样可以伪造节点以保证整个 SEH 链有效,并到达最终的 Safe 节点。

向量化异常处理(Vectored  Exception Handling)

这是在 Win XP 以上新增的一种异常处理机制。

  向量化异常处理的基本理念与 SEH 相似,也是注册一个回调函数,当发生异常时会被系统的异常处理过程调用。可以通过 API 函数 AddVectoredExceptionHandler 注册 VEH 回调函数,其原型如下。

WINBASEAPI PVOID WINAPI AddVectoredExceptionHandler(
    ULONG FirstHandler,
    PVECTORED_EXCEPTION_HANDLER VectoredHandler //回调函数地址
);

//回调函数原型
LONG CALLBACK VectoredHandler(
        PEXCEPTION_POINTERS ExceptionInfo
};
注册函数和VEH函数原型

注意注册函数的第一个元素,用于指明函数调用顺序,0 代表最末尾,大于 0 的将置于前端。 VEH 回调函数需要在程序退出前自己完成卸载工作,使用 API 函数

第8章:Windows 下的异常处理——SafeSEH、SEHOP

参数为前面注册函数给的返回值。

  VEH 用到的参数和顶层异常处理函数用到的参数是一样的。VEH 合理的返回值只有两个: EXCEPTION_CONTINUE _EXECUTION (0xfffffffff)和 EXCEPTION_CONTINUE_SEARCH (0x0),其意义与 SEH 回调函数的意义相同。

VEH 和 SEH 的区别

注册机制不同。SEH 的相关信息主要保存在栈中,而且后注册的回调函数总是处于 SEH 链的前端,也就是说,当异常发生时,异常总是由内层回调函数优先处理,只有在内层回调函数不处理异常时,外层回调函数才有机会获得控制权。而 VEH 不同,它的相关信息保存在独立的链表中(实际存储在 ntdll 中),在注册 VEH 时可以指定回调函数是位于 VEH 链表的前端还是尾部,这就避免了我们希望在 SEH 中获得优先处理权却常常不能如愿的问题。

优先级不同。VEH 优先于SEH 被调用,这对某些需要先于 SEH 取得异常处理权的特殊程序来说非常重要。如果 VEH 表明自己处理了异常,那么 SEH 将没有机会再处理该异常。作用范围不同。SEH 机制是基于线程的,也就是说,同一进程内的 A 线程无法捕获和处理 B 线程产生的异常,并且对特定的 SEH 处理程序来说,它的作用范围更是局限在安装它的那个函数内部(除了顶层异常处理这个特殊的全局回调函数)。而 VEH 在整个进程范围内都是有效的,它可以捕获和处理所有线程产生的异常。
VEH 不需要栈展开。由于 SEH 的注册和使用依赖于函数调用的栈帧,在调用 SEH 回调函数时会涉及栈展开的问题,这样 SEH 就有2次被调用的机会。因为 VEH 的实现不依赖栈所以在调用 VEH 回调函数前不需要进行栈展开,它只有1次被调用的机会。

VEH  ——>  VCH

PVOID WINAPI AddvectoredcontinueHandler(
__in    ULONG FirstHandler,
__in    PVECTORED_EXCEPTION_HANDLER VectoredHandler
);
ULONG WINAPI RemoveVectoredContinueHandlert(
__in    PVOID Handler
);
VCH 注册与卸载

值得注意的是 VCH 回调函数的调用时机。分析 ntdIl!RtIDispatchException 的代码可知,它会在两种情况下被调用。
① 在 SEH 机制无法正常运行的情况下(例如,相关数据结构被破坏,未通过 SafeSEH 或 SEHOP 验证),SEH 分发将被跳过。
② 当 SEH 回调函数能够返回 ntdll!RtIDispatchException 函数时。因为 SEH 处理程序具有特殊性,在执行 SEH 回调函数的时候,不仅有可能直接跳转到其他位置执行(例如EXCEPTION_EXECUTE_HANDLER 的情况,会跳到 Try 块的结束处),也有可能不再返回(例如 kernel32!UnhandledExceptionFilter 进行终结处理),所以,只有当 SEH 回调函数返回了 ExceptionContinueExecution,或者 UEF 函数修复了异常,或 UEF 函数因为调试器的存在其终结处理被跳过的情况下,SEH 回调函数才会返回 ntdll!RtIDispatchException 函数,此时才有机会执行VCH回调函数。