SEH 深入浅出

0x00 前言

之前的坑还没填完,现在开始挖新的坑的了 = =。其实基于 SEH 的实现已经不算什么秘密了,网上各种文章也满天飞( 当然理解 SEH 必读的肯定是 Matt Pietrek 于 1997 年发表在微软月刊的一篇文章:A Crash Course on the Depths of Win32™ Structured Exception Handling。作为整理和加深理解,在此总结前辈们的各种精♂经验。

0x01 SEH 实现

SEH 即 Structured Exception Handling,结构化异常处理。是 M$ 在 Windows 下实现的一套异常处理机制,用于支持软件和硬件异常处理。SEH 作为 Windows 特有的机制,同时也是 Windows 溢出攻击中常见的利用的途径之一。

一个简单的异常处理 🌰 栗子:

DWORD dwNum = 0;
__try {
    dwNum = 5 / dwNum;
} __except(EXCEPTION_EXECUTE_HANDLER) {
    cout << "Divide by Zero Exception";
}

由于 dwNum 的值初始化为 0,在执行除法指令时,CPU 会抛出异常,并通知系统进行处理,最后输出我们的错误信息。系统对异常的处理是通过调用回调函数实现的,该回调函数原型如下:

EXCEPTION_DISPOSITION _except_handler (
    EXCEPTION_RECORD    *ExceptionRecord;
    void                *EstablisherFrame,
    CONTEXT             *ContextRecord,
    void                *DispatcherContext
);

函数的细节我们后面再剖析,我们先来看看构成 SEH 的基础部分。当异常发生时,操作系统需要调用回调函数处理,那么,系统如何正确地调用回调函数?这是通过一个数据结构来索引的:

typedef struct _EXCEPTION_REGISTRATION
{
    struct _EXCEPTION_REGISTRATION*	prev;
    PEXCEPTION_HANDLER		handler;
} EXCEPTION_REGISTRATION, *PEXCEPTION_REGISTRATION;

handler 便是我们的回调函数地址,prev 则指向上一个 EXCEPTION_REGISTRATION 结构体,构成一个单向链表。当异常发生时,系统会遍历该链表,直到找到正确的异常处理函数。链表最后一项的 prev 值为 0xFFFFFFFF,表示链表结束。

链表的首个成员(严格来讲应该是最后的成员)存放于 TIB(Thread Information Block)结构的第一个成员中。在 Intel && Win32 架构中,fs 寄存器始终指向 TIB 结构,所以在反汇编中,我们经常会看到下面的代码:

push handler
push fs:[0]
mov fs:[0], esp

上述的代码便是构造一个 EXCEPTION_REGISTRATION 结构,并将新的异常处理结构地址放入fs:[0]中,形成一个 SEH 链表。在此我们可以注意到,SEH 链是存放于栈上的

这里使用一个流程图来描绘 SEH 链:

SEH.svg

在上面的代码中,我们并没有显式实现 _except_handler 函数,因为编译器已经帮我们实现了。在了解了 SEH 的基本工作原理后,尝试手工实现 _try/_except 功能:

#include <Windows.h>

DWORD dwNum = 0;

EXCEPTION_DISPOSITION _except_handler(EXCEPTION_RECORD *ExceptRecord,
                                      void *EstablisherFrame,
                                      CONTEXT *ContextRecord,
                                      void *DispatcherContext)
{
	dwNum = 1;    // 重新赋值
	return ExceptionContinueExecution;
}

int main()
{
	DWORD handler = (DWORD) _except_handler;
	__asm {
		push handler
		push fs:[0]
		mov fs:[0], esp
	}

	dwNum = 5 / dwNum;
	printf("dwNum = %d\n", dwNum);

	__asm {
		mov eax, [esp]
		mov fs:[0], eax    // 恢复上一个异常处理结构
		add esp, 8
	}

	return 0;
}

首先我们安装一个新的异常处理块。

当执行除法指令时,CPU 抛出异常,Windows 通过fs:[0]开始遍历 SEH 链表,并调用我们的回调函数,此时函数返回 ExceptionContinueExecution,表示重新从异常处开始执行。由于在回调函数中为 dwNum 赋予新值,此时程序正常执行并输出相应信息。

最后我们对异常处理块进行拆卸。

0x02 回调函数

这部分主要了解回调函数各项内容,对于了解 SEH 的工作原理,这一部分可以跳过,因为感觉更偏向于开发向,所以跳过也不会对后续的内容产生太大影响,当然喜欢刨坑问底的你肯定不会止步于此。;-)

