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 链:
在上面的代码中,我们并没有显式实现 _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) // 执行异常处理块,之后从异常处理块下面继续执行