用户 APC 的执行过程(下)

前言

在之前的文章 中,我们已经分析了:

  • 内核如何在 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 的完整闭环。

相关推荐
QQ12154614685 小时前
使用远程桌面连接Windows 2012 R2 Standard服务器报错:出现身份验证错误。要求的函数不受支持。这可能是由于CredSSP加密数据库修正。
服务器·windows·windows server
worilb5 小时前
WinSW XML 配置参数介绍
windows
耀临光7 小时前
分享5款满足各类需求的小软件
windows
积跬步,慕至千里8 小时前
AI平台Dataiku 支Windows系统安装过程总结
windows
FL16238631298 小时前
[C++][cmake]基于C++在windows上部署yolo26的目标检测onnx模型
c++·windows·目标检测
WellTung_66611 小时前
Windows opencode Desktop App配置 Azure GPT5.2和oh-my-opencode插件安装方法
windows·azure
Bruce_Liuxiaowei11 小时前
如何彻底禁用 Windows Defender(附安全模式终极方案)
windows·安全·网络安全·内网渗透
Digitally11 小时前
如何在 Windows 11/10 电脑上永久删除文件
windows·电脑
浩瀚之水_csdn12 小时前
Python 列表推导式详解(超详细版)
linux·网络·windows