Windows XP 异常处理(三)编译器扩展SEH

ʕ •̀ o •́ ʔ

1 SEH的编译

VC/VS编译器定义了 __try__except__finally 这3个扩展关键字,运行C和C++程序使用这套关键字来编写异常处理代码。编译器会将这些关键字编写的异常处理代码与操作系统的SEH机制衔接起来。

VC编译器优化以后的SEH为程序员提供了如下两种功能:

  • 异常处理功能:try-except,用于接收和处理被保护块中的代码所发生的异常。
  • 终结处理功能:try-finally,保证终结处理块始终可以得到执行。

概括来说,__try{}__except 结构将手工方法中的函数和嵌入式汇编代码简化成高级语言中的标记符和表达式。具体来说,将被保护块使用 __ty 关键字和大括号包围起来;使用 __except 块将原本写在异常回调函数中的分支判断结构分解成过滤表达式(if中的异常情况)和与之对应的异常处理块。

1.1 try-except

对应关系如下图:

21.png

通过手工方式将 SEH 挂入到 FS:[0] 链条、处理异常、卸载SEH结构的过程在编译器中对应的语法结构为:

1
2
3
4
5
6
7
8
__try	//挂入SEH
{
// 被保护体,也就是要保护的代码块
}
__except(过滤表达式)
{
// 异常处理块(exception-handling block)
}

从外部行为的角度来看,结构化异常处理的基本规则是,如果被保护体中的代码发生了异常,不论是CPU 级的硬件异常还是软件发起的软件异常,系统都应该评估过滤表达式的内容,也就是执行过滤表达式中的代码。这意味着,程序的执行路线是从被保护体中飞跃到过滤表达式中的。要正确地飞跃到表达式中显然不那么简单,需要准确地知道过滤表达式的位置,还要保持栈的平衡。

过滤表达式的使用:

过滤表达式既可以是常量函数调用,也可以是表达式,只要表达式的结果为 $-1$ ,$0$,$1$ 这三个值之一,它们的含义如下:

名称 含义
-1 EXCEPTION_CONTINUE_EXECUTION 返回出错位置重新执行。
0 EXCEPTION_CONTINUE_SEARCH 本异常处理块不处理该该异常,寻找其他异常处理块(不是异常处理器)。
1 EXCEPTION_EXECUTE_HANDLER 执行 异常处理块 中的代码。执行完后会继续执行本异常处理块下面的代码,即except块之后的第一条指令。

Visual C++编译器还提供了两个只能在过滤表达式中使用的宏来辅助编写异常处理代码:

  • DWORD GetExceptionCode():返回异常代码。
  • LPEXCEPTION_POINTERS GetExceptionInformation():返回一个指向 EXCEPTION_POINTERS 结构的指针。

1.2 try-finally

终结处理的语法结构如下:

1
2
3
4
5
6
7
8
__try
{
// 被保护体(guarded body),也就是要保护的代码块
}
__finally
{
// 终结代码块
}

终结处理由两部分构造,使用 __try 关键字定义的被保护体和使用 __finally关键字定义的终结处理块。终结处理的目标是只要被保护体被执行,那么终结处理块就也会被执行,除非被保护体中的代码终止了当前线程(比如调用ExitThreadExitProcess退出线程或整个进程)。因为终结处理块的这种特征,终结处理非常适合做状态恢复或资源释放等工作。比如释放被保护块中获得的信号量以防止被保护块内发生意外时因没有释放这个信号量而导致线程死锁的问题。

根据被保护块的执行路线,SEH 把被保护块的退出(执行完毕)分为正常结束和非正常结束两种。

  • 正常结束:如果被保护块得到自然执行并顺序进入终结处理块,就认为被保护块是正常结束的。
  • 非正常结束:如果被保护块是因为发生异常或由于return gotobreakcontinue 等流程控制语句离开被保护块的,就认为被保护块是非正常结束的。

一个只能在终结块中使用的函数知道被保护块的退出方式:

1
BOOL bRet = AbnormalTermination(void);
  • bRet == TRUE,非正常结束。
  • bRet == FALSE,正常结束。

一个只能在被保护块中使用的关键字:leave。该关键字的作用是立即离开(停止执行)被保护块,或者理解为立即跳转到被保护块的末尾(__try块的右大括号)。

举例说明:

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 <cstdio>
#include <windows.h>

int main()
{
__try
{
printf("开始执行__try中的代码\n");
printf("执行__leave之前的代码\n");
__leave;
printf("执行__leave之后的代码\n");
}
__finally
{
printf("执行__finally的代码\n");
}

return 0;
}

/*输出:
开始执行__try中的代码
执行__leave之前的代码
执行__finally的代码
*/

参考:Windows内核学习笔记之异常(下)– 看雪1900

1.3 32/64位SEH编译差异

在上一篇文章中 SEH 部分说过关于 SEH 的存放位置

  1. 栈帧(stack frame):将异常处理器注册在所在函数的栈帧中。使用这种方式注册的异常处理经常称为基于帧的异常处理(fame based exception handling )。32 位的 Windows 系统(x86)使用的就是此种方式。
  2. 表格(table):将异常处理器的基本信息以表格的形式存储在可执行文件(PE)的数据段中,这种方式简称为基于表的异常处理(table based exception handling )。64 位 Windows 系统(x64)中的64 位程序使用了这种方式。

所以编译器在编译 32 bit 程序和 64 bit 程序的时候,针对 try-except 的编译是不同的。就针对 32 位程序的编译来说,VC、VS编译器在编译时实际上差异不大,VS编译器因为加入了一些新的安全选项,所以编译时加入了一些额外的安全检查代码。

64 位基于表的异常处理:增强异常处理机制安全性的一种更彻底的方法是 x64 系统中的基于表的异常处理(table based exception handling)。其基本思想是将异常处理器的描述和登记信息都以表格的形式存储在可执行文件中,当有异常发生时,系统根据异常的发生位置自动在这些表格中寻找匹配的处理函数,不需要在栈上做任何登记,也不再使用 FS:[0] 链条。基于表的异常处理机制与基于帧的异常处理机制是不兼容的。运行在 x64 CPU 上的 64 位 Windows 使用了基于表的异常处理,编译运行在这样的目标系统中的 64 位应用序时,编译器会自动使用新的编译方式产生合适的代码。

本文主要讨论 32 位程序的编译,因为 32 位程序的 SEH 是注册在线程堆栈中的,可以在写代码时查看反汇编代码就能知道其编译情况。但是 64 位程序应该是需要逆向一下异常的处理框架才能知道怎么使用 SEH 的,try-except 源代码反汇编查看不到 SEH 相关情况。

1.4 try-except汇编

1.4.1 32位SEH编译

编译器不是为每一个 try-except 都注册 SEH,而是一个函数内不管使用了多少次对 try-except都只会注册一个异常处理器,并且只是用一个统一的异常处理回调函数__except_handler3/__except_handler4

每次执行到 __except_handler3 时,该函数会把回调函数的第二个参数 EstablisherFrame 指向的结构EXCEPTION_REGISTRATION_RECORD(SEH)进行拓展。__except_handler3 回调函数正是依靠扩展的 3 个字段来寻找过滤表达式和异常处理块的。

VC编译器对 Windows 提供的 EXCEPTION_REGISTRATION_RECORD 结构进行了拓展,拓展后的结构如下:

1
2
3
4
5
6
7
8
struct _EXCEPTION_REGISTRATION
{
struct _EXCEPTION_REGISTRATION* prev;
void (*handler)(PEXCEPTION_RECORD, PEXCEPTION_ REGISTRATION, PCONTEXT, PEXCEPTION_RECORD);
struct scopetable_entry * scopetable;
int trylevel;
int _ebp;
};
字段 作用
prev 指向上一个结构体的地址,上一个异常链
handler 指向异常处理函数
scopetable 范围表的起始地址
trylevel 这个结构对应的__try块的编号
ebp 栈帧的基地址

可以看到 EXCEPTION_REGISTRATION_RECORD 结构是拓展后_EXCEPTION_REGISTRATION 结构的第一个成员(类似于C++类的继承)。

其中前两个字段是操作系统规定的标准登记结构,后三个字段是编译器扩展的。__except_handler3 函数正是依靠这几个扩展字段来寻找过滤表达式和异常处理块。

在一个函数里面

  1. 不管有多少个 try-except 都只会挂入一个 _EXCEPTION_REGISTRATION 结构,也就是只有一个 excepthandler3 函数(对于递归函数,每一次调用都会创建一个 _EXCEPTION_REGISTRATION ,并挂入线程的异常链表中)。
  2. 有多少个 try-exceptscopetable 数组就有几项。

堆栈中的形成的 FS:[0] 链条:

22.png

下面用 VC6 将如下代码编译成 32 位程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include "stdafx.h"
#include <stdio.h>
#include <stdlib.h>
#include <Windows.h>

void TestSEH()
{
__try
{
}
__except (1)
{

}
}

int main(int argc, char* argv[])
{
TestSEH();
return 0;
}

VC6编译函数 TestSEH 汇编代码如下(Debug版):

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
.text:00401020 // =============== S U B R O U T I N E =======================================
.text:00401020
.text:00401020 // Attributes: bp-based frame
.text:00401020
.text:00401020 sub_401020 proc near // CODE XREF: sub_401005↑j
.text:00401020
.text:00401020 var_58 = byte ptr -58h
.text:00401020 ms_exc = CPPEH_RECORD ptr -18h
.text:00401020
.text:00401020 // __unwind { // __except_handler3
.text:00401020 push ebp
.text:00401021 mov ebp, esp
.text:00401023 push 0FFFFFFFFh
.text:00401025 push offset stru_422020
.text:0040102A push offset __except_handler3
.text:0040102F mov eax, large fs:0
.text:00401035 push eax
.text:00401036 mov large fs:0, esp
.text:0040103D add esp, 0FFFFFFB8h
.text:00401040 push ebx
.text:00401041 push esi
.text:00401042 push edi
.text:00401043 mov [ebp+ms_exc.old_esp], esp
.text:00401046 lea edi, [ebp+var_58]
.text:00401049 mov ecx, 10h
.text:0040104E mov eax, 0CCCCCCCCh
.text:00401053 rep stosd
.text:00401055 // __try { // __except at loc_40106B
.text:00401055 mov [ebp+ms_exc.registration.TryLevel], 0
.text:00401055 // } // starts at 401055
.text:0040105C mov [ebp+ms_exc.registration.TryLevel], 0FFFFFFFFh
.text:00401063 jmp short loc_401075
.text:00401065 // ---------------------------------------------------------------------------
.text:00401065
.text:00401065 loc_401065: // DATA XREF: .rdata:stru_422020↓o
.text:00401065 // __except filter // owned by 401055
.text:00401065 mov eax, 1
.text:0040106A retn
.text:0040106B // ---------------------------------------------------------------------------
.text:0040106B
.text:0040106B loc_40106B: // DATA XREF: .rdata:stru_422020↓o
.text:0040106B // __except(loc_401065) // owned by 401055
.text:0040106B mov esp, [ebp+ms_exc.old_esp]
.text:0040106E mov [ebp+ms_exc.registration.TryLevel], 0FFFFFFFFh
.text:00401075
.text:00401075 loc_401075: // CODE XREF: sub_401020+43↑j
.text:00401075 mov ecx, [ebp+ms_exc.registration.Next]
.text:00401078 mov large fs:0, ecx
.text:0040107F pop edi
.text:00401080 pop esi
.text:00401081 pop ebx
.text:00401082 mov esp, ebp
.text:00401084 pop ebp
.text:00401085 retn
.text:00401085 // } // starts at 401020
.text:00401085 sub_401020 endp
.text:00401085
.text:00401085 // ---------------------------------------------------------------------------
  1. 首先注册一个 _EXCEPTION_REGISTRATION 结构:

    1
    2
    3
    4
    5
    6
    7
    8
    .text:00401020                 push    ebp
    .text:00401021 mov ebp, esp
    .text:00401023 push 0FFFFFFFFh
    .text:00401025 push offset stru_422020
    .text:0040102A push offset __except_handler3
    .text:0040102F mov eax, large fs:0
    .text:00401035 push eax
    .text:00401036 mov large fs:0, esp
    • trylevel = 0xffffffff
    • scopetable = 0x00422020
    • handler = __except_handler3
    • prev = fs:[0]

1.4.2 trylevel

编译器是以函数为单位来登记异常处理器 _EXCEPTION_REGISTRATION 的,在函数的入口处进行登记,在出口处进行注销。一个函数只有一个
那么,如何确定导致异常的代码是否在保护块中呢?如果有多个保护块,又如何判断属于哪个保护块呢?答案是对每个 __try 结构进行编号,然后使用 _ EXCEPTION_ REGISTRATION.trylevel 判断属于哪个保护块。

trylevel 用来标识当前的代码处于哪一个 __try 里面,所有的 __except(Filter){} 编译信息都保存在 scopetable 结构数组中。trylevel__try 的保护块与__except(Filter){} 连接起来。这样不管在哪个 __try 中的发生的异常都能定位到对应的过滤表达式和异常处理块。也很好的解决了异常嵌套问题。

__except_handler3 函数根据 trylevel 知道当前异常处于函数的哪一个 __try,然后使用 scopetable[trylevel] 能对找到改异常块对应的过滤器和异常处理块。

  • trylevel == TRYLEVEL_NONE(-1)时,表示不在任何 __try 结构中。当进入函数时trylevel 成员被初始化为 0xffffffff

  • trylevel == 0时,表示进入第一个 __try,以此类推。

