news 2026/6/10 16:02:08

用户 APC 的执行过程(下)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用户 APC 的执行过程(下)

前言

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

  • 内核如何在 KiDeliverApc 中识别用户 APC

  • 如何调用 KiInitializeUserApc

  • 以及它如何修改 TrapFrame 与用户栈,为用户 APC 的执行提前“铺好路”

但需要特别强调的是:

KiInitializeUserApc 并不执行用户 APC。
它只是为“下一次返回用户态”布置好执行环境。

真正执行用户 APC 的地方,发生在 Ring3,并且位于 ntdll.dll 中。

本文将从线程返回用户态开始,完整分析
用户 APC 是如何在 Ring3 中被执行,又如何返回原执行流的。

一、从内核返回用户态:执行入口已经被篡改

回顾上一篇文章KiInitializeUserApc 在用户栈上写入:

  • NormalRoutine

  • NormalContext

  • SystemArgument1

  • SystemArgument2

  • 以及一整份 _CONTEXT 结构

    下面结合KiUserApcDispatcher汇编代码,对这些步骤逐一说明:

.text:77F06F58;===============S U B R O U T I N E=======================================.text:77F06F58.text:77F06F58;Attributes:noreturn.text:77F06F58.text:77F06F58;__stdcallKiUserApcDispatcher(x,x,x,x).text:77F06F58public_KiUserApcDispatcher@16.text:77F06F58 _KiUserApcDispatcher@16proc near;DATA XREF:.text:off_77EF61B8↑o.text:77F06F58.text:77F06F58 arg_C=byte ptr10h.text:77F06F58 arg_2D8=byte ptr2DCh.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 push1;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@16endp;sp-analysis failed

二、KiUserApcDispatcher 的第一步:建立异常保护

.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

.text:77F06F76 pop eax;eax=NormalRoutine

此行代码执行后堆栈结构如下:

.text:77F06F7B call eax;直接调用 NormalRoutine

此行代码执行后堆栈结构如下:

可以看出:

  • 在 KiInitializeUserApc 中,内核已经按固定布局把参数巧妙的写到了用户栈
  • 栈布局本身就是为 KiUserApcDispatcher 量身定制的

至此,用户 APC 的 NormalRoutine 正式在 Ring3 中执行。

四、用户 APC 执行完后:并不会“直接回去”

这是用户 APC 机制中最容易被误解的一点。

当 NormalRoutine 执行结束后:

线程并不会直接回到原来的用户代码。

原因是:

  • 当前的寄存器状态

  • 当前的栈指针

  • 当前的执行位置

全部都是为了 APC 临时构造的

必须有一个步骤:

把执行现场恢复成“APC 发生前”的样子

五、恢复异常链,ZwContinue:再次进入内核

.text:77F06F7D mov ecx,[edi+2CCh];ecx=Context.ExceptionList.text:77F06F83 mov large fs:0,ecx;恢复异常链.text:77F06F8A push1;TestAlert.text:77F06F8C push edi;CONTEXT.text:77F06F8D call _ZwContinue@8;ZwContinue(x,x)

这里传入的 _CONTEXT,正是:

当初在 KiInitializeUserApc 中,由内核复制到用户栈上的那一份。

这一步发生了什么?

  • ZwContinue 触发系统调用,进入 Ring0

  • 内核中的 NtContinue 被执行

  • 内核根据 _CONTEXT:

  • 恢复寄存器

  • 恢复栈

  • 恢复 EIP

随后,内核再次走返回路径:

NtContinue ↓ KiServiceExit ↓ iret

这一次,线程才真正回到 APC 发生前的用户执行流。

六、执行链路总结(完整闭环)

至此,用户 APC 的完整执行流程可以总结为:

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 的完整闭环。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/1 6:06:11

⚡_实时系统性能优化:从毫秒到微秒的突破[20260113173734]

作为一名专注于实时系统性能优化的工程师,我在过去的项目中积累了丰富的低延迟优化经验。实时系统对性能的要求极其严格,任何微小的延迟都可能影响系统的正确性和用户体验。今天我要分享的是在实时系统中实现从毫秒到微秒级性能突破的实战经验。 &#…

作者头像 李华
网站建设 2026/6/10 15:05:18

按键去抖动电路实现:vhdl课程设计大作业小白指南

按键去抖动电路设计实战:从原理到VHDL实现你有没有遇到过这种情况——在FPGA开发板上按下按键,明明只按了一次,数码管却加了好几次?或者LED闪烁次数远超预期?别急,这不是你的代码写错了,而是机械…

作者头像 李华
网站建设 2026/6/8 23:08:52

2026 AI营销战力榜:破解高客单价获客难题,原圈科技、高合汽车做对了什么?

在AI营销成为企业增长核心引擎的时代,一份深度榜单揭示了真正的领航者。其中,原圈科技凭借其深厚的行业积累和强大的"智能体矩阵"解决方案,在AI营销领域表现突出。该公司被普遍视为高净值服务行业实现AI系统化转型的关键赋能者,通过提供从市场洞察到内容生成、再到交…

作者头像 李华