前言
在之前的文章 中,我们已经分析了:
-
内核如何在 KiDeliverApc 中识别用户 APC
-
如何调用 KiInitializeUserApc
-
以及它如何修改 TrapFrame 与用户栈,为用户 APC 的执行提前"铺好路"
但需要特别强调的是:
KiInitializeUserApc 并不执行用户 APC。
它只是为"下一次返回用户态"布置好执行环境。
真正执行用户 APC 的地方,发生在 Ring3,并且位于 ntdll.dll 中。
本文将从线程返回用户态开始,完整分析
用户 APC 是如何在 Ring3 中被执行,又如何返回原执行流的。
一、从内核返回用户态:执行入口已经被篡改
回顾上一篇文章KiInitializeUserApc 在用户栈上写入:
-
NormalRoutine
-
NormalContext
-
SystemArgument1
-
SystemArgument2
-
以及一整份 _CONTEXT 结构

下面结合KiUserApcDispatcher汇编代码,对这些步骤逐一说明:
cpp
.text:77F06F58 ; =============== S U B R O U T I N E =======================================
.text:77F06F58
.text:77F06F58 ; Attributes: noreturn
.text:77F06F58
.text:77F06F58 ; __stdcall KiUserApcDispatcher(x, x, x, x)
.text:77F06F58 public _KiUserApcDispatcher@16
.text:77F06F58 _KiUserApcDispatcher@16 proc near ; DATA XREF: .text:off_77EF61B8↑o
.text:77F06F58
.text:77F06F58 arg_C = byte ptr 10h
.text:77F06F58 arg_2D8 = byte ptr 2DCh
.text:77F06F58
.text:77F06F58 lea eax, [esp+2DCh]
.text:77F06F5F mov ecx, large fs:_TEB ; ecx = TEB(准确说是把 TEB 基址拿出来备用)
.text:77F06F66 mov edx, offset _KiUserApcExceptionHandler@16 ; edx = KiUserApcExceptionHandler 的地址
.text:77F06F66 ; 这是用户 APC 执行期间专用的异常处理函数(SEH handler)
.text:77F06F6B mov [eax], ecx ; 构造一个 SEH 节点
.text:77F06F6B ; 写入 Next 指针
.text:77F06F6D mov [eax+4], edx ; 写入 Handler = KiUserApcExceptionHandle
.text:77F06F70 mov large fs:0, eax ; fs:[0] 就是 SEH 异常链表头(NT_TIB.ExceptionList)
.text:77F06F70 ; 这句就是:把我们新构造的异常帧挂到 fs:[0] 上
.text:77F06F76 pop eax ; eax = NormalRoutine
.text:77F06F77 lea edi, [esp+0Ch] ; edi 指向 Context 结构
.text:77F06F7B call eax ; 直接调用 NormalRoutine
.text:77F06F7D mov ecx, [edi+2CCh] ; ecx = Context.ExceptionList
.text:77F06F83 mov large fs:0, ecx ; 恢复异常链
.text:77F06F8A push 1 ; TestAlert
.text:77F06F8C push edi ; CONTEXT
.text:77F06F8D call _ZwContinue@8 ; ZwContinue(x,x)
.text:77F06F92 mov esi, eax
.text:77F06F94
.text:77F06F94 loc_77F06F94: ; CODE XREF: .text:77F06F9A↓j
.text:77F06F94 push esi
.text:77F06F95 call _RtlRaiseStatus@4 ; RtlRaiseStatus(x)
.text:77F06F95 _KiUserApcDispatcher@16 endp ; sp-analysis failed
二、KiUserApcDispatcher 的第一步:建立异常保护
cpp
.text:77F06F58 lea eax, [esp+2DCh]
.text:77F06F5F mov ecx, large fs:_TEB ; ecx = TEB(准确说是把 TEB 基址拿出来备用)
.text:77F06F66 mov edx, offset _KiUserApcExceptionHandler@16 ; edx = KiUserApcExceptionHandler 的地址
.text:77F06F66 ; 这是用户 APC 执行期间专用的异常处理函数(SEH handler)
.text:77F06F6B mov [eax], ecx ; 构造一个 SEH 节点
.text:77F06F6B ; 写入 Next 指针
.text:77F06F6D mov [eax+4], edx ; 写入 Handler = KiUserApcExceptionHandle
.text:77F06F70 mov large fs:0, eax ; fs:[0] 就是 SEH 异常链表头(NT_TIB.ExceptionList)
.text:77F06F70 ; 这句就是:把我们新构造的异常帧挂到 fs:[0] 上
KiUserApcDispatcher 进入后,做的第一件事并不是执行用户回调
而是:
在用户栈上临时构造一个 SEH 异常处理节点
并将其挂入当前线程的异常链表(fs:[0])。
该异常处理器专用于本次 APC 回调, 用于捕获 NormalRoutine 执行过程中可能发生的异常,
防止异常直接破坏线程的原始执行上下文。
在 NormalRoutine 执行结束后, 该异常节点会被立即移除, 不影响线程后续的异常处理行为。
关于用户态 APC 回调期间的异常处理机制,
本文不再展开,
其设计目的仅在于保证 APC 回调对线程执行流的透明性。
此阶段后用户栈结构:

