APC队列-滴水逆向观看笔记
APC队列
主要结构体
KAPC
其中:
- 类型是当前这个结构体对应的类型,比如线程,中断之类的
- 大小就是整个结构体的大小
- 目标线程,因为每个线程都有自己对应的apc双向链表,所以用这个来指定对应的线程
- 挂的位置是apc队列里的下标
- KernelRoutine,就是指向一个ExFreePoolWithTag,用来释放APC函数的内存的
- NormalRoutine 在用户层,指向的就是用户APC的总入口;在内核层,就是指向内核的APC函数的地址、
- NormalContext 内核中的话是空,用户中的话是真正的APC函数。
- ApcStateIndex 挂哪个队列,之后重点讲
- ApcMode是内核或用户的apc
- Inserted指是否插入队列
KeInitializeApc
作用是分配空间,初始化KAPC结构体
- 第一个参数是KAPC指针,3环调用中上一个函数NtQueueApcThread就是分配了一个KAPC的空间,但是未初始化,然后传给我们这个函数来进行初始化
- 目标线程会挂到KAPC里的当前线程那儿
- 第三个是挂到哪儿
- 第四个是销毁KAPC的函数地址,和KAPC里的KernelRoutine是一样的,这里会传到KAPC里的那个元素里去
- 下面几个都和KAPC里的是一样的
KAPC_STATE
主要成员
ApcQueueable
代表能否在Apc队列中插入Apc
当前线程在执行退出代码时,会将该值设为0,意思是退出的时候不用插入,插入也没用。
ApcStateIndex(KAPC)
SaveApcState指向的是挂靠之前的原APC的状态,这里可以保证当传入为0的时候肯定指向原ApcState
index为2的时候,就是初始化KAPC的时候将当时的一个环境赋值到ApcState中,index为3的时候就是插入的时候再检查然后赋值。
Alertable(KTHREAD)
如果调用sleep或者wait。。。object的话不会改变alertable,但是后面加ex可以改变。
主要函数
SwapContext
用于线程切换
KiServiceExit
是系统调用,异常或中断返回用户空间的必进函数
KiDeliverApc
内核执行流程
用户层执行流程
执行apc函数的一个函数
KiInsertQueueApc
真正的将KAPC插入指定APC队列的一个函数
其中最后一步,要是是成功插入了一个内核的APC,直接将KernelApcPending置1,就是说明有内核APC需要执行,而如果是用户的APC的话需要进行下面的三个判断,全部满足才会置UserApcPending为1.所以这里,虽然apc可能插入队列里,但是可能不会执行对应的apc函数
下面有堆alertable的详细解释
KiInitializeUserApc
从0环切换到3环的时候,他要去执行对应的3环的一个apc函数。而因为0环和3环的堆栈并不是一个堆栈,而切换到三环后,Frame本身是存储的进入0环时的一个寄存器信息,而回到3环执行apc函数时ip并不是Frame里存储的ip,而是apc函数对应的地址,所以这里涉及到一个换栈,且涉及到一个备份寄存器的一个操作。
具体流程如上图
会先将这个context结构体里的信息复制到3环的堆栈里去,然后讲上方的四个参数同样复制进去,随后修改esp,再修改eip,让他指向ntdll.KiUserApcDispatcher,让程序从内核跳到用户态然后从KiUserApcDispatcher开始执行。ntdll.KiUserApcDispatcher的值在系统启动的时候已经赋值好了
ntdll.KiUserApcDispatcher
执行这个的时候,代表已经从0环回到3环了
初探
线程无法被杀掉,挂起,恢复,线程执行的时候是自己占有的CPU,其他的无法干预它。如果不调用API,屏蔽中断,不出现异常,线程将一直运行下去,一直占据CPU,无法被杀掉,所以线程只能是自杀而不是他杀。
想改变线程的行为,可以给他提供一个函数让他自己调用,就是APC,就是异步过程调用
备用APC调用
进程挂靠
a进程的线程t,可以通过修改Cr3,改为b进程的页目录基址,来访问b进程的地址空间,就是所谓进程挂靠
挂靠环境下的ApcState
3环的APC挂入过程
如果在3环通过API调用APC的话,调用QueueUserApc,他真正调用的是NtQueueApcThread,然后调用了下面那几个函数
重要的就俩标红的,一个是初始化KAPC结构体,一个是插入APC
0环的APC挂入过程
直接调用上面的俩内核函数
内核层的APC执行过程
调用SwapContext后,会在该函数里面进行判断
0代表只处理内核apc,1代表都要处理
如果有内核apc直接执行就完事
执行点有线程切换
还有0环返回3环的:系统调用,中断或者异常(KiServiceExit)。执行用户APC之前,先执行内核APC。
用户层的APC执行过程
堆栈操作
流程
首先判断传入的第一个参数的值是不是1,如果是1才需要执行用户的APC然后看UserApcPending的值是否为1,为1就往下走,先赋值为0,代表接下来要调用用户层的apc。为0就跳走。
接下来的减0c代表的是,当前挂入位置是KAPC的便宜0xc的位置,可以去看KAPC的结构体的0xc的偏移对应位置。
然后调用KernelRoutine去释放KAPC内存空间,随后调用一个KiInitializeUserApc,这里是用户和内核的最大差别,因为内核只有一个栈,而用户得从3环到0环,栈是不一样的。所以这里无法直接调用用户的apc函数,而是要调用这个函数去换栈
该函数里会调用下方的函数来将trap frame的内容复制到context里面
然后下方提取出3环的esp,取四字节对齐后,在0环中修改3环栈,减去0x2dc = 2cc + 10个字节。
因为原来esp指向的是蓝色的顶部,加2dc后刚好是context加四个参数的位置。于是esp提升至图的最上面。
随后改变完context之后,跳到KiUserApcDispatcher,也就是代表正式返回r3。然后调用用户的apc,随后执行ZwContinue,返回内核层。