每当进、出每一个 __try 时都需要修改 trylevel 的值

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
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
1:    #include "stdafx.h"
2: #include <windows.h>
3:
4: void Test__try__except(void)
5: {
0040DF00 push ebp
0040DF01 mov ebp,esp
0040DF03 push 0FFh
0040DF05 push offset string "trylevel = 0"+14h (00422ff0)
0040DF0A push offset __except_handler3 (004041d0)
0040DF0F mov eax,fs:[00000000]
0040DF15 push eax
0040DF16 mov dword ptr fs:[0],esp
0040DF1D add esp,0B0h
0040DF20 push ebx
0040DF21 push esi
0040DF22 push edi
0040DF23 mov dword ptr [ebp-18h],esp
0040DF26 lea edi,[ebp-60h]
0040DF29 mov ecx,12h
0040DF2E mov eax,0CCCCCCCCh
0040DF33 rep stos dword ptr [edi]
6: __try
0040DF35 mov dword ptr [ebp-4],0
7: {
8: printf("trylevel = 0\n");
0040DF3C push offset string "trylevel = 0" (00422fdc)
0040DF41 call printf (00401060)
0040DF46 add esp,4
9:
10: __try
0040DF49 mov dword ptr [ebp-4],1
11: {
12: printf("trylevel = 1\n");
0040DF50 push offset string "trylevel = 1" (00422fcc)
0040DF55 call printf (00401060)
0040DF5A add esp,4
13: }
0040DF5D mov dword ptr [ebp-4],0
0040DF64 jmp $L16993+11h (0040df7d)
14: __except(EXCEPTION_EXECUTE_HANDLER)//1
0040DF66 mov eax,1
$L16994:
0040DF6B ret
$L16993:
0040DF6C mov esp,dword ptr [ebp-18h]
15: {
16: int a = 0;
0040DF6F mov dword ptr [a],0
17: }
0040DF76 mov dword ptr [ebp-4],0
18:
19: }
0040DF7D mov dword ptr [ebp-4],0FFFFFFFFh
0040DF84 jmp $L16989+0Ah (0040df94)
20: __except(EXCEPTION_CONTINUE_EXECUTION)//-1
0040DF86 or eax,0FFh
$L16990:
0040DF89 ret
$L16989:
0040DF8A mov esp,dword ptr [ebp-18h]
21: {}
0040DF8D mov dword ptr [ebp-4],0FFFFFFFFh
22:
23: __try
0040DF94 mov dword ptr [ebp-4],2
24: {
25: printf("trylevel = 2\n");
0040DF9B push offset string "trylevel = 2\n" (00422fbc)
0040DFA0 call printf (00401060)
0040DFA5 add esp,4
26:
27: __try
0040DFA8 mov dword ptr [ebp-4],3
28: {
29: printf("trylevel = 3\n");
0040DFAF push offset string "trylevel = 3\n" (0042201c)
0040DFB4 call printf (00401060)
0040DFB9 add esp,4
30: }
0040DFBC mov dword ptr [ebp-4],2
0040DFC3 jmp $L17001+0Ah (0040dfe9)
31: __except(GetExceptionCode() == EXCEPTION_INT_DIVIDE_BY_ZERO ? EXCEPTION_EXECUTE_HANDLER :\
32: EXCEPTION_CONTINUE_SEARCH)
0040DFC5 mov eax,dword ptr [ebp-14h]
0040DFC8 mov ecx,dword ptr [eax]
0040DFCA mov edx,dword ptr [ecx]
0040DFCC mov dword ptr [ebp-20h],edx
0040DFCF mov eax,dword ptr [ebp-20h]
0040DFD2 xor ecx,ecx
0040DFD4 cmp eax,0C0000094h
0040DFD9 sete cl
0040DFDC mov eax,ecx
$L17002:
0040DFDE ret
$L17001:
0040DFDF mov esp,dword ptr [ebp-18h]
33: {}
0040DFE2 mov dword ptr [ebp-4],2
34: }
0040DFE9 mov dword ptr [ebp-4],0FFFFFFFFh
0040DFF0 jmp $L16997+0Ah (0040e002)
35: __except(EXCEPTION_EXECUTE_HANDLER)//1
0040DFF2 mov eax,1
$L16998:
0040DFF7 ret
$L16997:
0040DFF8 mov esp,dword ptr [ebp-18h]
36: {}
0040DFFB mov dword ptr [ebp-4],0FFFFFFFFh
37:
38: return;
39: }
0040E002 mov ecx,dword ptr [ebp-10h]
0040E005 mov dword ptr fs:[0],ecx
0040E00C pop edi
0040E00D pop esi
0040E00E pop ebx
0040E00F add esp,60h
0040E012 cmp ebp,esp
0040E014 call __chkesp (004010e0)
0040E019 mov esp,ebp
0040E01B pop ebp
0040E01C ret
  1. 进入函数时注册异常处理器 _EXCEPTION_REGISTRATION ,即0040DF00~0040DF16,并将 trylevel 初始化为 0xffffffff

  2. 进入第一个 __try 的第一句代码就 trylevel = 0

    1
    0040DF35		mov	dword ptr [ebp-4],0
  3. 进入第二个 __try 时立即修改 trylevel = 1

    1
    0040DF49		mov	dword ptr [ebp-4],1
  4. 离开第二个 __try 时理解修改 trylevel = 0。以及离开第一个 __try 时理解修改 trylevel = 0xffffffff

    1
    2
    3
    0040DF5D		mov	dword ptr [ebp-4],0
    ...
    0040DF7D mov dword ptr [ebp-4],0FFFFFFFFh
  5. 当最后离开函数时,注销异常处理器 _EXCEPTION_REGISTRATION

    1
    2
    0040E002		mov	ecx,dword ptr [ebp-10h]
    0040E005 mov dword ptr fs:[0],ecx
  6. 可以看到,在正常的代码执行流当中,并不会执行 __except(Filter){} 的代码。只有当异常产生才会有机会执行 __except(){}

  7. 可以看到针对每一个 __except() 括号中过滤器编译时,都会有一个 ret 指令,这是因为编译器将每一个过滤器都当成一个函数来编译,并将过滤器的首地址放在 scopetable_entry.lpfnFilter 中。 方便 __except_handler3 找到并执行过滤器然后做判断。

  8. __except 花括号里面的代码没有 ret,也就是说 __except 执行完成后继续往下执行,并不是返回。

1.4.3 scopetable

为了描述应用程序代码中的 __try{}__except 结构,编译器在编译每个使用此结构的函数时会为其建立一个数组,并存储在模块文件的数据区(通常称为异常处理范围表)中,也叫做范围表。数组的每个元素是一个 scopetable_entry 结构,用来描述一个 __try{}__except 结构。

1
2
3
4
5
6
struct scopetable_entry
{
DWORD previousTryLevel //上一个try{}结构编号
PARPROC lpfnFilter //过滤函数的起始地址
PARPROC lpfnHandler //异常处理程序的地址
};
  • IpfnFiter:指向过滤表达式(被编译成的)函数 。
  • lpfhHlandler: 异常处理块的起始地址。

如上一节 Test__try__except 函数的范围表 scopetable 地址为 0x00422ff0

24.png

说明:在 scopetable 表中,对于有效的 scopetable_entry 项:

  • lpfnFilter == NULL,对应于 __try{}__finallylpfnHandler 在局部展开中执行。
  • lpfnFilter != NULL,对应于 __try{}__exceptlpfnHandler 在异常处理函数中执行。

