SEH异常处理机制
简介
基本说明
SEH 是 Windows 操作系统提供的异常处理机制,在程序源代码中使用__try
、__except
、__finally
关键字来具体实现。在逆向分析中,SEH 除了基本的异常处理功能外,还大量应用于反调试程序。
基本使用
在 C 语言中使用__try
、__except
、__finally
关键字就可以很容易地向代码添加 SEH。在汇编语言中添加 SEH 的方法更加简单。
- 在 C 语言中使用 SEH
1 |
|
- 在汇编语言中使用 SEH
1 |
|
在程序代码中使用 SEH 就是指,将自身的异常处理器添加到已有的 SEH 链。从技术层面讲就是自身的EXCEPTION_REGISTRATION_RECORD
结构体链接到EXCEPTION_REGISTRATION_RECORD
结构体链表。
OS的异常处理方法
正常运行时的异常处理方法
进程运行过程中若发生异常,OS 会委托进程处理。若进行代码中存在具体的异常处理(如 SEH 异常处理器)代码,则能顺利处理相关异常,程序继续运行。但如果进程内部没有具体实现 SEH,那么相关异常就无法处理,OS 就会启动默认的异常处理机制,终止进程运行。
调试运行时的异常处理方法
调试运行中发生异常时,处理方法与上面有些不同。若被调试进程内存发生异常,OS 会首先把异常抛给调试进程处理。调试器几乎拥有被调试者的所有权限,它不仅可以运行、终止被调试者,还拥有被调试进程的虚拟内存、寄存器的读写权限。需要特别指出的是,被调试者内部发生的所有异常(错误)都由调试器处理。所以调试过程中发生的所有异常(错误)都要先交由调试器管理(被调试者的 SEH 依据优先顺序推给调试器)。像这样,被调试者发生异常时,调试器就会暂停运行,必须采取某种措施来处理异常,完成后继续调试。
遇到异常时经常采用的几种处理方法如下所示:
- 直接修改异常:代码、寄存器、内存
被调试者发生异常时,调试器会在发生异常的代码处暂停,此时可以通过调试器直接修改有问题的代码、内存、寄存器等,排除异常后,调试器继续运行程序。
- 将异常抛给被调试者处理
如果被调试者内部存在 SEH (异常处理函数)能够处理异常,那么异常通知会发送给被调试者,由被调试者自行处理。这与程序正常运行时的异常处理方式是一样的。
- OS 默认的异常处理机制
若调试器与被调试器都无法处理(或故意不处理)当前发生的异常,则 OS 的默认异常处理机制会处理它,终止被调试进程,同时结束调试。
异常
学习异常处理前,有必要了解操作系统中定义的异常。
以上异常列表中,我们调试时会经常接触 5 种最具代表性的异常,接下来分别介绍。
EXCEPTION_ACCESS_VIOLATION(C0000005)
试图访问不存在或不具权限的内存区域时,就会发生EXCEPTION_ACCESS_VIOLATION
(非法访问异常,该异常最常见)。
- 例
EXCEPTION_BREAKPOINT(80000003)
在运行代码中设置断点后,CPU 尝试执行该地址处的指令时,将发生EXCEPTION_BREAKPOINT
异常。调试器就是利用该异常实现断点功能的。
INT3
设置断点命令对应的汇编指令为 INT3
,对应的机器指令为 0xCC
。CPU 运行代码的过程中若遇到汇编指令 INT3
,则会触发EXCEPTION_BREAKPOINT
异常。
EXCEPTION_ILLEGAL_INSTRUCTION(C000001D)
CPU 遇到无法解析的指令时引发该异常。比如 0xFFF
指令在 x86 CPU 中未定义,CPU 遇到该指令将引发 EXCEPTION_ILLEGAL_INSTRUCTION
异常。
EXCEPTION_INT_DIVIDE_BY_ZERO(C0000094)
INTEGER(整数)除法运算中,若分母为 0(即被 0 除),则引发EXCEPTION_INT_DIVIDE_BY_ZERO
异常。
编写应用程序时偶尔会发生该异常,分母为变量时,该变量在某个瞬间变为 0,执行除法运算就会引发EXCEPTION_INT_DIVIDE_BY_ZERO
异常。
EXCEPTION_SINGLE_STEP(80000004)
Singel Step(单步)的含义是执行 1 条指令,然后暂停。CPU 进入单步模式后,每执行一条指令就会引发EXCEPTION_SINGLE_STEP
异常,暂停运行。将EFLAGS
寄存器的TF
(陷阱标志)位设置为 1 后,CPU就会进入单步工作模式。
SEH 详细说明
SEH 链
SEH 以链的形式存在。第一个异常处理器中若未处理相关异常,它就会被传递到下一个异常处理器,直到得到处理。从技术层面看,SEH 是由_EXCEPTION_REGISTRATION_RECORD
结构体组成的链表。
1 |
|
Next
成员是指向下一个_EXCEPTION_REGISTRATION_RECORD
结构体的指针,Handler
成员是异常处理函数(异常处理器)。若Next
成员的值为FFFFFFFF
,则表示它是链表的最后一个结点。
上图中共存在3个 SEH (异常处理器),发生异常时,该异常会按照(A)->(B)->(C)的顺序依次传递,直到有异常处理器处理。
异常处理函数的定义
SEH 异常处理函数(SEH 函数)定义如下:
1 |
|
异常处理函数(异常处理器)接收 4 个参数输入,返回名为EXCEPTION_DISPOSITION
的枚举类型(enum)。该异常处理函数由系统调用,是一个回调函数,系统调用它时会给出代码中的 4 个参数,这 4 个参数中保存着与异常相关的信息。首先,第一个参数是指向EXCEPTION_RECORD
结构体的指针,EXCEPTION_RECORD
结构体的定义如下:
1 |
|
请注意该结构体中ExceptionCode
与ExceptionAddress
这 2 个成员,ExceptionCode
成员用来指出异常类型,ExceptionAddress
成员表示发生异常的代码地址。异常处理函数的第三个参数是指向CONTEXT
结构体的指针。
CONTEXT
结构体的定义如下:
1 |
|
CONTEXT
结构体用来备份 CPU 寄存器的值,因为多线程环境下需要这样做。每个线程内部都拥有 1 个CONTEXT
结构体。CPU 暂时离开当前线程去运行其他线程时,CPU 寄存器的值就会保存到当前线程的CONTEXT
结构体;CPU 再次运行该线程时,会使用保存在CONTEXT
结构体的值来覆盖 CPU 寄存器的值,然后从之前暂停的代码处继续执行。通过这种方式,OS 可以在多线程环境下安全运行各线程。
异常发生时,执行异常代码的线程就会中断,转而运行 SEH(异常处理器/异常处理函数),此时 OS 会把线程的CONTEXT
结构体的指针传递给异常处理函数(异常处理器)的相应参数。CONTENT
结构体成员中有 1 个Eip
成员(偏移量:B8)。在异常处理函数中将参数传递过来的CONTENT.Eip
设置为其他地址,然后返回异常处理函数。这样,之前暂停的线程会执行新设置的EIP
地址处的代码。在异常函数定义中可以看到异常处理函数的返回值为EXCEPTION_DISPOSITION
枚举类型,下面了解一下该类型。
1 |
|
异常处理器处理异常后会返回ExceptionContinueExecution(0)
,从发生异常的代码处继续运行。若当前异常处理器无法处理异常,则返回ExceptionContinueSearch(1)
,将异常派送到 SEH 链的下一个异常处理器。
TEB.NtTib.ExceptionList
通过 TEB 结构体的NtTib
成员可以很容易地访问进程的 SEH 链,方法非常简单。
如下图所示,TEB.NtTib.ExceptionList
成员是 TEB 结构体的第一个成员。FS
段寄存器执行段内存的起始地址,TEB 结构体即位于此,所以通过下列公式可以轻松获取TEB.NtTib.ExceptionListd
的地址。
1 |
|