SEH-结构化异常处理

SEH-结构化异常处理

本文为《逆向工程核心原理》读书笔记,采用的程序示例均来源于此书

什么是异常

要学习异常处理,首先要了解什么异常

常见的异常如下:

EXCEPTION_ACCESS_VIOLATION(C0000005)

访问内存异常,例如访问不存在的或者是不具权限的内存区域

例如,这里将0x1移入0x0的地址,在x64dbg中可以看到不同段的权限信息

1
2
xor eax, eax
mov dword ptr ds:[eax], 0x1

image-20230115214338599

可以看出,0x0是非法地址,于是就会触发异常。

不仅是访问,修改无“write”权限的地址,或者内核区域,都会引发异常

EXCEPTION_BREAKPOINT(80000003)

断点异常

调试器利用断点异常来实现断点功能

INT3断点对应的16进制为0xCC(IA-32)

image-20230115215403355

在0x401000处dump内存,再用010打开

image-20230115215442507

很明显发现5A10400前面的68变成了CC,即这里已经被下了断点

EXCEPTION_ILLEGAL_INSTRUCTION(C000001D)

顾名思义,非法指令,当调试器遇到无法解析的指令的时候就会触发该异常

EXCEPTION_INT_DIVIDE_BY_ZERO(C0000094)

除零异常

1
2
xor eax, eax
div eax

除法的分母为零的时候,触发异常

SEH异常处理工作原理

SEH 是针对于异常的一种处理机制,这个异常分为硬件异常和软件异常,这里所说的硬件异常是狭义的异常,也就是 CPU 产生的异常。比如除零操作,CPU 执行除零操作时候,会自主启动异常处理机制。软件异常,就是程序模拟的异常,比如调用 RaiseException 函数。软件异常是可以随意触发的,windows 系统内部遇到问题会触发。

要让SEH处理异常,首先得注册异常。SEH是线程相关的,也就是说,每个线程都有他自己的异常处理注册表。异常处理注册表在内存中以结构体链表的形式储存

1
2
3
4
typedef struct _EXCEPTION_REGISTRATION_RECORD {
struct _EXCEPTION_REGISTRATION_RECORD *Next;
PEXCEPTION_ROUTINE Handler;
} EXCEPTION_REGISTRATION_RECORD;

结构体指针Next则指向下一个结点。当触发异常的时候,遍历该链表以找到对应的Hander来处理,否则则直接结束程序。

若Next == 0xFFFFFFFF,则说明异常处理注册表结束。

image-20230115221235264

Handler指向异常处理函数

该函数接收到异常后会返回EXCEPTION_DISPOSITION这个枚举类型,每个枚举类型将会指示异常处理的下一步进程

1
2
3
4
5
6
typedef enum _EXCEPTION_DISPOSITION {
ExceptionContinueExecution, 已处理异常,从断点处继续执行
ExceptionContinueSearch, 未处理异常,继续搜索异常处理注册表
ExceptionNestedException, OS内部使用
ExceptionCollidedUnwind OS内部使用
} EXCEPTION_DISPOSITION;

该函数会接收EXCEPTION_RECORD这个结构体来储存异常相关的信息

1
2
3
4
5
6
7
8
typedef struct _EXCEPTION_RECORD {
DWORD ExceptionCode; //异常代码
DWORD ExceptionFlags;
struct _EXCEPTION_RECORD *ExceptionRecord;
PVOID ExceptionAddress; //异常发生的地址
DWORD NumberParameters;
ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD;

因为SEH是线程相关的,因此为了在多线程的环境下安全运行,Handler函数还会接收一个CONTEXT结构体来保存寄存器的相关信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
typedef struct _CONTEXT {
//用来表示该结构中的哪些域有效
DWORD ContextFlags;

//调试寄存器
DWORD Dr0, Dr1, Dr2, Dr3, Dr6, Dr7;
//偏移值 04h 08h 0Ch 10h 14h 18h

//浮点寄存器区
FLOATING_SAVE_AREA FloatSave;

//段寄存器
DWORD SegGs, SegFs, SegEs, SegDs;
//偏移值 88h 90h 94h 98h

//通用寄存器组
DWORD Edi, Esi, Ebx, Edx, Ecx, Eax;
//偏移值 9Ch A0h A4h A8h ACh B0h

//控制寄存器组
DWORD Ebp, Eip, SegCs, EFlags, Esp, SegSs;
//偏移值 B4h B8h BCh C0h C4h C8h

//扩展寄存器,只有特定的处理器才有
BYTE ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION]; //512bytes
} CONTEXT;

当异常发生时,执行异常代码的线程中断,转而运行SEH,而发生异常代码的地址将会储存在CONTEXT.EIP(偏移:0xB8)中。当异常处理函数执行完之后依靠CONTEXT.EIP来使得中断的线程继续执行。

调试样例

seh.exe

整体代码如下

image-20230115224420795

下面进一步调试分析

首先是注册异常处理函数

image-20230115224459999

通过TEB结构体成员NtTib访问SEH链。而TEB.NtTib.ExceptionList成员为TEB结构体的第一个成员。段寄存器FS指向段内存的起始地址,TEB结构体即位于此。因此通过FS:[0]来访问SEH链并且将位于0x40105A地址的异常处理函数加入到SEH链中。

image-20230115225043825

在触发了异常后,将会立即跳到位于0x40105A的异常处理函数

image-20230115225140825

而此时的栈中已经储存了异常处理函数所需要的参数

image-20230115225308763

0x0019FA64

第一个参数指向的是EXCEPTION_RECORD结构体

image-20230115225442803

可以看到异常代码0xC0000005以及触发异常的地址0x00401019

0x0019FF24

第二个参数指向SEH链的起始位置,在x64dbg的SEH选项卡中可以看到

image-20230115225705260

0x0019FAB4

第三个参数指向CONTEXT结构体

image-20230115225943085

在B8偏移位可以找到异常触发的地址0x00401019

在进入了SEH函数后,就是反调试的代码

image-20230115230144578

这里将fs:[30]指向PEB结构体,而PEB结构体0x2的地方是PEB.BeingDebugged

当程序被调试的时候,返回1

image-20230115230411196

于是便打印Debugger detected :(

image-20230115230544948

最后将压入栈的参数全部出栈,删除SEH函数

删除前的栈

image-20230115230629953

删除后的栈

image-20230115230657856

更加完善的异常处理

在实际使用的过程中,微软提供的SEH存在不少缺陷。现在常用的基本上都是编译器的增强版,例如微软的编译器MSC里提供的__try, __finally, __except