前言
在上一篇《VEH 的派发》中,我们已经分析了用户态异常分发入口 ntdll!KiUserExceptionDispatcher,以及 RtlDispatchException 在处理异常时对 VEH 的调度流程。
我们知道,当内核将异常转交到用户态后,执行流会进入:
cpp
ntdll!KiUserExceptionDispatcher
↓
RtlDispatchException
其中,RtlDispatchException 是用户态异常处理的总入口。
在上一篇中,我们重点分析了它的第一阶段流程,也就是:
-
调用 RtlCallVectoredExceptionHandlers
-
进入 RtlpCallVectoredHandlers
-
遍历并调用进程中注册的 VEH 回调函数
但这只是 RtlDispatchException 的前半部分逻辑。
如果 VEH 没有处理当前异常,那么 RtlDispatchException 并不会立即返回失败,而是会继续进入第二阶段:
遍历当前线程上的 SEH链表,依次调用每一层异常处理程序。
这也说明:
-
VEH 是进程级的异常处理机制
-
SEH 是线程级、栈上的异常处理机制
在 Windows x86 下,SEH 链表通过 FS:[0] 进行组织,本质上是一条位于线程栈上的单向链表。
每当程序使用 __try / __except时,编译器都会在栈上构造相应的异常注册记录,并将其挂入这条链中。
本篇文章中,我们就继续沿着上一篇 RtlDispatchException 的分析往下走,重点研究它的 SEH 分发部分
RtlDispatchException完整反汇编:
c
.text:77EDF89C ; __stdcall RtlDispatchException(x, x)
.text:77EDF89C _RtlDispatchException@8 proc near ; CODE XREF: KiUserExceptionDispatcher(x,x)+A↓p
.text:77EDF89C
.text:77EDF89C ExceptionRecord = EXCEPTION_RECORD ptr -64h
.text:77EDF89C var_14 = dword ptr -14h
.text:77EDF89C ProcessInformation= dword ptr -10h
.text:77EDF89C StackBase = dword ptr -0Ch
.text:77EDF89C StackBaseLimit = dword ptr -8
.text:77EDF89C isHandler = byte ptr -1
.text:77EDF89C EXCEPTION_RECORD= dword ptr 8
.text:77EDF89C CONTEXT = dword ptr 0Ch
.text:77EDF89C
.text:77EDF89C ; FUNCTION CHUNK AT .text:77EC6019 SIZE 00000021 BYTES
.text:77EDF89C ; FUNCTION CHUNK AT .text:77EDE855 SIZE 00000058 BYTES
.text:77EDF89C ; FUNCTION CHUNK AT .text:77F3A882 SIZE 00000014 BYTES
.text:77EDF89C ; FUNCTION CHUNK AT .text:77F3AAD5 SIZE 0000001C BYTES
.text:77EDF89C ; FUNCTION CHUNK AT .text:77F504A8 SIZE 0000005D BYTES
.text:77EDF89C
.text:77EDF89C mov edi, edi
.text:77EDF89E push ebp
.text:77EDF89F mov ebp, esp
.text:77EDF8A1 sub esp, 64h
.text:77EDF8A4 push esi ; 不是参数,备份
.text:77EDF8A5 push [ebp+CONTEXT]
.text:77EDF8A8 mov esi, [ebp+EXCEPTION_RECORD]
.text:77EDF8AB push esi
.text:77EDF8AC mov [ebp+isHandler], 0
.text:77EDF8B0 call _RtlCallVectoredExceptionHandlers@8 ; 处理veh
.text:77EDF8B5 test al, al
.text:77EDF8B7 jnz loc_77F504A8 ; 处理成功跳转返回
.text:77EDF8BD push ebx
.text:77EDF8BE push edi ; 不是参数,保存寄存器
.text:77EDF8BF lea eax, [ebp+StackBase]
.text:77EDF8C2 push eax
.text:77EDF8C3 lea eax, [ebp+StackBaseLimit]
.text:77EDF8C6 push eax
.text:77EDF8C7 call _RtlpGetStackLimits@8 ; 获取当前线程的栈顶和栈底
.text:77EDF8CC call _RtlpGetRegistrationHead@0 ; 获取当前线程SEH异常处理链表的头指针。
.text:77EDF8CC ; eax =SEH异常处理链表的头指针
.text:77EDF8D1 and [ebp+ProcessInformation], 0
.text:77EDF8D5 push 0 ; ReturnLength 实际返回长度(可选)
.text:77EDF8D7 push 4 ; ProcessInformationLength 输出缓冲区大小
.text:77EDF8D9 mov ebx, eax ; eax=SEH异常处理链表的头指针
.text:77EDF8DB lea eax, [ebp+ProcessInformation]
.text:77EDF8DE push eax ; ProcessInformation 输出缓冲区
.text:77EDF8DF push 22h ; '"' ; ProcessInformationClass 要查询的信息类型
.text:77EDF8DF ; 0x22是ProcessExecuteFlags
.text:77EDF8E1 or edi, 0FFFFFFFFh
.text:77EDF8E4 push edi ; ProcessHandle 进程句柄
.text:77EDF8E5 mov byte ptr [ebp+EXCEPTION_RECORD+3], 1
.text:77EDF8E9 call _ZwQueryInformationProcess@20 ; 查询EPROCESS->Pcb.ExecuteOptions,
.text:77EDF8E9 ; 存到ProcessInformation
.text:77EDF8EE test eax, eax
.text:77EDF8F0 jl loc_77EDE8A3 ; 查询失败跳转
.text:77EDF8F6 test byte ptr [ebp+ProcessInformation], 40h ; ExecuteOptions.DisableExceptionChainValidation
.text:77EDF8F6 ; 如果此位是1表示:关闭 SEH 异常链合法性校验
.text:77EDF8FA jz loc_77EDE8A3 ; 如果此位是0就跳转
.text:77EDF900 mov byte ptr [ebp+EXCEPTION_RECORD+3], 0 ; 参数槽作为临时变量
.text:77EDF900 ; SEH异常链合法性校验开关
.text:77EDF904
.text:77EDF904 loc_77EDF904: ; CODE XREF: RtlDispatchException(x,x)-FF7↑j
.text:77EDF904 call _RtlpGetRegistrationHead@0 ; 获取当前线程SEH异常处理链表的头指针。
.text:77EDF904 ; eax =_TEB.NtTib.ExceptionList
.text:77EDF909 mov ebx, eax
.text:77EDF90B xor edi, edi
.text:77EDF90D
.text:77EDF90D loc_77EDF90D: ; CODE XREF: RtlDispatchException(x,x)+F8↓j
.text:77EDF90D cmp ebx, 0FFFFFFFFh ; 判断链表是否为空
.text:77EDF910 jz loc_77EC6027 ; 如果为空跳转返回
.text:77EDF916 cmp byte ptr [ebp+EXCEPTION_RECORD+3], 0
.text:77EDF91A jnz short loc_77EDF94B ; SEH异常链合法性校验开关位,为1跳转
.text:77EDF91C cmp ebx, [ebp+StackBaseLimit] ; 判断seh链表指针是否小于栈顶
.text:77EDF91F jb loc_77F3A88D ; 如果小于栈顶,非法seh,说明结构不在栈范围内
.text:77EDF91F ; 跳转返回
.text:77EDF925 lea eax, [ebx+8] ; 因为seh链表大小是8字节,
.text:77EDF925 ; 所以eax是这个seh链表的结束地址
.text:77EDF928 cmp eax, [ebp+StackBase] ; 判断seh链表的结束地址是否大于栈底
.text:77EDF92B ja loc_77F3A88D ; 如果大于栈底,非法seh,说明结构不在栈范围内
.text:77EDF92B ; 跳转返回
.text:77EDF931 test bl, 3 ; 判断是否对齐
.text:77EDF934 jnz loc_77F3A88D ; 没对齐,跳转返回
.text:77EDF93A mov eax, [ebx+_EH3_EXCEPTION_REGISTRATION.ExceptionHandler]
.text:77EDF93D cmp eax, [ebp+StackBaseLimit] ; 判断seh异常处理函数地址是否小于栈顶
.text:77EDF940 jb short loc_77EDF94B ; 小于跳转,小于说明seh异常处理函数在栈外合法
.text:77EDF942 cmp eax, [ebp+StackBase] ; 判断seh异常处理函数地址是否小于栈底
.text:77EDF945 jb loc_77F3A88D ; 小于跳转,小于说明seh异常函数在栈内,不合法
.text:77EDF94B
.text:77EDF94B loc_77EDF94B: ; CODE XREF: RtlDispatchException(x,x)+7E↑j
.text:77EDF94B ; RtlDispatchException(x,x)+A4↑j
.text:77EDF94B push [ebp+ProcessInformation]
.text:77EDF94E push [ebx+_EH3_EXCEPTION_REGISTRATION.ExceptionHandler] ; handler
.text:77EDF951 call _RtlIsValidHandler@8 ; 判断seh异常处理函数合法性
.text:77EDF956 test al, al
.text:77EDF958 jz loc_77F3A88D ; 不合法走返回路径
.text:77EDF95E push [ebx+_EH3_EXCEPTION_REGISTRATION.ExceptionHandler] ; handler
.text:77EDF961 lea eax, [ebp+var_14]
.text:77EDF964 push eax
.text:77EDF965 push [ebp+CONTEXT] ; CONTEXT
.text:77EDF968 push ebx ; _EXCEPTION_REGISTRATION
.text:77EDF969 push esi ; ExceptionRecord
.text:77EDF96A call _RtlpExecuteHandlerForException@20 ; 执行handler
.text:77EDF96F cmp edi, ebx ; 判断seh链表指针是否为0
.text:77EDF971 jz loc_77F3A882
.text:77EDF977
.text:77EDF977 loc_77EDF977: ; CODE XREF: RtlDispatchException(x,x)+5AFEC↓j
.text:77EDF977 xor ecx, ecx
.text:77EDF979 sub eax, ecx
.text:77EDF97B jz loc_77EC6019 ; 如果返回值为0跳转
.text:77EDF981 dec eax
.text:77EDF982 jnz loc_77F3AAD5
.text:77EDF988
.text:77EDF988 loc_77EDF988: ; CODE XREF: RtlDispatchException(x,x)+70C64↓j
.text:77EDF988 test byte ptr [esi+EXCEPTION_RECORD.ExceptionFlags], 8
.text:77EDF98C jnz loc_77EC6027
.text:77EDF992
.text:77EDF992 loc_77EDF992: ; CODE XREF: RtlDispatchException(x,x)+5B247↓j
.text:77EDF992 ; RtlDispatchException(x,x)+5B250↓j ...
.text:77EDF992 mov ebx, [ebx+_EH3_EXCEPTION_REGISTRATION.Next]
.text:77EDF994 jmp loc_77EDF90D
.text:77EDF994 _RtlDispatchException@8 endp
一、从 VEH 阶段进入 SEH 阶段
在上一篇中我们已经知道,RtlDispatchException 一开始会先调用 VEH:
c
RtlCallVectoredExceptionHandlers(ExceptionRecord, ContextRecord)
对应反汇编就是:
c
.text:77EDF8A5 push [ebp+CONTEXT]
.text:77EDF8A8 mov esi, [ebp+EXCEPTION_RECORD]
.text:77EDF8AB push esi
.text:77EDF8AC mov [ebp+isHandler], 0
.text:77EDF8B0 call _RtlCallVectoredExceptionHandlers@8 ; 处理veh
.text:77EDF8B5 test al, al
.text:77EDF8B7 jnz loc_77F504A8 ; 处理成功跳转返回
这里的逻辑非常直接:
-
如果 VEH 返回非 0,说明异常已经被处理
-
那么
RtlDispatchException直接走成功返回路径 -
如果 VEH 返回 0,说明 VEH 没处理当前异常
-
此时才继续进入 SEH 分发阶段
也就是说,SEH 的遍历,是建立在 VEH 未处理该异常 的前提下进行的。
二、获取当前线程的栈范围与 SEH 链表头
当 VEH 未处理异常后,函数先做了两件事:
c
.text:77EDF8BF lea eax, [ebp+StackBase]
.text:77EDF8C2 push eax
.text:77EDF8C3 lea eax, [ebp+StackBaseLimit]
.text:77EDF8C6 push eax
.text:77EDF8C7 call _RtlpGetStackLimits@8 ; 获取当前线程的栈顶和栈底
.text:77EDF8CC call _RtlpGetRegistrationHead@0 ; 获取当前线程SEH异常处理链表的头指针。
这里分别对应:
1. RtlpGetStackLimits
用于获取当前线程栈的边界:
-
StackBase:栈的高地址端 -
StackLimit:栈的低地址端
在 x86 下,栈是向低地址增长的,所以:
c
StackLimit <= 栈上对象地址 < StackBase
后面所有对 SEH 节点合法性的判断,基本都依赖这两个边界。
2. RtlpGetRegistrationHead
用于获取当前线程异常注册链表头。
在 Windows x86 中,它本质上就是:
c
FS:[0]
也就是当前线程 TEB->NtTib.ExceptionList。
所以这里拿到的 eax,就是 SEH 单向链表的头节点:
c
.text:77EDF8D9 mov ebx, eax
此后 ebx 就一直作为当前正在遍历的异常注册记录指针。
三、查询 ProcessExecuteFlags:决定如何校验异常链
c
.text:77EDF8D1 and [ebp+ProcessInformation], 0
.text:77EDF8D5 push 0 ; ReturnLength 实际返回长度(可选)
.text:77EDF8D7 push 4 ; ProcessInformationLength 输出缓冲区大小
.text:77EDF8D9 mov ebx, eax ; eax=SEH异常处理链表的头指针
.text:77EDF8DB lea eax, [ebp+ProcessInformation]
.text:77EDF8DE push eax ; ProcessInformation 输出缓冲区
.text:77EDF8DF push 22h ; '"' ; ProcessInformationClass 要查询的信息类型
.text:77EDF8DF ; 0x22是ProcessExecuteFlags
.text:77EDF8E1 or edi, 0FFFFFFFFh
.text:77EDF8E4 push edi ; ProcessHandle 进程句柄
.text:77EDF8E5 mov byte ptr [ebp+EXCEPTION_RECORD+3], 1
.text:77EDF8E9 call _ZwQueryInformationProcess@20 ; 查询EPROCESS->Pcb.ExecuteOptions,
.text:77EDF8E9 ; 存到ProcessInformation
.text:77EDF8EE test eax, eax
.text:77EDF8F0 jl loc_77EDE8A3 ; 查询失败跳转
.text:77EDF8F6 test byte ptr [ebp+ProcessInformation], 40h ; ExecuteOptions.DisableExceptionChainValidation
.text:77EDF8F6 ; 如果此位是1表示:关闭 SEH 异常链合法性校验
.text:77EDF8FA jz loc_77EDE8A3 ; 如果此位是0就跳转
.text:77EDF900 mov byte ptr [ebp+EXCEPTION_RECORD+3], 0 ; 参数槽作为临时变量
.text:77EDF900 ; SEH异常链合法性校验开关
这里调用的是:
c
ZwQueryInformationProcess(
NtCurrentProcess(),
ProcessExecuteFlags, // 0x22
&ProcessInformation,
4,
NULL
);
查询的是查询EPROCESS->Pcb.ExecuteOptions那一套标志位。
其中这里最关心的是:
c
0x40 = DisableExceptionChainValidation
也就是:
是否关闭"异常链整链合法性校验"。
这里那个 [ebp+EXCEPTION_RECORD+3] 是什么?
c
.text:77EDF900 mov byte ptr [ebp+EXCEPTION_RECORD+3], 0 ; 参数槽作为临时变量
注意:
这里 不是在修改真正的 EXCEPTION_RECORD结构体,而是在把 第一个参数槽位的最高字节当成临时变量使用。
因为函数一开始就已经把真正的参数取到了寄存器里:
c
.text:77EDF8A8 mov esi, [ebp+EXCEPTION_RECORD]
之后原来的参数槽就不再需要了,于是 ntdll 偷了一个字节出来当临时标志位。
它的含义不是"是否完全不校验",而更接近于:
-
1:这次不需要在主循环里重复做节点范围校验
-
0:在主循环里边走边校验当前节点
这一点很重要,因为它后面会影响主循环里的分支。
四、两种 SEH 校验模式
这段代码其实实现了两种不同的校验策略。
模式一:整条链预校验
如果:
-
ZwQueryInformationProcess查询失败,或者 -
DisableExceptionChainValidation没有置位
那么就走这条路径:
c
.text:77EDE8A3 cmp ebx, edi ; 判断链表是否为空
.text:77EDE8A5 jz loc_77EDF904 ; 链表为空跳转
.text:77EDE8AB jmp short loc_77EDE855
进入:
c
loc_77EDE855
这是一段 对整条 SEH 链进行预扫描 的逻辑。
模式二:边遍历边校验
如果 DisableExceptionChainValidation == 1,则不会先整链预扫,而是:
c
mov byte ptr [ebp+EXCEPTION_RECORD+3], 0
然后直接进入主循环,在遍历每一个节点前再对当前节点单独做检查。
所以准确地说:
-
DisableExceptionChainValidation = 0→ 先把整条链合法性检查一遍
→ 主循环里就不再重复做边界检查
-
DisableExceptionChainValidation = 1→ 不做整链预扫描
→ 主循环里逐节点检查
这也是为什么那个临时标志位最后反而会被置 0,而不是 1
五、整链预校验逻辑:loc_77EDE855
c
.text:77EDE855 ; START OF FUNCTION CHUNK FOR RtlDispatchException(x,x)
.text:77EDE855
.text:77EDE855 loc_77EDE855: ; CODE XREF: RtlDispatchException(x,x)-1014↓j
.text:77EDE855 ; RtlDispatchException(x,x)-FF1↓j
.text:77EDE855 cmp ebx, [ebp+StackBaseLimit] ; 判断seh链表是否小于栈顶
.text:77EDE858 jb loc_77F3A88D ; 小于跳转,表示seh链表在堆栈外,不合法
.text:77EDE85E lea eax, [ebx+8]
.text:77EDE861 cmp eax, [ebp+StackBase] ; 判断这个链表结束地址是否大于栈底
.text:77EDE864 ja loc_77F3A88D ; 大于跳转,表示seh链表在堆栈外,不合法
.text:77EDE86A test bl, 3 ; 判断是否4字节对齐
.text:77EDE86D jnz loc_77F3A88D ; 未对齐跳转
.text:77EDE873 mov eax, [ebx+4] ; eax=handler
.text:77EDE876 cmp eax, [ebp+StackBaseLimit] ; 判断handler是否小于栈顶
.text:77EDE879 jb short loc_77EDE884 ; 小于跳转,小于合法,handler必须在栈外
.text:77EDE87B cmp eax, [ebp+StackBase] ; 判断handler是否小于栈底
.text:77EDE87E jb loc_77F3A88D ; 小于跳转,小于不合法,handler在站内
.text:77EDE884
.text:77EDE884 loc_77EDE884: ; CODE XREF: RtlDispatchException(x,x)-1023↑j
.text:77EDE884 mov ebx, [ebx] ; 继续扫下一个节点
.text:77EDE886 cmp ebx, edi ; 判断链表的next是否==0xffffffff
.text:77EDE886 ; 说明是最后一个空链表
.text:77EDE886 ; 只要不为空就继续循环
.text:77EDE888 jnz short loc_77EDE855 ; 是有效链表跳转
.text:77EDE88A mov ecx, large fs:_TEB.NtTib.Self
.text:77EDE891 mov edx, 200h
.text:77EDE896 test [ecx+_TEB.___u74.SameTebFlags], dx ; RtlExceptionAttached位
.text:77EDE89D jnz loc_77F504B1
.text:77EDE8A3
.text:77EDE8A3 loc_77EDE8A3: ; CODE XREF: RtlDispatchException(x,x)+54↓j
.text:77EDE8A3 ; RtlDispatchException(x,x)+5E↓j ...
.text:77EDE8A3 cmp ebx, edi ; 判断链表是否为空
.text:77EDE8A5 jz loc_77EDF904 ; 链表为空跳转
.text:77EDE8AB jmp short loc_77EDE855
.text:77EDE8AB ; END OF FUNCTION CHUNK FOR RtlDispatchException(x,x)
- 节点本身必须位于当前线程栈范围内
- 节点必须按 4 字节对齐
- ExceptionHandler` 不能落在线程栈区间内
- 一直扫到链尾
0xFFFFFFFF
六、重新取链表头,正式进入 SEH 分发主循环
无论是整链预校验完成,还是决定边走边校验,最后都会来到这里:
c
.text:77EDF904 loc_77EDF904: ; CODE XREF: RtlDispatchException(x,x)-FF7↑j
.text:77EDF904 call _RtlpGetRegistrationHead@0 ; 获取当前线程SEH异常处理链表的头指针。
.text:77EDF904 ; eax =_TEB.NtTib.ExceptionList
.text:77EDF909 mov ebx, eax
.text:77EDF90B xor edi, edi
注意这里有个细节:
前面 edi 还是 0xFFFFFFFF,表示 SEH 链尾哨兵值。
但到了这里:
c
xor edi, edi
edi 被重新清零了。
也就是说:
-
在前半段,edi 用来表示链表结束值 -1
-
在主循环里,edi 不再表示链尾,而是开始承担 嵌套异常相关的跟踪变量 的角色
七、主循环:逐个遍历 SEH 节点
主循环入口:
c
.text:77EDF90D loc_77EDF90D: ;
.text:77EDF90D cmp ebx, 0FFFFFFFFh ; 判断链表是否为空
.text:77EDF910 jz loc_77EC6027 ; 如果为空跳转返回
如果已经走到链尾,说明没有任何 SEH 处理成功,直接走未处理返回路径。
否则就处理当前节点。
1. 是否需要对当前节点再做一次边界检查
c
.text:77EDF916 cmp byte ptr [ebp+EXCEPTION_RECORD+3], 0
.text:77EDF91A jnz short loc_77EDF94B ; SEH异常链合法性校验开关位,为1跳转
如果这个临时标志位不是 0,直接跳过这段节点边界检查。
也就是:
- 前面做过"整链预校验"
→ 这里不重复检查
如果标志位为 0,就说明这次走的是"边遍历边校验"模式,于是继续做与前面几乎相同的检查:
c
.text:77EDF91C cmp ebx, [ebp+StackBaseLimit] ; 判断seh链表指针是否小于栈顶
.text:77EDF91F jb loc_77F3A88D ; 如果小于栈顶,非法seh,说明结构不在栈范围内
.text:77EDF91F ; 跳转返回
.text:77EDF925 lea eax, [ebx+8] ; 因为seh链表大小是8字节,
.text:77EDF925 ; 所以eax是这个seh链表的结束地址
.text:77EDF928 cmp eax, [ebp+StackBase] ; 判断seh链表的结束地址是否大于栈底
.text:77EDF92B ja loc_77F3A88D ; 如果大于栈底,非法seh,说明结构不在栈范围内
.text:77EDF92B ; 跳转返回
.text:77EDF931 test bl, 3 ; 判断是否对齐
.text:77EDF934 jnz loc_77F3A88D ; 没对齐,跳转返回
.text:77EDF93A mov eax, [ebx+_EH3_EXCEPTION_REGISTRATION.ExceptionHandler]
.text:77EDF93D cmp eax, [ebp+StackBaseLimit] ; 判断seh异常处理函数地址是否小于栈顶
.text:77EDF940 jb short loc_77EDF94B ; 小于跳转,小于说明seh异常处理函数在栈外合法
.text:77EDF942 cmp eax, [ebp+StackBase] ; 判断seh异常处理函数地址是否小于栈底
.text:77EDF945 jb loc_77F3A88D ; 小于跳转,小于说明seh异常函数在栈内,不合法
逻辑和前面的整链扫描完全一致。
八、RtlIsValidHandler:对 handler 做进一步合法性验证
通过栈范围与对齐检查后,并不会马上调用处理函数,而是还要再过一层:
c
.text:77EDF94B push [ebp+ProcessInformation]
.text:77EDF94E push [ebx+_EH3_EXCEPTION_REGISTRATION.ExceptionHandler] ; handler
.text:77EDF951 call _RtlIsValidHandler@8 ; 判断seh异常处理函数合法性
.text:77EDF956 test al, al
.text:77EDF958 jz loc_77F3A88D ; 不合法走返回路径
也就是说,handler 还要通过 RtlIsValidHandler 的检查。
这说明 RtlDispatchException 对 SEH 处理函数的合法性判断,并不只是:
-
结构在不在栈上
-
地址对不对齐
-
handler 在不在栈里
它还会进一步调用 RtlIsValidHandler 做更深一层的合法性判定。
至于 RtlIsValidHandler内部,通常还会涉及映像、执行选项、异常处理策略等检查。
这里从调用方式看,它还拿到了前面查询的 ProcessExecuteFlags。
所以可以理解为:
前面的检查是"栈结构合法性检查",
而 RtlIsValidHandler 更像是"handler 目标地址本身是否允许作为异常处理函数"的策略检查。
九、真正执行处理函数:RtlpExecuteHandlerForException
当一个 SEH 节点通过所有检查后,才会真正调用它:
c
.text:77EDF95E push [ebx+_EH3_EXCEPTION_REGISTRATION.ExceptionHandler] ; handler
.text:77EDF961 lea eax, [ebp+var_14]
.text:77EDF964 push eax
.text:77EDF965 push [ebp+CONTEXT] ; CONTEXT
.text:77EDF968 push ebx ; _EXCEPTION_REGISTRATION
.text:77EDF969 push esi ; ExceptionRecord
.text:77EDF96A call _RtlpExecuteHandlerForException@20 ; 执行handler
对应逻辑大致可以理解为:
c
RtlpExecuteHandlerForException(
ExceptionRecord,
RegistrationFrame,
ContextRecord,
&DispatcherContextLikeValue,
Handler
);
这里有几个点要注意:
它不是直接 call handler
ntdll 这里没有直接跳过去,而是通过RtlpExecuteHandlerForException 包了一层。
这说明:
系统在真正调用异常处理函数前,还要做一层统一包装。
这层包装通常用于:
-
构造标准调用环境
-
调用 handler
-
处理 handler 自己在执行过程中再次引发异常的情况
-
记录一些 dispatcher 侧需要的附加信息
十、SEH 处理函数的返回值分派
RtlpExecuteHandlerForException 返回后,eax 会保存 handler 的返回值。
这里本质上就是在处理 EXCEPTION_DISPOSITION。
对应反汇编:
c
.text:77EDF977 xor ecx, ecx
.text:77EDF979 sub eax, ecx
.text:77EDF97B jz loc_77EC6019 ; 如果返回值为0跳转
.text:77EDF981 dec eax
.text:77EDF982 jnz loc_77F3AAD5
.text:77EDF988
这段可以对应为:
-
eax == 0 → ExceptionContinueExecution
-
eax == 1 → ExceptionContinueSearch
-
eax >= 2 → 继续后面的特殊分支处理
也就是经典的 EXCEPTION_DISPOSITION 语义。
十一、 统一退出路径:无论成功还是失败,都会调用 VCH
RtlDispatchException 的所有返回路径,最后基本都会汇聚到这里:
c
.text:77EC6029 loc_77EC6029: ;
.text:77EC6029 push [ebp+CONTEXT]
.text:77EC602C push esi
.text:77EC602D call _RtlCallVectoredContinueHandlers@8 ; RtlCallVectoredContinueHandlers(x,x)
.text:77EC6032 mov al, [ebp+isHandler]
.text:77EC6035 pop esi
.text:77EC6036 leave
.text:77EC6037 retn 8
也就是说:
无论这次异常最终有没有被 VEH / SEH 处理掉,
RtlDispatchException 在返回前都会调用一次
RtlCallVectoredContinueHandlers。
c
VEH
↓
SEH
↓
VCH
↓
return
十二、总结
到这里,RtlDispatchException 的 SEH 分发部分就基本清楚了。
它的后半段核心逻辑可以概括为:
-
先获取当前线程的栈边界和 SEH 链表头
-
根据
ProcessExecuteFlags决定采用哪种异常链校验模式
-
要么先整链预扫
-
要么在主循环中逐节点校验
- 对每个 SEH 节点做合法性检查
-
节点必须位于当前线程栈上
-
节点必须对齐
-
handler 不能落在当前线程栈区间内
-
handler 还要通过
RtlIsValidHandler
- 通过
RtlpExecuteHandlerForException真正调用异常处理函数 - 根据
handler返回的EXCEPTION_DISPOSITION决定后续动作
-
ContinueExecution:恢复执行 -
ContinueSearch:继续找下一层 SEH -
NestedException:记录嵌套异常状态 -
其它返回值:抛出
STATUS_INVALID_DISPOSITION
- 退出前统一调用
RtlCallVectoredContinueHandlers
因此,从整体上看,RtlDispatchException 在用户态完成的是一套非常完整的异常派发流程:
c
VEH 进程级分发
↓
SEH 线程级栈链遍历
↓
VCH 收尾通知
这也说明,Windows 用户态异常分发并不是"找到一个 handler 调一下"这么简单,
而是在真正调用 SEH 处理函数之前,已经做了大量针对:
-
栈边界
-
链表结构
-
handler 地址
-
执行策略
-
返回值语义
的安全检查与一致性维护。