1.4.4 VC6编译拓展FS:[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
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
91
92
93
94
95
96
97
98
99
100
101
#include "stdafx.h"
#include <windows.h>


struct scopetable_entry
{
DWORD previousTryLevel;
FARPROC lpfnFilter;
FARPROC lpfnHandler;
};

typedef struct _EXCEPTION_REGISTRATION
{
_EXCEPTION_REGISTRATION* prev;
FARPROC handler;
scopetable_entry * scopetable;
DWORD trylevel;
DWORD _ebp;
}EXCEPTION_REGISTRATION;

void ShowExceptionList(void)
{
DWORD ExpList = 0;
__asm
{
mov eax, fs:[0];
mov ExpList, eax;
}

EXCEPTION_REGISTRATION* pExpList = (EXCEPTION_REGISTRATION*)ExpList;
scopetable_entry * pScopeTableEntry = pExpList->scopetable;

while(pExpList != (EXCEPTION_REGISTRATION*)0xFFFFFFFF)
{
printf( "Frame: %08X, Prev: %08X, Handler: %08X, Scopetable: %08X, TryLevel: %08X\n",
pExpList, pExpList->prev, pExpList->handler, pExpList->scopetable, pExpList->trylevel );


for (DWORD i = 0; i <= pExpList->trylevel; i++ )
{
printf( " scopetable[%u] PrevTryLevel: %08X "
"filter: %08X __except: %08X\n", i,
pScopeTableEntry->previousTryLevel,
pScopeTableEntry->lpfnFilter,
pScopeTableEntry->lpfnHandler );

pScopeTableEntry++;
}
printf( "\n" );

pExpList = (EXCEPTION_REGISTRATION*)pExpList->prev;
}

}

void Test__try__except(void)
{
__try
{
printf("trylevel = 0\n");

__try
{
printf("trylevel = 1\n");
}
__except(EXCEPTION_EXECUTE_HANDLER)//1
{
int a = 0;
}

}
__except(EXCEPTION_CONTINUE_EXECUTION)//-1
{}

__try
{
printf("trylevel = 2\n");

__try
{
ShowExceptionList();
printf("trylevel = 3\n");
}
__except(GetExceptionCode() == EXCEPTION_INT_DIVIDE_BY_ZERO ? EXCEPTION_EXECUTE_HANDLER :\
EXCEPTION_CONTINUE_SEARCH)
{}
}
__except(EXCEPTION_EXECUTE_HANDLER)//1
{}

return;
}

int main(int argc, char* argv[])
{
//ShowExceptionList();
Test__try__except();

getchar();
return 0;
}

结构 _EXCEPTION_REGISTRATION 后三个成员是编译器拓展的,所以这三个成员在不同的时刻所指向的地址空间的值是会变化的,这些地址是可以被其他程序读写。但是前两个成员 prevhander 一直是正确的(因为他们由系统维护)。

27.png

可以看到,除了函数 Test__try__except 中有 Hander: 0x00401520(__except_hander3)外,下面还有一个,同时 fs:[0] 链上孩挂着一个 Hander: 0x7c839ac0 异常处理回调函数。

实际上第二个帧来自Visual C++运行时库。Visual C++ 运行时库源代码中的 CRT0.C文件清楚地表明了对 mainWinMain 的调用也被一个__try__except块封装着。这个 __try 块的过滤器表达式代码可以在 WINXFLTR.C 文件中找到。

注意到最后一个帧的异常处理程序的地址是 0x7c839ac0,这与其它两个不同。仔细观察一下,你会发现这个地址在 KERNEL32.DLL中。这个特别的帧就是由 KERNEL32.DLL 中的 BaseProcessStart 函数安装的。(会在未执行异常中讲解)

参考:《Windows异常处理》。

1.4.5 VS2019编译32位SEH

VS 编译器对 Windows 提供的 EXCEPTION_REGISTRATION_RECORD 结构进行了拓展,也在 VC6 编译器基础上做了拓展后:

1
2
3
4
5
6
7
8
struct _EXCEPTION_REGISTRATION
{
struct _EXCEPTION_REGISTRATION* prev;
void (*handler)(PEXCEPTION_RECORD, PEXCEPTION_ REGISTRATION, PCONTEXT, PEXCEPTION_RECORD);
struct VS_scopetable_entry * scopetable;
int trylevel;
int _ebp;
};
字段 作用
prev 指向上一个结构体的地址,上一个异常链
handler 指向异常处理函数
scopetable 范围表的起始地址
trylevel 这个结构对应的__try块的编号
ebp 栈帧的基地址

但是 VS 编译器在 VC6 编译器上又做了部分拓展,主要有两点:

  1. 异常处理回调函数统一入口变成 __except_handler4 函数。

  2. trylevel 初始化为 0xfffffffe,主要是为了同 FS:[0] == -1 做一个区别,第一个 __try 还是从 0 开始的。

  3. VS 编译器对 scopetable 在 VC6 基础上进行了拓展:

    1
    2
    3
    4
    5
    6
    7
    8
    struct VS_scopetable_entry
    {
    DWORD GSCookieOffset;
    DWORD GSCookieXOROffset;
    DWORD EHCookieOffset;
    DWORD EHCookieXOROffset;
    struct scopetable_entry[_MAX_LENTH];
    };

    这里扩展后的 VS_scopetable_entry 名字是我自己取的,在 scopetable_entry 前面的 4 个成员名字是从 IDA 反汇编看到的。

    一个函数里如果有多个 __try{}__except 则会记录在 struct scopetable_entry 数组表里。

VS2019编译 1.4.2 示例函数 TestSEH 汇编代码如下(Debug版):

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
#include "stdio.h"
#include <windows.h>

void Test__try__except(void)
{
__try
{
printf("trylevel = 0\n");

__try
{
printf("trylevel = 1\n");
}
__except (EXCEPTION_EXECUTE_HANDLER)//1
{
int a = 0;
}

}
__except (EXCEPTION_CONTINUE_EXECUTION)//-1
{
}

__try
{
printf("trylevel = 2\n");

__try
{
printf("trylevel = 3\n");
}
__except (GetExceptionCode() == EXCEPTION_INT_DIVIDE_BY_ZERO ? EXCEPTION_EXECUTE_HANDLER : \
EXCEPTION_CONTINUE_SEARCH)
{
}
}
__except (EXCEPTION_EXECUTE_HANDLER)//1
{
}

return;
}

int main(int argc, char* argv[])
{
Test__try__except();

getchar();
return 0;
}

函数 Test__try__except 反汇编代码如下:

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
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
.text:00411780 // =============== S U B R O U T I N E =======================================
.text:00411780
.text:00411780 // Attributes: bp-based frame
.text:00411780
.text:00411780 sub_411780 proc near // CODE XREF: sub_41104B↑j
.text:00411780
.text:00411780 var_F4 = dword ptr -0F4h
.text:00411780 var_EC = dword ptr -0ECh
.text:00411780 var_34 = byte ptr -34h
.text:00411780 var_20 = dword ptr -20h
.text:00411780 ms_exc = CPPEH_RECORD ptr -18h
.text:00411780
.text:00411780 push ebp
.text:00411781 mov ebp, esp
.text:00411783 push 0FFFFFFFEh
.text:00411785 push offset stru_419160
.text:0041178A push offset SEH_4130C0
.text:0041178F mov eax, large fs:0
.text:00411795 push eax
.text:00411796 add esp, 0FFFFFF1Ch
.text:0041179C push ebx
.text:0041179D push esi
.text:0041179E push edi
.text:0041179F lea edi, [ebp+var_34]
.text:004117A2 mov ecx, 7
.text:004117A7 mov eax, 0CCCCCCCCh
.text:004117AC rep stosd
.text:004117AE mov eax, ___security_cookie
.text:004117B3 xor [ebp+ms_exc.registration.ScopeTable], eax
.text:004117B6 xor eax, ebp
.text:004117B8 push eax
.text:004117B9 lea eax, [ebp+ms_exc.registration] //[ebp-0x10]即为刚才建立的registration结构
.text:004117BC mov large fs:0, eax
.text:004117C2 mov [ebp+ms_exc.old_esp], esp
.text:004117C5 mov ecx, offset unk_41C00F
.text:004117CA call sub_411320
.text:004117CF mov [ebp+ms_exc.registration.TryLevel], 0
.text:004117D6 push offset aTrylevel0 // "trylevel = 0\n"
.text:004117DB call sub_4110D7
.text:004117E0 add esp, 4
.text:004117E3 mov [ebp+ms_exc.registration.TryLevel], 1
.text:004117EA push offset aTrylevel1 // "trylevel = 1\n"
.text:004117EF call sub_4110D7
.text:004117F4 add esp, 4
.text:004117F7 mov [ebp+ms_exc.registration.TryLevel], 0
.text:004117FE jmp short loc_411817
.text:00411800 // ---------------------------------------------------------------------------
.text:00411800
.text:00411800 loc_411800: // DATA XREF: .rdata:stru_419160↓o
.text:00411800 mov eax, 1
.text:00411805 retn
.text:00411806 // ---------------------------------------------------------------------------
.text:00411806
.text:00411806 loc_411806: // DATA XREF: .rdata:stru_419160↓o
.text:00411806 mov esp, [ebp+ms_exc.old_esp]
.text:00411809 mov [ebp+var_20], 0
.text:00411810 mov [ebp+ms_exc.registration.TryLevel], 0
.text:00411817
.text:00411817 loc_411817: // CODE XREF: sub_411780+7E↑j
.text:00411817 mov [ebp+ms_exc.registration.TryLevel], 0FFFFFFFEh
.text:0041181E jmp short loc_41182E
.text:00411820 // ---------------------------------------------------------------------------
.text:00411820
.text:00411820 loc_411820: // DATA XREF: .rdata:stru_419160↓o
.text:00411820 or eax, 0FFFFFFFFh
.text:00411823 retn
.text:00411824 // ---------------------------------------------------------------------------
.text:00411824
.text:00411824 loc_411824: // DATA XREF: .rdata:stru_419160↓o
.text:00411824 mov esp, [ebp+ms_exc.old_esp]
.text:00411827 mov [ebp+ms_exc.registration.TryLevel], 0FFFFFFFEh
.text:0041182E
.text:0041182E loc_41182E: // CODE XREF: sub_411780+9E↑j
.text:0041182E mov [ebp+ms_exc.registration.TryLevel], 2
.text:00411835 push offset aTrylevel2 // "trylevel = 2\n"
.text:0041183A call sub_4110D7
.text:0041183F add esp, 4
.text:00411842 mov [ebp+ms_exc.registration.TryLevel], 3
.text:00411849 push offset aTrylevel3 // "trylevel = 3\n"
.text:0041184E call sub_4110D7
.text:00411853 add esp, 4
.text:00411856 mov [ebp+ms_exc.registration.TryLevel], 2
.text:0041185D jmp short loc_41189F
.text:0041185F // ---------------------------------------------------------------------------
.text:0041185F
.text:0041185F loc_41185F: // DATA XREF: .rdata:stru_419160↓o
.text:0041185F mov eax, [ebp+ms_exc.exc_ptr]
.text:00411862 mov ecx, [eax]
.text:00411864 mov edx, [ecx]
.text:00411866 mov [ebp+var_EC], edx
.text:0041186C cmp [ebp+var_EC], 0C0000094h
.text:00411876 jnz short loc_411884
.text:00411878 mov [ebp+var_F4], 1
.text:00411882 jmp short loc_41188E
.text:00411884 // ---------------------------------------------------------------------------
.text:00411884
.text:00411884 loc_411884: // CODE XREF: sub_411780+F6↑j
.text:00411884 mov [ebp+var_F4], 0
.text:0041188E
.text:0041188E loc_41188E: // CODE XREF: sub_411780+102↑j
.text:0041188E mov eax, [ebp+var_F4]
.text:00411894 retn
.text:00411895 // ---------------------------------------------------------------------------
.text:00411895
.text:00411895 loc_411895: // DATA XREF: .rdata:stru_419160↓o
.text:00411895 mov esp, [ebp+ms_exc.old_esp]
.text:00411898 mov [ebp+ms_exc.registration.TryLevel], 2
.text:0041189F
.text:0041189F loc_41189F: // CODE XREF: sub_411780+DD↑j
.text:0041189F mov [ebp+ms_exc.registration.TryLevel], 0FFFFFFFEh
.text:004118A6 jmp short loc_4118B8
.text:004118A8 // ---------------------------------------------------------------------------
.text:004118A8
.text:004118A8 loc_4118A8: // DATA XREF: .rdata:stru_419160↓o
.text:004118A8 mov eax, 1
.text:004118AD retn
.text:004118AE // ---------------------------------------------------------------------------
.text:004118AE
.text:004118AE loc_4118AE: // DATA XREF: .rdata:stru_419160↓o
.text:004118AE mov esp, [ebp+ms_exc.old_esp]
.text:004118B1 mov [ebp+ms_exc.registration.TryLevel], 0FFFFFFFEh
.text:004118B8
.text:004118B8 loc_4118B8: // CODE XREF: sub_411780+126↑j
.text:004118B8 mov ecx, [ebp+ms_exc.registration.Next]
.text:004118BB mov large fs:0, ecx
.text:004118C2 pop ecx
.text:004118C3 pop edi
.text:004118C4 pop esi
.text:004118C5 pop ebx
.text:004118C6 add esp, 0F4h
.text:004118CC cmp ebp, esp
.text:004118CE call sub_411249
.text:004118D3 mov esp, ebp
.text:004118D5 pop ebp
.text:004118D6 retn
.text:004118D6 sub_411780 endp
.text:004118D6
.text:004118D6 // ---------------------------------------------------------------------------

_EXCEPTION_REGISTRATION 结构挂入:

1
2
.text:004117B9                 lea     eax, [ebp+ms_exc.registration]
.text:004117BC mov large fs:0, eax

卸载 _EXCEPTION_REGISTRATION 结构:

1
2
.text:004118B8                 mov     ecx, [ebp+ms_exc.registration.Next]
.text:004118BB mov large fs:0, ecx

此时的 VS_scopetable_entry 数据如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.rdata:00419160 stru_419160     dd 0FFFFFFFEh           ; GSCookieOffset
.rdata:00419160 ; DATA XREF: sub_411780+5↑o
.rdata:00419160 dd 0 ; GSCookieXOROffset ; SEH scope table for function 411780
.rdata:00419160 dd 0FFFFFEFCh ; EHCookieOffset
.rdata:00419160 dd 0 ; EHCookieXOROffset
.rdata:00419160 dd 0FFFFFFFEh ; ScopeRecord.EnclosingLevel
.rdata:00419160 dd offset loc_411820 ; ScopeRecord.FilterFunc
.rdata:00419160 dd offset loc_411824 ; ScopeRecord.HandlerFunc
.rdata:00419160 dd 0 ; ScopeRecord.EnclosingLevel
.rdata:00419160 dd offset loc_411800 ; ScopeRecord.FilterFunc
.rdata:00419160 dd offset loc_411806 ; ScopeRecord.HandlerFunc
.rdata:00419160 dd 0FFFFFFFEh ; ScopeRecord.EnclosingLevel
.rdata:00419160 dd offset loc_4118A8 ; ScopeRecord.FilterFunc
.rdata:00419160 dd offset loc_4118AE ; ScopeRecord.HandlerFunc
.rdata:00419160 dd 2 ; ScopeRecord.EnclosingLevel
.rdata:00419160 dd offset loc_41185F ; ScopeRecord.FilterFunc
.rdata:00419160 dd offset loc_411895 ; ScopeRecord.HandlerFunc
.rdata:004191A0 dd 0
.rdata:004191A4 dd 0

VC6 和 VS2019 编译的 SEH 大同小异。主要不同点:

  • handler 初始化

    • VC6:handler = __except_handler3
    • VS2019:handler = __except_handler4
  • trylevel 初始化

    • VC6:trylevel = 0xffffffff
    • VS2019:trylevel = 0xfffffffe
  • scopetable_entry 结构拓展:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    //VC6
    struct scopetable_entry
    {
    DWORD previousTryLevel //上一个try{}结构编号
    PARPROC lpfnFilter //过滤函数的起始地址
    PARPROC lpfnHandler //异常处理程序的地址
    };

    //VS2019
    struct VS_scopetable_entry
    {
    DWORD GSCookieOffset;
    DWORD GSCookieXOROffset;
    DWORD EHCookieOffset;
    DWORD EHCookieXOROffset;
    struct scopetable_entry[_MAX_LENTH];
    };

1.4.6 VS2019和VC6编译32位SEH区别

它们对 SEH 的具体实现整体上是一致的,VS2019有变化的是:

  1. VS2019 对 scopetable_entry 做了扩展:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    struct _EXCEPTION_REGISTRATION	// VC6/VS2019通用
    {
    struct _EXCEPTION_REGISTRATION* prev;
    void (*handler)(PEXCEPTION_RECORD, PEXCEPTION_ REGISTRATION, PCONTEXT, PEXCEPTION_RECORD);
    struct scopetable_entry * scopetable;
    int trylevel;
    int _ebp;
    };

    //VS2019扩展的scopetable_entry
    struct VS_scopetable_entry
    {
    DWORD GSCookieOffset;
    DWORD GSCookieXOROffset;
    DWORD EHCookieOffset;
    DWORD EHCookieXOROffset;
    struct scopetable_entry[_MAX_LENTH];
    };

    其中增加了 SeeurityCookie 的相关内容,这是微软为了防止缓冲区溢出而设置的栈验证机制(即从 Visual Studio 2003 开始增加的CS 保护机制),在函数开头会对栈中的 SeopeTable 使用 Cookie 作为密钥进行加密,异常处理函数也变成了 __except _handler4。在该函数中,除了增加了对 Security Cookie 和 ScopeTable 的验证之外,整体流程与 __except_handler3 完全一致。

  2. VS2019 的 trylevel 初始值为 0xfffffffe(VC6为 0xffffffff)。

  3. VS2019 的异常处理统一入口为 __except_hander4(VC6为 __except_hander3)。

可以参考《加密与解密第四版8.3.4》。

2 _except_handler3执行过程

2.1 执行流程

虽然编译器扩展了 Windows 的 SEH 处理机制,但是也仅在执行到 __except_handler3 时才会体现出差异。

如前面练习过的除零异常:CPU检测到异常 --> 查中断表执行处理函数 --> CommonDispatchException --> KiDispatchException --> KiUserExceptionDispatcher --> ntdll!RtlDispatchException --> VEH --> SEH --> __except_handler3。

执行 __except_handler3 函数的过程如下:

  1. 将 SEH 异常回调函数的第二个参数 EstablisherFrame 强制转化为 _EXCEPTION_REGISTRATION 结构。
  2. _EXCEPTION_REGISTRATION 的成员 trylevel 赋值给变量 nTrylevel,然后取出 scopetable[nTrylevel]
  3. 判断 scopetable[nTrylevel].lpfnFilter 过滤函数,如果不为空则直接调用之,如果为空则执行下面第 5 步。
  4. 判断 lpfnFilter 函数的返回值:
    • EXCEPTION_EXECUTE_HANDLER(1),执行 lpfnHandler,执行完后由于 lpfnHandler 里面没有 ret 所以会继续往下执行代码。
    • EXCEPTION_CONTINUE_EXECUTION(-1),说明异常已经处理了,直接返回即可。
    • EXCEPTION_CONTINUE_SEARCH(0),执行第 5 步。
  5. 判断当前的是否已经是最外层:
    • previousTryLevel != -1,则 nTrylevel = previousTryLevel,并取出 scopetable[nTrylevel],去执行第 3 步。
    • previousTryLevel == -1,已经是最外层,则执行第 6 步。
  6. __except_handler3 返回 EXCEPTION_CONTINUE_SEARCH,让 ntdll!RtlDispatchException 继续寻找其他异常处理器。

注意:

  • 上面第 4 步过滤器返回 EXCEPTION_CONTINUE_SEARCH 是说明本异常处理块不处理当前异常,需要寻找其他异常处理块,并不是寻找其他异常处理器。寻找其他异常处理器是需要循环链条的,尽管链条上的其他 SEH 并不一定可以解决当前异常。
  • 只有当 __except_handler3 处理到 previousTryLevel == -1 时,返回的 EXCEPTION_CONTINUE_SEARCH 才是继续寻找其他异常处理器。

2.2 源代码堆栈分析

如下的 TestSEH 代码进行分析:

源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include "stdafx.h"
#include <stdio.h>
#include <stdlib.h>
#include <Windows.h>

void TestSEH()
{
__try
{
}
__except (GetExceptionInformation() == NULL?1:0)
{
int a = 9;
}
}

int main(int argc, char* argv[])
{
TestSEH();
return 0;
}

TestSEH 函数反汇编代码:

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

.text:00401020 // =============== S U B R O U T I N E =======================================
.text:00401020
.text:00401020 // Attributes: bp-based frame
.text:00401020
.text:00401020 sub_401020 proc near // CODE XREF: sub_401005↑j
.text:00401020
.text:00401020 var_5C = byte ptr -5Ch
.text:00401020 var_1C = dword ptr -1Ch
.text:00401020 ms_exc = CPPEH_RECORD ptr -18h
.text:00401020
.text:00401020 // __unwind { // __except_handler3
.text:00401020 push ebp
.text:00401021 mov ebp, esp
.text:00401023 push 0FFFFFFFFh
.text:00401025 push offset stru_422020
.text:0040102A push offset __except_handler3
.text:0040102F mov eax, large fs:0
.text:00401035 push eax
.text:00401036 mov large fs:0, esp
.text:0040103D add esp, 0FFFFFFB4h
.text:00401040 push ebx
.text:00401041 push esi
.text:00401042 push edi
.text:00401043 mov [ebp+ms_exc.old_esp], esp
.text:00401046 lea edi, [ebp+var_5C]
.text:00401049 mov ecx, 11h
.text:0040104E mov eax, 0CCCCCCCCh
.text:00401053 rep stosd
.text:00401055 // __try { // __except at loc_40106E
.text:00401055 mov [ebp+ms_exc.registration.TryLevel], 0
.text:00401055 // } // starts at 401055
.text:0040105C mov [ebp+ms_exc.registration.TryLevel], 0FFFFFFFFh
.text:00401063 jmp short loc_40107F
.text:00401065 // ---------------------------------------------------------------------------
.text:00401065
.text:00401065 loc_401065: // DATA XREF: .rdata:stru_422020↓o
.text:00401065 // __except filter // owned by 401055
.text:00401065 mov eax, [ebp+ms_exc.exc_ptr] //[ebp-0x14]
.text:00401068 neg eax
.text:0040106A sbb eax, eax
.text:0040106C inc eax
.text:0040106D retn
.text:0040106E // ---------------------------------------------------------------------------
.text:0040106E
.text:0040106E loc_40106E: // DATA XREF: .rdata:stru_422020↓o
.text:0040106E // __except(loc_401065) // owned by 401055
.text:0040106E mov esp, [ebp+ms_exc.old_esp] // [ebp-0x18]
.text:00401071 mov [ebp+var_1C], 9
.text:00401078 mov [ebp+ms_exc.registration.TryLevel], 0FFFFFFFFh
.text:0040107F
.text:0040107F loc_40107F: // CODE XREF: sub_401020+43↑j
.text:0040107F mov ecx, [ebp+ms_exc.registration.Next]
.text:00401082 mov large fs:0, ecx
.text:00401089 pop edi
.text:0040108A pop esi
.text:0040108B pop ebx
.text:0040108C mov esp, ebp
.text:0040108E pop ebp
.text:0040108F retn
.text:0040108F // } // starts at 401020
.text:0040108F sub_401020 endp
.text:0040108F
.text:0040108F // ---------------------------------------------------------------------------

TestSEH 函数进入 __try 之前函数堆栈情况如下:

32.png

注意

  1. ebp-0x18 处将当前 esp 保存起来是必须的,因为从 __except_handler3 中调用 lpfnHandler 进入到 except(){} 中后,由于 except(){} 不会再返回到 __except_handler3 函数,所以要用这里的 esp 恢复当前堆栈:

    1
    .text:0040106E                 mov     esp, [ebp+ms_exc.old_esp]  // [ebp-0x18]
  2. ebp-0x14 指向的 &_EXCEPTION_POINTERS 是由 __except_handler3 函数压栈的,这是为了在产生异常的函数过滤表达式中使用到才压栈的。

    1
    .text:00401065                 mov     eax, [ebp+ms_exc.exc_ptr]  //[ebp-0x14]

2.3 _except_handler3逆向分析

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
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
.text:004011D4 // =============== S U B R O U T I N E =======================================
.text:004011D4
.text:004011D4 // Attributes: library function
.text:004011D4
.text:004011D4 // int __cdecl _except_handler3(int, PVOID TargetFrame, int)
.text:004011D4 __except_handler3 proc near // DATA XREF: sub_401020+A↑o
.text:004011D4 // start+A↓o ...
.text:004011D4
.text:004011D4 var_ExceptionRecord= dword ptr -8
.text:004011D4 var_ContextRecord= dword ptr -4
.text:004011D4 arg_ExceptionRecord= dword ptr 8
.text:004011D4 TargetFrame = dword ptr 0Ch
.text:004011D4 arg_ContextRecord= dword ptr 10h
.text:004011D4
.text:004011D4 push ebp
.text:004011D5 mov ebp, esp
.text:004011D7 sub esp, 8
.text:004011DA push ebx
.text:004011DB push esi
.text:004011DC push edi
.text:004011DD push ebp
.text:004011DE cld
.text:004011DF mov ebx, [ebp+TargetFrame] // 为 _EXCEPTION_REGISTRATION 结构
.text:004011E2 mov eax, [ebp+arg_ExceptionRecord]
.text:004011E5 test [eax+_EXCEPTION_RECORD.ExceptionFlags], 6 //如果在_local_unwind2产生
//异常,会将ExceptionFlags = EXCEPTION_UNWINDING|EXCEPTION_EXIT_UNWIND
.text:004011EC jnz _lh_unwinding
.text:004011F2 mov [ebp+var_ExceptionRecord], eax
.text:004011F5 mov eax, [ebp+arg_ContextRecord]
.text:004011F8 mov [ebp+var_ContextRecord], eax
.text:004011FB lea eax, [ebp+var_ExceptionRecord] // =================================================
.text:004011FB // 这里函数的局部变量应该是这个结构:
.text:004011FB // typedef struct _EXCEPTION_POINTERS {
.text:004011FB // PEXCEPTION_RECORD ExceptionRecord //异常记录
.text:004011FB // PCONTEXT ContextRecord //异常发生时的线程上下文环境
.text:004011FB // } EXCEPTION_POINTERS, *PEXCEPTION_POINTERS
.text:004011FB // ================================================================
.text:004011FE mov [ebx-4], eax // 放在产生异常函数的堆栈里
.text:00401201 mov esi, [ebx+_EXCEPTION_REGISTRATION.trylevel]
.text:00401204 mov edi, [ebx+_EXCEPTION_REGISTRATION.scopetable]
.text:00401207
.text:00401207 _lh_top: // CODE XREF: __except_handler3+90↓j
.text:00401207 cmp esi, 0FFFFFFFFh
.text:0040120A jz short _lh_bagit // 返回 1
.text:0040120C lea ecx, [esi+esi*2]
.text:0040120F cmp [edi+ecx*4+scopetable_entry.lpfnFilter], 0
.text:00401214 jz short _lh_continue
.text:00401216 push esi
.text:00401217 push ebp
.text:00401218 lea ebp, [ebx+_EXCEPTION_REGISTRATION._ebp]
.text:0040121B call [edi+ecx*4+scopetable_entry.lpfnFilter]
.text:0040121F pop ebp
.text:00401220 pop esi
.text:00401221 mov ebx, [ebp+TargetFrame]
.text:00401224 or eax, eax
.text:00401226 jz short _lh_continue
.text:00401228 js short _lh_dismiss // 结果为负则跳转,SF == 1 则跳转
.text:0040122A mov edi, [ebx+_EXCEPTION_REGISTRATION.scopetable]
.text:0040122D push ebx // TargetFrame
.text:0040122E call __global_unwind2
.text:00401233 add esp, 4
.text:00401236 lea ebp, [ebx+10h]
.text:00401239 push esi
.text:0040123A push ebx
.text:0040123B call __local_unwind2
.text:00401240 add esp, 8
.text:00401243 lea ecx, [esi+esi*2]
.text:00401246 push 1
.text:00401248 mov eax, [edi+ecx*4+8]
.text:0040124C call __NLG_Notify
.text:00401251 mov eax, [edi+ecx*4]
.text:00401254 mov [ebx+0Ch], eax
.text:00401257 call [edi+ecx*4+scopetable_entry.lpfnHandler] // 调用异常处理函数之后就 不会再返回 到本函数了
.text:0040125B
.text:0040125B _lh_continue: // CODE XREF: __except_handler3+40↑j
.text:0040125B // __except_handler3+52↑j
.text:0040125B mov edi, [ebx+_EXCEPTION_REGISTRATION.scopetable]
.text:0040125E lea ecx, [esi+esi*2]
.text:00401261 mov esi, [edi+ecx*4+scopetable_entry.previousTryLevel] //scopetable[i].previousTryLevel
.text:00401264 jmp short _lh_top
.text:00401266 // ---------------------------------------------------------------------------
.text:00401266
.text:00401266 _lh_dismiss: // CODE XREF: __except_handler3+54↑j
.text:00401266 mov eax, 0
.text:0040126B jmp short _lh_return
.text:0040126D // ---------------------------------------------------------------------------
.text:0040126D
.text:0040126D _lh_bagit: // CODE XREF: __except_handler3+36↑j
.text:0040126D mov eax, 1 // 返回 1
.text:00401272 jmp short _lh_return
.text:00401274 // ---------------------------------------------------------------------------
.text:00401274
.text:00401274 _lh_unwinding: // CODE XREF: __except_handler3+18↑j
.text:00401274 push ebp
.text:00401275 lea ebp, [ebx+10h]
.text:00401278 push 0FFFFFFFFh
.text:0040127A push ebx
.text:0040127B call __local_unwind2
.text:00401280 add esp, 8
.text:00401283 pop ebp
.text:00401284 mov eax, 1
.text:00401289
.text:00401289 _lh_return: // CODE XREF: __except_handler3+97↑j
.text:00401289 // __except_handler3+9E↑j
.text:00401289 pop ebp
.text:0040128A pop edi
.text:0040128B pop esi
.text:0040128C pop ebx
.text:0040128D mov esp, ebp
.text:0040128F pop ebp
.text:00401290 retn
.text:00401290 __except_handler3 endp

这里主要分析几点:

  1. 在函数堆栈中构建一个 _EXCEPTION_POINTERS 结构,然后将这个结构的起始地址放到 TestSEH 函数堆栈 ebp-0x14 的位置。

    1
    2
    3
    .text:004011DF                 mov     ebx, [ebp+TargetFrame] // 为 _EXCEPTION_REGISTRATION 结构
    ...
    .text:004011FE mov [ebx-4], eax // 放在产生异常函数的堆栈里

    此时的 ebx 指向回调函数 _except_handler3 第二个参数TargetFrame ,VC6编译器将其转换为扩展的 _EXCEPTION_REGISTRATION 结构。

    在 Windows 中,当异常产生的时候,会将当前堆栈中的 EXCEPTION_REGISTR_RECORD 结构地址传给异常处理回调函数的第二个参数。所以在这里__except_handler3 第二个参数TargetFrame 也就是本例中 _EXCEPTION_REGISTRATION 的起始地址,如下图可以知道这里的 ebx-4 即为产生异常那个函数 ebp-0x14 的位置。

    25.png

  2. 对于过滤器如何过滤已经在 2.1 讲过,参照代码看看即可。

  3. __except_hander3 开始调用 lpfnHander 后就不再返回到当前函数了,就会从产生异常的函数继续往下执行。

    在产生异常的函数 TestSEHebp-0x18 处将当前 esp 保存起来是必须的,因为从 __except_handler3 中调用 lpfnHandler 进入到 except(){} 中后,由于 except(){} 不会再返回到 __except_handler3 函数,所以要用这里的 esp 恢复当前堆栈:

    1
    .text:0040106E                 mov     esp, [ebp+ms_exc.old_esp]  // [ebp-0x18]
  4. 调用过滤器函数时,会将 TestSEH 函数的 ebp 当参数传进去,是为了寻址用。

    1
    2
    .text:00401218                 lea     ebp, [ebx+_EXCEPTION_REGISTRATION._ebp]
    .text:0040121B call [edi+ecx*4+scopetable_entry.lpfnFilter]
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
// _except_handler4(_EXCEPTION_RECORD *, _EXCEPTION_REGISTRATION_RECORD *, _CONTEXT *, void *)
00761DC0 55 push ebp
00761DC1 8B EC mov ebp,esp
00761DC3 8B 45 08 mov eax,dword ptr [ExceptionRecord]
00761DC6 8B 08 mov ecx,dword ptr [eax]
00761DC8 51 push ecx
00761DC9 E8 C5 F5 FF FF call __filter_x86_sse2_floating_point_exception (0761393h)
00761DCE 83 C4 04 add esp,4
00761DD1 8B 55 08 mov edx,dword ptr [ExceptionRecord]
00761DD4 89 02 mov dword ptr [edx],eax
00761DD6 8B 45 14 mov eax,dword ptr [DispatcherContext]
00761DD9 50 push eax
00761DDA 8B 4D 10 mov ecx,dword ptr [ContextRecord]
00761DDD 51 push ecx
00761DDE 8B 55 0C mov edx,dword ptr [EstablisherFrame]
00761DE1 52 push edx
00761DE2 8B 45 08 mov eax,dword ptr [ExceptionRecord]
00761DE5 50 push eax
00761DE6 68 4F 11 76 00 push offset @__security_check_cookie@4 (076114Fh)
00761DEB 68 04 A0 76 00 push offset __security_cookie (076A004h)
00761DF0 E8 FA F3 FF FF call __except_handler4_common (07611EFh)
00761DF5 83 C4 18 add esp,18h
00761DF8 5D pop ebp
00761DF9 C3 ret

__except_handler4_common:

3 栈展开

try-finally 结构的终结处理是为了保证 finally 块中的代码可以执行(除非被保护体中的代码终止了当前线程,比如调用ExitThreadExitProcess退出线程或整个进程。)。

局部栈展开的意义:为了保证 __fianlly 在正常情况下及产生异常进入 __except_hander3 后都能够执行。

全局栈展开的意义:在当一个函数产生异常时,FS:[0] 链条上可能又挂入了一些新的_EXCEPTION_REGISTR_RECORD 结构,全局展开的意义就在于让这些新挂入的 _EXCEPTION_REGISTR_RECORD 异常处理函数有机会得到执行。

局部展开是为 __finally 服务的,全局展开是让 FS:[0] 链条上新挂入的 _EXCEPTION_REGISTR_RECORD 异常处理函数得到机会执行。

需要注意__finally 终结块的代码汇编后ret 指令,而 __except 异常处理块的代码汇编指令没有 ret 指令

全局展开即遍历TEB中异常链表中当前遍历中命中的异常信息记录节点之前的所有节点(遍历中要对每一个节点执行局部展开)。而局部展开是针对某一个具体的异常信息记录节点,即对应一个函数的异常处理信息。它内部可能包含多个try语句,并且try可能进行嵌套。

说明:在 scopetable 表中,对于有效的 scopetable_entry 项:

  • lpfnFilter == NULL,对应于 __try{}__finallylpfnHandler 在局部展开中执行。
  • lpfnFilter != NULL,对应于 __try{}__exceptlpfnHandler 在异常处理函数中执行。

3.1 局部展开

一、没有使用局部展开

__try 中如果没有使用 return gotobreakcontinue 等流程控制语句时,是不会使用到局部展开/全局展开的,如下代码如果将 __try 中的 return 注释掉,则在即将离开 __try 代码时会调用 __finally 的代码,如下 0x0040106E 处的汇编代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
5:        __try
00401052 mov dword ptr [ebp-4],0
6: {
7: int a = 6;
00401059 mov dword ptr [a],6
8: int b = 9;
00401060 mov dword ptr [b],9
9: //return;
10: }
00401067 mov dword ptr [ebp-4],0FFFFFFFFh
0040106E call $L544 (00401075)
00401073 jmp $L547 (00401083)
11: __finally
12: {
13: printf("This is finally.\n");
00401075 push offset string "This is finally.\n" (0042301c)
0040107A call printf (00401190)
0040107F add esp,4
$L545:
00401082 ret
14: }
15: }

