了解APC
线程是不能被结束、暂停、恢复的,线程在执行的时候自己占据着CPU。设想一种极端的例子:如果不调用API、屏蔽中断,并保证代码不出现异常,线程将永久占用CPU,线程只有自己执行代码将自己结束。
如果我们想要控制线程的行为,就需要提供给它一个函数,让它自己去调用,这个函数就是APC(Asyncroneus Procedure Call,即异步过程调用)。
APC队列
线程结构体0x34偏移位成员指向了一个结构体_KAPC_STATE,该结构体有5个成员:
0: kd> dt _KAPC_STATE
nt!_KAPC_STATE
//2个APC队列(2个双向链表,单表占用8字节,4字节指向链表头,4字节指向链表尾)分别是用户APC、内核APC
+0x000 ApcListHead : [2] _LIST_ENTRY
+0x010 Process : Ptr32 _KPROCESS // 线程所属或者所挂靠的进程
+0x014 KernelApcInProgress : UChar // 内核APC是否正在执行
+0x015 KernelApcPending : UChar // 是否有正在等待执行的内核APC
+0x016 UserApcPending : UChar // 是否有正在等待执行的用户APC
成员ApcListHead有2个APC队列,本质上就是双向链表,在双向链表内存储的就是给线程提供的APC函数,你想让线程做什么事情,就给给它提供一个APC函数挂进这个双向链表内,挂进去之后线程在某一时刻会去检查这个链表,当发现链表内有函数就会去调用,因此你可以在提供的APC函数中来控制线程的行为,当线程去检查列表且执行就会进行设定的行为。
成员Process表示当前线程所属或者所挂靠的进程,如果当前线程没有所挂靠的进程,那么该成员的值与ETHREAD结构体0x220偏移位成员的值一致。
APC结构
我们向APC队列中挂入的函数,准确来说不能称之为函数,而是一个APC结构体。这个结构看着有些复杂,在本章节我们只需要知道0x1c偏移位成员NormalRoutine,该成员的作用就是可以帮助我们找到提供的APC函数地址。
APC执行
KiServiceExit函数是系统调用、异常或中断返回用户空间的必经之路,因此当线程调用API、程序出现异常或中断时就会调用KiServiceExit函数,该函数会检查当前KTHREAD.KAPC_STATE.UserApcPending是否为0,即表示是否有用户空间的APC请求,如果有的话就会向下继续走,最后KiDeliverApc函数专门用于处理APC。(只判断用户空间的APC请求是因为在KiDeliverApc函数中会优先处理内核空间的APC然后处理用户空间的APC,因此我们不需要再多余进行判断)
备用APC队列
在上一章节的学习中,我们知道如果想控制线程的行为,就需要给它的APC队列里面挂一个APC,也就是在线程结构体0x34偏移位成员ApcState的成员ApcListHead其中一个链表中挂入,除了ApcState成员以外,在线程结构体KTHREAD中存着与APC有关的字段不止这一处。
如下所示KTHREAD结构体中,除了ApcState,还有4处与APC有关的成员,本章将会依次介绍这几个字段的含义,并学习一个新的知识,即备用APC队列。
0: kd> dt _KTHREAD
nt!_KTHREAD
+0x034 ApcState : _KAPC_STATE
+0x138 ApcStatePointer : [2] Ptr32 _KAPC_STATE
+0x14c SavedApcState : _KAPC_STATE
+0x165 ApcStateIndex : UChar
+0x166 ApcQueueable : UChar
...
SavedApcState
线程APC队列中的APC函数都是与进程相关联的,如A进程的1线程中的所有APC函数要访问的内存地址都是A进程的。但线程是可以挂靠到其他的进程,如A进程的线程1通过修改Cr3,即改为B进程的页目录基址,就可以访问B进程地址空间,即所谓的进程挂靠。
当A进程的1线程挂靠B进程后,APC队列中存储的却仍然是原来的APC,那么如果某个APC函数要读取一个地址为0x12345678的数据,读到的将是B进程的地址空间,这样逻辑就错误了。
为了避免混乱,在1线程挂靠B进程时,会将ApcState中的值暂时存储到SavedApcState中,等回到原进程A时,再将APC队列恢复。所以,SavedApcState又称为备用APC队列。
那么在这种进程挂靠的场景下,也是可以向线程APC队列中插入APC的,ApcState内所存储的就是B进程相关的APC信息(挂靠进程),而SavedApcState所存储的就是A进程相关的APC信息(所属进程)。
ApcStatePointer
为了方便寻址,在KTHREAD结构体中定义了一个指针数组ApcStatePointer ,数组内有两个成员。
该成员存储的信息也分为两种场景,分别是正常场景和挂靠场景:
// 正常场景
ApcStatePointer[0] // 指向 ApcState
ApcStatePointer[1] // 指向 SavedApcState
// 挂靠场景
ApcStatePointer[0] // 指向 SavedApcState
ApcStatePointer[1] // 指向 ApcState
ApcStateIndex
ApcStateIndex用来标识当前线程处于什么状态,正常状态下该值为0,挂靠状态下该值为1。那么我们将该成员结合ApcStatePointer,就会发现一个设计细节的地方。
无论是正常场景,还是挂靠场景,我们使用ApcStatePointer[ApcStateIndex]的组合方式,都可以获取到ApcState,即线程当前使用的APC信息。
ApcQueueable
ApcQueueable用于表示是否可以向线程的APC队列中挂入APC。当线程正在执行退出的代码时,会将这个值设置为0 ,如果此时执行插入APC的代码(在内核中会使用KeInsertQueueApc插入APC),在插入函数中会判断这个值的状态,如果为0则插入失败。
APC挂入过程
无论是正常状态还是挂靠状态,都有两个APC队列,一个是内核队列,一个是用户队列。每当要挂入一个APC时,不管是内核空间还是用户空间的函数,内核都要准备一个KAPC的数据结构,并将这个结构挂到相应的APC队列中。