内核APC&用户APC详解
字数 1679 2025-08-06 23:10:35

内核APC与用户APC详解

0x01 内核APC

线程切换中的APC处理

  1. SwapContext判断

    • 线程切换时,SwapContext函数会检查KernelApcPending值是否为空
    • 不为空则跳转执行内核APC
  2. 调用链

    • SwapContext → KiSwapContext → KiSwapThread → KiDeliverApc

系统调用、中断或异常中的APC处理

  1. 执行顺序

    • 在执行用户APC前,必须先执行内核APC
    • 在KiServiceExit中检查UserApcPending值
  2. 调用链

    • KiServiceExit → KiDeliverApc

内核APC执行流程(KiDeliverApc)

  1. 执行步骤

    • 判断内核APC链表是否为空
    • 检查KTHREAD.ApcState.KernelApcInProgress是否为1
    • 检查是否禁用内核APC(KTHREAD.KernelApcDisable)
    • 将当前KAPC结构体从链表中摘除
    • 执行KAPC.KernelRoutine函数释放KAPC结构体
    • 设置KernelApcInProgress为1(标识正在执行)
    • 执行真正的内核APC函数(KAPC.NormalRoutine)
    • 执行完毕后将KernelApcInProgress改为0
    • 循环处理下一个APC
  2. NormalRoutine处理

    • 判断存储的是内核APC地址还是APC总入口
    • 根据判断结果进行跳转执行
    • 如果为空则调用KernelRoutine销毁APC

0x02 用户APC

触发条件

  • 当产生系统调用、中断或异常时
  • 线程在返回用户空间前(KiServiceExit)会检查用户APC
  • 调用KiDeliverApc(第一个参数为1)进行处理

用户APC的特殊性

  1. 执行环境

    • 用户APC函数需要在用户空间执行
    • 涉及大量换栈操作
  2. 执行流程

    • 内核 → 用户空间 → 再回到内核空间

KiDeliverApc处理流程

  1. 初始检查

    • 判断用户APC链表是否为空
    • 检查第一个参数是否为1
    • 检查ApcState.UserApcPending是否为1
    • 将UserApcPending设置为0
  2. APC处理

    • 将当前APC从用户队列中拆除
    • 调用KernelRoutine释放KAPC结构体
    • 调用KiInitializeUserApc函数

KiInitializeUserApc关键操作

  1. 环境备份

    • 将_Trap_Frame的值备份到CONTEXT结构体
    • 通过KeContextFromKframes完成备份
  2. 堆栈调整

    • 获取3环原来的栈顶(ESP)
    • 以4字节对齐将3环堆栈减去0x2DC字节
      • CONTEXT结构体大小: 0x2CC
      • KAPC的4个参数大小: 0x10
  3. 寄存器修改

    • 修改SS、DS、ES、FS、GS和EFLAGS寄存器
    • 修改ESP到3环堆栈
    • 修改EIP到KiUserApcDispatcher

KiUserApcDispatcher处理

  1. 获取参数

    • 获取指向CONTEXT结构的指针
    • pop eax得到NormalRoutine结构
  2. APC类型区分

    • 内核APC: NormalRoutine存储真正的APC地址
    • 用户APC: 存储指向用户APC的总入口
  3. QueueUserAPC特殊情况

    • 未指定NormalRoutine时
    • 由kernel32.dll的BaseDispatchAPC调用真正的用户APC函数
  4. 返回处理

    • 调用ZwContinue返回内核
    • 如果还有用户APC,重复执行过程
    • 没有APC时,将CONTEXT赋值给Trap_Frame

用户APC执行流程总结

  1. 内核APC特点

    • 在线程切换时执行
    • 不需要换栈
    • 简单循环执行完毕
  2. 用户APC特点

    • 在系统调用/中断/异常返回3环前判断执行
    • 执行前会先执行内核APC
    • 涉及复杂的栈切换操作
  3. 关键点

    • 使用0x20调用号通过调用门回到0环
    • 需要备份和恢复完整的执行环境
    • 通过固定的KiUserApcDispatcher入口统一处理
内核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入口统一处理