来学习一下调度相关的结构

很早就听说过断链隐藏的操作,因为 Windows 都是使用链表去管理进程,线程等结构的,所以断链可以达到隐藏自身的目的。那么这里就引申出来一个问题,为什么断链可以隐身且不破坏大部分的功能呢,下面的线程调度会给出答案。

线程调度

操作系统的一些理论,线程有三种状态:就绪(ready)、等待(wait)、运行(running)。

至于为什么进程/线程断链可以达到隐藏且继续执行的目的,这里先给出答案:

  • 因为 CPU 调度/执行时基于线程的,所以进程断链只会影响获取进程的 API 获取的结果而不会影响 CPU 调度。
  • 因为 CPU 调度使用的和线程断链的链表不是同一个链表,因此线程断链也不会影响线程本身继续被 CPU 调度。

等待链表

在上篇文章中讲到了线程的结构,其中有一个对象:

1
2
+0x074 WaitListEntry    : _LIST_ENTRY
+0x074 SwapListEntry : _SINGLE_LIST_ENTRY

因为它们在同一位置,所以同一时刻一个线程只能属于 WaitListEntry 中或者 SwapListEntry 中,其中等待链表是双链表结构。线程调用了Sleep或者WaitForSingleObject等函数时,就挂到一个链表之中,它是等待链表。

似乎 Windows7 版本开始删除了该全局变量,挂在了 KPCR 结构体下,我们可以通过以下方式找到:

1
2
3
4
5
6
7
8
9
10
11
12
13
kd> dg 0x30
P Si Gr Pr Lo
Sel Base Limit Type l ze an es ng Flags
---- -------- -------- ---------- - -- -- -- -- --------
0030 80b97000 00004fff Data RW Ac 0 Bg By P Nl 00000493

kd> dt _KPRCB 80b97000+0x120
ntdll!_KPRCB
//...
+0x31e0 WaitListHead : _LIST_ENTRY [ 0x884ded7c - 0x86934dbc ]
//...
+0x3220 DispatcherReadyListHead : [32] _LIST_ENTRY [ 0x868e569c - 0x868e569c ]
//...

先从 0x30 指示的段描述符中取得 KPCR 的结构体地址,然后输出它的 data 字段,可以看到 WaitListHead 链表和 DispatcherReadyListHead 的 32 个链表。

这里验证一下,如果线程挂在 WaitListHead 中,那么线程状态应该是 waiting 状态的,观察 KTHREAD 字段说明,可以得到。

状态 描述
0x00 Initialized 线程已初始化,但尚未开始运行。
0x01 Ready 线程处于就绪状态,可以被调度器分配给处理器执行。
0x02 Running 线程正在处理器上运行。
0x03 Standby 线程已被选择为下一个执行的线程,等待处理器空闲。
0x04 Terminated 线程已终止,正在清理资源。
0x05 Waiting 线程正在等待某个事件或资源(如 I/O、同步对象)。
0x06 Transition 线程处于等待状态,但缺少必要的资源(例如尚未加载到内存的线程堆栈)。
0x07 DeferredReady 线程曾处于等待状态,现在已准备好执行,但调度尚未发生。
0x08 GateWaitObsolete 该状态已过时,仅用于向后兼容旧版 Windows。

那么理论上来说,上面的线程 State 字段取值应该为 5。

取得 Flink 上的值 0x884ded7c,因为该字段在 KTHREAD+0x74 中,而指针一般都指向对应的链表字段,所以需要将地址 -0x74,下面给出输出的部分数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
kd> dt _KTHREAD 884ded7c-0x74
ntdll!_KTHREAD
+0x028 InitialStack : 0x80fb2ed0 Void
+0x02c StackLimit : 0x80fb0000 Void
+0x030 KernelStack : 0x80fb2a60 Void
+0x034 ThreadLock : 0
+0x038 WaitRegister : _KWAIT_STATUS_REGISTER
+0x039 Running : 0 ''
+0x03a Alerted : [2] ""
+0x03c Alertable : 0y1
+0x057 Priority : 12 ''
+0x058 NextProcessor : 0
+0x05c DeferredProcessor : 0
+0x060 ApcQueueLock : 0
+0x064 ContextSwitches : 4
+0x068 State : 0x5 ''
+0x069 NpxState : 0 ''
+0x06a WaitIrql : 0 ''
+0x06b WaitMode : 1 ''
+0x06c WaitStatus : 0n0
+0x070 WaitBlockList : 0x884dedc8 _KWAIT_BLOCK
+0x074 WaitListEntry : _LIST_ENTRY [ 0x883ac92c - 0x80b9a300 ]
+0x074 SwapListEntry : _SINGLE_LIST_ENTRY

可以看到对应上了基本,线程优先级 12,线程状态 5(Waiting)。

调度链表

调度链表有 32 个圈,就是优先级是 0-31,0为最低优先级,31 为最高,默认优先级一般是 8。改变优先级就是从一个圈里面卸下来挂到另外一个圈上,这 32 个圈是正在调度中的线程,包括准备运行的线程(Ready)。比如:只有一个 CPU 但有10 个线程在运行,那么某一时刻,正在运行的线程在 KPCRdata 中,其他 9 个在这 32 个圈中。

调度链表不包括正在运行的线程这一点是可以肯定的,可以做如下实验:

  1. 找到 KPCR 的 CurrentThread,查看对应的优先级
  2. 从根据优先级找到对应的调度链表,发现对应优先级的链表为空。

然后查看对应的调度链表

发现为空,可以说明,正在运行的线程不会出现在调度链表中,而是直接挂在 KPCR 的 CurrentThread 字段。


通过学习这两个结构,也可以得出一个结论了:

线程调度是基于线程,也依赖等待链表和调度链表的,不管如何断链隐藏,遍历这两个链表一定能遍历得到真实的所有线程。如果尝试把线程从这两个链表断开,那么这个线程就永远不会被调度,也就永远跑不起来了,这背离了我们隐藏线程的初衷。

参考文献