二、使用局部展开

局部展开的意义:如果在 __try 中如果使用 return gotobreakcontinue 等流程控制语句离开当前保护块时,为了保证 finally 终止块的代码得到执行,就会使用到局部展开

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include "stdafx.h"

void Test_local_try_finally(void)
{
__try
{
int a = 6;
int b = 9;
return;
}
__finally
{
printf("This is finally.\n");
}
}

int main(int argc, char* argv[])
{
Test_local_try_finally();

getchar();
return 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
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
.text:00401020 // =============== S U B R O U T I N E =======================================
.text:00401020
.text:00401020 // Attributes: bp-based frame
.text:00401020
.text:00401020 sub_401020 proc near // CODE XREF: sub_40100A↑j
.text:00401020
.text:00401020 var_60 = byte ptr -60h
.text:00401020 var_20 = dword ptr -20h
.text:00401020 var_1C = dword ptr -1Ch
.text:00401020 var_10 = dword ptr -10h
.text:00401020 var_4 = dword ptr -4
.text:00401020
.text:00401020 // __unwind { // __except_handler3
.text:00401020 push ebp
.text:00401021 mov ebp, esp
.text:00401023 push 0FFFFFFFFh
.text:00401025 push offset dword_423048 // scopetable_entry
.text:0040102A push offset __except_handler3
.text:0040102F mov eax, large fs:0
.text:00401035 push eax
.text:00401036 mov large fs:0, esp
.text:0040103D add esp, 0FFFFFFB0h
.text:00401040 push ebx
.text:00401041 push esi
.text:00401042 push edi
.text:00401043 lea edi, [ebp+var_60]
.text:00401046 mov ecx, 12h
.text:0040104B mov eax, 0CCCCCCCCh
.text:00401050 rep stosd
.text:00401052 // __try { // __finally(loc_401077)
.text:00401052 mov [ebp+var_4], 0
.text:00401059 mov [ebp+var_1C], 6
.text:00401060 mov [ebp+var_20], 9
.text:00401067 push 0FFFFFFFFh
.text:00401069 lea eax, [ebp+var_10]
.text:0040106C push eax
.text:0040106D call __local_unwind2 // void _local_unwind2(
.text:0040106D // PEXCEPTION_REGISTRATION xr,
.text:0040106D // int stop)
.text:00401072 add esp, 8
.text:00401075 jmp short loc_401085
.text:00401077 // ---------------------------------------------------------------------------
.text:00401077
.text:00401077 loc_401077: // DATA XREF: .rdata:00423050↓o
.text:00401077 // __finally // owned by 401052
.text:00401077 push offset aThisIsFinally // "This is finally.\n"
.text:0040107C call _printf
.text:00401081 add esp, 4
.text:00401084 retn
.text:00401085 // ---------------------------------------------------------------------------
.text:00401085
.text:00401085 loc_401085: // CODE XREF: sub_401020+55↑j
.text:00401085 mov ecx, [ebp+var_10]
.text:00401088 mov large fs:0, ecx
.text:0040108F pop edi
.text:00401090 pop esi
.text:00401091 pop ebx
.text:00401092 add esp, 60h
.text:00401095 cmp ebp, esp
.text:00401097 call __chkesp
.text:0040109C mov esp, ebp
.text:0040109E pop ebp
.text:0040109F retn
.text:0040109F // } // starts at 401052
.text:0040109F // } // starts at 401020
.text:0040109F sub_401020 endp
.text:0040109F
.text:0040109F // ---------------------------------------------------------------------------

此时的 scopetable 如下:

1
2
3
4
.rdata:00423048 dword_423048    dd 0FFFFFFFFh           // DATA XREF: sub_401020+5↑o
.rdata:0042304C dd 0
.rdata:00423050 dd offset loc_401077
.rdata:00423054 dd 0

在离开 __try 之前调用了局部展开函数,如下代码:

1
.text:0040106D                 call    __local_unwind2

函数原型:

1
2
3
4
void _local_unwind2(
PEXCEPTION_REGISTRATION xr, //与一个范围表关联的注册记录
int stop //展开到哪一层__try才停,即stop层不展开
);

__local_unwind2 函数分析:

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
.text:004012AA // =============== S U B R O U T I N E =======================================
.text:004012AA
.text:004012AA // void _local_unwind2(
.text:004012AA // PEXCEPTION_REGISTRATION xr, //A registration record that is associated with one scope table.
.text:004012AA // int stop //展开到哪一层__try才停
.text:004012AA // )//
.text:004012AA // Attributes: library function
.text:004012AA
.text:004012AA __local_unwind2 proc near // CODE XREF: sub_401020+4D↑p
.text:004012AA // __except_handler3+67↓p ...
.text:004012AA
.text:004012AA var_previousTryLevel= dword ptr -14h
.text:004012AA arg_Registion = dword ptr 4
.text:004012AA arg_0xFF = dword ptr 8
.text:004012AA
.text:004012AA push ebx
.text:004012AB push esi
.text:004012AC push edi
.text:004012AD mov eax, [esp+0Ch+arg_Registion]
.text:004012B1 push eax
.text:004012B2 push 0FFFFFFFEh
.text:004012B4 push offset __unwind_handler //用来解决在__local_unwind2中产生的异常
.text:004012B9 push large dword ptr fs:0
.text:004012C0 mov large fs:0, esp
.text:004012C7
.text:004012C7 _lu_top: // CODE XREF: __local_unwind2:__NLG_Return2↓j
.text:004012C7 mov eax, [esp+1Ch+arg_Registion]
.text:004012CB mov ebx, [eax+_EXCEPTION_REGISTRATION.scopetable]
.text:004012CE mov esi, [eax+_EXCEPTION_REGISTRATION.trylevel] // 从第一个__try过来的,此时trylevel == 0
.text:004012D1 cmp esi, 0FFFFFFFFh
.text:004012D4 jz short _lu_done
.text:004012D6 cmp esi, [esp+1Ch+arg_0xFF]
.text:004012DA jz short _lu_done
.text:004012DC lea esi, [esi+esi*2]
.text:004012DF mov ecx, [ebx+esi*4+scopetable_entry.previousTryLevel]
.text:004012E2 mov [esp+1Ch+var_previousTryLevel], ecx
.text:004012E6 mov [eax+_EXCEPTION_REGISTRATION.trylevel], ecx // 更新 trylecel,为下次循环作准备
.text:004012E9 cmp [ebx+esi*4+scopetable_entry.lpfnFilter], 0
.text:004012EE jnz short __NLG_Return2 // 说明当前的__try对应是__except,而不是__finally
.text:004012F0 push 101h
.text:004012F5 mov eax, [ebx+esi*4+8]
.text:004012F9 call __NLG_Notify
.text:004012FE call [ebx+esi*4+scopetable_entry.lpfnHandler] // ===============================
.text:004012FE // 如果 lpfnFilter == 0,则直接调用__finally
.text:004012FE // 调用__finally之后就 还会 再返回到这里,这与调用__except不同
.text:004012FE // 由于此时 _EXCEPTION_REGISTRATION.trylevel == -1,所以跳上去
.text:004012FE // 循环以后直接就返回了
.text:004012FE // ========================================================
.text:00401302
.text:00401302 __NLG_Return2: // CODE XREF: __local_unwind2+44↑j
.text:00401302 jmp short _lu_top // 说明当前的__try对应是__except,而不是__finally
.text:00401304 // ---------------------------------------------------------------------------
.text:00401304
.text:00401304 _lu_done: // CODE XREF: __local_unwind2+2A↑j
.text:00401304 // __local_unwind2+30↑j
.text:00401304 pop large dword ptr fs:0
.text:0040130B add esp, 0Ch
.text:0040130E pop edi
.text:0040130F pop esi
.text:00401310 pop ebx
.text:00401311 retn
.text:00401311 __local_unwind2 endp

该函数的功能:

  1. FS:[0] 链头挂入一个新的节点,异常处理回调函数为__unwind_handler,用来解决在 __local_unwind2 中产生的异常。

    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
    .text:00401288 // =============== S U B R O U T I N E =======================================
    .text:00401288
    .text:00401288 // Attributes: library function static
    .text:00401288
    .text:00401288 __unwind_handler proc near // DATA XREF: __local_unwind2+A↓o
    .text:00401288 // __abnormal_termination+9↓o
    .text:00401288
    .text:00401288 arg_ExceptionRecord= dword ptr 4
    .text:00401288 arg_EstablisherFrame= dword ptr 8
    .text:00401288 arg_DispatcherContext= dword ptr 10h
    .text:00401288
    .text:00401288 mov ecx, [esp+arg_ExceptionRecord]
    .text:0040128C test [ecx+_EXCEPTION_RECORD.ExceptionFlags], 6
    .text:00401293 mov eax, 1
    .text:00401298 jz short _uh_return
    .text:0040129A mov eax, [esp+arg_EstablisherFrame]
    .text:0040129E mov edx, [esp+arg_DispatcherContext]
    .text:004012A2 mov [edx+_DISPATCHER_CONTEXT], eax
    .text:004012A4 mov eax, 3
    .text:004012A9
    .text:004012A9 _uh_return: // CODE XREF: __unwind_handler+10↑j
    .text:004012A9 retn
    .text:004012A9 __unwind_handler endp
    ============================================================================================
    typedef struct _DISPATCHER_CONTEXT {
    _EXCEPTION_REGISTRATION_RECORD* RegistrationPointer;
    } DISPATCHER_CONTEXT;

    typedef struct _EXCEPTION_REGISTRATION_RECORD {
    struct _EXCEPTION_REGISTRATION_RECORD *Next;
    PEXCEPTION_ROUTINE Handler;
    } EXCEPTION_REGISTRATION_RECORD;
  2. 如果 scopetable_entry.lpfnFilter == NULL,则直接调用 scopetable_entry.lpfnHandler(__finally)

  3. 需要注意__finally 终结块的代码汇编后有 ret 指令,而 __except 异常处理块的代码汇编指令没有 ret 指令

3.2 全局展开

全局栈展开的意义:在当一个函数产生异常时,FS:[0] 链条上可能又挂入了一些新的_EXCEPTION_REGISTR_RECORD 结构,全局展开的意义就在于让这些新挂入的 _EXCEPTION_REGISTR_RECORD 异常处理函数有机会得到执行。

举例如下代码:

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 "stdafx.h"

void Test_try_finally(void)
{
__try
{
__try
{
int a = 6;
int b = 0;
a = a/b;
}
__finally
{
printf("This is finally.\n");
}
}
//------------
__except(1)
{
printf("This is except.\n");
}
}

int main(int argc, char* argv[])
{
Test_try_finally();

getchar();
return 0;
}

26.png

如果 __try 中代码产生异常了,虽然当前函数已经将 _EXCEPTION_REGISTRATION 结构挂在 FS:[0] 链条上了,但是在 __except_handler3 进行异常处理时,链条 FS:[0] 在此之后又挂入了一些新的 _EXCEPTION_REGISTRATION 结构,为了保证这些新挂入的能得到机会执行就设计了全局展开。虽然在全局展开时会判断链条上的结构在当前线程堆栈中,并且异常处理函数不在当前堆栈中时才能够满足执行条件,估计全局展开还有其他什么用途吧。

1、如果在 __try 不发生异常,正常的执行指令流能够执行到 __finally 中的代码:

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
12:               int a = 6;
0040D7F3 mov dword ptr [a],6
13: int b = 0;
0040D7FA mov dword ptr [b],0
14: a = a/b;
0040D801 mov eax,dword ptr [a]
0040D804 cdq
0040D805 idiv eax,dword ptr [b]
0040D808 mov dword ptr [a],eax
15: }
0040D80B mov dword ptr [ebp-4],0
0040D812 call $L551 (0040d819)
0040D817 jmp $L554 (0040d827)
16: __finally
17: {
18: printf("This is finally.\n");
0040D819 push offset string "This is finally.\n" (00422044)
0040D81E call printf (00401120)
0040D823 add esp,4
$L552:
0040D826 ret
19: }
20: //---------------
21: }
0040D827 mov dword ptr [ebp-4],0FFFFFFFFh
0040D82E jmp $L548+17h (0040d84d)
22: __except(1)
0040D830 mov eax,1
$L549:
0040D835 ret
$L548:
0040D836 mov esp,dword ptr [ebp-18h]
23: {
24: printf("This is except.\n");
0040D839 push offset string "This is except.\n" (0042201c)
0040D83E call printf (00401120)
0040D843 add esp,4
25: }
0040D846 mov dword ptr [ebp-4],0FFFFFFFFh
26: }
0040D84D mov ecx,dword ptr [ebp-10h]
0040D850 mov dword ptr fs:[0],ecx
0040D857 pop edi
0040D858 pop esi
0040D859 pop ebx
0040D85A add esp,60h
0040D85D cmp ebp,esp
0040D85F call __chkesp (004011a0)
0040D864 mov esp,ebp
0040D866 pop ebp
0040D867 ret

