在本系列的上一篇文章Windows异常世界历险记(四)——VC6中结构化异常处理机制的反汇编分析(中)中,给出了针对VC6的异常处理机制进行逆向后得到的伪码。在本文中,我们仍然以之前写的小程序为例,通过调试的方式结合我们得到的VC6结构化异常处理的伪码,对其运行机制进行分析。
在RaiseExcept中,即将触发异常时的情形如下:
0:000> p
eax=cccccccc ebx=003ea000 ecx=00000000 edx=00694687 esi=00401540 edi=0019fe5c
eip=00401143 esp=0019fe0c ebp=0019fe74 iopl=0 nv up ei pl nz na pe cy
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000207
SEHTest1!RaiseExcept+0x43:
00401143 c7050000000000000000 mov dword ptr ds:[0],0 ds:002b:00000000=????????
先看看SEH链:
0:000> dd fs:[0]
0053:00000000 0019fe64 001a0000 0019c000 00000000
0:000> dt _EXCEPTION_REGISTRATION_RECORD 0019fe64
ntdll!_EXCEPTION_REGISTRATION_RECORD
+0x000 Next : 0x0019fed0 _EXCEPTION_REGISTRATION_RECORD
+0x004 Handler : 0x00401460 _EXCEPTION_DISPOSITION SEHTest1!_except_handler3+0
0:000> dt _EXCEPTION_REGISTRATION_RECORD 0x0019fed0
ntdll!_EXCEPTION_REGISTRATION_RECORD
+0x000 Next : 0x0019ff60 _EXCEPTION_REGISTRATION_RECORD
+0x004 Handler : 0x00401460 _EXCEPTION_DISPOSITION SEHTest1!_except_handler3+0
0:000> dt _EXCEPTION_REGISTRATION_RECORD 0x0019ff60
ntdll!_EXCEPTION_REGISTRATION_RECORD
+0x000 Next : 0x0019ffcc _EXCEPTION_REGISTRATION_RECORD
+0x004 Handler : 0x00401460 _EXCEPTION_DISPOSITION SEHTest1!_except_handler3+0
0:000> dt _EXCEPTION_REGISTRATION_RECORD 0x0019ffcc
ntdll!_EXCEPTION_REGISTRATION_RECORD
+0x000 Next : 0x0019ffe4 _EXCEPTION_REGISTRATION_RECORD
+0x004 Handler : 0x775286d0 _EXCEPTION_DISPOSITION ntdll!_except_handler4+0
0:000> dt _EXCEPTION_REGISTRATION_RECORD 0x0019ffe4
ntdll!_EXCEPTION_REGISTRATION_RECORD
+0x000 Next : 0xffffffff _EXCEPTION_REGISTRATION_RECORD
+0x004 Handler : 0x7753518e _EXCEPTION_DISPOSITION ntdll!FinalExceptionHandlerPad14+0
由上述结果可得出结论:目前一共安装有5个SEH异常处理函数,其中前3个是VC6自己的,后两个是系统的。对于前3个VC6安装的SEH节点,是在标准的SEH链结构EXCEPTION_REGISTRATION_RECORD的基础上“加了料”形成的,在上篇文章中已经给出了其结构,我们将其暂且命名为VC6_EXCEPTION_REGISTRATION_RECORD吧:
typedef struct VC6_EXCEPTION_REGISTRATION_RECORD
{
DWORD dwCurrentESP; // 保存的ESP值
PVC6_EXCEPTION_INFORMATION pExceptInfo; // 异常信息结构指针
EXCEPTION_REGISTRATION_RECORD pSEHNode; // 经典的异常链节点
PVC6_SCOPETABLE pScopeTable; // VC的惯用伎俩——表结构
DWORD dwTryLevel; // 当前try的层级
DWORD dwEBP; // 这个其实不是本结构的成员,只是本结构在栈上挨着函数入口处压入的EBP而已
}VC6_EXCEPTION_REGISTRATION_RECORD, *PVC6_EXCEPTION_REGISTRATION_RECORD;
需要指出的是,VC6_EXCEPTION_REGISTRATION_RECORD结构中最后一个成员dwEBP是我们根据栈结构添加进去的,因为在except_handler3中会用到。下面就按VC6_EXCEPTION_REGISTRATION_RECORD结构对前3个属于VC6的异常处理节点进行解析:
结构 | 第一节点 | 第二节点 | 第三节点 |
---|---|---|---|
dwCurrentESP | 0019fe0c | 0019fe7c | 0019ffcc |
pExceptInfo(此时还没填) | 00000800 | 0019fee0 | 00401460 |
pSEHNode.Next | 0019fed0 | 0019ff60 | 00422248 |
pSEHNode.Handler | 00401460 | 00401460 | 00000000 |
pScopeTable | 004220c8 | 00422138 | 0019ff80 |
dwTryLevel | 00000001 | 00000001 | 74d6fe09 |
dwEBP | 0019fee0 | 0019ff70 | 003ea000 |
OK了,下面主要来看下各节点的ScopeTable,ScopeTable的结构定义在上一篇中被我们命名为了
typedef struct VC6_SCOPETABLE
{
DWORD dwOuterLevel;
PVOID pfnFilter;
PVOID pfnHandler;
}VC6_SCOPETABLE, *PVC6_SCOPETABLE;
实际上ScopeTable是一个结构数组而已。
在except_handler3处下断后,继续运行程序,由于触发异常该段点命中,命中时的情形如下:
SEHTest1!_except_handler3:
00401460 55 push ebp
0:000> kb
# ChildEBP RetAddr Args to Child
00 0019f854 77535032 0019f95c 0019fe64 0019f9ac SEHTest1!_except_handler3
01 0019f878 77535004 0019f95c 0019fe64 0019f9ac ntdll!ExecuteHandler2+0x26
02 0019f944 77522936 0019f95c 0019f9ac 0019f95c ntdll!ExecuteHandler+0x24
03 0019f944 00401143 0019f95c 0019f9ac 0019f95c ntdll!KiUserExceptionDispatcher+0x26
04 0019fe74 00401268 00401540 00401540 003ea000 SEHTest1!RaiseExcept+0x43 [Main.c @ 30]
05 0019fee0 00401653 00400000 00000000 00694687 SEHTest1!WinMain+0x48 [Main.c @ 49]
我们重点关注其中访问ScopeTable的部分,我们从上篇文章中节选出这部分等价的C伪码
while (tryLevel != -1)
{
// 若为try-except型
if (pScopeTable[tryLevel].pfnFilter)
{
// 执行过滤函数,并根据其返回值进行判定
int nRet = pScopeTable[tryLevel].pfnFilter();
if (nRet != 0)
{
if (nRet < 0)
{
return ExceptionContinueExecution;
}
// 执行全局展开
global_unwind2(pSEHNode);
DWORD dwOrgEBP = pVC6SEHNode->dwEBP;
// 栈基址指针切换
_asm
{
mov ebp, dwOrgEBP
}
// 执行局部展开
local_unwind2(pSEHNode, tryLevel);
NLG_Notify(1);
pVC6SEHNode->dwTryLevel = pScopeTable[tryLevel].dwOuterLevel;
// 调用异常处理函数
pScopeTable[tryLevel].pfnHandler();
}
}
tryLevel = pScopeTable[tryLevel].dwOuterLevel;
}
第一次访问ScopeTable时,从栈上读到的tryLevel为1,因此pScopeTable[tryLevel].pfnFilter是什么呢?
0:000> p
eax=0019f84c ebx=0019fe64 ecx=00000003 edx=77535050 esi=00000001 edi=004220c8
eip=0040149b esp=0019f83c ebp=0019f854 iopl=0 nv up ei pl nz ac po cy
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000213
SEHTest1!_except_handler3+0x3b:
0040149b 837c8f0400 cmp dword ptr [edi+ecx*4+4],0 ds:002b:004220d8=00000000
0:000> dd 004220c8
004220c8 ffffffff 00401182 00401199 00000000
004220d8 00000000 0040115b
是004220d8处的0,由于没有过滤函数,因此这应该是一个try-finally型异常处理函数,我们根据pfnHandler=0040115b来看看这是谁家的finally函数:
0:000> u 0040115b
SEHTest1!RaiseExcept+0x5b [C:\SRC\Test\SEHTest1\Main.c @ 34]:
0040115b 8bf4 mov esi,esp
0040115d 6a00 push 0
0040115f 68b0204200 push offset SEHTest1!`string' (004220b0)
00401164 688c204200 push offset SEHTest1!`string' (0042208c)
00401169 6a00 push 0
0040116b ff15aca24200 call dword ptr [SEHTest1!_imp__MessageBoxA (0042a2ac)]
00401171 3bf4 cmp esi,esp
00401173 e8b8010000 call SEHTest1!_chkesp (00401330)
0:000> da 004220b0
004220b0 "In Inner::finally"
很明显,可以实锤这是RaiseExcept中的内层try-finally。按照我们分析出来的伪码,由于不存在pfnFilter,这里将不执行pfnHandler。实证一下,确实跳到后面了:
0:000> t
eax=0019f84c ebx=0019fe64 ecx=00000003 edx=77535050 esi=00000001 edi=004220c8
eip=004014a0 esp=0019f83c ebp=0019f854 iopl=0 nv up ei pl zr na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246
SEHTest1!_except_handler3+0x40:
004014a0 7445 je SEHTest1!_except_handler3+0x87 (004014e7) [br=1]
0:000> t
eax=0019f84c ebx=0019fe64 ecx=00000003 edx=77535050 esi=00000001 edi=004220c8
eip=004014e7 esp=0019f83c ebp=0019f854 iopl=0 nv up ei pl zr na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246
SEHTest1!_except_handler3+0x87:
004014e7 8b7b08 mov edi,dword ptr [ebx+8] ds:002b:0019fe6c=004220c8
接下面是执行
tryLevel = pScopeTable[tryLevel].dwOuterLevel;
应该就是刚刚pfnFilter前面的那个dwOuterTryLevel,实证下:
0:000> p
eax=0019f84c ebx=0019fe64 ecx=00000003 edx=77535050 esi=00000001 edi=004220c8
eip=004014ed esp=0019f83c ebp=0019f854 iopl=0 nv up ei pl zr na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246
SEHTest1!_except_handler3+0x8d:
004014ed 8b348f mov esi,dword ptr [edi+ecx*4] ds:002b:004220d4=00000000
然后开始下一轮循环,这次由于tryLevel值为0,因此pfnFilter值为00401182,而pfnHandler的值00401199,这是谁家的try-except结构呢?
0:000> u 00401199
SEHTest1!RaiseExcept+0x99 [C:\SRC\Test\SEHTest1\Main.c @ 37]:
00401199 8b65e8 mov esp,dword ptr [ebp-18h]
0040119c 8bf4 mov esi,esp
0040119e 6a00 push 0
004011a0 6884204200 push offset SEHTest1!`string' (00422084)
004011a5 6874204200 push offset SEHTest1!`string' (00422074)
004011aa 6a00 push 0
004011ac ff15aca24200 call dword ptr [SEHTest1!_imp__MessageBoxA (0042a2ac)]
004011b2 3bf4 cmp esi,esp
0:000> da 00422084
00422084 "haha"
0:000> da 00422074
00422074 "Never execute"
实锤这是RaiseExcept中外层的try-except结构。步过后果然看到了MessageBox弹框,执行完成后,该函数返回0,即所谓的EXCEPTION_CONTINUE_SEARCH,拒绝处理该异常。
eax=00000000 ebx=0019fe64 ecx=00690000 edx=00690000 esi=00000000 edi=004220c8
eip=004014ab esp=0019f834 ebp=0019fe74 iopl=0 nv up ei pl nz ac pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000216
SEHTest1!_except_handler3+0x4b:
004014ab 5d pop ebp
接下来又要取dwOuterTryLevel的值了:
0:000> p
eax=00000000 ebx=0019fe64 ecx=00000000 edx=00690000 esi=00000000 edi=004220c8
eip=004014ed esp=0019f83c ebp=0019f854 iopl=0 nv up ei pl zr na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246
SEHTest1!_except_handler3+0x8d:
004014ed 8b348f mov esi,dword ptr [edi+ecx*4] ds:002b:004220c8=ffffffff
这次是-1了,except_handler3函数也到了该返回的时候,返回值是1,即ExceptionContinueSearch。
本次except_handler3函数执行,主要的工作是执行了RaiseExcept函数中外层try-except的pfnFilter,然而“落花有意,流水无情”,pfnFilter返回了0,拒绝对该异常进行处理。
上面已经分析了在RaiseExcept函数中注册的、位于SEH链首的except_handler3的行为,其中包含一个内层的try-finally块和外层的try-except块。其中外层的try-except块的过滤函数拒绝处理该异常,因此该函数返回。系统应用层的SEH机制将继续定位到下一个SEH节点,运行其异常处理函数。由于VC6为所有的SEH异常设置了统一的异常处理函数,即except_handler3,故执行流程又回到except_handler3中,这一次,究竟会是归人还是过客呢?
0:000> g
Breakpoint 6 hit
eax=00000000 ebx=00000000 ecx=00401460 edx=77535050 esi=00000000 edi=00000000
eip=00401460 esp=0019f858 ebp=0019f878 iopl=0 nv up ei pl zr na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246
SEHTest1!_except_handler3:
00401460 55 push ebp
0:000> kb
# ChildEBP RetAddr Args to Child
00 0019f854 77535032 0019f95c 0019fed0 0019f9ac SEHTest1!_except_handler3
01 0019f878 77535004 0019f95c 0019fed0 0019f9ac ntdll!ExecuteHandler2+0x26
02 0019f944 77522936 0019f95c 0019f9ac 0019f95c ntdll!ExecuteHandler+0x24
03 0019f944 00401143 0019f95c 0019f9ac 0019f95c ntdll!KiUserExceptionDispatcher+0x26
04 0019fe74 00401268 00401540 00401540 003ea000 SEHTest1!RaiseExcept+0x43 [C:\SRC\Test\SEHTest1\Main.c @ 30]
05 0019fee0 00401653 00400000 00000000 00694687 SEHTest1!WinMain+0x48 [C:\SRC\Test\SEHTest1\Main.c @ 49]
06 0019ff70 74d6fe09 003ea000 74d6fdf0 0019ffdc SEHTest1!WinMainCRTStartup+0x113 [crt0.c @ 198]
07 0019ff80 7751662d 003ea000 5a14741e 00000000 KERNEL32!BaseThreadInitThunk+0x19
08 0019ffdc 775165fd ffffffff 7753518e 00000000 ntdll!__RtlUserThreadStart+0x2f
09 0019ffec 00000000 00401540 003ea000 00000000 ntdll!_RtlUserThreadStart+0x1b
有了上次的经验,我们就可以不必再全程跟踪一遍了。直接找到当前SEH节点的tryLevel和ScopeTable。其中,tryLevel为1,毕竟WinMain中我们也设置了内外两个try。ScopeTable内容如下:
0:000> dd 00422138
00422138 ffffffff 0040129d 004012a3 00000000
00422148 00000000 00401276
可以看出,序号为1的ScopeTable中是一个try-finally型的,其pfnFilter为0;而序号为0的ScopeTable中是一个try-except型的,它有自己的pfnFilter和pfnHandler。通过分析其函数入口可确定,它们分别对应WinMain中的内外两个try。
首先由于tryLevel为1,取到的pfnFilter为NULL:
0:000> p
eax=0019f84c ebx=0019fed0 ecx=00000003 edx=77535050 esi=00000001 edi=00422138
eip=0040149b esp=0019f83c ebp=0019f854 iopl=0 nv up ei pl nz ac po cy
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000213
SEHTest1!_except_handler3+0x3b:
0040149b 837c8f0400 cmp dword ptr [edi+ecx*4+4],0 ds:002b:00422148=00000000
0:000> p
eax=0019f84c ebx=0019fed0 ecx=00000003 edx=77535050 esi=00000001 edi=00422138
eip=004014ed esp=0019f83c ebp=0019f854 iopl=0 nv up ei pl zr na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246
SEHTest1!_except_handler3+0x8d:
004014ed 8b348f mov esi,dword ptr [edi+ecx*4] ds:002b:00422144=00000000
; esi为取到的tryLevel
因此取其ScopeTable中的dwOuterTryLevel即0作为当前的tryLevel,此时定位到Scopetable[0].pfnFilter
eax=00000001 ebx=0019fed0 ecx=00690000 edx=00690000 esi=00000000 edi=00422138
eip=004014ab esp=0019f834 ebp=0019fee0 iopl=0 nv up ei pl zr na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246
SEHTest1!_except_handler3+0x4b:
004014ab 5d pop ebp
由于该函数返回1即EXCEPTION_EXECUTE_HANDLER,表示自己愿意处理该异常,因此此时要执行global_unwind2函数执行。该函数只是对RtlUnwind的封装而已,关于这个API,我们已经在之前的文章里详细分析过其行为。
0:000> p
eax=00000001 ebx=0019fed0 ecx=00690000 edx=00690000 esi=00000000 edi=00422138
eip=0040137b esp=0019f810 ebp=0019f830 iopl=0 nv up ei pl nz na po nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000202
SEHTest1!_global_unwind2+0x13:
0040137b e842c50000 call SEHTest1!RtlUnwind (0040d8c2)
0:000> dd esp
0019f810 0019fed0 00401380 00000000 00000000
由于传入的TargetFrame为0019fed0,TargetIP为00401380,其余两个参数为NULL,因此RtlUnwind执行的是局部展开(别和上面global_unwind2的名字搞混了,这里指的是对于操作系统用户级SEH机制是局部展开),因此会再次调用位于SEH链首的except_handler3,并在传入的EXCEPTION_RECORD.ExceptionFlags中加上EXCEPTION_UNWINDING标志(该宏为2)。下面验证一下:
0:000> p
Breakpoint 6 hit
eax=00000000 ebx=00000000 ecx=00401460 edx=77535080 esi=00000000 edi=00000000
eip=00401460 esp=0019f410 ebp=0019f430 iopl=0 nv up ei pl zr na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246
SEHTest1!_except_handler3:
00401460 55 push ebp
0:000> kb
# ChildEBP RetAddr Args to Child
00 0019f40c 77535032 0019f4e0 0019fe64 0019f530 SEHTest1!_except_handler3
01 0019f430 77535004 0019f4e0 0019fe64 0019f530 ntdll!ExecuteHandler2+0x26
02 0019f808 00401380 0019fed0 00401380 00000000 ntdll!ExecuteHandler+0x24
03 0019f830 004014bf 0019fed0 0019f854 00000000 SEHTest1!_global_unwind2+0x18
04 0019f854 77535032 0019f95c 0019fed0 0019f9ac SEHTest1!_except_handler3+0x5f
05 0019f878 77535004 0019f95c 0019fed0 0019f9ac ntdll!ExecuteHandler2+0x26
06 0019f944 77522936 0019f95c 0019f9ac 0019f95c ntdll!ExecuteHandler+0x24
07 0019f944 00401143 0019f95c 0019f9ac 0019f95c ntdll!KiUserExceptionDispatcher+0x26
08 0019fe74 00401268 00401540 00401540 003ea000 SEHTest1!RaiseExcept+0x43 [Main.c @ 30]
09 0019fee0 00401653 00400000 00000000 00694687 SEHTest1!WinMain+0x48 [Main.c @ 49]
0a 0019ff70 74d6fe09 003ea000 74d6fdf0 0019ffdc SEHTest1!WinMainCRTStartup+0x113 [crt0.c @ 198]
0b 0019ff80 7751662d 003ea000 5a14741e 00000000 KERNEL32!BaseThreadInitThunk+0x19
0c 0019ffdc 775165fd ffffffff 7753518e 00000000 ntdll!__RtlUserThreadStart+0x2f
0d 0019ffec 00000000 00401540 003ea000 00000000 ntdll!_RtlUserThreadStart+0x1b
0:000> dt _EXCEPTION_RECORD 019f4e0
ntdll!_EXCEPTION_RECORD
+0x000 ExceptionCode : 0n-1073741785
+0x004 ExceptionFlags : 2
+0x008 ExceptionRecord : (null)
+0x00c ExceptionAddress : 0x00401380 Void
+0x010 NumberParameters : 0
+0x014 ExceptionInformation : [15] 0x6c8b60
需要注意这里调用except_handler3时,传入的pSEHNode值为0x0019fe64,即位于链首的SEH节点。这里对应的C伪码如下:
// 若为退出展开,则调用local_unwind2执行局部展开
if (pExceptionRecord->ExceptionFlags & (EXCEPTION_UNWINDING | EXCEPTION_EXIT_UNWIND))
{
local_unwind2(pSEHNode, -1);
result = ExceptionContinueSearch;
}
因此执行流程转到local_unwind2,下面再看其伪码,再次提醒,传入的pSEHNode为0x0019fe64,是RaiseExcept中注册的SEH节点:
// 局部展开(注意与原生SEH的局部展开概念加以区分),以函数为单位,当前的tryLevel为起点,
// 传入的nFinishTryLevel为终点(或达到本函数内最外层的try之外),由内向外执行所有的try-finally中的finally函数
PEXCEPTION_REGISTRATION_RECORD __cdecl local_unwind2(PEXCEPTION_REGISTRATION_RECORD pSEHNode, int nFinishTryLevel)
{
PVC6_EXCEPTION_REGISTRATION_RECORD pVC6SEHNode = CONTAINING_RECORD(pSEHNode, VC6_EXCEPTION_REGISTRATION_RECORD, pSEHNode);
PVC6_SCOPETABLE pScopeTable;
int tryLevel;
while (TRUE)
{
pScopeTable = pVC6SEHNode->pScopeTable;
tryLevel = pVC6SEHNode->dwTryLevel;
if (tryLevel == -1 || tryLevel == nFinishTryLevel)
{
break;
}
pVC6SEHNode->dwTryLevel = pScopeTable[tryLevel].dwOuterLevel;
if (!pScopeTable[tryLevel].pfnFilter) // finally型pfnFilter为NULL
{
NLG_Notify(257);
pScopeTable[tryLevel].pfnHandler(); // 执行finally函数
}
}
return pSEHNode;
}
从VC6_EXCEPTION_REGISTRATION_RECORD中取得的tryLevel为1,因此ScopeTable里的finally型函数都会被执行。在本例中,RaiseExcept中的finally块将被执行。可以概括出,local_unwind2的作用即是执行当前ScopeTable中所有在当前tryLevel之外(即包含当前异常代码)的finally函数。
在执行完global_unwind2后,RaiseExcept中的finally已经执行完了。下面接着执行local_unwind2,但传入的是本节点的SEHNode,因此会把当前tryLevel之上的finally块全部都执行了。
// 执行全局展开
global_unwind2(pSEHNode);
DWORD dwOrgEBP = pVC6SEHNode->dwEBP;
// 栈基址指针切换
_asm
{
mov ebp, dwOrgEBP
}
// 执行局部展开
local_unwind2(pSEHNode, tryLevel);
NLG_Notify(1);
pVC6SEHNode->dwTryLevel = pScopeTable[tryLevel].dwOuterLevel;
// 调用异常处理函数
pScopeTable[tryLevel].pfnHandler();
若某个函数中使用了VC的结构化异常处理机制,则VC编译器会在函数入口和出口分别在栈上安装和卸载一个异常处理节点,该节点在EXCEPTION_REGISTRATION_RECORD的基础上“加了料”,成了VC6_EXCEPTION_REGISTRATION_RECORD(名字是咱自己取的),其中的Handler指向共用的except_handler3函数。
当被try保护的代码出现异常时,本函数内注册的except_handler3首先被执行,通过栈上的tryLevel和pScopeTable,定位到当前的异常处理函数,找到能够处理该异常的try-except。在找到try-except后,通过global_unwind2执行局部展开(相对系统用户层SEH来说只能算局部展开),将SEH链首到当前节点之间注册的SEH的except_handler3逐一进行调用,在except_handler3中检测到传入参数的ExceptRecord中的flags字段EXCEPTION_UNWINDING(2)标志置位后,则以当前tryLevel为起点,在本ScopeTable中依次向上调用各finally块。接着调用local_unwind2对当前层完成的各finally函数进行执行。完成这一切后再执行pfnHandler。