avatar

Catalog
备用APC队列

前言

前一篇在介绍APC的文章中提到过,如果想让线程做什么事情,就给它的APC队列里面挂一个APC。我们也了解了,在位于KTHREAD+0x34的位置处有一个ApcState,该字段是一个存着描述APC信息的_KAPC_STATE结构。但实际上,线程结构体KTHREAD中存着与APC有关的字段不止这一处。

由图可以知道,KTHREAD结构体中,除了+0x034偏移处的ApcState,还有4处与APC有关的字段。本篇将会从依次介绍这几个字段的含义,并会引入一个新的概念“备用APC队列”。

+0x14c SavedApcState

备用APC队列

由上图可以发现,SavedApcState字段与ApcState字段一样,都是_KAPC_STATE结构体。这里先回顾一下该结构体:

那么问题来了,弄两个一模一样的结构体有什么用呢?再讨论这个问题之前,我们必须了解一个知识,那就是挂靠。关于挂靠,可以参考我之前写的这篇进程挂靠

这里再简单总结一下,线程APC队列中的APC函数都是与进程相关联的,具体点说:“A进程的T线程中的所有APC函数,要访问的内存地址都是A进程的“。但线程是可以挂靠到其它进程上的,比如A进程的线程T,通过修改Cr3(改为B进程的页目录基址),就可以访问B进程的地址空间,即所谓”进程挂靠“。

当T线程挂靠B进程后,APC队列中的存储的却仍然是原来的APC!具体点说,比如某个APC函数要读取一个地址为0x12345678的数据,如果此时进行读取,读到的将是B进程的地址空间,这样逻辑就错误了!

为了避免混乱,在T线程挂靠B进程时,会将ApcState中的值暂时存储到SavedApcState中,等回到原进程A时,再将APC队列恢复。所以SavedApcState又称为备用APC队列

再看NtReadVirtualMemory

进程挂靠这篇中,我们分析了NtReadVirtualMemory这个内核函数,这个函数是ReadProcessMemory这个API在0环中的实现。根据上次分析,我们可以得到这样一个调用步骤:

Code
1
NtReadVirtualMemory -> _MmCopyVirtualMemory -> _MiDoPoolCopy -> _KeStackAttachProcess -> _KiAttachProcess

在上一次分析时,我们分析到了_KiAttachProcess,该函数内部会先修改养父母(KTHREAD.ApcState.Process),再调用KiSwapProcess进行Cr3的修改,从而完成线程的挂靠。接着就可以读取另一个进程的内存了。当然,上一次分析时并没有学习APC相关的知识,这次,我们需要重新审视这个KiAttachProcess函数到底做了哪些事。

再次观察KiAttachProcess函数,在完成挂靠过程之前,其上方有一个叫做KiMoveApcState,这个函数看上去和APC有关,所以我们进入该函数进行分析。

分析该函数,可以发现,这个函数主要做了一件事,就是将ApcState中的内容复制一份到SavedApcState中,而这个操作刚好就位于挂靠进程之前,这也再一次验证了SavedApcState的作用,就是作为备用APC队列暂存ApcState中的值

挂靠环境下ApcState的意义

在挂靠的环境下,也是可以先线程APC队列插入APC的,那这种情况下,使用的是哪个APC队列呢?

根据前面的分析,可以总结成如下:

Code
1
2
3
4
- A进程的T线程挂靠B进程,A是T的所属进程,B是T的挂靠进程。
- ApcState B进程相关的APC函数
- SavedApcState A进程相关的APC函数
- 在正常情况下,当前进程就是所属进程A,如果是挂靠情况下,当前进程就是挂靠进程B。(这也说明了GetCurrentProcess函数查找的其实是KTHREAD+0x44位置处的值)

+0x138 ApcStatePointer

为了操作方便,_KTHREAD结构体中定义了一个指针数组ApcStatePointer,长度为2.

Code
1
2
3
4
5
6
正常情况下:
ApcStatePointer[0] 指向 ApcState
ApcStatePointer[1] 指向 SavedApcState
挂靠情况下:
ApcStatePointer[0] 指向 SavedApcState
ApcStatePointer[1] 指向 ApcState

+0x165 ApcStateIndex

Code
1
2
3
ApcStateIndex用来标识当前线程处于什么状态:
0:正常状态
1:挂靠状态

ApcStatePointer 与 ApcStateIndex组合寻址

Code
1
2
3
4
5
6
正常情况下,向ApcState队列中插入APC时:
ApcStatePointer[0]指向ApcState此时ApcStateIndex的值为0
ApcStatePointer[ApcStateIndex]指向ApcState
挂靠情况下,向ApcState队列中插入APC时:
ApcStatePointer[1]指向ApcState此时ApcStateIndex的值为1
ApcStatePointer[ApcStateIndex]指向ApcState

总结:无论什么环境下,ApcStatePointer[ApcStateIndex] 指向的都是ApcState,ApcState则总是表示线程当前使用的apc状态。

+0x166 ApcQueueable

ApcQueueable用于表示是否可以向线程的APC队列中插入APC

当线程正在执行退出的代码时,会将这个值设置为0 ,如果此时执行
插入APC的代码KeInsertQueueApc后面会讲),在插入函数中会判断这个值的状态,如果为0,则插入失败。

参考链接

  1. https://www.bilibili.com/video/BV1NJ411M7aE?p=70 (滴水预习班-备用APC队列)
  2. https://cataloc.gitee.io/blog/2020/04/05/%E8%BF%9B%E7%A8%8B%E6%8C%82%E9%9D%A0/ (cataLoc的博客-进程挂靠)
  3. https://blog.csdn.net/weixin_42052102/article/details/83304995 (MyClassmates博客-备用APC队列)
Author: cataLoc
Link: http://cataloc.gitee.io/blog/2020/06/04/%E5%A4%87%E7%94%A8APC%E9%98%9F%E5%88%97/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
Donate
  • 微信
    微信
  • 支付寶
    支付寶