APC队列

主要结构体

KAPC

image-20220818085811024

其中:

  • 类型是当前这个结构体对应的类型,比如线程,中断之类的
  • 大小就是整个结构体的大小
  • 目标线程,因为每个线程都有自己对应的apc双向链表,所以用这个来指定对应的线程
  • 挂的位置是apc队列里的下标
  • KernelRoutine,就是指向一个ExFreePoolWithTag,用来释放APC函数的内存的
  • NormalRoutine 在用户层,指向的就是用户APC的总入口;在内核层,就是指向内核的APC函数的地址、
  • NormalContext 内核中的话是空,用户中的话是真正的APC函数。
  • ApcStateIndex 挂哪个队列,之后重点讲
  • ApcMode是内核或用户的apc
  • Inserted指是否插入队列

KeInitializeApc

image-20220818092137920

作用是分配空间,初始化KAPC结构体

  • 第一个参数是KAPC指针,3环调用中上一个函数NtQueueApcThread就是分配了一个KAPC的空间,但是未初始化,然后传给我们这个函数来进行初始化
  • 目标线程会挂到KAPC里的当前线程那儿
  • 第三个是挂到哪儿
  • 第四个是销毁KAPC的函数地址,和KAPC里的KernelRoutine是一样的,这里会传到KAPC里的那个元素里去
  • 下面几个都和KAPC里的是一样的

KAPC_STATE

image-20220819224849964

主要成员

ApcQueueable

代表能否在Apc队列中插入Apc

image-20220820001022342

当前线程在执行退出代码时,会将该值设为0,意思是退出的时候不用插入,插入也没用。

ApcStateIndex(KAPC)

image-20220818093050689

SaveApcState指向的是挂靠之前的原APC的状态,这里可以保证当传入为0的时候肯定指向原ApcState

index为2的时候,就是初始化KAPC的时候将当时的一个环境赋值到ApcState中,index为3的时候就是插入的时候再检查然后赋值。

Alertable(KTHREAD)

image-20220818105141679

如果调用sleep或者wait。。。object的话不会改变alertable,但是后面加ex可以改变。

image-20220818105448687

主要函数

SwapContext

用于线程切换

KiServiceExit

是系统调用,异常或中断返回用户空间的必进函数

KiDeliverApc

内核执行流程

image-20220818111726263

用户层执行流程

image-20220818121812543

执行apc函数的一个函数

KiInsertQueueApc

image-20220818093739723

真正的将KAPC插入指定APC队列的一个函数

其中最后一步,要是是成功插入了一个内核的APC,直接将KernelApcPending置1,就是说明有内核APC需要执行,而如果是用户的APC的话需要进行下面的三个判断,全部满足才会置UserApcPending为1.所以这里,虽然apc可能插入队列里,但是可能不会执行对应的apc函数

image-20220818104710932

下面有堆alertable的详细解释

KiInitializeUserApc

image-20220818122627234

image-20220816230539327

从0环切换到3环的时候,他要去执行对应的3环的一个apc函数。而因为0环和3环的堆栈并不是一个堆栈,而切换到三环后,Frame本身是存储的进入0环时的一个寄存器信息,而回到3环执行apc函数时ip并不是Frame里存储的ip,而是apc函数对应的地址,所以这里涉及到一个换栈,且涉及到一个备份寄存器的一个操作。

image-20220816231707236

具体流程如上图

会先将这个context结构体里的信息复制到3环的堆栈里去,然后讲上方的四个参数同样复制进去,随后修改esp,再修改eip,让他指向ntdll.KiUserApcDispatcher,让程序从内核跳到用户态然后从KiUserApcDispatcher开始执行。ntdll.KiUserApcDispatcher的值在系统启动的时候已经赋值好了

ntdll.KiUserApcDispatcher

执行这个的时候,代表已经从0环回到3环了

image-20220816232409534

初探

线程无法杀掉,挂起,恢复,线程执行的时候是自己占有的CPU,其他的无法干预它。如果不调用API,屏蔽中断,不出现异常,线程将一直运行下去,一直占据CPU,无法被杀掉,所以线程只能是自杀而不是他杀。

想改变线程的行为,可以给他提供一个函数让他自己调用,就是APC,就是异步过程调用

image-20220819224552534

备用APC调用

image-20220819230427547

进程挂靠

a进程的线程t,可以通过修改Cr3,改为b进程的页目录基址,来访问b进程的地址空间,就是所谓进程挂靠

挂靠环境下的ApcState

image-20220820000506262

3环的APC挂入过程

image-20220818091020717

如果在3环通过API调用APC的话,调用QueueUserApc,他真正调用的是NtQueueApcThread,然后调用了下面那几个函数

重要的就俩标红的,一个是初始化KAPC结构体,一个是插入APC

0环的APC挂入过程

直接调用上面的俩内核函数

内核层的APC执行过程

调用SwapContext后,会在该函数里面进行判断

image-20220818110026006

0代表只处理内核apc,1代表都要处理

如果有内核apc直接执行就完事

执行点有线程切换

image-20220818111150926

还有0环返回3环的:系统调用,中断或者异常(KiServiceExit)。执行用户APC之前,先执行内核APC。

image-20220818111946046

用户层的APC执行过程

image-20220818112826900

堆栈操作

image-20220818113007238

流程

image-20220819171143467

首先判断传入的第一个参数的值是不是1,如果是1才需要执行用户的APC然后看UserApcPending的值是否为1,为1就往下走,先赋值为0,代表接下来要调用用户层的apc。为0就跳走。

接下来的减0c代表的是,当前挂入位置是KAPC的便宜0xc的位置,可以去看KAPC的结构体的0xc的偏移对应位置。

然后调用KernelRoutine去释放KAPC内存空间,随后调用一个KiInitializeUserApc,这里是用户和内核的最大差别,因为内核只有一个栈,而用户得从3环到0环,栈是不一样的。所以这里无法直接调用用户的apc函数,而是要调用这个函数去换栈

该函数里会调用下方的函数来将trap frame的内容复制到context里面

image-20220818124429955

image-20220818124646216

然后下方提取出3环的esp,取四字节对齐后,在0环中修改3环栈,减去0x2dc = 2cc + 10个字节。

image-20220818124812981

因为原来esp指向的是蓝色的顶部,加2dc后刚好是context加四个参数的位置。于是esp提升至图的最上面。

随后改变完context之后,跳到KiUserApcDispatcher,也就是代表正式返回r3。然后调用用户的apc,随后执行ZwContinue,返回内核层。

总结

image-20220819223947810