[toc]

终止处理SEH

终结处理器由 __try__finally__leave 构成,能够保证无论 __try 中的指令以何种方式退出,都必然会执行 __finally 块,但不会处理异常,只是做清理操作

SEH 的使用范围是线程相关的,每个线程都有自己的函数

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
#include <iostream>
#include <windows.h>
int main()
{
__try{
// 被检查的代码块,通常是程序的逻辑部分
printf("__try { ... }\n");
// 如果当前以包括 return goto break 的跳转指令退出就会
// 产生一些额外的函数调用,用于执行 __finally 块
// 推荐使用 __leave 跳出当前的 __try
__leave;
}
__finally{
// 终结处理块,通常编写的是用于清理当前程序的代码
// 无论 __try 以何种方式退出,都会执行这里的指令
printf("__finally { ... }\n");
// 使用 AbnormalTermination 判断 __try 的退出方
// 式,程序如果是正常退出的,那么返回值是 false
if (AbnormalTermination())
printf("异常退出\n");
else
printf("正常退出\n");
}
return 0;
}

异常处理SEH

异常处理器由关键字 __try__except 构成,能够保证 __try 中如果产生异常,会执行过滤表达式中的内容,应该在过滤表达式提供的过滤函数中处理想要处理的异常

__except 后的括号中会存在一个异常过滤表达式,表达式中的返回值必定是以下说明的几个之一

1
2
3
EXCEPTION_EXECUTE_HANDLER(1);		//执行异常处理器报告异常,但并不处理,不处理返回
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
32
33
34
// 异常过滤表达式中最常见的情况就是编写一个异常过滤函数,对异常进行处理
DWORD ExceptionFilter(EXCEPTION_POINTERS* ExceptionInfo, DWORD ExceptionCode)
{
printf("ExceptionCode: %X\n", ExceptionCode);
// 如果当前产生的异常是除零异常,那么就通过修改寄存器处理异常
if (EXCEPTION_INT_DIVIDE_BY_ZERO == ExceptionCode){
// 通过查看汇编代码可以知道产生异常的指令是 idiv eax, ecx
// 在这个位置对寄存器执行的所有修改都会直接被应用到程序中
ExceptionInfo->ContextRecord->Eax = 30;
ExceptionInfo->ContextRecord->Edx = 0;
ExceptionInfo->ContextRecord->Ecx = 1;
// 异常如果被处理了,那么就返回重新执行当前的代码
return EXCEPTION_CONTINUE_EXECUTION;
}
// 如果不是自己能够处理的异常,就不处理只报告
return EXCEPTION_EXECUTE_HANDLER;
}
int main()
{
int number = 0;
__try{
// __try 中保存的是可能产生异常的代码
number /= 0;
}
// 通常会为异常过滤表达式提供一个异常处理函数用于处理异常,并返回处理结果
// - GetExceptionCode: 用于获取异常的类型,能在过滤表达式和异常处理器中使用
// - GetExceptionInformation: 用于获取异常的信息,只能写在过滤表达式中
__except (ExceptionFilter(GetExceptionInformation(), GetExceptionCode())){
// 异常处理器,只有 __except 返回 EXCEPTION_EXECUTE_HANDLER 才会执行
printf("__try 中 产生了异常,但是并没有处理异常 %x\n", GetExceptionCode());
}
printf("numebr = %d\n", number);
return 0;
}

顶层异常UEH

UEH 全称为顶层异常处理器,这个函数只能有一个,被保存在全局变量中,由于只会被系统默认的最底层 SEH 调用,故又被称为是 SEH 的一种,是整个异常处理中的最后一环,所以通常不会再次执行异常处理操作,而是进行内存 dump,将消息发送给服务器,进行异常分析

