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的关系
867232_UQX698BZWQ7UZ8Y

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;//x86偏移0x14,x64偏移0x20
} PEB_LDR_DATA, *PPEB_LDR_DATA;

实际

1
2
3
4
5
6
7
8
9
typedef struct _PEB_LDR_DATA
{
 ULONG Length; // +0x00
 BOOLEAN Initialized; // +0x04
 PVOID SsHandle; // +0x08内存对齐
 LIST_ENTRY InLoadOrderModuleList; // +0x0c
 LIST_ENTRY InMemoryOrderModuleList; // +0x14
 LIST_ENTRY InInitializationOrderModuleList;// +0x1c
} PEB_LDR_DATA,*PPEB_LDR_DATA; // +0x24
1
2
3
4
5
6
7
8
9
typedef struct _PEB_LDR_DATA
{
 ULONG Length; // +0x00
 BOOLEAN Initialized; // +0x04
 PVOID SsHandle; // +0x08内存对齐
 LIST_ENTRY InLoadOrderModuleList; // +0x10
 LIST_ENTRY InMemoryOrderModuleList; // +0x20
 LIST_ENTRY InInitializationOrderModuleList;// +0x30
} PEB_LDR_DATA,*PPEB_LDR_DATA; // +0x40

其中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(/*过滤表达式*/)
{
//异常处理
}

过滤表达式的取值如下:

  1. EXCEPTION_EXECUTE_HANDLER (1) 执行except里面的代码
  2. EXCEPTION_CONTINUE_SEARCH (0) 寻找下一个异常处理函数
  3. 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; // 修改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 {//安装SEH,push需要倒置
push myExceptionHandler;//自己异常处理函数
push dword ptr fs : [0] ;//next
mov dword ptr fs : [0] , esp;//设置新的链首
}
int c = a / b;
printf("hihi");
__asm {//卸载SEH
mov dword ptr fs : [0] , esp;
pop eax;
pop eax;
}
}

SEH只能在栈上使用,而VEH可以在堆上使用

unwind

在try块结束后会依次删除seh链,并释放资源