内核APC&用户APC详解
字数 1679 2025-08-06 23:10:35
内核APC与用户APC详解
0x01 内核APC
线程切换中的APC处理
-
SwapContext判断:
- 线程切换时,SwapContext函数会检查KernelApcPending值是否为空
- 不为空则跳转执行内核APC
-
调用链:
- SwapContext → KiSwapContext → KiSwapThread → KiDeliverApc
系统调用、中断或异常中的APC处理
-
执行顺序:
- 在执行用户APC前,必须先执行内核APC
- 在KiServiceExit中检查UserApcPending值
-
调用链:
- KiServiceExit → KiDeliverApc
内核APC执行流程(KiDeliverApc)
-
执行步骤:
- 判断内核APC链表是否为空
- 检查KTHREAD.ApcState.KernelApcInProgress是否为1
- 检查是否禁用内核APC(KTHREAD.KernelApcDisable)
- 将当前KAPC结构体从链表中摘除
- 执行KAPC.KernelRoutine函数释放KAPC结构体
- 设置KernelApcInProgress为1(标识正在执行)
- 执行真正的内核APC函数(KAPC.NormalRoutine)
- 执行完毕后将KernelApcInProgress改为0
- 循环处理下一个APC
-
NormalRoutine处理:
- 判断存储的是内核APC地址还是APC总入口
- 根据判断结果进行跳转执行
- 如果为空则调用KernelRoutine销毁APC
0x02 用户APC
触发条件
- 当产生系统调用、中断或异常时
- 线程在返回用户空间前(KiServiceExit)会检查用户APC
- 调用KiDeliverApc(第一个参数为1)进行处理
用户APC的特殊性
-
执行环境:
- 用户APC函数需要在用户空间执行
- 涉及大量换栈操作
-
执行流程:
- 内核 → 用户空间 → 再回到内核空间
KiDeliverApc处理流程
-
初始检查:
- 判断用户APC链表是否为空
- 检查第一个参数是否为1
- 检查ApcState.UserApcPending是否为1
- 将UserApcPending设置为0
-
APC处理:
- 将当前APC从用户队列中拆除
- 调用KernelRoutine释放KAPC结构体
- 调用KiInitializeUserApc函数
KiInitializeUserApc关键操作
-
环境备份:
- 将_Trap_Frame的值备份到CONTEXT结构体
- 通过KeContextFromKframes完成备份
-
堆栈调整:
- 获取3环原来的栈顶(ESP)
- 以4字节对齐将3环堆栈减去0x2DC字节
- CONTEXT结构体大小: 0x2CC
- KAPC的4个参数大小: 0x10
-
寄存器修改:
- 修改SS、DS、ES、FS、GS和EFLAGS寄存器
- 修改ESP到3环堆栈
- 修改EIP到KiUserApcDispatcher
KiUserApcDispatcher处理
-
获取参数:
- 获取指向CONTEXT结构的指针
- pop eax得到NormalRoutine结构
-
APC类型区分:
- 内核APC: NormalRoutine存储真正的APC地址
- 用户APC: 存储指向用户APC的总入口
-
QueueUserAPC特殊情况:
- 未指定NormalRoutine时
- 由kernel32.dll的BaseDispatchAPC调用真正的用户APC函数
-
返回处理:
- 调用ZwContinue返回内核
- 如果还有用户APC,重复执行过程
- 没有APC时,将CONTEXT赋值给Trap_Frame
用户APC执行流程总结
-
内核APC特点:
- 在线程切换时执行
- 不需要换栈
- 简单循环执行完毕
-
用户APC特点:
- 在系统调用/中断/异常返回3环前判断执行
- 执行前会先执行内核APC
- 涉及复杂的栈切换操作
-
关键点:
- 使用0x20调用号通过调用门回到0环
- 需要备份和恢复完整的执行环境
- 通过固定的KiUserApcDispatcher入口统一处理