avatar

Catalog
APC的本质

前言

线程是不能“杀掉”、“挂起”或“恢复”的,线程在执行的时候自己占据着CPU,其它线程是没法控制它的。举个极端的例子:如果一个线程不调用API,也屏蔽了中断,且保证代码不会出现异常,那么该线程将永久占用CPU,何谈控制呢?所以说一个线程如果想“死“,一定是自己执行代码把自己杀死,不存在”他杀“这种情况。

那么如果想改变一个线程的行为该怎么做呢?这就是今天需要学习的一种机制:APC(Asyncroneus Procedure Call),即异步过程调用

在学习内核的过程中,想要真正达到通透的地步,则至少需要掌握3种0环的进出方式,之前我们已经学习了API调用是一种,接下来将学习的APC机制也是一种,还有以后会学到的异常

线程与APC

线程的状态

在正式学习APC之前,需要先了解线程的状态,准确说,一个正在执行的线程只有两种状态,一种是挂靠,一种是未挂靠。(这里说明一下,尽管挂靠的对象说明的是进程,但是CPU调度的最小单位是线程,这里用正在执行的线程替代其所在的进程)

APC的执行过程

来作一个简单的比喻,假设一个APC就是一封寄出的信,而线程就是送信员。例如当我们想要寄信给别人时,就可以把信放入信箱,送信员每周一,周三,周五会来信箱取信,然后再依据信中写的地址,送到不同的地方。通过APC来改变线程的行为也是类似,首先编写一个能够按照我们期望让线程执行特定动作或行为的APC(信),接着将这个APC插入到线程中的某个位置(信箱),该正在执行的线程会在某个时刻来检查是否有未执行的APC(送信员每周来取信),若存在,则取出来执行(送信)。如果该APC执行的功能是让线程终止(例如一个举报信,送到政府),则该线程在执行后便会结束执行(丢了工作),让出CPU,所以说线程都是自杀的

谁插入的APC

前面介绍了APC的执行过程,现在来讨论几个问题,线程中的APC是谁插入的,例如此时有个线程A,发现有未执行的APC,正要去执行,那么线程A中的APC是哪来的?是线程A自己插入的吗?

答:首先肯定不是线程A自己插入的,就好比送信员,总不至于自己写一封信寄给自己吧,所以肯定是别的线程将APC插入到线程A中的。

APC插入到哪

那么别的线程会将APC会插入到线程A的什么位置呢?在之前学习线程结构体KTHREAD相关内容时,曾见过APC相关字段,由于当时还没学到APC,因此没有进一步分析。

参考上图,在KTHREAD+0x34的位置有一个字段ApcState,这是一个_KAPC_STATE结构,查看该结构

该结构有5个字段:

c
1
2
3
4
5
+0x000 ApcListHead			//2个APC队列,用户APC和内核APC
+0x010 Process //养父母,曾在进程挂靠章节提过,该字段指向线程所属或所挂靠的进程
+0x014 KernelApcInProgress //内核APC是否正在执行
+0x015 KernelApcPending //是否有正在等待执行的内核APC
+0x016 UserApcPending //是否有正在等待执行的用户APC

可以发现,_KAPC_STATE结构+0x000(KTHREAD+0x034)位置是两个ApcListHead队列(这里的队列用链表实现,一个指向链表头,一个指向链表尾,共8个字节,所以这里有两个队列),一个是存放用户APC的队列,一个是存放内核APC的队列,这就是APC插入的地方。

另一个值得关注的地方是+0x010(KTHREAD+0x044)的位置,这个字段的值我们就很熟悉了,在进程挂靠那一篇我们称其为养父母。这个字段的值指向了当前线程的所属进程所挂靠的进程。通过与ETHREAD+0x220位置的值进行对比,可以判断当前线程是否挂靠到了别的进程上。

APC结构初探

我们现在知道了,APC会插入到KTHREAD+0x034处APC队列中,会根据内核APC还是用户APC从而插入到不同的队列中。下面就来看一下这个APC是啥样的。

APC结构中很多字段都是非常重要的,本节中先不深究,仅混个脸熟,认识一个字段即可。位于_KAPC+0x01c处的一个字段:NormalRoutine,通过这个字段,可以找到你提供的APC函数,但该字段并不等于APC函数的地址。本篇作此了解即可,后续会再作讨论。

APC的执行

1)APC在哪执行:

用户APC:APC函数地址位于用户空间,在用户空间执行