对于回调函数,我们的关注点主要有两个:函数返回值参数类型

  • 函数返回值

EXCEPTION_DISPOSITION 是一个枚举类型,该返回值用于向系统反馈异常的处理结果,定义如下:

typedef enum _EXCEPTION_DISPOSITION
{
    ExceptionContinueExecution = 0,
    ExceptionContinueSearch = 1,
    ExceptionNestedException = 2,
    ExceptionCollidedUnwind = 3
} EXCEPTION_DISPOSITION;

例如,当函数返回 ExceptionContinueExecution 时,系统会重新在异常处执行程序;ExceptionContinueSearch 则通知系统:这异常我也救不了,你另请高明吧。此时系统会继续搜索其他回调函数来处理异常,其他返回值我们可以暂且忽略。

  • 参数类型

当异常产生时,必要的,需要通过特定的数据结构来记录异常信息,而这个信息便是由第一个参数 EXCEPTION_RECORD 提供,其中包括错误代码、异常发生地址等:

typedef struct _EXCEPTION_RECORD {
  DWORD                    ExceptionCode;
  DWORD                    ExceptionFlags;
  struct _EXCEPTION_RECORD  *ExceptionRecord;
  PVOID                    ExceptionAddress;
  DWORD                    NumberParameters;
  ULONG_PTR                ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD, *PEXCEPTION_RECORD;

例如上面的程序在发生异常时,ExceptionCode 的值为 EXCEPTION_INT_DIVIDE_BY_ZERO,ExceptionAddress 指向引发异常的指令地址。

NumberParameters 指明异常信息的附加参数个数,参数内容存放于 ExceptionInformation 数组中。Jeffrey Richter 在《Windows 核心编程》中指出,目前只有 EXCEPTION_ACCESS_VIOLATION 异常使用了这两个参数,该异常是由于用户尝试访问未授权区域。当该异常发生时,附加信息包含了用户尝试访问的地址。

第二个参数,EstablisherFrame 便是我们的异常处理结构 EXCEPTION_REGISTRATION( 对于编译器生成的回调函数,这个结构会被扩展,后面我们会看到。

当异常发生时,我们可能还会尝试获取 CPU 的状态,这个信息由参数三 CONTEXT 进行记录,该信息记录了各个寄存器的状态:

typedef struct _CONTEXT
{
     ULONG ContextFlags;
     ULONG Dr0;
     ULONG Dr1;
     ULONG Dr2;
     ULONG Dr3;
     ULONG Dr6;
     ULONG Dr7;
     FLOATING_SAVE_AREA FloatSave;
     ULONG SegGs;
     ULONG SegFs;
     ULONG SegEs;
     ULONG SegDs;
     ULONG Edi;
     ULONG Esi;
     ULONG Ebx;
     ULONG Edx;
     ULONG Ecx;
     ULONG Eax;
     ULONG Ebp;
     ULONG Eip;
     ULONG SegCs;
     ULONG EFlags;
     ULONG Esp;
     ULONG SegSs;
     UCHAR ExtendedRegisters[512];
} CONTEXT, *PCONTEXT;

乍看一下成员多得吓人,但实际上只是各个寄存器的信息汇总。

0x03 编译器级 SEH 实现

通常而言,异常捕捉代码会通过下面的方式实现:

__try {
    // blabla...
} __except(filter-expression) {
    // exception handler
}

在 Java 中,filter-expression 是传入一个异常类型,正确匹配异常类型后执行处理块。而在 VC++ 中,则是传入一个常量值,告知异常处理下一步行为,取值必须是下面其中之一:

EXCEPTION_CONTINUE_EXECUTION (1) // 不执行异常处理,重新从异常处开始执行
EXCEPTION_CONTINUE_SEARCH (0) // 不执行异常处理,交由外层(嵌套异常捕捉)进行处理
EXCEPTION_EXECUTE_HANDLER (1) // 执行异常处理块,之后从异常处理块下面继续执行

0x03 全局展开和局部展开

0x04 异常调用过程

0x05 未处理异常

0x06 SEH 利用

0x06 SafeSEH 实现