可以看到如下代码能够执行 __finally

1
2
3
4
5
6
7
0040D812   call        $L551 (0040d819)
...
0040D819 push offset string "This is finally.\n" (00422044)
0040D81E call printf (00401120)
0040D823 add esp,4
$L552:
0040D826 ret

此时的 scopetable 表:

1
2
3
.rdata:00422FE0 stru_422FE0     _SCOPETABLE_ENTRY <0FFFFFFFFh, offset loc_40D830, offset loc_40D836>
.rdata:00422FE0 // DATA XREF: sub_40D7B0+5↑o
.rdata:00422FE0 _SCOPETABLE_ENTRY <0, 0, offset loc_40D819> // SEH scope table for function 40D7B0

2、如果在 __try 中发生异常,那将会在 __except_handler3 中进行全局展开,使 __finally 的代码有机会执行,其代码参见 2.3,这里主要看的 __global_unwind2 代码:

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
.text:004011D8 ; =============== S U B R O U T I N E =======================================
.text:004011D8
.text:004011D8 ; Attributes: library function bp-based frame
.text:004011D8
.text:004011D8 ; int __cdecl _global_unwind2(PVOID _EXCEPTION_REGISTRATION)
.text:004011D8 __global_unwind2 proc near ; CODE XREF: __except_handler3+5A↓p
.text:004011D8
.text:004011D8 _EXCEPTION_REGISTRATION= dword ptr 8
.text:004011D8
.text:004011D8 push ebp
.text:004011D9 mov ebp, esp
.text:004011DB push ebx
.text:004011DC push esi
.text:004011DD push edi
.text:004011DE push ebp
.text:004011DF push 0 ; ReturnValue
.text:004011E1 push 0 ; ExceptionRecord
.text:004011E3 push offset _gu_return ; TargetIp
.text:004011E8 push [ebp+_EXCEPTION_REGISTRATION] // EstablisherFrame
.text:004011EB call RtlUnwind
.text:004011F0
.text:004011F0 _gu_return: // DATA XREF: __global_unwind2+B↑o
.text:004011F0 pop ebp
.text:004011F1 pop edi
.text:004011F2 pop esi
.text:004011F3 pop ebx
.text:004011F4 mov esp, ebp
.text:004011F6 pop ebp
.text:004011F7 retn
.text:004011F7 __global_unwind2 endp

