操作系统或程序在运行,难免会遇到各种各样的错误,如除零,非法内存访问,文件打开错误,内存不足,磁盘读写错误,外设操作失败。为了保证系统在遇到错误时不至于崩溃,仍能够健壮稳定地继续运行下去,windows会对运行在其中的程序提供一次补救的机会来处理错误这种机制就是异常处理机制。 S.E.H即异常处理结构体(Structure Exception Handler),它是windows异常处理机制所采用的重要数据结构,每个S.E.H包含两个DWORD指针:S.E.H链表指针和异常处理函数句柄,共8个字节,如下图
当线程初始化时,会自动向栈中安装一个异常处理结构,作为线程默认的异常处理。SEH 最基本的数据结构是保存在堆栈中的称为EXCEPTION_REGISTRATION 的结构体,结构体包括2个元素:第1个元素是指向下一个EXCEPTION_REGISTRATION 结构的指针(prev),第2个元素是指向异常处理程序的指针(handler)。这样一来,基于堆栈的异常处理程序就相互连接成一个链表。异常处理结构在堆栈中的典型分布如图1 所示。最顶端的异常处理结构通过线程控制块(TEB)0 Byte 偏移处指针标识,即FS:[0]处地址。
用于进行实际异常处理的函数原型可表示如下:
- EXCEPTION_DISPOSITION __cdecl _except_handler(
- struct _EXCEPTION_RECORD *ExceptionRecord,
- void * EstablisherFrame,
- struct _CONTEXT *ContextRecord,
- void * DispatcherContext
- )
该函数的最重要的2个参数是指向_EXCEPTION_RECORD 结构的ExceptionRecord 参数和指向_CONTEXT 结构的ContextRecord 参数,前者主要包括异常类别编码、异常发生地址等重要信息;后者主要包括异常发生时的通用寄存器、调试寄存器和指令寄存器的值等重要的线程执行环境。而用于注册异常处理函数的典型汇编代码可表示如下:
- PUSH handler ; handler 是新的异常处理函s数地址
- PUSH FS:[0] ;指向原来的处理函数的地址压入栈内
- MOV FS:[0],ESP ;注册新的异常处理结构
封装型SEH
通过使用_try{}/_except(){}/_finally{}等关键字,使开发人员更方便地在软件中使用SEH 是封装型SEH 的主要特点。该机制的异常处理数据结构定义如下:
- struct VC_EXCEPTION_REGISTRATION
- {
- VC_EXCEPTION_REGISTRATION* prev;
- FARPROC handler;
- scopetable_entry* scopetable; //指向scopetable 数组指针
- int _index; //在scopetable_entry 中索引
- DWORD _ebp; //当前EBP 值
- }
显而易见,结构体中后3 个成员是新增的。而scopetable_entry 的结构如下所示:
- struct scopetable_entry
- {
- DWORD prev_entryindex; //前一scopetable_entry 的索引
- FARPROC lpfnFilter; //过滤函数地址
- FARPROC lpfnHandler; //处理异常代码地址
- }
- void A()
- {
- __try // 0 号try 块
- {
- __try // 1 号try 块
- {
- *(PDWORD)0 = 0;
- }
- __except(EXCEPTION_CONTINUE_SEARCH)
- {
- printf("Exception Handler!");
- }
- }
- __finally
- {
- puts("in finally");
- }
- }
对应该函数的序言部分反汇编代码如下:
- push ebp
- mov ebp, esp
- push -1
- push offset _A_scopetable
- push offset _except_handler3
- mov eax, large fs:0
- push eax
- mov large fs:0, esp
显而易见, 压入堆栈的结构与VC_EXCEPTION_REGISTRATION 结构是一致的。查找scopetable 的地址为0x00422048,在调试器中查找该地址起始的内容如下:
FFFFFFFF ;scopetable_entry0 的prev_entryindex 值
00000000 ;lpfnFilter 地址值,为0,对应_finally{}
004010EE ;lpfnHandler 地址值
00000000 ;scopetable_entry1 的prev_entryindex 值
004010C6 ;lpfnFilter 地址值
004010C9 ;lpfnHandler 地址值
…
第1 组值对应0 号try 块,而该块对应_finally{}块,所以过滤函数地址为0;第2 组值对应1 号try 块,而该块对应_except(){}块,所以有过滤函数和处理函数的地址。进一步查看0x004010EE 地址处的反汇编代码如下:
- PUSH OFFSET ??_C@0L@PEFD@in?5finally?$AA@; ”in finally”
- CALL puts
- ADD ESP,4
- RETN
显然上述语句与_finally{}块中的C 语言语句是对应的,而其他的地址经过查找也是分别对应的。在进入0 号try 块时的反汇编语句如下:
- MOV DWORD PTR SS:[EBP-4],0 ;对应第1 个_try 语句
- MOV DWORD PTR SS:[EBP-4],1 ;对应第2 个_try 语句
- …
- MOV DWORD PTR SS:[EBP-4],0 ;退出第2 个_try 块
- MOV DWORD PTR SS:[EBP-4],-1 ;退出第1 个_try 块
- …
实验开始:
0040104F . 64:A1 00000000 mov eax,dword ptr fs:[0]
00401055 . 50 push eax //指向原来的处理函数的地址压入栈内
00401056 . 64:8925 000000>mov dword ptr fs:[0],esp //注册新的异常处理结构
0012FF68 0012FFB0 指针到下一个 SEH 记录
0012FF6C 0040139C SE 句柄 //这里是我们需要修改的异常函数回调函数句柄
0012FF70 004060B8 SEH.004060B8
0012FF74 00000000
0012FF78 /0012FFC0 //这里是PUSH EBP 的EBP
0012FF7C |004010DA 返回到 SEH.004010DA 来自 SEH.00401040 这里是返回函数地址
0012FF80 |00407030 SEH.00407030
0012FF84 |00401528 返回到 SEH.<ModuleEntryPoint>+0B4 来>
0012FF88 |00000001
0012FF8C |00340CF0
0012FF90 |00340D48
0012FF94 |00730061
0012FF98 |005C0065
0012FF9C |7FFDF000
0012FFA0 |80000003
0012FFA4 |8043138F
0012FFA8 |0012FF94
0012FFAC |0012FAD0
0012FFB0 |0012FFE0 指针到下一个 SEH 记录
0012FFB4 |0040139C SE 句柄
0012FFB8 |004060C8 SEH.004060C8
0012FFBC |00000000
0012FFC0