用户态异常处理:SEH 调用链分析(五)

前言

在上一篇《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)
  1. 节点本身必须位于当前线程栈范围内
  2. 节点必须按 4 字节对齐
  3. ExceptionHandler` 不能落在线程栈区间内
  4. 一直扫到链尾 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 分发部分就基本清楚了。

它的后半段核心逻辑可以概括为:

  1. 先获取当前线程的栈边界和 SEH 链表头

  2. 根据 ProcessExecuteFlags 决定采用哪种异常链校验模式

  • 要么先整链预扫

  • 要么在主循环中逐节点校验

  1. 对每个 SEH 节点做合法性检查
  • 节点必须位于当前线程栈上

  • 节点必须对齐

  • handler 不能落在当前线程栈区间内

  • handler 还要通过 RtlIsValidHandler

  1. 通过 RtlpExecuteHandlerForException 真正调用异常处理函数
  2. 根据 handler 返回的 EXCEPTION_DISPOSITION 决定后续动作
  • ContinueExecution:恢复执行

  • ContinueSearch:继续找下一层 SEH

  • NestedException:记录嵌套异常状态

  • 其它返回值:抛出 STATUS_INVALID_DISPOSITION

  1. 退出前统一调用 RtlCallVectoredContinueHandlers

因此,从整体上看,RtlDispatchException 在用户态完成的是一套非常完整的异常派发流程:

c 复制代码
VEH 进程级分发
    ↓
SEH 线程级栈链遍历
    ↓
VCH 收尾通知

这也说明,Windows 用户态异常分发并不是"找到一个 handler 调一下"这么简单,

而是在真正调用 SEH 处理函数之前,已经做了大量针对:

  • 栈边界

  • 链表结构

  • handler 地址

  • 执行策略

  • 返回值语义

的安全检查与一致性维护。

相关推荐
love530love2 小时前
Duix-Avatar 去 Docker Desktop 本地化完整复盘
人工智能·pytorch·windows·python·docker·容器·数字人
skywalk81632 小时前
iwr -useb https://openclaw.ai/install.ps1 | iex 这里的iwr怎么安装?
windows
浩瀚之水_csdn2 小时前
++ Lambda 表达式详解
java·jvm·windows
Anesthesia丶2 小时前
Windows WSL子系统设置独立IP访问
windows·网络协议·tcp/ip
weixin_531651813 小时前
Python 渐进式学习指南
开发语言·windows·python
夏日听雨眠3 小时前
文件学习终
windows·学习
无限进步_3 小时前
深入解析C++容器适配器:stack、queue与deque的实现与应用
linux·开发语言·c++·windows·git·github·visual studio
初圣魔门首席弟子4 小时前
bug2026.03.18
linux·服务器·windows
阿富软件园4 小时前
绿色便携免安装,双击即用零门槛排版预览一步到位照片排版工具
windows·电脑·开源软件