SEH-结构化异常处理
SEH-结构化异常处理
本文为《逆向工程核心原理》读书笔记,采用的程序示例均来源于此书
什么是异常
要学习异常处理,首先要了解什么异常
常见的异常如下:
EXCEPTION_ACCESS_VIOLATION(C0000005)
访问内存异常,例如访问不存在的或者是不具权限的内存区域
例如,这里将0x1移入0x0的地址,在x64dbg中可以看到不同段的权限信息
1 | xor eax, eax |
可以看出,0x0是非法地址,于是就会触发异常。
不仅是访问,修改无“write”权限的地址,或者内核区域,都会引发异常
EXCEPTION_BREAKPOINT(80000003)
断点异常
调试器利用断点异常来实现断点功能
INT3断点对应的16进制为0xCC(IA-32)
在0x401000处dump内存,再用010打开
很明显发现5A10400前面的68变成了CC,即这里已经被下了断点
EXCEPTION_ILLEGAL_INSTRUCTION(C000001D)
顾名思义,非法指令,当调试器遇到无法解析的指令的时候就会触发该异常
EXCEPTION_INT_DIVIDE_BY_ZERO(C0000094)
除零异常
1 | xor eax, eax |
除法的分母为零的时候,触发异常
SEH异常处理工作原理
SEH 是针对于异常的一种处理机制,这个异常分为硬件异常和软件异常,这里所说的硬件异常是狭义的异常,也就是 CPU 产生的异常。比如除零操作,CPU 执行除零操作时候,会自主启动异常处理机制。软件异常,就是程序模拟的异常,比如调用 RaiseException 函数。软件异常是可以随意触发的,windows 系统内部遇到问题会触发。
要让SEH处理异常,首先得注册异常。SEH是线程相关的,也就是说,每个线程都有他自己的异常处理注册表。异常处理注册表在内存中以结构体链表的形式储存
1 | typedef struct _EXCEPTION_REGISTRATION_RECORD { |
结构体指针Next则指向下一个结点。当触发异常的时候,遍历该链表以找到对应的Hander来处理,否则则直接结束程序。
若Next == 0xFFFFFFFF,则说明异常处理注册表结束。
Handler指向异常处理函数
该函数接收到异常后会返回EXCEPTION_DISPOSITION这个枚举类型,每个枚举类型将会指示异常处理的下一步进程
1 | typedef enum _EXCEPTION_DISPOSITION { |
该函数会接收EXCEPTION_RECORD这个结构体来储存异常相关的信息
1 | typedef struct _EXCEPTION_RECORD { |
因为SEH是线程相关的,因此为了在多线程的环境下安全运行,Handler函数还会接收一个CONTEXT结构体来保存寄存器的相关信息
1 | typedef struct _CONTEXT { |
当异常发生时,执行异常代码的线程中断,转而运行SEH,而发生异常代码的地址将会储存在CONTEXT.EIP(偏移:0xB8)中。当异常处理函数执行完之后依靠CONTEXT.EIP来使得中断的线程继续执行。
调试样例
seh.exe
整体代码如下
下面进一步调试分析
首先是注册异常处理函数
通过TEB结构体成员NtTib访问SEH链。而TEB.NtTib.ExceptionList成员为TEB结构体的第一个成员。段寄存器FS指向段内存的起始地址,TEB结构体即位于此。因此通过FS:[0]来访问SEH链并且将位于0x40105A地址的异常处理函数加入到SEH链中。
在触发了异常后,将会立即跳到位于0x40105A的异常处理函数
而此时的栈中已经储存了异常处理函数所需要的参数
0x0019FA64
第一个参数指向的是EXCEPTION_RECORD结构体
可以看到异常代码0xC0000005以及触发异常的地址0x00401019
0x0019FF24
第二个参数指向SEH链的起始位置,在x64dbg的SEH选项卡中可以看到
0x0019FAB4
第三个参数指向CONTEXT结构体
在B8偏移位可以找到异常触发的地址0x00401019
在进入了SEH函数后,就是反调试的代码
这里将fs:[30]指向PEB结构体,而PEB结构体0x2的地方是PEB.BeingDebugged
当程序被调试的时候,返回1
于是便打印Debugger detected :(
最后将压入栈的参数全部出栈,删除SEH函数
删除前的栈
删除后的栈
更加完善的异常处理
在实际使用的过程中,微软提供的SEH存在不少缺陷。现在常用的基本上都是编译器的增强版,例如微软的编译器MSC里提供的__try, __finally, __except