可以看到,在 __global_unwind2 里面调用了导入函数 kernel32!RtlUnwind-->ntdll!RtlUnwind

1
2
3
4
5
6
VOID __stdcall RtlUnwind (
IN PVOID TargetFrame OPTIONAL, //指向待展开的调用帧的指针。如果此参数为NULL,则函数将执行退出展开。
IN PVOID TargetIp OPTIONAL, //继续执行的地址,较为重要。如果TargetFrame为NULL,则忽略此参数。
IN PEXCEPTION_RECORD ExceptionRecord OPTIONAL, //如果为NULL,则在RtlUnwind函数中构建该结构。否则使用该参数。
IN PVOID ReturnValue //RtlUnwind函数执行的结果值
)

3.3 RtlUnwind分析

RtlUnwind 函数原型:

1
2
3
4
5
6
VOID __stdcall RtlUnwind (
IN PVOID TargetFrame OPTIONAL, //指向待展开的调用帧的指针。如果此参数为NULL,则函数将执行退出展开。
IN PVOID TargetIp OPTIONAL, //继续执行的地址,较为重要。如果TargetFrame为NULL,则忽略此参数。
IN PEXCEPTION_RECORD ExceptionRecord OPTIONAL, //如果为NULL,则在RtlUnwind函数中构建该结构。否则使用该参数。
IN PVOID ReturnValue //RtlUnwind函数执行的结果值(EXCEPTION_DISPOSITION)
)

逆向分析如下:

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
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
.text:7C94ABA5 // =============== S U B R O U T I N E =======================================
.text:7C94ABA5
.text:7C94ABA5 // Attributes: bp-based frame
.text:7C94ABA5
.text:7C94ABA5 // void __stdcall RtlUnwind(PVOID TargetFrame, PVOID TargetIp, PEXCEPTION_RECORD ExceptionRecord, PVOID ReturnValue)
.text:7C94ABA5 public _RtlUnwind@16
.text:7C94ABA5 _RtlUnwind@16 proc near // CODE XREF: __global_unwind2+13↑p
.text:7C94ABA5 // DATA XREF: .text:off_7C923428↑o
.text:7C94ABA5
.text:7C94ABA5 var_37C_ExceptionCode= dword ptr -37Ch
.text:7C94ABA5 var_378_ExceptionFlags= dword ptr -378h
.text:7C94ABA5 var_374_ExceptionRecord= dword ptr -374h
.text:7C94ABA5 var_370_ExceptionAddress= dword ptr -370h
.text:7C94ABA5 var_36C_NumberParameters= dword ptr -36Ch
.text:7C94ABA5 var_32C = dword ptr -32Ch
.text:7C94ABA5 var_328 = dword ptr -328h
.text:7C94ABA5 var_324 = dword ptr -324h
.text:7C94ABA5 var_31C = dword ptr -31Ch
.text:7C94ABA5 var_2DC = dword ptr -2DCh
.text:7C94ABA5 var_StackLimit = dword ptr -2D8h
.text:7C94ABA5 var_StackBase = dword ptr -2D4h
.text:7C94ABA5 Context = CONTEXT ptr -2D0h
.text:7C94ABA5 var_4 = dword ptr -4
.text:7C94ABA5 EstablisherFrame= dword ptr 8
.text:7C94ABA5 __global_unwind2_ReturnAddr= dword ptr 0Ch
.text:7C94ABA5 ExceptionRecord_0= dword ptr 10h
.text:7C94ABA5 ReturnValue = dword ptr 14h
.text:7C94ABA5
.text:7C94ABA5 // FUNCTION CHUNK AT .text:7C96E373 SIZE 00000029 BYTES
.text:7C94ABA5 // FUNCTION CHUNK AT .text:7C96E3A1 SIZE 0000002D BYTES
.text:7C94ABA5 // FUNCTION CHUNK AT .text:7C96E3D3 SIZE 00000034 BYTES
.text:7C94ABA5 // FUNCTION CHUNK AT .text:7C96E40C SIZE 0000002C BYTES
.text:7C94ABA5
.text:7C94ABA5 mov edi, edi
.text:7C94ABA7 push ebp
.text:7C94ABA8 mov ebp, esp
.text:7C94ABAA sub esp, 37Ch
.text:7C94ABB0 mov eax, ___security_cookie
.text:7C94ABB5 push esi
.text:7C94ABB6 mov esi, [ebp+ExceptionRecord_0]
.text:7C94ABB9 mov [ebp+var_4], eax
.text:7C94ABBC push edi
.text:7C94ABBD lea eax, [ebp+var_StackBase]
.text:7C94ABC3 push eax
.text:7C94ABC4 lea eax, [ebp+var_StackLimit]
.text:7C94ABCA push eax
.text:7C94ABCB call _RtlpGetStackLimits@8 // RtlpGetStackLimits(x,x)
.text:7C94ABD0 xor edi, edi
.text:7C94ABD2 cmp esi, edi // 从 __global_unwind2 传来的是 0
.text:7C94ABD4 jnz short loc_7C94AC01 // ExceptionRecord_0 != NULL
.text:7C94ABD6 mov eax, [ebp+4] // __global_unwind2的返回地址,跟参数2相等
.text:7C94ABD9 lea esi, [ebp+var_37C_ExceptionCode] // ------------------------------------------------------------------------
.text:7C94ABD9 // var_37C为一个_EXCEPTION_RECORD:
.text:7C94ABD9 // struct _EXCEPTION_RECORD, 6 elements, 0x50 bytes
.text:7C94ABD9 // +0x000 ExceptionCode : Int4B //异常代码
.text:7C94ABD9 // +0x004 ExceptionFlags : Uint4B //异常标志
.text:7C94ABD9 // +0x008 ExceptionRecord : Ptr32 _EXCEPTION_RECORD //相关的下一个异常
.text:7C94ABD9 // +0x00c ExceptionAddress : Ptr32 Void //异常发生的地址
.text:7C94ABD9 // +0x010 NumberParameters : Uint4B //参数数组中的元素个数
.text:7C94ABD9 // +0x014 ExceptionInformation : [15] Uint4B //参数数组
.text:7C94ABD9 // ------------------------------------------------------------------------
.text:7C94ABDF mov [ebp+var_37C_ExceptionCode], 0C0000027h
.text:7C94ABE9 mov [ebp+var_378_ExceptionFlags], edi // ----------
.text:7C94ABE9 // 如果参数传过来的ExceptionRecord == NULL 那就在堆栈上自己建立一个
.text:7C94ABE9 // 如果参数传过来的ExceptionRecord != NULL 那就用参数的结构
.text:7C94ABE9 // ----------
.text:7C94ABEF mov [ebp+var_374_ExceptionRecord], edi
.text:7C94ABF5 mov [ebp+var_370_ExceptionAddress], eax // global_unwind2返回地址
.text:7C94ABFB mov [ebp+var_36C_NumberParameters], edi
.text:7C94AC01
.text:7C94AC01 loc_7C94AC01: // CODE XREF: RtlUnwind(x,x,x,x)+2F↑j
.text:7C94AC01 cmp [ebp+EstablisherFrame], edi
.text:7C94AC04 jz loc_7C94ACD8 // 如果EstablisherFrame参数为NULL,则函数将执行退出展开
.text:7C94AC0A or [esi+_EXCEPTION_RECORD.ExceptionFlags], 2 // ------------------------------------------------------------------------
.text:7C94AC0A // // end_ntddk end_wdm
.text:7C94AC0A // #define EXCEPTION_UNWINDING 0x2 // Unwind is in progress
.text:7C94AC0A // #define EXCEPTION_EXIT_UNWIND 0x4 // Exit unwind is in progress
.text:7C94AC0A // #define EXCEPTION_STACK_INVALID 0x8 // Stack out of limits or unaligned
.text:7C94AC0A // #define EXCEPTION_NESTED_CALL 0x10 // Nested exception handler call
.text:7C94AC0A // #define EXCEPTION_TARGET_UNWIND 0x20 // Target unwind in progress
.text:7C94AC0A // #define EXCEPTION_COLLIDED_UNWIND 0x40 // Collided exception handler call
.text:7C94AC0A //
.text:7C94AC0A // #define EXCEPTION_UNWIND (EXCEPTION_UNWINDING | EXCEPTION_EXIT_UNWIND | \
.text:7C94AC0A // EXCEPTION_TARGET_UNWIND | EXCEPTION_COLLIDED_UNWIND)
.text:7C94AC0A // ------------------------------------------------------------------------
.text:7C94AC0E
.text:7C94AC0E loc_7C94AC0E: // CODE XREF: RtlUnwind(x,x,x,x)+137↓j
.text:7C94AC0E push ebx
.text:7C94AC0F lea eax, [ebp+Context] // 如果判断此处为CONTEXT结构,则对着变量双击进去,然后Alt+Q赋一个结构即可
.text:7C94AC15 push eax // eax == &CONTEXT
.text:7C94AC16 mov [ebp+Context.ContextFlags], 10007h
.text:7C94AC20 call _RtlpCaptureContext@4 // 给局部变量Context赋值:段寄存器、EFlags、esp、ebp、eip、(通用寄存器 = 0)
.text:7C94AC25 mov eax, [ebp+ReturnValue]
.text:7C94AC28 add [ebp+Context._Esp], 10h // -----------------------------------------------------------
.text:7C94AC28 // push ebx
.text:7C94AC28 // push esi
.text:7C94AC28 // push edi
.text:7C94AC28 // push ebp
.text:7C94AC28 // push 0 // ReturnValue
.text:7C94AC28 // push 0 // ExceptionRecord
.text:7C94AC28 // push offset _gu_return // __global_unwind2 的返回地址
.text:7C94AC28 // push [ebp+_EXCEPTION_REGISTRATION] // 回调函数的第二个参数 EstablisherFrame
.text:7C94AC28 // call RtlUnwind
.text:7C94AC28 //
.text:7C94AC28 // 此时Context.esp指向“push ebp”,即为调用RtlUnwind前的esp
.text:7C94AC28 // ------------------------------------------------------------------------
.text:7C94AC2F mov [ebp+Context._Eax], eax
.text:7C94AC35 call _RtlpGetRegistrationHead@0 // 获取 fs:[0],因为此时在当前的SEH前面可能又插入了新的
.text:7C94AC35 // 得保证新插入的也有机会执行
.text:7C94AC3A mov ebx, eax
.text:7C94AC3C cmp ebx, 0FFFFFFFFh
.text:7C94AC3F jz loc_7C96E40E // --------------------------------------
.text:7C94AC3F // 如果 fs:[0] == -1,链条上没有SEH
.text:7C94AC3F // 如果 EstablisherFrame != -1,则触发异常,调用ZwRaiseException
.text:7C94AC3F // 如果 EstablisherFrame == -1,则返回0环,调用ZwContinue
.text:7C94AC3F // --------------------------------------------------------
.text:7C94AC45 xor edi, edi
.text:7C94AC47 inc edi
.text:7C94AC48
.text:7C94AC48 loc_7C94AC48: // CODE XREF: RtlUnwind(x,x,x,x)+11C↓j
.text:7C94AC48 cmp ebx, [ebp+EstablisherFrame]
.text:7C94AC4B jz short loc_7C94ACC8 // ------------------------------------------------------
.text:7C94AC4B // 判断链条上的这个EXCEPTION_REGISTR_RECORD如果等于
.text:7C94AC4B // 回调函数的这个EXCEPTION_REGISTR_RECORD,则说明当前找到的就是异常处理函数了
.text:7C94AC4B // 调用ZwContinue进0环后又返回到Context.Eip的__global_unwind2._gu_return
.text:7C94AC4B // 然后__except_handler3往下执行__local_unwind2去局部展开这个__finally
.text:7C94AC4B // ------------------------------------------------------
.text:7C94AC4D cmp [ebp+EstablisherFrame], 0
.text:7C94AC51 jz short loc_7C94AC5C
.text:7C94AC53 cmp [ebp+EstablisherFrame], ebx
.text:7C94AC56 jb loc_7C96E373
.text:7C94AC5C
.text:7C94AC5C loc_7C94AC5C: // CODE XREF: RtlUnwind(x,x,x,x)+AC↑j
.text:7C94AC5C // RtlUnwind(x,x,x,x)+131↓j ...
.text:7C94AC5C cmp ebx, [ebp+var_StackLimit]
.text:7C94AC62 jb loc_7C96E3DE
.text:7C94AC68 lea eax, [ebx+8] // fs:[0]链条上的上一个SEH结构
.text:7C94AC6B cmp eax, [ebp+var_StackBase]
.text:7C94AC71 ja loc_7C96E3DE
.text:7C94AC77 test bl, 3
.text:7C94AC7A jnz loc_7C96E3DE
.text:7C94AC80 mov eax, [ebx+4]
.text:7C94AC83 cmp eax, [ebp+var_StackLimit]
.text:7C94AC89 jb short loc_7C94AC97
.text:7C94AC8B cmp eax, [ebp+var_StackBase]
.text:7C94AC91 jb loc_7C96E3DE
.text:7C94AC97
.text:7C94AC97 loc_7C94AC97: // CODE XREF: RtlUnwind(x,x,x,x)+E4↑j
.text:7C94AC97 push eax
.text:7C94AC98 lea eax, [ebp+var_2DC]
.text:7C94AC9E push eax
.text:7C94AC9F lea eax, [ebp+Context]
.text:7C94ACA5 push eax
.text:7C94ACA6 push ebx
.text:7C94ACA7 push esi
.text:7C94ACA8 call _RtlpExecuteHandlerForUnwind@20 // RtlpExecuteHandlerForUnwind(x,x,x,x,x)
.text:7C94ACAD dec eax
.text:7C94ACAE jnz loc_7C96E3A1
.text:7C94ACB4
.text:7C94ACB4 loc_7C94ACB4: // CODE XREF: .text:7C96E3CE↓j
.text:7C94ACB4 // RtlUnwind(x,x,x,x)+23834↓j
.text:7C94ACB4 mov eax, ebx
.text:7C94ACB6 mov ebx, [ebx]
.text:7C94ACB8 push eax
.text:7C94ACB9 call _RtlpUnlinkHandler@4 // RtlpUnlinkHandler(x)
.text:7C94ACBE
.text:7C94ACBE loc_7C94ACBE: // CODE XREF: .text:7C96E407↓j
.text:7C94ACBE cmp ebx, 0FFFFFFFFh
.text:7C94ACC1 jnz short loc_7C94AC48
.text:7C94ACC3 jmp loc_7C96E40C
.text:7C94ACC8 // ---------------------------------------------------------------------------
.text:7C94ACC8
.text:7C94ACC8 loc_7C94ACC8: // CODE XREF: RtlUnwind(x,x,x,x)+A6↑j
.text:7C94ACC8 push 0 // ------------------------------------------------------
.text:7C94ACC8 // 判断链条上的这个EXCEPTION_REGISTR_RECORD如果等于
.text:7C94ACC8 // 回调函数的这个EXCEPTION_REGISTR_RECORD,则就不需要展开了
.text:7C94ACC8 // 调用ZwContinue进0环后又返回到Context.Eip的__global_unwind2._gu_return
.text:7C94ACC8 // 然后__except_handler3往下执行__local_unwind2去局部展开这个__finally
.text:7C94ACC8 // ------------------------------------------------------
.text:7C94ACCA lea eax, [ebp+Context]
.text:7C94ACD0 push eax
.text:7C94ACD1 call _ZwContinue@8 // NTSTATUS __stdcall NtContinue(PCONTEXT Context, BOOLEAN TestAlert)
.text:7C94ACD6 jmp short loc_7C94AC5C
.text:7C94ACD8 // ---------------------------------------------------------------------------
.text:7C94ACD8
.text:7C94ACD8 loc_7C94ACD8: // CODE XREF: RtlUnwind(x,x,x,x)+5F↑j
.text:7C94ACD8 or [esi+_EXCEPTION_RECORD.ExceptionFlags], 6 // 如果EstablisherFrame参数为NULL,则函数将执行退出展开
.text:7C94ACDC jmp loc_7C94AC0E
.text:7C94ACDC _RtlUnwind@16 endp
.text:7C94ACDC
.text:7C94ACE1 // ---------------------------------------------------------------------------