注意: UEH 在 win7 之后,只有在非调试模式下才会被调用,故可被用于反调试

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
LONG WINAPI TopLevelExceptionFilter(EXCEPTION_POINTERS* ExceptionInfo)
{
printf("ExceptionCode: %X\n", ExceptionInfo->ExceptionRecord->ExceptionCode);
// 如果当前产生的异常是除零异常,那么就通过修改寄存器处理异常
if (EXCEPTION_INT_DIVIDE_BY_ZERO == ExceptionInfo->ExceptionRecord->ExceptionCode){
ExceptionInfo->ContextRecord->Eax = 30;
ExceptionInfo->ContextRecord->Edx = 0;
ExceptionInfo->ContextRecord->Ecx = 1;
// 异常如果被处理了,那么就返回重新执行当前的代码
return EXCEPTION_CONTINUE_EXECUTION;
}
// 如果不是自己能够处理的异常,就不处理只报告
return EXCEPTION_EXECUTE_HANDLER;
}
int main()
{
int number = 0;
// 通过一个函数可以直接的安装 UEH
SetUnhandledExceptionFilter(TopLevelExceptionFilter);
// 安装一个 SEH 处理器
__try{
number /= 0;
}
// 异常一旦被 SEH 处理,就不会再传递给 UEH
__except (EXCEPTION_EXECUTE_HANDLER){//这里不会执行UEH,但会执行以下的输出,若换成EXCEPTION_CONTINUE_SEARCH继续查找,就能执行UEH的内容
printf("这个地方永远不会执行\n");
}
printf("number = %d\n", number);
system("pause");
return 0;
}

向量异常VEH

VEH 是向量化异常处理的一种,被保存在一个全局的链表中,进程内的所有线程都可以使用这个函数,是第一个处理异常的函数

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
LONG WINAPI VectoredExceptionHandler(EXCEPTION_POINTERS* ExceptionInfo)
{
printf("ExceptionCode: %X\n", ExceptionInfo->ExceptionRecord->ExceptionCode);
// 如果当前产生的异常是除零异常,那么就通过修改寄存器处理异常
if (EXCEPTION_INT_DIVIDE_BY_ZERO == ExceptionInfo->ExceptionRecord->ExceptionCode){
ExceptionInfo->ContextRecord->Eax = 30;
ExceptionInfo->ContextRecord->Edx = 0;
ExceptionInfo->ContextRecord->Ecx = 1;
// 异常如果被处理了,那么就返回重新执行当前的代码
return EXCEPTION_CONTINUE_EXECUTION;
}
// 如果不是自己能够处理的异常,就不处理只报告
return EXCEPTION_EXECUTE_HANDLER;
}
int main()
{
int number = 0;
// 通过一个函数可以直接的安装 VEH,参数一是布尔值,如果为 TRUE
// 就将当前的函数添加到全局 VEH 函数的链表头部,否则尾部
AddVectoredExceptionHandler(TRUE, VectoredExceptionHandler);
// 安装一个 SEH 处理器
__try{
number /= 0;
}
// 异常首先被 VEH 接收到,如果无法处理才会传递给 SEH
__except (EXCEPTION_EXECUTE_HANDLER){//EXCEPTION_CONTINUE_EXECUTION
printf("这个地方永远不会执行\n");
}
printf("number = %d\n", number);
system("pause");
return 0;
}

向量异常VCH

VCH:和 VEH 类似,但是只会在异常被处理的情况下最后调用

VCH 不会对异常进行处理,调用的时机和异常处理的情况有关

