SEH结构化异常处理:
本篇建议和Windows调试原理一起看
由于这个东西是基于线程的,所以先学习一下PEB和TEB
TEB&PEB
线程环境块
系统在TEB中保存了最频繁使用的线程相关数据,大小为4kb。系统中每个进程都有一个自己的TEB,一个进程的所有TEB都以堆栈的形式存放在内存中。同理,PEB为进程环境块,TEB中有指向PEB的指针,每个进程的PEB也以堆栈形式放在内存中
- 在x64架构环境下,
GS + 30h
处存储的是Teb结构体的基地址,GS + 60h
处存储的是Peb结构体的基地址。
- 在x86架构环境下,
FS + 18h
处存储的是Teb结构体的基地址,FS + 30h
处存储的是Peb结构体的基地址。
而指向SEH链开头就是TEB的0号位
以下是TEB与seh的关系

TEB重要成员
- 偏移0的NtTib线程信息块,含有ExceptionList
- 偏移0x30的ProcessEnvironmentBlock指向PEB,即进程环境块
访问TEB :NtCurrentTeb()或者按上面的方法直接取段寄存器
PEB重要成员
peb被teb偏移0x30的指针指向。
- 偏移2的BeingDebugged标志
- 偏移8的ImageBaseAddress
- 偏移c的Ldr,保存加载模块的链表
- 偏移0x18的ProcessHeap,可用于反调试
- 偏移0x68的NtGlobalFlag,可用于反调试
ImageBaseAddress表示进程的ImageBase,可以用GetModuleHandle(lpModuleName)获取
Ldr是一个结构体
visual studio中:
1 2 3 4 5
| typedef struct _PEB_LDR_DATA { BYTE Reserved1[8]; PVOID Reserved2[3]; LIST_ENTRY InMemoryOrderModuleList; } PEB_LDR_DATA, *PPEB_LDR_DATA;
|
实际
1 2 3 4 5 6 7 8 9
| typedef struct _PEB_LDR_DATA { ULONG Length; BOOLEAN Initialized; PVOID SsHandle; LIST_ENTRY InLoadOrderModuleList; LIST_ENTRY InMemoryOrderModuleList; LIST_ENTRY InInitializationOrderModuleList; } PEB_LDR_DATA,*PPEB_LDR_DATA;
|
1 2 3 4 5 6 7 8 9
| typedef struct _PEB_LDR_DATA { ULONG Length; BOOLEAN Initialized; PVOID SsHandle; LIST_ENTRY InLoadOrderModuleList; LIST_ENTRY InMemoryOrderModuleList; LIST_ENTRY InInitializationOrderModuleList; } PEB_LDR_DATA,*PPEB_LDR_DATA;
|
其中LIST_ENTRY是双向链表
1 2 3 4
| typedef struct _LIST_ENTRY { struct _LIST_ENTRY *Flink; struct _LIST_ENTRY *Blink; } LIST_ENTRY, *PLIST_ENTRY, *RESTRICTED_POINTER PRLIST_ENTRY;
|
而每一个节点都是这样的结构体
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 27 28 29 30
| typedef struct _LDR_DATA_TABLE_ENTRY { LIST_ENTRY InLoadOrderLinks; LIST_ENTRY InMemoryOrderLinks; LIST_ENTRY InInitializationOrderLinks; PVOID DllBase; PVOID EntryPoint; ULONG SizeOfImage; UNICODE_STRING FullDllName; UNICODE_STRING BaseDllName; ULONG Flags; WORD LoadCount; WORD TlsIndex; union { LIST_ENTRY HashLinks; struct { PVOID SectionPointer; ULONG CheckSum; } s1; } DUMMYUNIONNAME; union { ULONG TimeDateStamp; PVOID LoadedImports; } DUMMYUNIONNAME2; _ACTIVATION_CONTEXT *EntryPointActivationContext; PVOID PatchInformation; LIST_ENTRY ForwarderLinks; LIST_ENTRY ServiceTagLinks; LIST_ENTRY StaticLinks; } LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;
|
InLoadOrderModuleList,InMemoryOrderModuleList,InInitializationOrderModuleList的每个元素都指向的是上一个/下一个节点的对应元素
比如InLoadOrderModuleList的Flink和Blink就是指向LDR_DATA_TABLE_ENTRY结构的低一个元素InLoadOrderLinks的地址,也就是LDR_DATA_TABLE_ENTRY的地址,通过这种方法,可以用三种顺序串起加载的模块,同时链接所有的模块结构。
SEH
正向实现
在VS中,C语言可以直接使用关键字实现Windows结构化异常处理
1 2 3 4 5 6 7 8
| _try {
} _except() { }
|
过滤表达式的取值如下:
EXCEPTION_EXECUTE_HANDLER
(1) 执行except
里面的代码
EXCEPTION_CONTINUE_SEARCH
(0) 寻找下一个异常处理函数
EXCEPTION_CONTINUE_EXECUTION
(-1) 返回出错位置重新执行
举例:
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 27 28 29 30 31
| #include <stdio.h> #include <windows.h>
int modify_ecx(EXCEPTION_POINTERS* pExceptionInfo) { pExceptionInfo->ContextRecord->Ecx = 1; return EXCEPTION_CONTINUE_EXECUTION; }
int main(int argc, char* argv[]) { printf("hello\n"); int a = 0; int b = 1;
__try { a++; b--; int c = a / b; printf("c = %d\n", c); } __except (b = 9, modify_ecx(GetExceptionInformation())) { if (b == 9) { puts("异常处理:已更改除数"); } else { puts("异常处理"); } }
return 0; }
|
这个程序,在except中修改b的值,然后返回原处继续执行,以跳过bug
还有一个不常用的语法__finally{}
,效果是无论有没有异常处理,只要try块全部处理结束就会转到finally块中执行操作
在汇编中实现,需要先知道SEH的结构,所以先看后面的吧
异常分类
上面的例子中使用的是除零异常,还有下面几种常见异常
- EXCEPTION_ACCESS_VIOLATION 0xC0000005 程序企图读写一个不可访问的地址时引发的异常。例如企图读取0地址处的内存。
- EXCEPTION_ARRAY_BOUNDS_EXCEEDED 0xC000008C 数组访问越界时引发的异常。
- EXCEPTION_BREAKPOINT 0x80000003 触发断点时引发的异常。
- EXCEPTION_DATATYPE_MISALIGNMENT 0x80000002 程序读取一个未经对齐的数据时引发的异常。
- EXCEPTION_FLT_DENORMAL_OPERAND 0xC000008D 如果浮点数操作的操作数是非正常的,则引发该异常。所谓非正常,即它的值太小以至于不能用标准格式表示出来。
- EXCEPTION_FLT_DIVIDE_BY_ZERO 0xC000008E 浮点数除法的除数是0时引发该异常。
- EXCEPTION_FLT_INEXACT_RESULT 0xC000008F 浮点数操作的结果不能精确表示成小数时引发该异常。
- EXCEPTION_FLT_INVALID_OPERATION 0xC0000090 该异常表示不包括在这个表内的其它浮点数异常。
- EXCEPTION_FLT_OVERFLOW 0xC0000091 浮点数的指数超过所能表示的最大值时引发该异常。
- EXCEPTION_FLT_STACK_CHECK 0xC0000092 进行浮点数运算时栈发生溢出或下溢时引发该异常。
- EXCEPTION_FLT_UNDERFLOW 0xC0000093 浮点数的指数小于所能表示的最小值时引发该异常。
- EXCEPTION_ILLEGAL_INSTRUCTION 0xC000001D 程序企图执行一个无效的指令时引发该异常。
- EXCEPTION_IN_PAGE_ERROR 0xC0000006 程序要访问的内存页不在物理内存中时引发的异常。
- EXCEPTION_INT_DIVIDE_BY_ZERO 0xC0000094 整数除法的除数是0时引发该异常。
- EXCEPTION_INT_OVERFLOW 0xC0000095 整数操作的结果溢出时引发该异常。
- EXCEPTION_INVALID_DISPOSITION 0xC0000026 异常处理器返回一个无效的处理的时引发该异常。
- EXCEPTION_NONCONTINUABLE_EXCEPTION 0xC0000025 发生一个不可继续执行的异常时,如果程序继续执行,则会引发该异常。
- EXCEPTION_PRIV_INSTRUCTION 0xC0000096 程序企图执行一条当前CPU模式不允许的指令时引发该异常。
- EXCEPTION_SINGLE_STEP 0x80000004 标志寄存器的TF位为1时,每执行一条指令就会引发该异常。主要用于单步调试。
- EXCEPTION_STACK_OVERFLOW 0xC00000FD 栈溢出时引发该异常。
异常处理流程
当触发了一个异常,会根据优先级来分配谁来处理这个异常,如果是程序本身,系统就会查看线程的fs:[0]有没有安装SEH,如果有就会调用这里的函数。
fs:[0]即NT_TIB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| typedef struct _NT_TIB32 { DWORD ExceptionList; DWORD StackBase; DWORD StackLimit; DWORD SubSystemTib;
#if defined(_MSC_EXTENSIONS) union { DWORD FiberData; DWORD Version; }; #else DWORD FiberData; #endif
DWORD ArbitraryUserPointer; DWORD Self; } NT_TIB32, *PNT_TIB32;
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| typedef struct _NT_TIB64 { DWORD64 ExceptionList; DWORD64 StackBase; DWORD64 StackLimit; DWORD64 SubSystemTib;
#if defined(_MSC_EXTENSIONS) union { DWORD64 FiberData; DWORD Version; };
#else DWORD64 FiberData; #endif
DWORD64 ArbitraryUserPointer; DWORD64 Self; } NT_TIB64, *PNT_TIB64;
|
的第一个元素是指向EXCEPTION_REGISTRATION_RECORD类型的指针。
1 2 3 4
| typedef struct _EXCEPTION_REGISTRATION_RECORD { struct _EXCEPTION_REGISTRATION_RECORD *Next; PEXCEPTION_ROUTINE Handler; } EXCEPTION_REGISTRATION_RECORD;
|
可以看出,这是一个链表,其中
1 2 3 4 5 6 7 8 9 10 11 12 13
| typedef _IRQL_requires_same_ _Function_class_(EXCEPTION_ROUTINE) EXCEPTION_DISPOSITION NTAPI EXCEPTION_ROUTINE ( _Inout_ struct _EXCEPTION_RECORD *ExceptionRecord, _In_ PVOID EstablisherFrame, _Inout_ struct _CONTEXT *ContextRecord, _In_ PVOID DispatcherContext );
typedef EXCEPTION_ROUTINE *PEXCEPTION_ROUTINE;
|
PEXCEPTION_ROUTINE是一个函数指针,其中第一个传参详细描述了异常。ContextRecord保留CPU处理异常前的状态
1 2 3 4 5 6 7 8 9
| typedef struct _EXCEPTION_RECORD { DWORD ExceptionCode; DWORD ExceptionFlags; struct _EXCEPTION_RECORD *ExceptionRecord; PVOID ExceptionAddress; DWORD NumberParameters; ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]; } EXCEPTION_RECORD; typedef EXCEPTION_RECORD *PEXCEPTION_RECORD;
|
Windows会依次访问Handler,如果函数不处理,返回ExceptionContinueSearch,那么就会向后查Next,直到找到能处理的。
汇编实现
那么,想要实现上面的操作就是给系统注册一个回调函数来调用就好了,即:手动实现EXCEPTION_REGISTRATION_RECORD然后挂到链上
1 2 3 4 5 6 7 8 9 10 11 12
| int main(int argc, char* argv[]) { int a = 1; int b = 0; __try { int c = a / b; } __except (1) { printf("divide 0!"); } }
|
等价于
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 27 28 29 30 31 32 33 34 35 36 37
| #include <stdio.h> #include <windows.h>
EXCEPTION_DISPOSITION myExceptionHandler( _Inout_ struct _EXCEPTION_RECORD* ExceptionRecord, _In_ PVOID EstablisherFrame, _Inout_ struct _CONTEXT* ContextRecord, _In_ PVOID DispatcherContext) { if (ExceptionRecord->ExceptionCode == EXCEPTION_INT_DIVIDE_BY_ZERO) { printf("divide 0!\n"); ContextRecord->Eip += 3; return ExceptionContinueExecution; } return ExceptionContinueSearch; }
int main(int argc, char* argv[]) { int a = 1; int b = 0; DWORD tmp = 0; __asm { push myExceptionHandler; push dword ptr fs : [0] ; mov dword ptr fs : [0] , esp; } int c = a / b; printf("hihi"); __asm { mov dword ptr fs : [0] , esp; pop eax; pop eax; } }
|
SEH只能在栈上使用,而VEH可以在堆上使用
unwind
在try块结束后会依次删除seh链,并释放资源