重点关注的代码在:

1
2
.text:7C94AC48 loc_7C94AC48:
.text:7C94AC48 cmp ebx, [ebp+EstablisherFrame]

RtlpCaptureContext 函数如下:

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
.text:7C92334A // =============== S U B R O U T I N E =======================================
.text:7C92334A
.text:7C92334A // 给局部变量Context赋值:段寄存器、EFlags、esp、ebp、eip、(通用寄存器 = 0)
.text:7C92334A
.text:7C92334A // __stdcall RtlpCaptureContext(x)
.text:7C92334A _RtlpCaptureContext@4 proc near // CODE XREF: RtlUnwind(x,x,x,x)+7B↓p
.text:7C92334A
.text:7C92334A arg_&CONTEXT = dword ptr 4
.text:7C92334A
.text:7C92334A push ebx
.text:7C92334B mov ebx, [esp+4+arg_&CONTEXT]
.text:7C92334F mov [ebx+CONTEXT._Eax], 0
.text:7C923359 mov [ebx+CONTEXT._Ecx], 0
.text:7C923363 mov [ebx+CONTEXT._Edx], 0
.text:7C92336D mov [ebx+CONTEXT._Ebx], 0
.text:7C923377 mov [ebx+CONTEXT._Esi], 0
.text:7C923381 mov [ebx+CONTEXT._Edi], 0
.text:7C92338B
.text:7C92338B loc_7C92338B: // CODE XREF: RtlCaptureContext(x)+2C↑j
.text:7C92338B mov word ptr [ebx+CONTEXT.SegCs], cs
.text:7C923392 mov word ptr [ebx+CONTEXT.SegDs], ds
.text:7C923399 mov word ptr [ebx+CONTEXT.SegEs], es
.text:7C9233A0 mov word ptr [ebx+CONTEXT.SegFs], fs
.text:7C9233A7 mov word ptr [ebx+CONTEXT.SegGs], gs
.text:7C9233AE mov word ptr [ebx+CONTEXT.SegSs], ss
.text:7C9233B5 pushf
.text:7C9233B6 pop [ebx+CONTEXT.EFlags]
.text:7C9233BC mov eax, [ebp+4] // 异常函数.返回地址
.text:7C9233BF mov [ebx+CONTEXT._Eip], eax
.text:7C9233C5 mov eax, [ebp+0] // 异常函数.ebp
.text:7C9233C8 mov [ebx+CONTEXT._Ebp], eax
.text:7C9233CE lea eax, [ebp+8] // 异常函数.EstablisherFrame,__global_unwind2压入的参数一
.text:7C9233D1 mov [ebx+CONTEXT._Esp], eax
.text:7C9233D7 pop ebx
.text:7C9233D8 retn 4
.text:7C9233D8 _RtlpCaptureContext@4 endp

函数 RtlpExecuteHandlerForUnwind 包含 ExecuteHandler

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
.text:7C92324F // =============== S U B R O U T I N E =======================================
.text:7C92324F
.text:7C92324F
.text:7C92324F // __stdcall RtlpExecuteHandlerForUnwind(x, x, x, x, x)
.text:7C92324F _RtlpExecuteHandlerForUnwind@20 proc near
.text:7C92324F // CODE XREF: RtlUnwind(x,x,x,x)+103↓p
.text:7C92324F mov edx, offset sub_7C9232E3
.text:7C923254 lea ecx, [ecx]
.text:7C923254 _RtlpExecuteHandlerForUnwind@20 endp
.text:7C923254
.text:7C923256
.text:7C923256 // =============== S U B R O U T I N E =======================================
.text:7C923256
.text:7C923256 // IN PEXCEPTION_RECORD ExceptionRecord,
.text:7C923256 // IN PVOID EstablisherFrame,
.text:7C923256 // IN OUT PCONTEXT ContextRecord,
.text:7C923256 // OUT PVOID DispatcherContext,
.text:7C923256 // IN PEXCEPTION_ROUTINE ExceptionRoutine
.text:7C923256
.text:7C923256 // __fastcall ExecuteHandler(x, x, x, x, x)
.text:7C923256 ExecuteHandler@20 proc near // CODE XREF: RtlpExecuteHandlerForException(x,x,x,x,x)+5↑j
.text:7C923256
.text:7C923256 arg_ExceptionRecord= dword ptr 4
.text:7C923256 arg_EstablisherFrame= dword ptr 8
.text:7C923256 arg_ContextRecord= dword ptr 0Ch
.text:7C923256 arg_OUT_DispatcherContext= dword ptr 10h
.text:7C923256 arg_ExceptionRoutine= dword ptr 14h
.text:7C923256
.text:7C923256 push ebx
.text:7C923257 push esi
.text:7C923258 push edi
.text:7C923259 xor eax, eax
.text:7C92325B xor ebx, ebx
.text:7C92325D xor esi, esi
.text:7C92325F xor edi, edi
.text:7C923261 push [esp+0Ch+arg_ExceptionRoutine]
.text:7C923265 push [esp+10h+arg_OUT_DispatcherContext]
.text:7C923269 push [esp+14h+arg_ContextRecord]
.text:7C92326D push [esp+18h+arg_EstablisherFrame]
.text:7C923271 push [esp+1Ch+arg_ExceptionRecord]
.text:7C923275 call ExecuteHandler2@20 // IN PEXCEPTION_RECORD ExceptionRecord,
.text:7C923275 // IN PVOID EstablisherFrame,
.text:7C923275 // IN OUT PCONTEXT ContextRecord,
.text:7C923275 // OUT PVOID DispatcherContext,
.text:7C923275 // IN PEXCEPTION_ROUTINE ExceptionRoutine
.text:7C92327A pop edi
.text:7C92327B pop esi
.text:7C92327C pop ebx
.text:7C92327D retn 14h
.text:7C92327D ExecuteHandler@20 endp

RtlpUnlinkHandler 函数如下:

1
2
3
4
5
6
7
8
9
10
.text:7C92330A ; __stdcall RtlpUnlinkHandler(x)
.text:7C92330A _RtlpUnlinkHandler@4 proc near ; CODE XREF: RtlUnwind(x,x,x,x)+114↓p
.text:7C92330A
.text:7C92330A arg_0 = dword ptr 4
.text:7C92330A
.text:7C92330A mov ecx, [esp+arg_0]
.text:7C92330E mov ecx, [ecx]
.text:7C923310 mov large fs:0, ecx
.text:7C923317 retn 4
.text:7C923317 _RtlpUnlinkHandler@4 endp

x86下的源码为:

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
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
VOID RtlUnwind (
IN PVOID TargetFrame OPTIONAL,
IN PVOID TargetIp OPTIONAL,
IN PEXCEPTION_RECORD ExceptionRecord OPTIONAL,
IN PVOID ReturnValue
)
/*++
Routine Description:
This function initiates an unwind of procedure call frames. The machine
state at the time of the call to unwind is captured in a context record
and the unwinding flag is set in the exception flags of the exception
record. If the TargetFrame parameter is not specified, then the exit unwind
flag is also set in the exception flags of the exception record. A backward
walk through the procedure call frames is then performed to find the target
of the unwind operation.

N.B. The captured context passed to unwinding handlers will not be
a completely accurate context set for the 386. This is because
there isn't a standard stack frame in which registers are stored.

Only the integer registers are affected. The segement and
control registers (ebp, esp) will have correct values for
the flat 32 bit environment.

N.B. If you change the number of arguments, make sure you change the
adjustment of ESP after the call to RtlpCaptureContext (for
STDCALL calling convention)

Arguments:
TargetFrame - Supplies an optional pointer to the call frame that is the
target of the unwind. If this parameter is not specified, then an exit
unwind is performed.
TargetIp - Supplies an optional instruction address that specifies the
continuation address of the unwind. This address is ignored if the
target frame parameter is not specified.
ExceptionRecord - Supplies an optional pointer to an exception record.
ReturnValue - Supplies a value that is to be placed in the integer
function return register just before continuing execution.
Return Value: None.
--*/
{
PCONTEXT ContextRecord;
CONTEXT ContextRecord1;
DISPATCHER_CONTEXT DispatcherContext;
EXCEPTION_DISPOSITION Disposition;
PEXCEPTION_REGISTRATION_RECORD RegistrationPointer;
PEXCEPTION_REGISTRATION_RECORD PriorPointer;
ULONG HighAddress;
ULONG HighLimit;
ULONG LowLimit;
EXCEPTION_RECORD ExceptionRecord1;
EXCEPTION_RECORD ExceptionRecord2;

// Get current stack limits.
RtlpGetStackLimits(&LowLimit, &HighLimit);

// If an exception record is not specified, then build a local exception
// record for use in calling exception handlers during the unwind operation.
if (ARGUMENT_PRESENT(ExceptionRecord) == FALSE) {
ExceptionRecord = &ExceptionRecord1;
ExceptionRecord1.ExceptionCode = STATUS_UNWIND;
ExceptionRecord1.ExceptionFlags = 0;
ExceptionRecord1.ExceptionRecord = NULL;
ExceptionRecord1.ExceptionAddress = _ReturnAddress();
ExceptionRecord1.NumberParameters = 0;
}

// If the target frame of the unwind is specified, then set EXCEPTION_UNWINDING
// flag in the exception flags. Otherwise set both EXCEPTION_EXIT_UNWIND and
// EXCEPTION_UNWINDING flags in the exception flags.
if (ARGUMENT_PRESENT(TargetFrame) == TRUE) {
ExceptionRecord->ExceptionFlags |= EXCEPTION_UNWINDING;
} else {
ExceptionRecord->ExceptionFlags |= (EXCEPTION_UNWINDING |
EXCEPTION_EXIT_UNWIND);
}

// Capture the context.
ContextRecord = &ContextRecord1;
ContextRecord1.ContextFlags = CONTEXT_INTEGER | CONTEXT_CONTROL | CONTEXT_SEGMENTS;
RtlpCaptureContext(ContextRecord);

#ifdef STD_CALL

// Adjust captured context to pop our arguments off the stack
ContextRecord->Esp += sizeof(TargetFrame) +
sizeof(TargetIp) +
sizeof(ExceptionRecord) +
sizeof(ReturnValue);
#endif
ContextRecord->Eax = (ULONG)ReturnValue;

// Scan backward through the call frame hierarchy, calling exception
// handlers as they are encountered, until the target frame of the unwind is reached.
RegistrationPointer = RtlpGetRegistrationHead();
while (RegistrationPointer != EXCEPTION_CHAIN_END) {

// If this is the target of the unwind, then continue execution
// by calling the continue system service.
if ((ULONG)RegistrationPointer == (ULONG)TargetFrame) {
ZwContinue(ContextRecord, FALSE);

// If the target frame is lower in the stack than the current frame,
// then raise STATUS_INVALID_UNWIND exception.
} else if ( (ARGUMENT_PRESENT(TargetFrame) == TRUE) &&
((ULONG)TargetFrame < (ULONG)RegistrationPointer) ) {
ExceptionRecord2.ExceptionCode = STATUS_INVALID_UNWIND_TARGET;
ExceptionRecord2.ExceptionFlags = EXCEPTION_NONCONTINUABLE;
ExceptionRecord2.ExceptionRecord = ExceptionRecord;
ExceptionRecord2.NumberParameters = 0;
RtlRaiseException(&ExceptionRecord2);
}

// If the call frame is not within the specified stack limits or the
// call frame is unaligned, then raise the exception STATUS_BAD_STACK.
// Else restore the state from the specified frame to the context record.
HighAddress = (ULONG)RegistrationPointer +
sizeof(EXCEPTION_REGISTRATION_RECORD);

if ( ((ULONG)RegistrationPointer < LowLimit) ||
(HighAddress > HighLimit) ||
(((ULONG)RegistrationPointer & 0x3) != 0)
#if !defined(NTOS_KERNEL_RUNTIME)
||
(((ULONG)RegistrationPointer->Handler >= LowLimit) && ((ULONG)RegistrationPointer->Handler < HighLimit))
#endif
) {

#if defined(NTOS_KERNEL_RUNTIME)

// Allow for the possibility that the problem occured on the DPC stack.
ULONG TestAddress = (ULONG)RegistrationPointer;

if (((TestAddress & 0x3) == 0) &&
KeGetCurrentIrql() >= DISPATCH_LEVEL) {

PKPRCB Prcb = KeGetCurrentPrcb();
ULONG DpcStack = (ULONG)Prcb->DpcStack;

if ((Prcb->DpcRoutineActive) &&
(HighAddress <= DpcStack) &&
(TestAddress >= DpcStack - KERNEL_STACK_SIZE)) {

// This error occured on the DPC stack, switch
// stack limits to the DPC stack and restart the loop.

HighLimit = DpcStack;
LowLimit = DpcStack - KERNEL_STACK_SIZE;
continue;
}
}

#endif
ExceptionRecord2.ExceptionCode = STATUS_BAD_STACK;
ExceptionRecord2.ExceptionFlags = EXCEPTION_NONCONTINUABLE;
ExceptionRecord2.ExceptionRecord = ExceptionRecord;
ExceptionRecord2.NumberParameters = 0;
RtlRaiseException(&ExceptionRecord2);
} else {

// The handler must be executed by calling another routine
// that is written in assembler. This is required because
// up level addressing of the handler information is required
// when a collided unwind is encountered.
Disposition = RtlpExecuteHandlerForUnwind(
ExceptionRecord,
(PVOID)RegistrationPointer,
ContextRecord,
(PVOID)&DispatcherContext,
RegistrationPointer->Handler);

// Case on the handler disposition.
switch (Disposition) {

// The disposition is to continue the search. Get next
// frame address and continue the search.
case ExceptionContinueSearch :
break;

// The disposition is colided unwind. Maximize the target
// of the unwind and change the context record pointer.
case ExceptionCollidedUnwind :

// Pick up the registration pointer that was active at
// the time of the unwind, and simply continue.
RegistrationPointer = DispatcherContext.RegistrationPointer;
break;

// All other disposition values are invalid. Raise
// invalid disposition exception.
default :
ExceptionRecord2.ExceptionCode = STATUS_INVALID_DISPOSITION;
ExceptionRecord2.ExceptionFlags = EXCEPTION_NONCONTINUABLE;
ExceptionRecord2.ExceptionRecord = ExceptionRecord;
ExceptionRecord2.NumberParameters = 0;
RtlRaiseException(&ExceptionRecord2);
break;
}

// Step to next registration record
PriorPointer = RegistrationPointer;
RegistrationPointer = RegistrationPointer->Next;

// Unlink the unwind handler, since it's been called.
RtlpUnlinkHandler(PriorPointer);

// If chain goes in wrong direction or loops, raise an
// exception.
}
}

if (TargetFrame == EXCEPTION_CHAIN_END) {

// Caller simply wants to unwind all exception records.
// This differs from an exit_unwind in that no "exit" is desired.
// Do a normal continue, since we've effectively found the
// "target" the caller wanted.
ZwContinue(ContextRecord, FALSE);

} else {

// Either (1) a real exit unwind was performed, or (2) the
// specified TargetFrame is not present in the exception handler
// list. In either case, give debugger and subsystem a chance
// to see the unwind.
ZwRaiseException(ExceptionRecord, ContextRecord, FALSE);

}
return;
}