异常传递顺序:VEH -> SEH -> UEH -> VCH

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
LONG WINAPI VectoredContinueHandler(EXCEPTION_POINTERS* ExceptionInfo)
{
printf("VCH: ExceptionCode: %X\n", ExceptionInfo->ExceptionRecord->ExceptionCode);
return EXCEPTION_CONTINUE_SEARCH;
}
LONG WINAPI VectoredExceptionHandler(EXCEPTION_POINTERS* ExceptionInfo)
{
printf("VEH: ExceptionCode: %X\n", ExceptionInfo->ExceptionRecord->ExceptionCode);
if (EXCEPTION_INT_DIVIDE_BY_ZERO == ExceptionInfo->ExceptionRecord->ExceptionCode){
ExceptionInfo->ContextRecord->Eax = 30;
ExceptionInfo->ContextRecord->Edx = 0;
ExceptionInfo->ContextRecord->Ecx = 1;
// 异常如果被处理了,那么就返回重新执行当前的代码
return EXCEPTION_CONTINUE_SEARCH;
}
// 如果不是自己能够处理的异常,就不处理只报告
return EXCEPTION_EXECUTE_HANDLER;
}
LONG WINAPI TopLevelExceptionFilter(EXCEPTION_POINTERS* ExceptionInfo)
{
printf("UEH: ExceptionCode: %X\n", ExceptionInfo->ExceptionRecord->ExceptionCode);
if (EXCEPTION_INT_DIVIDE_BY_ZERO == ExceptionInfo->ExceptionRecord->ExceptionCode){
ExceptionInfo->ContextRecord->Eax = 30;
ExceptionInfo->ContextRecord->Edx = 0;
ExceptionInfo->ContextRecord->Ecx = 1;
return EXCEPTION_CONTINUE_SEARCH;
}
return EXCEPTION_EXECUTE_HANDLER;
}
DWORD StructedExceptionFilter(EXCEPTION_POINTERS* ExceptionInfo)
{
printf("SEH: ExceptionCode: %X\n", ExceptionInfo->ExceptionRecord->ExceptionCode);
if (EXCEPTION_INT_DIVIDE_BY_ZERO == ExceptionInfo->ExceptionRecord->ExceptionCode){
ExceptionInfo->ContextRecord->Eax = 30;
ExceptionInfo->ContextRecord->Edx = 0;
ExceptionInfo->ContextRecord->Ecx = 1;
return EXCEPTION_CONTINUE_EXECUTION;
}
return EXCEPTION_EXECUTE_HANDLER;
}
int main()
{
int number = 0;
AddVectoredExceptionHandler(TRUE, VectoredExceptionHandler);//VEH
AddVectoredContinueHandler(TRUE, VectoredContinueHandler); //VCH
SetUnhandledExceptionFilter(TopLevelExceptionFilter); //UEH
// 安装一个 SEH 处理器
__try{
number /= 0;
}
__except (StructedExceptionFilter(GetExceptionInformation())){
printf("SEH: 异常处理器\n");
}
printf("number = %d\n", number);
system("pause");
return 0;
}

SEH探究原理

1
2
3
4
5
6
7
8
9
10
11
PEXCEPTION_REGISTRATION_RECORD ExceptionList = nullptr;
__asm push fs:[0]
__asm pop ExceptionList //寻找 ExceptionList 的头节点

__asm push ExceptionRoutine
__asm push fs : [0]
__asm mov fs : [0], esp //手动的安装一个异常处理函数

__asm mov eax, ExceptionList
__asm mov fs:[0], eax
__asm add esp, 0x08 //卸载一个异常处理函数
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
// 带有异常处理函数的函数
void test1()
{
// 在 VS 的同一个函数中无论编写了多少个 SEH, 编译
// 器实际上只会安装一个叫做 except_handler4 的函数
__try
{
printf("__try { 1... }\n");
__try
{
printf("__try { 2... }\n");
}
__except (1)
{
printf("__except (1) { ... }\n");
}
}
__except (1)
{
printf("__except (1) { ... }\n");
}
}
// 没有异常处理函数的函数
void test2() { }
// 遍历当前程序中已经存在的异常处理函数
void ShowSEH()
{
// 定义一个结构体指针,用于保存 SEH 链表的头节点
EXCEPTION_REGISTRATION_RECORD* header = nullptr;
// 通过 FS:[0] 找到 ExceptionList 的头节点
__asm push fs : [0]
__asm pop header
// 遍历异常处理链表,链表以 -1 结尾
while (header != (EXCEPTION_REGISTRATION_RECORD*)-1)
{
printf("function: %08X\n", header->Handler);
header = header->Next;
}
printf("\n");
}
EXCEPTION_DISPOSITION NTAPI ExceptionRoutine(
// 产生的异常信息
_Inout_ struct _EXCEPTION_RECORD* ExceptionRecord,
_In_ PVOID EstablisherFrame,
// 产生异常时的线程上下文
_Inout_ struct _CONTEXT* ContextRecord,
_In_ PVOID DispatcherContext
)
{
printf("自定义SEH: ExceptionCode: %X\n", ExceptionRecord->ExceptionCode);
// 如果当前产生的异常是除零异常,那么就通过修改寄存器处理异常
if (EXCEPTION_INT_DIVIDE_BY_ZERO == ExceptionRecord->ExceptionCode)
{
// 通过查看汇编代码可以知道产生异常的指令是 idiv eax, ecx
// 在这个位置对寄存器执行的所有修改都会直接被应用到程序中
ContextRecord->Eax = 30;
ContextRecord->Edx = 0;
ContextRecord->Ecx = 1;
// 异常如果被处理了,那么就返回重新执行当前的代码
return ExceptionContinueExecution;
}
// 如果不是自己能够处理的异常,就不处理只报告
return ExceptionContinueSearch;
}
int main()
{
test1();
test2();
// 遍历到了异常处理函数
ShowSEH();
// 手动的安装一个异常处理函数,操作 FS:[0]
__asm push ExceptionRoutine // 新的异常处理函数
__asm push fs : [0] // 上一个节点
__asm mov fs : [0], esp // esp 指向创建的结构体首地址

int number = 0;
number /= 0;

// 遍历到了异常处理函数
ShowSEH();

// 卸载一个异常处理函数
__asm mov eax, fs:[0] // 获取到了安装完之后的节点
__asm mov eax, [eax] // 上一个 SEH 节点,修改前的
__asm mov fs : [0], eax // 修改前的重新设置为 SEH 头节点
__asm add esp, 8
// 遍历到了异常处理函数
ShowSEH();
return 0;
}

