查看原文
其他

内核APC&用户APC详解

szbuffer 红队蓝军 2023-03-20

首发于奇安信攻防社区:https://forum.butian.net/share/1640

内核APC

线程切换

SwapContext 判断是否有内核APC 
    
KiSwapThread

KiDeliverApc 执行内核APC函数

定位到SwapContext函数,然后查看KernelApcPending的值是否为空,不为空则跳转,这里只是进行判断,我们往上跟

然后回到KiSwapContext

再往上走得到KiSwapThread

这里判断后进行跳转

然后调用KiDeliverApc

系统调用、中断或者异常

当要执行用户APC之前,先要执行内核APC,这里找到KiServiceExit,有一个比较检验UserApcPending的值是否有APC请求

然后调用KiDeliverApc

内核层APC执行

KiDeliverApc

继续往里面跟,判断内核APC的链表是否为空,若不为空则跳转

NormalRoutine

跳转后判断NormalRoutine里面存储的是内核APC的地址还是APC的的总入口,然后再跳转

如果为空向下执行则会调用KernelRoutine对APC进行销毁

跳转过后执行真正的内核APC函数NormalRoutine

内核APC执行流程

KiDeliverApc函数执行流程

1) 判断第一个链表是否为空

2) 判断KTHREAD.ApcState.KernelApcInProgress是否为1

3) 判断是否禁用内核APC(KTHREAD.KernelApcDisable是否为1)

4) 将当前KAPC结构体从链表中摘除

5) 执行KAPC.KernelRoutine指定的函数 释放KAPC结构体占用的空间

6) 将KTHREAD.ApcState.KernelApcInProgress设置为1 标识正在执行内核APC

7) 执行真正的内核APC函数(KAPC.NormalRoutine)

8) 执行完毕 将KernelApcInProgress改为0  

9) 循环

用户APC

当产生系统调用、中断或者异常,线程在返回用户空间前都会调用KiServiceExit函数,在KiServiceExit会判断是否有要执行的用户APC,如果有则调用KiDeliverApc函数(第一个参数为1)进行处理。

处理用户APC要比内核APC复杂的多,因为,用户APC函数要在用户空间执行的,这里涉及到大量换栈的操作:

当线程从用户层进入内核层时,要保留原来的运行环境,比如各种寄存器,栈的位置等等 (_Trap_Frame),然后切换成内核的堆栈,如果正常返回,恢复堆栈环境即可。

但如果有用户APC要执行的话,就意味着线程要提前返回到用户空间去执行,而且返回的位置不是线程进入内核时的位置,而是返回到其他的位置,每处理一个用户APC都会涉及到:

内核-->用户空间-->再回到内核空间

KiDeliverApc

1) 判断用户APC链表是否为空

2) 判断第一个参数是为1  

3) 判断ApcState.UserApcPending是否为1

4) 将ApcState.UserApcPending设置为0

5) 链表操作 将当前APC从用户队列中拆除

6) 调用函数(KAPC.KernelRoutine)释放KAPC结构体内存空间

7) 调用KiInitializeUserApc函数

线程进0环时,原来的运行环境(寄存器栈顶等)保存到_Trap_Frame结构体中,如果要提前返回3环去处理用户APC,就必须要修改_Trap_Frame结构体:

比如:进0环时的位置存储在EIP中,现在要提前返回,而且返回的并不是原来的位置,那就意味着必须要修改EIP为新的返回位置。还有堆栈ESP,也要修改为处理APC需要的堆栈。那原来的值怎么办呢?处理完APC后该如何返回原来的位置呢?

KiInitializeUserApc要做的第一件事就是备份:

将原来_Trap_Frame的值备份到一个新的结构体中(CONTEXT),这个功能由其子函数KeContextFromKframes来完成,代码如下

首先判断参数是否为1,当参数为1的时候处理用户APC

然后进行一系列的操作

KiInitializeUserApc

接着转到KiInitializeUserApc函数

CONTEXTTrapFrame传入KeContextFromKframes

这里接着往下看,这里得到C4

C4对应的Esp存储的是3环原来的栈顶

然后以4字节对齐将3环堆栈减去0x2DC个字节,这里是因为要将CONTEXT结构和KAPC的4个参数传给3环

原本三环的ESP如图所示

CONTEXT结构体的大小为0x2CC,KAPC的4个参数的大小为0x10,所以减去0x2DC

这一部分代码主要是将CONTEXT结构复制到3环的堆栈


当windows把CONTEXT结构复制到堆栈之后,准备用户层执行环境,首先修改SS、DS、ES、FS、GS和EFLAGS寄存器

然后修改esp到3环堆栈

KiUserApcDispatcher

然后修改eip,这里永远返回一个固定的位置,但是这个位置在每次系统启动的时候都不相同,存放在3环的ntdll里的KiUserApcDispatcher参数里面

然后到ntdll里面定位到KiUserApcDispatcher,首先得到指向CONTEXT结构的指针,然后pop eax得到NormalRoutine结构,这里当APC是内核APC的时候存储的是真正的APC地址,当APC是用户APC的时候存储的是指向用户APC的总入口

当我们调用QueueUserAPC,并没有指定NormalRoutine结构,只指定了NormalContextSystemArgument1,那么这个参数在QueueUserAPC内部指定,在kernel32.dllBaseDispatchAPC,用来调用真正的用户APC函数

再继续往下跟,调用了ZwContinue

1) 返回内核,如果还有用户APC,重复上面的执行过程。

2) 如果没有需要执行的用户APC,会将CONTEXT赋值给Trap_Frame结构体。就像从来没有修改过一样。ZwContinue后面的代码不会执行,线程从哪里进0环仍然会从哪里回去。

使用0x20的调用号利用调用门回到0环

用户APC执行流程

总结:

1.内核APC在线程切换时执行,不需要换栈,比较简单,一个循环执行完毕。

2.用户APC在系统调用、中断或异常返回3环前会进行判断,如果有要执行的用户APC,再执行。

3.用户APC执行前会先执行内核APC。

加下方wx,拉你一起进群学习

往期推荐

Demo版菜刀

浅谈EDR绕过

ETW的攻与防

什么?你还不会webshell免杀?(四)

初探UAF漏洞

SEH异常之编译器原理探究

什么?你还不会webshell免杀?(三)

初探栈溢出



您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存