可参阅:

4 异常返回值、错误码

可以参考《加密与解密第四版8.3.4 P344》

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
//异常回调函数原型
typedef EXCEPTION_DISPOSITION (__cdecl *PEXCEPTION_ROUTINE)(
struct _EXCEPTION_RECORD *ExceptionRecord,
void* EstablisherFrame,
struct _CONTEXT *ContextRecord,
void* DispatcherContext
)

/*
* 过滤表达式返回值
* Legal values for expression in except().
*/
#define EXCEPTION_EXECUTE_HANDLER 1
#define EXCEPTION_CONTINUE_SEARCH 0
#define EXCEPTION_CONTINUE_EXECUTION -1

/*
* VEH、SEH的hander返回值
* Exception disposition return values.
*/
typedef enum _EXCEPTION_DISPOSITION {
ExceptionContinueExecution, //0
ExceptionContinueSearch, //1
ExceptionNestedException, //2
ExceptionCollidedUnwind //3
} EXCEPTION_DISPOSITION;

#define EXCEPTION_NONCONTINUABLE 0x1 // Noncontinuable exception

// Define maximum number of exception parameters.
#define EXCEPTION_MAXIMUM_PARAMETERS 15 // maximum number of exception parameters

// Exception record definition.
typedef struct _EXCEPTION_RECORD {
NTSTATUS ExceptionCode;
ULONG ExceptionFlags;
struct _EXCEPTION_RECORD *ExceptionRecord;
PVOID ExceptionAddress;
ULONG NumberParameters;
ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD;
//
// Exception Record Offset, Flag, and Enumerated Type Definitions
//

#define EXCEPTION_NONCONTINUABLE 0x1
#define EXCEPTION_UNWINDING 0x2 // Unwind is in progress
#define EXCEPTION_EXIT_UNWIND 0x4 // Exit unwind is in progress
#define EXCEPTION_STACK_INVALID 0x8 // Stack out of limits or unaligned
#define EXCEPTION_NESTED_CALL 0x10 // Nested exception handler call
#define EXCEPTION_TARGET_UNWIND 0x20 // Target unwind in progress
#define EXCEPTION_COLLIDED_UNWIND 0x40 // Collided exception handler call
#define EXCEPTION_UNWIND 0x66

#define ExceptionContinueExecution 0x0
#define ExceptionContinueSearch 0x1
#define ExceptionNestedException 0x2
#define ExceptionCollidedUnwind 0x3

#define ErExceptionCode 0x0
#define ErExceptionFlags 0x4
#define ErExceptionRecord 0x8
#define ErExceptionAddress 0xc
#define ErNumberParameters 0x10
#define ErExceptionInformation 0x14
#define ExceptionRecordLength 0x50

5 __SEH_prolog 和 __SEH_epilog

因为几乎所有由 MSC 编译生成的 sys、dll、exe 文件都需要使用__except_handle3 异常处理函数,并且都需要进行 SEH 的安装和卸载,所以编译器把这部分代码提取出来,形成了两个独立的函数,分别叫作 __SEH_prolog__SEH_epilog。它们的主要作用就是把__excep_handler3 安装为 SEH 处理函数及卸载,这也是在反汇编那些使用了 SEH 的系统 API时总会看到如下代码的原因。

如下在 0x7C80B6E3处:

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
.text:7C80B6DC // __stdcall BaseThreadStart(x, x)
.text:7C80B6DC _BaseThreadStart@8 proc near // CODE XREF: BaseThreadStartThunk(x,x)+6↓j
.text:7C80B6DC // BaseFiberStart()+12↓p
.text:7C80B6DC
.text:7C80B6DC var_20 = dword ptr -20h
.text:7C80B6DC uExitCode = dword ptr -1Ch
.text:7C80B6DC ms_exc = CPPEH_RECORD ptr -18h
.text:7C80B6DC arg_0 = dword ptr 8
.text:7C80B6DC arg_4 = dword ptr 0Ch
.text:7C80B6DC // __unwind { // __SEH_prolog
.text:7C80B6DC push 10h
.text:7C80B6DE push offset stru_7C80B720
.text:7C80B6E3 call __SEH_prolog //在FS:[0]链条上挂入一个EXCEPTION_REGISTR_RECORD(最后一道防线)
.text:7C80B6E8 // __try { // __except at loc_7C83AB3E
.text:7C80B6E8 and [ebp+ms_exc.registration.TryLevel], 0
.text:7C80B6EC mov eax, large fs:18h
.text:7C80B6F2 mov [ebp+var_20], eax
.text:7C80B6F5 cmp dword ptr [eax+10h], 1E00h
.text:7C80B6FC jnz short loc_7C80B70D
.text:7C80B6FE cmp _BaseRunningInServerProcess, 0
.text:7C80B705 jnz short loc_7C80B70D
.text:7C80B707 call ds:__imp__CsrNewThread@0 // CsrNewThread()
.text:7C80B70D
.text:7C80B70D loc_7C80B70D: // CODE XREF: BaseThreadStart(x,x)+20↑j
.text:7C80B70D // BaseThreadStart(x,x)+29↑j
.text:7C80B70D push [ebp+arg_4]
.text:7C80B710 call [ebp+arg_0]
.text:7C80B713 push eax // dwExitCode
.text:7C80B714
.text:7C80B714 loc_7C80B714: // CODE XREF: BaseThreadStart(x,x)+2F46F↓j
.text:7C80B714 call _ExitThread@4 // ExitThread(x)
.text:7C80B714 // } // starts at 7C80B6E8
.text:7C80B714 // } // starts at 7C80B6DC
.text:7C80B714 _BaseThreadStart@8 endp

5.1 __SEH_prolog

__SEH_prolog 函数分析如下:

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
.text:7C8024D6 // =============== S U B R O U T I N E =======================================
.text:7C8024D6
.text:7C8024D6 // Attributes: library function
.text:7C8024D6
.text:7C8024D6 __SEH_prolog proc near // CODE XREF: DeviceIoControl(x,x,x,x,x,x,x,x)+7↑p
.text:7C8024D6 // ReadFile(x,x,x,x,x)+7↑p ...
.text:7C8024D6
.text:7C8024D6 arg_scopetable = dword ptr 8
.text:7C8024D6
.text:7C8024D6 push offset __except_handler3
.text:7C8024DB mov eax, large fs:0
.text:7C8024E1 push eax
.text:7C8024E2 mov eax, [esp+8+arg_scopetable] // eax = 0xc
.text:7C8024E6 mov [esp+8+arg_scopetable], ebp
.text:7C8024EA lea ebp, [esp+8+arg_scopetable]
.text:7C8024EE sub esp, eax
.text:7C8024F0 push ebx
.text:7C8024F1 push esi
.text:7C8024F2 push edi
.text:7C8024F3 mov eax, [ebp-8] // eax = 上一个函数返回地址
.text:7C8024F6 mov [ebp-18h], esp // 保存 esp
.text:7C8024F9 push eax
.text:7C8024FA mov eax, [ebp-4] // eax = scopetable
.text:7C8024FD mov dword ptr [ebp-4], 0FFFFFFFFh
.text:7C802504 mov [ebp-8], eax
.text:7C802507 lea eax, [ebp-10h] // eax = fs:[0]
.text:7C80250A mov large fs:0, eax
.text:7C802510 retn
.text:7C802510 __SEH_prolog endp // sp-analysis failed

33.png

5.2 CPPEH_RECORD与ms_exc

参考本文 2.2、上面对 __SEH_prolog 源代码的分析、对 __SEH_prolog4 源代码的分析,以及《加密与解密第二版 8.3.4 P343》有如下结论。

实际上 __SEH_prolog 函数每次都在栈上建立的是 CPPEH_RECORD 结构,这是编译器的扩展,VS2019也都在用该结构。

C/C++ 编译器扩展 SEH 的异常帧结构为 CPPEH_RECORD

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct CPPEH_RECORD
{
DWORD old_esp;
EXCEPTION_POINTERS *exc_ptr; //在异常处理函数中压入栈,方便给异常过滤器使用
struct EXCEPTION_REGISTRATION registration;
};

typedef struct _EXCEPTION_POINTERS {
PEXCEPTION_RECORD ExceptionRecord;
PCONTEXT ContextRecord;
} EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;

struct _EXCEPTION_REGISTRATION
{
struct _EXCEPTION_REGISTRATION* prev;
void (*handler)(PEXCEPTION_RECORD, PEXCEPTION_ REGISTRATION, PCONTEXT, PEXCEPTION_RECORD);
struct scopetable_entry * scopetable;
int trylevel;
int _ebp;
};

32.png

33.png

在 IDA 中经常会在一个函数开头看到这样的局部变量:

1
2
3
4
5
6
7
.text:7C817044 ms_exc          = CPPEH_RECORD ptr -18h

.text:7C817044 ; __unwind { // __SEH_prolog
.text:7C817044 push 0Ch
.text:7C817046 push offset stru_7C817070 ; scopetable
.text:7C81704B call __SEH_prolog
...

实际上这里的 ms_exc 就是 CPPEH_RECORD 结构,调用 __SEH_prolog 在当前堆栈构建 CPPEH_RECORD 结构,当 __SEH_prolog 函数返回后,esp 指向 edi 寄存器。

5.3 __SEH_epilog

__SEH_epilog 函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.text:7C802511 // =============== S U B R O U T I N E =======================================
.text:7C802511
.text:7C802511 // Attributes: library function
.text:7C802511
.text:7C802511 __SEH_epilog proc near // CODE XREF: DeviceIoControl(x,x,x,x,x,x,x,x):loc_7C801693↑p
.text:7C802511 // ReadFile(x,x,x,x,x):loc_7C801897↑p ...
.text:7C802511 mov ecx, [ebp-10h]
.text:7C802514 mov large fs:0, ecx // ecx == EXCEPTION_REGISTRATION
.text:7C80251B pop ecx
.text:7C80251C pop edi
.text:7C80251D pop esi
.text:7C80251E pop ebx
.text:7C80251F leave
.text:7C802520 push ecx
.text:7C802521 retn
.text:7C802521 __SEH_epilog endp // sp-analysis failed

6 异常处理相关的流程图

异常处理流程.png