内核APC:APC函数地址位于内核空间,在内核空间执行

这个概念还是比较好理解的,但这同时又抛出一个问题,由于用户APC必须在用户空间执行,又由上文可知用户APC队列与内核APC队列均位于KTHREAD结构体中,而这个KTHREAD结构体,是一个0环的内核结构体,也就意味着,在执行用户APC时,必须要考虑如何让用户APC在用户空间中执行,这个问题,在后续学到用户APC执行时会再作讨论,这里暂先设下疑问。

2)APC函数何时被执行:

KiServiceExit函数:这个函数是系统调用异常中断返回用户空间的必经之路。

KiDeliverApc函数负责执行APC的函数。

至于为什么是这两个函数呢,在后续介绍APC的文章中会进一步论证。

TerminateThread函数简要分析

开头提到过,线程是不能它杀的,只能自杀,想让改变一个线程的行为,例如杀死一个线程,就得考虑使用发送APC的方式来达成。有了这个概念后,我们来看一个3环的API函数TerminateThread,该函数的作用是在线程外终止一个线程,下面我们进入IDA跟随反汇编来分析一下TerminateThread是否利用了APC机制实现终止线程的功能。

TerminateThread(Kernel32.dll)

观察TerminateThread代码可以发现,该函数只是个跳板函数,并未实现终止线程的功能,而是跳转到函数NtTerminateThread中。查找导入表中,可以找到NtTerminateThread是位于Ntdll.dll中的导出函数

NtTerminateThread(Ntdll.dll)

由图,需要注意的一点,ZwTerminateThread和NtTerminateThread是同一份代码导出的,因此没有本质的差别,这一部分就可以看到我们很熟悉的代码了,先保存了一个系统服务号到寄存器中,接着根据CPU是否支持sysenter来决定调用相应的SYstemCall。这是我们熟悉的API调用,3环进0环的过程,看来TerminateThread的实现是在0环。

NtTerminateThread(ntkrnlpa.exe)

根据导出的全局变量KeServiceDescriptorTable,可以找到SSDT,然后根据系统服务号就可以确定要查找的内核函数地址。如上图所示。

发现,NtTerminateThread要调用的内核函数仍然叫做NtTerminateThread,只不过这个是内核函数,用IDA打开ntkrnlpa.exe(本次实验采用PAE分页,原因是10-10-12分页对应的内核函数代码比较分散,分析起来过于麻烦,就换成PAE分页进行实验)

观察内核函数NtTerminateThread,发现PspTerminateThreadByPointer函数在多个分支的条件下都会被调用,名字也比较相似,遂继续跟进该函数寻找线索。

PspTerminateThreadByPointer(ntkrnlpa.exe)

可以看到,在PspTerminateThreadByPointer这个函数后半部分的位置,有一个初始化APC和一个将APC插入至队列的函数,这验证了,TerminateThread这个三环API函数的执行最终会依靠APC完成。

小结

根据TerminateThread函数的调用流程,可以总结出,改变一个线程的行为的本质就是向它的APC队列中插入能够执行特定行为的APC块,在该线程执行的某个时刻中,会自己将APC取出来执行。所以说,线程的死亡不存在它杀的情况,一定是其自身执行了相应的APC,导致结束线程,让出CPU。因此,只要设置一个完美的条件,使得线程不会进行线程切换,且不给该线程发送APC时,这个线程将会永久占用CPU。

代码编写

自己尝试编写一个3环的插入APC的程序,不过由于xp上vc6的头文件太旧了,导致无法使用QueueUserAPC函数,遂放弃,总体思路不难,首先创建/打开一个线程,然后通过QueueUserAPC函数将你自己定义的APC函数放入到该线程的队列,可以仅作简单的打印,主要是看一个效果。有一个注意点,就线程创建/打开后,需要sleep一秒,留出时间先让子线程进入等待(可提醒)状态。

参考资料

参考链接:

  1. https://blog.csdn.net/qq_41988448/article/details/103609068 别人博客总结的内容
  2. https://www.bilibili.com/video/BV1NJ411M7aE?p=69 滴水中级班教程(Apc部分)

参考笔记:

  1. 张嘉杰的笔记
  2. 舒默的笔记
Author: cataLoc
Link: http://cataloc.gitee.io/blog/2020/05/26/APC%E7%9A%84%E6%9C%AC%E8%B4%A8/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
Donate
  • 微信
    微信
  • 支付寶
    支付寶