对 SEH 的浅显认识

SEH 是 Windows 操作系统提供的异常处理机制。

使用 __try 关键字注册 SEH

使用 __try 关键字可以创建 SEH:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int FilterFunc(DWORD dwExceptionCode)
{
if (dwExceptionCode == EXCEPTION_ACCESS_VIOLATION) //非法访问异常
{
printf("SEH catch\n");
return EXCEPTION_EXECUTE_HANDLER;
}
return EXCEPTION_CONTINUE_SEARCH;
}
int main()
{
__try {
int *a=NULL;
*a=1;
}
__except (FilterFunc(GetExceptionCode())){
printf("SEH final\n");
}
printf("in main");
return 0;
}

该段代码在 32 位和 64 位上均有效,不过在汇编层面的具体实现方式不同。

其中__try关键字和__except关键字共同完成异常处理,__try关键字用来捕获异常,__except关键字根据后面表达式的值来确定当前的异常处理模块是否可以处理该种异常。括号内表达式值有三种:

1
2
3
4
//https://docs.microsoft.com/en-us/cpp/cpp/try-except-statement?view=msvc-160
EXCEPTION_CONTINUE_EXECUTION (–1) //异常被忽略,控制流将在异常出现的点之后,继续恢复运行。
EXCEPTION_CONTINUE_SEARCH (0) //异常不被识别,也即当前的这个__except模块不是这个异常错误所对应的正确的异常处理模块。
EXCEPTION_EXECUTE_HANDLER (1) //异常已经被识别,控制流将进入到__except模块中运行异常处理代码

FilterFunc函数参数中的GetExceptionCode函数作用是获取异常代码:

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
//minwinbase.h
/* compatibility macros */
#define EXCEPTION_ACCESS_VIOLATION STATUS_ACCESS_VIOLATION
#define EXCEPTION_DATATYPE_MISALIGNMENT STATUS_DATATYPE_MISALIGNMENT
#define EXCEPTION_BREAKPOINT STATUS_BREAKPOINT
#define EXCEPTION_SINGLE_STEP STATUS_SINGLE_STEP
#define EXCEPTION_ARRAY_BOUNDS_EXCEEDED STATUS_ARRAY_BOUNDS_EXCEEDED
#define EXCEPTION_FLT_DENORMAL_OPERAND STATUS_FLOAT_DENORMAL_OPERAND
#define EXCEPTION_FLT_DIVIDE_BY_ZERO STATUS_FLOAT_DIVIDE_BY_ZERO
#define EXCEPTION_FLT_INEXACT_RESULT STATUS_FLOAT_INEXACT_RESULT
#define EXCEPTION_FLT_INVALID_OPERATION STATUS_FLOAT_INVALID_OPERATION
#define EXCEPTION_FLT_OVERFLOW STATUS_FLOAT_OVERFLOW
#define EXCEPTION_FLT_STACK_CHECK STATUS_FLOAT_STACK_CHECK
#define EXCEPTION_FLT_UNDERFLOW STATUS_FLOAT_UNDERFLOW
#define EXCEPTION_INT_DIVIDE_BY_ZERO STATUS_INTEGER_DIVIDE_BY_ZERO
#define EXCEPTION_INT_OVERFLOW STATUS_INTEGER_OVERFLOW
#define EXCEPTION_PRIV_INSTRUCTION STATUS_PRIVILEGED_INSTRUCTION
#define EXCEPTION_IN_PAGE_ERROR STATUS_IN_PAGE_ERROR
#define EXCEPTION_ILLEGAL_INSTRUCTION STATUS_ILLEGAL_INSTRUCTION
#define EXCEPTION_NONCONTINUABLE_EXCEPTION STATUS_NONCONTINUABLE_EXCEPTION
#define EXCEPTION_STACK_OVERFLOW STATUS_STACK_OVERFLOW
#define EXCEPTION_INVALID_DISPOSITION STATUS_INVALID_DISPOSITION
#define EXCEPTION_GUARD_PAGE STATUS_GUARD_PAGE_VIOLATION
#define EXCEPTION_INVALID_HANDLE STATUS_INVALID_HANDLE
#define EXCEPTION_POSSIBLE_DEADLOCK STATUS_POSSIBLE_DEADLOCK
//确切的常数定义在winnt.h

其中常用的如下:

1
2
3
4
5
EXCEPTION_ACCESS_VIOLATION    非法访问异常
EXCEPTION_BREAKPOINT int 3 断点异常
EXCEPTION_ILLEGAL_INSTRUCTION 无法解析指令异常
EXCEPTION_INT_DIVIDE_BY_ZERO 整数除 0 异常
EXCEPTION_SINGLE_STEP 单步工作模式异常

GetExceptionCode外,还有一个函数GetExceptionInformation用于获取异常的详细信息。

另外,还有__try-__finally语句,finally没有表达式,即异常直接被finally中的语句处理(或直接跳过)。

在32位汇编中注册 SEH 异常处理函数

1
2
3
push SEHfunction
push DWORD PTR FS:[0]
mov DWORD PTR FS:[0],esp

该段代码仅适用于 32 位,64 位中 SEH 由编译器直接注册

对反汇编的 SEH 进行分析

对32位来说很简单,查看上面汇编代码的特征即可:
该段代码由上面的示例代码在 vs2019 中生成
32位SEH
而对64位来说,由于 SEH 不在函数中动态注册,分析起来有一定难度。
对于64位 PE 文件,编译器在其头部嵌入了几乎所有(一小部分特殊函数没有)函数的 SEH 信息。每个函数是否使用了SEH,使用的是什么 SEH(__except还是__finally)等,就在 PE 的Exception Directory里。所以我们如果要找64位函数所使用的 SEH 的话,需要先取得该函数的相对偏移地址(RVA),然后到该目录下找,示例还是上文的代码,所用工具为CFF Explorer,异常就在main函数内触发,main的偏移为0x11900。可以看到这里出现的BeginAddress为异常的__try块起始位置,EndAddress__try的结束位置,而HandlerAddress是调用FilterFunc函数的代码,JumpTarget__except的代码。
32位SEH
32位SEH
32位SEH
(详细信息可参考这位大牛的SEH分析笔记(X64篇) - 云+社区 - 腾讯云)。