异常发生时的处理流程:

  1. CPU 检测到异常,查 IDT 表执行中断处理程序 CommonDispatchException (若是模拟异常,则顺序为 CxxThrowException RaiseException RtlRaiseException() NtRaiseException KiRaiseException
  2. KiDispatchException 对异常进行分发,查找哪个处理程序处理异常,通过 IRETD 返回3环(模拟异常则通过系统调用返回3环)
  3. KiUserExceptionDispatcher 若是3环的异常,KiDispatchException 会修改返回3环的 eip,将 eip 指向这个函数,当线程回到3环时,将从这个函数开始执行
  4. RtlDispatchException 查找异常处理程序在哪里,先查 VEH
  5. VEH,(SEH)
  6. 代码返回到 KiUserExceptionDispatcher
  7. 调用 ZwContinue 再次进入0环(ZwContinue 调用 NtContinue,主要作用是恢复 _TRAP_FRAME,然后通过 _KiServiceExit 返回到3环)
  8. 线程回到3环后从修正的位置开始执行

总结:

  1. 当异常交由用户处理时,按照以下顺序调用异常处理方式:VEH -> SEH -> UEH -> VCH
  2. 当 VEH 表示处理了异常,就不会传递给 SEH,但会传递异常给 VCH
  3. 当 VEH 没处理,就会传递给 SEH
  4. 当 SEH 的所有异常处理函数没有能够处理异常,会调用默认的 UEH 处理函数
  5. 当 SEH 处理了异常,从 except 开始执行,就不会再将异常传递给 VCH
  6. 当 SEH 返回异常产生处执行,在返回前会调用 VCH

课堂复习

  1. 若所有的 SEH 都不能处理异常,那么最后会由谁处理异常?

    若 SEH 不能处理异常,异常会传递给 UEH,UEH 实际上是系统默认的 SEH 调用的

  2. VCH 处理程序会在什么情况下被调用?

    只有之前的 SEH,VEH 或 UEH 中的任何一个处理了异常,这个函数才会被调用

  3. SEH 是全局有效吗?它们被保存在什么地方?

    • SEH 是线程相关的,保存在 FS:[0] 内,对应的就是 TEB.NT_TIB.ExceptionList 字段
    • UEH 是进程相关的,若任何一个异常处理程序无法处理,就被调用,实际保存在一个全局的函数指针中
    • VEH 和 VCH 是进程相关的,保存在全局链表中,两个函数只是保存的标志位不同
  4. 什么是异常?什么是中断?

    • 异常通常是在 CPU 满足特定的条件时内部产生的,是一个同步事件,必须立即进行处理
    • 中断通常由外部设备产生,如鼠标键盘等,是一个异步事件,可以不进行处理
  5. 异常的种类有哪些?有什么特点?

    • 错误类:通常可修复,异常产生时,eip 指向的是产生异常的指令(除零错误,硬件断点,内存断点)
    • 陷阱类:通常可修复,异常产生时,eip 指向的是下一条指令 (int 3)
    • 终止类:无法修复,寄存器的指向是无意义的
  6. windbg 中分别使用哪些系列的指令查看数据、修改数据、设置断点?

    • 查看数据:db/dq/da/du/dw/dd/dt(查看结构体)
    • 修改数据:eb/eq/ea/eu/ew
    • 断点相关:bp/bu/bl/be/bd/bc
    • 为指定模块加载符号:.reload /f /i demo.exe -> ml
    • 流程相关:t(F11)/p(F10)/g(F5)
  7. 陷阱处理器被保存在哪里?windbg 中使用什么指令可以查看它?

    Windows 中中断和异常是统一管理的,所有的处理函数都被保存在 CPU 相关的 IDT 中,使用 !IDT 可以进行查看

  8. 异常的产生方式有几种?分别是什么?

    • 指令满足特定的条件,CPU 自动触发
    • 用户使用 RaiseException 函数主动抛出异常

异常分发

异常分发过程使用的函数及具体的功能(以 int 3 为例)

  1. KiTrrap03

    • 谁调用的:CPU 产生了异常,从 IDT[3] 找到这个函数并调用

    • 调用了谁:ENTER_TRAP(宏),CommonDispatchException(函数)

    • 功能:

      1. 使用 ENTER_TRAP 宏填充一个陷阱帧,目的是为了处理异常后继续执行
      2. 调用 CommonDispatchException 并传递相关的参数(异常类型/异常地址/异常参数)
  1. CommonDispatchException

    • 谁调用的:KiTrrap03
    • 调用了谁:KiDispatchException
    • 功能:
      1. 构建了一个 EXCEPTION_RECORD 结构体并使用接收的参数填充
      2. 调用了 KiDispatchException 并传递相关的参数(先前模式/分发次数/陷阱帧/异常记录)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    typedef _EXCEPTION_RECORD{
    DWORD ExceptionCode; //异常代码
    DWORD ExceptionFlags; //异常状态,若是CPU产生的异常,值非0,若是软件模拟产生的异常,值为1
    struct _EXCEPTION_RECORD* ExceptionRecord; //下一个异常
    PVOID ExceptionAddress; //异常发生地址
    DWORD NumberParameters; //附加参数个数
    ULONG_PTR ExceptionInformation
    [EXCEPTION_MAXIMUN_PARAMETERS]; //附加参数指针
    }
  1. KiDispatchException

    • 谁调用的:CommonDispatchException

    • 调用了谁:RtlDispatchException(R0),间接调用 RtlDispatchException(R3),KiUserExceptionDispatcher

    • 功能:

      1. 当前接收到的异常产生于内核态

        1. 尝试将异常信息发送给内核调试器(KD/WINDBG)
        2. 若不成功,通过 RtlDispatchException(R0) 函数调用 SEH
        3. 若不成功,重复 1. 操作
        4. 若不成功,调用 KeDebugCheck 函数使系统蓝屏并显示错误码
      2. 当前接收到的异常产生于用户态

        1. 若未被 R3 调试,就尝试将异常发送给内核调试器

        2. 若不成功,通过 DbgkForwardException 将异常传递给 R3 调试器(OD/x64)

        3. 若不成功,在用户栈中填充一个 EXCEPTION_POINTER 结构

          设置 eip 指向 ntdll 中的函数 KiUserExceptionDispatcher

        4. 执行到用户代码,在 KiUserExceptionDispatcher 中调用 RtlDispatchException(R3)

          这个函数内部会依次调用 VEH,SEH,UEH, (UEH)

        5. 依然无法处理就执行第二次分发,将异常发送到调试端口(调试器)/异常端口(子系统)

        6. 仍然无法处理,就结束进程

        KeUserExceptionDispatcher 这个全局变量在操作系统初始化时,会被赋值为 KiUserExceptionDispatcher 函数的值

![2019.12.30-1](D:\git png\2019.12.30-1.png)

![2019.12.30-2](D:\git png\2019.12.30-2.png)

![2019.12.30-3](D:\git png\2019.12.30-3.png)