1. 什么是SEH
程序中可能包含有 SEH 链处理机制,在 Window 中异常处理是通过 SEH 来实现的,相关的资料描述可以查看这些文档:Windows SEH 和 IDA Manual about SEH 中较为详细的描述,SEH 是 Windows 系统的底层异常处理方式,C++中的异常处理形式的实现是依赖于这套机制的 in Windows(from IDA Manual about SEH)。
在编写 C/C++ 源码时的应用层面很简单,只需要使用如下结构就可以实现:
int main()
{
__try
{
TestExceptions();
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
printf("Executing SEH __except block\n");
}
return 0;
}
那么可以这么简单的理解异常处理就是 __try…__except 语句构成的模块(C++中是try…catch)。
2.逆向层面的SEH
2.1 SEH
2.1.1 SEH数据结构
- 首先来看一下相关的数据结构,感谢 Reverse SEH 提供的相应结构体,我这里就直接引用了。
异常处理函数 _except_handler3 是一个经常使用的 SEH 函数,用来处理 SEH3 相关的内容。它的参数只有 arg0 和 agr1 是有效的,后面两个参数是无效参数。
int _except_handler3(
PEXCEPTION_RECORD exception_record,
PEXCEPTION_REGISTRATION registration,
PCONTEXT context,
PEXCEPTION_REGISTRATION dispatcher
);
_EXCEPTION_RECORD 是传入的第0个参数,较为有用的是两个成员,ExceptionCode 和 ExceptionAddress 其余成员在逆向过程中的价值不大。
typedef struct _EXCEPTION_RECORD {
DWORD ExceptionCode; //exception code
DWORD ExceptionFlags;
struct _EXCEPTION_RECORD *ExceptionRecord;
PVOID ExceptionAddress; // exception address
DWORD NumberParameters;
ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD;
_EXCEPTION_REGISTRATION_RECORD 是传入的第1个参数,实际使用中它的大小是不确定的,可能会发生变化,但最开始的两个成员是确定的,分别为 Next 和 Handler 记录着下一个 node 的位置和当前异常的处理函数。
struct _EXCEPTION_REGISTRATION_RECORD
_EXCEPTION_REGISTRATION_RECORD* Next
_EXCEPTION_DISPOSITION* Handler
...
}
2.1.2 SEH注册
SEH 是一个链表,这个链表的的结构日下图所示:
第一个字段是 Next 指针,第二字段则是异常处理函数。而链表的起始位置记录在了 fs 段寄存器的第一个字段,所以一个添加 SEH 常规操作就是下面所示的汇编代码。
push offset sub_403E53 ;exception handler function
mov eax, large fs:0 ;get the list header
push eax ;save the Next node to the Next member
mov large fs:0, esp ; ;change the header to current
上面的操作过程就是链表的头插法,而这里的 header 就是 fs:0,首先在栈上创建一个 node 并将 Next 字段的值进行设置,记录上一个的位置,最后将 fs:0 中的值修改。
2.2 __SEH_prolog
A Reverse Example 和 Reverse SEH 对于 SEH3 有一些描述,我自己对其进行了一些参考。首先要说明的是 __SEH_prolog 不是一个能够被用户调用的 API 函数,它是在使用了__try…__except 语句之后由 Compiler 生成的,旨在实现该 Statement 功能的函数。
逆向过程中这个函数就变得可见了,IDA对其的命名是 __SEH_prolog 这个是注册函数,对应还有注销函数 __SEH_epilog,最主要的是识别注册函数,这个注销函数我就不进行分析了。
2.2.1 函数声明
下面是 __SEH_prolog 的函数接口在 IDA 中的体现。他具有两个参数,参数0是一个异常处理的 ScopeTable 参数1是分配栈的大小。
那么可以从上面的截图中推导出 __SEH_prolog 的函数接口方式。
void __SEH_prolog(_SCOPETABLE_ENTRY* scopetable, unsigned int StackSize);
2.2.2 函数定义
__SEH_prolog 的函数体在 IDA 是这样显示的,它首先完成对于 SEH 节点的添加,该节点的异常处理函数是 _except_handler3,然后按照第1个参数的大小分配数据栈。
2.2.3 SEH3运行流程
认识流程之前我们先来看一下 _SCOPETABLE_ENTRY 这个结构体。它的定义如下:
struct _SCOPETABLE_ENTRY {
DWORD EnclosingLevel;
PVOID FilterFunc;
PVOID HandlerFunc;
}
主要关注它的第三个成员 HandlerFunc 这个是在调用 _except_handler3 后会跳转到的位置。
异常触发之后的运行流程是这样的:
- 首先会进入调用 SEH 中记录中的第一个异常处理函数,在 SEH3 中的就是在函数开头通过 __SEH_prolog 函数注册的 _except_handler3 函数。
- _except_handler3 中的第一个参数 会传入一个 _EXCEPTION_REGISTRATION_RECORD 的结构体指针。
struct _EXCEPTION_REGISTRATION_RECORD {
struct _EXCEPTION_REGISTRATION_RECORD* Next;
struct _EXCEPTION_DISPOSITION* Handler;
struct _SCOPETABLE_ENTRY* ScopeTable;
}
- 最后经过 _except_handler3 函数后会跳转到 ScopeTable->HandlerFunc 处重新运行。需要注意的是:ScopeTable 所在的内存区域必须是 read only。
3.逆向层面的EH
前面介绍的 SEH 对应的是 Windows 的 __try…__except,而这里将记录下的是 C++ 的 try…catch。
3.1 EH数据结构
In EH 主要涉及到两个数据结构 FuncInfoV1 和 UnwindMapEntry。
1.在 FuncInfoV1 中我们主要关注两个成员 maxState 和 pUnwindMap。
struct FuncInfoV1 {
int magicNumber;
int maxState;
void *pUnwindMap;
int nTryBlocks;
void *pTryBlockMap;
int nIPMapEntries;
void *pIPtoStateMap;
};
- maxState: 最大的状态数量。也就是该 EH 中的 UnwindMapEntry数组中元素的个数。
- pUnwindMap:指向 UnwindMapEntry 结构体数组的指针。
2.在 UnwindMapEntry 中有两个成员 toState 和 action 这两个都是我们需要关注的。
struct UnwindMapEntry {
int toState;
void *action;
};
- toState:下一个状态的序号,这个就是数组中的下标,当其为 -1 表示处理结束没有下一个阶段。
- action:处理的 Code,将该异常进行处理的代码。
3.2 EH 注册
EH 的注册流程是将处理函数的一部分作为内容处理函数进行注册,也就是上图中的跳转到 __CxxFrameHandler 函数的那部分代码。
而在 __EH_prolog 内部则是直接将存入到 eax 中的地址作为 SEH 链的一部分进行注册。
3.3 EH 运行流程
When 程序发生异常时,通过 SEH 链 will jump 到 address,which 传递一个 FuncInfoV1 结构体后 will 跳转到 __CxxFrameHandler 函数执行。
该函数的执行流程大致如下图所示:
通过 pUnwindMap 指针获取到相应的 UnwindMapEntry 数组。然后按照状态表依次执行。完成异常处理。