三、执行用户 APC:调用 NormalRoutine
cpp
.text:77F06F76 pop eax ; eax = NormalRoutine
此行代码执行后堆栈结构如下:

cpp
.text:77F06F7B call eax ; 直接调用 NormalRoutine
此行代码执行后堆栈结构如下:

可以看出:
- 在 KiInitializeUserApc 中,内核已经按固定布局把参数巧妙的写到了用户栈
- 栈布局本身就是为 KiUserApcDispatcher 量身定制的
至此,用户 APC 的 NormalRoutine 正式在 Ring3 中执行。
四、用户 APC 执行完后:并不会"直接回去"
这是用户 APC 机制中最容易被误解的一点。
当 NormalRoutine 执行结束后:
线程并不会直接回到原来的用户代码。
原因是:
-
当前的寄存器状态
-
当前的栈指针
-
当前的执行位置
全部都是为了 APC 临时构造的
必须有一个步骤:
把执行现场恢复成"APC 发生前"的样子
五、恢复异常链,ZwContinue:再次进入内核
cpp
.text:77F06F7D mov ecx, [edi+2CCh] ; ecx = Context.ExceptionList
.text:77F06F83 mov large fs:0, ecx ; 恢复异常链
.text:77F06F8A push 1 ; TestAlert
.text:77F06F8C push edi ; CONTEXT
.text:77F06F8D call _ZwContinue@8 ; ZwContinue(x,x)
这里传入的 _CONTEXT,正是:
当初在 KiInitializeUserApc 中,由内核复制到用户栈上的那一份。
这一步发生了什么?
-
ZwContinue 触发
系统调用,进入 Ring0 -
内核中的 NtContinue 被执行
-
内核根据 _CONTEXT:
-
恢复寄存器
-
恢复栈
-
恢复 EIP
随后,内核再次走返回路径:
cpp
NtContinue
↓
KiServiceExit
↓
iret
这一次,线程才真正回到 APC 发生前的用户执行流。
六、执行链路总结(完整闭环)
至此,用户 APC 的完整执行流程可以总结为:
cpp
Ring0:
KiDeliverApc
→ KiInitializeUserApc
- 构造用户栈
- 复制 CONTEXT
- 修改 TrapFrame(EIP/ESP)
→ KiServiceExit
→ iret
Ring3:
ntdll!KiUserApcDispatcher
- 建立 SEH
- call NormalRoutine
- 恢复 SEH
- ZwContinue(Context)
Ring0:
nt!NtContinue
- 从Context中恢复trapframe
- 返回内核出口
Ring3:
原用户代码继续执行
这是一次 Ring3 → Ring0 → Ring3 的完整闭环。