Windows XP 进程线程(二)

ʕ •ᴥ•ʔ ɔ:

1 线程链表

进程结构体EPROCESS(0x50和0x190)是2个链表,里面圈着当前进程所有的线程。
32.png
对进程断链,程序可以正常运行,原因是CPU执行与调度是基于线程的,进程断链只是影响一些遍历系统进程的API,并不会影响程序执行。
对线程断链也是一样的,断链后在Windbg、任务管理器或者OD中无法看到被断掉的线程,但并不影响其执行(仍然再跑)。

线程的三种状态:等待(Wait),运行(Running),就绪(Ready)。每种状态下的线程实际上都由相应的双向循环链表串起来,操作系统通过这些链表来调度线程。

正在运行中的线程存储在KPCR中,就绪和等待的线程全在另外的33个链表中。一个等待链表,32个就绪链表

1.1 等待链表(Wait)

等待链表(一个):Wait,挂起,阻塞。

线程调用了Sleep()WaitForSingleObject()SuspendThread()或者以挂起状态CreateThread()等函数时,就将线程挂到这个链表。

  1. KTHREAD偏移0x60处的成员WaitListEntry串起来的链表即位系统中所有处于等待的线程。

  2. 等待线程存储在等待链表头KiWaitListHead中,KiWaitListHead是一个全局变量,可以dd查看。

    1
    2
    kd> dd KiWaitListHead
    8055d4a8 862da080 860f0988 00000011 00000000

    地址0x8055d4a8存储了WaitListEntry,这是一个_LIST_ENTRY,它属于某个线程_KTHREAD + 0x60的位置。

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

    验证一下:看到[ 0x862da080 - 0x860f0988 ]则说明没问题。

    1
    2
    3
    4
    5
    6
    kd> dt _KTHREAD 0x8055d4a8-0x60
    nt!_KTHREAD
    +0x000 Header : _DISPATCHER_HEADER
    ...
    +0x060 WaitListEntry : _LIST_ENTRY [ 0x862da080 - 0x860f0988 ]
    +0x060 SwapListEntry : _SINGLE_LIST_ENTRY

_KTHREAD + 0x60是一个共用体union,线程处于等待Wait或者调度Ready状态就会存到这个位置的链表里,如果是等待状态,这个地方就是等待链表;如果是调度状态,这里就是调度链表。因为一个线程同时只能处于一种状态。如上WaitListEntry有值,而SwapListEntry是空的,说明此时线程处于等待状态。

举例说明,我们可以看看当前的 WaitListEntry.FLink 线程是属于哪个进程:
首先通过 ETHREAD 找到 EPROCESS:

1
2
3
4
5
kd> dt _ETHREAD 0x860f0988-0x60
nt!_ETHREAD
...
+0x220 ThreadsProcess : 0x863d5380 _EPROCESS
...

然后看看镜像名:

1
2
3
4
5
kd> dt _EPROCESS 0x863d5380
nt!_EPROCESS
...
+0x174 ImageFileName : [16] "Dbgview.exe"
...

1.2 运行线程(Running)

一个核只有一个运行中的线程,运行中的线程存储在 KPCR 中。

1
2
3
4
5
6
kd> r fs
fs=00000030
kd> r gdtr
gdtr=8003f000
kd> dq 0x8003f000+0x6*0x8
8003f030 ffc093df`f0000001 0040f300`00000fff

KPCR == FS:[0] == 0xFFDFF000。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
kd> dt _KPCR 0xFFDFF000
nt!_KPCR
+0x000 NtTib : _NT_TIB
+0x01c SelfPcr : 0xffdff000 _KPCR
+0x020 Prcb : 0xffdff120 _KPRCB
+0x024 Irql : 0 ''
+0x028 IRR : 0
+0x02c IrrActive : 0
+0x030 IDR : 0xffffffff
+0x034 KdVersionBlock : 0x8054e2b8 Void
+0x038 IDT : 0x8003f400 _KIDTENTRY
+0x03c GDT : 0x8003f000 _KGDTENTRY
+0x040 TSS : 0x80042000 _KTSS
+0x044 MajorVersion : 1
+0x046 MinorVersion : 1
+0x048 SetMember : 1
+0x04c StallScaleFactor : 0x960
+0x050 DebugActive : 0 ''
+0x051 Number : 0 ''
+0x052 Spare0 : 0 ''
+0x053 SecondLevelCacheAssociativity : 0 ''
+0x054 VdmAlert : 0
+0x058 KernelReserved : [14] 0
+0x090 SecondLevelCacheSize : 0
+0x094 HalReserved : [16] 0
+0x0d4 InterruptMode : 0
+0x0d8 Spare1 : 0 ''
+0x0dc KernelReserved2 : [17] 0
+0x120 PrcbData : _KPRCB

kd> dt _NT_TIB 0xFFDFF000
nt!_NT_TIB
+0x000 ExceptionList : 0x80551cb0 _EXCEPTION_REGISTRATION_RECORD
+0x004 StackBase : 0x805524f0 Void
+0x008 StackLimit : 0x8054f700 Void
+0x00c SubSystemTib : (null)
+0x010 FiberData : (null)
+0x010 Version : 0
+0x014 ArbitraryUserPointer : (null)
+0x018 Self : (null)

可以看到:

  • GDT:0x8003f000。
  • IDT:0x8003f400。
  • TSS:0x80042000。

1.3 调度链表(Ready)

调度链表有32个,所有就绪线程根据32个不同的优先级,各自存储在32个链表中。优先级:0 - 31(0最低,31最高)。默认优先级一般是8。

改变线程优先级就是从一个链表里面卸下来挂到另外一个链表上。

这32个链表是正在调度中的线程:包括正在运行的和准备运行的。比如:只有一个CPU但有10个线程在运行,那么某一时刻,正在运行的线程在KPCR中,其他9个在这32个链表中。

通过全局变量KiDispatcherReadyListHead可以查看这32个链表的链表头:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
kd> dd  KiDispatcherReadyListHead L70
8055df80 8055df80 8055df80 8055df88 8055df88
8055df90 8055df90 8055df90 8055df98 8055df98
8055dfa0 8055dfa0 8055dfa0 8055dfa8 8055dfa8
8055dfb0 8055dfb0 8055dfb0 8055dfb8 8055dfb8
8055dfc0 8055dfc0 8055dfc0 8055dfc8 8055dfc8
8055dfd0 8055dfd0 8055dfd0 8055dfd8 8055dfd8
8055dfe0 8055dfe0 8055dfe0 8055dfe8 8055dfe8
8055dff0 8055dff0 8055dff0 8055dff8 8055dff8
8055e000 8055e000 8055e000 8055e008 8055e008
8055e010 8055e010 8055e010 8055e018 8055e018
8055e020 8055e020 8055e020 8055e028 8055e028
8055e030 8055e030 8055e030 8055e038 8055e038
8055e040 8055e040 8055e040 8055e048 8055e048
8055e050 8055e050 8055e050 8055e058 8055e058
8055e060 8055e060 8055e060 8055e068 8055e068
8055e070 8055e070 8055e070 8055e078 8055e078
8055e080 00000000 00000000 00000000 00000000
8055e090 00000000 00000000 00000000 00000000

每两个4字节就构成了一个LIST_ENTRY,我们发现这里32个链表都是空的(BLINK == FLINK == 当前地址,说明链表是空的。),原因是现在Windbg把系统挂起了,所有线程都处于等待状态,不能被调度了。

说明:等待链表和调度链表都是在_KTHREAD + 0x60,是一个共用体union,线程处于等待Wait或者调度Ready状态就会存到这个位置的链表里,如果是等待状态,这个地方就是等待链表;如果是调度状态,这里就是调度链表。

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

1.4 总结

  1. XP只有一套这样的33个链表,也就是说上面这个数组只有一个,多核也只有一个。Win7也是一样的只有一个组,如果是64位的,那就有64个链表。
    服务器版本:KiWaitListHead整个系统只有一个,但KiDispatcherReadyListHead这个数组有几个CPU就有几组。
  2. 正在运行的线程在KPCR中。
  3. 准备运行的线程在32个调度链表中(0 - 31级),KiDispatcherReadyListHead 是个数组存储了这32个链表头。
  4. 等待状态的线程存储在等待链表中,KiWaitListHead存储链表头。
  5. 等待链表都挂一个相同的位置:_KTHREAD(0x060)。

2 模拟线程切换

2.1 代码模版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
#include <stdio.h>
#include <tchar.h>
#include <string.h>
#include <Windows.h>

#pragma warning(disable: 4996)

//--------------------------------------------------------------------------------------------
//--------------------------------------------------------------------------------------------

#define MAXGMTHREAD 0x100

#define GMTHREAD_CREATE 0x01
#define GMTHREAD_READY 0x02
#define GMTHREAD_RUNNING 0x04
#define GMTHREAD_SLEEP 0x08
#define GMTHREAD_EXIT 0x100

#define GMTHREADSTACKSIZE 0x80000

//--------------------------------------------------------------------------------------------
//--------------------------------------------------------------------------------------------

// 线程结构体(仿ETHREAD)
typedef struct {
char *name; // 线程名,相当于线程TID
int Flags; // 线程状态
int SleepMillisecondDot; // 休眠时间
void *InitialStack; // 线程堆栈起始位置
void *StackLimit; // 线程堆栈界限
void *KernelStack; // 线程堆栈当前位置,即ESP0
void *lpParameter; // 线程函数参数
void (*func)(void *lpParameter); // 线程函数
} GMThread_t;

//--------------------------------------------------------------------------------------------
//--------------------------------------------------------------------------------------------

// 当前调度线程下标
int CurrentThreadIndex = 0;

// 线程调度队列
GMThread_t GMThreadList[MAXGMTHREAD] = { 0 };

void *WindowsStackLimit = NULL;

//--------------------------------------------------------------------------------------------
//--------------------------------------------------------------------------------------------

void SwitchContext(GMThread_t *OldGMThreadp, GMThread_t *NewGMThreadp);
void GMThreadStartup(GMThread_t *GMThreadp);
void IdleGMThread(void *lpParameter);
void PushStack(unsigned int **Stackpp, unsigned int v);
void InitGMThread (GMThread_t *GMThreadp, char *name, void (*func)(void *lpParameter), void *lpParameter);
int RegisterGMThread(char *name, void (*func)(void *lpParameter), void *lpParameter);
void Scheduling();
void GMSleep(int Milliseconds);
void Thread1(void *lpParameter);
void Thread2(void *lpParameter);
void Thread3(void *lpParameter);
void Thread4(void *lpParameter);

//--------------------------------------------------------------------------------------------
//--------------------------------------------------------------------------------------------

int _tmain(int argc, _TCHAR* argv[])
{
// 初始化线程环境
RegisterGMThread("Thread1", Thread1, NULL);
RegisterGMThread("Thread2", Thread2, NULL);
RegisterGMThread("Thread3", Thread3, NULL);
RegisterGMThread("Thread4", Thread4, NULL);

// 仿Windows线程切换,模拟系统时钟中断,是被动切换
//Scheduling();
for (;;)
{
Sleep(20);
Scheduling();
// 如果回到主线程,说明没有找到就绪线程,CurrentThreadIndex 一定是 0
//printf("时钟中断. %d\n", CurrentThreadIndex);
}
return 0;
}

// 线程切换函数
__declspec(naked) void SwitchContext(GMThread_t *OldGMThreadp, GMThread_t *NewGMThreadp)
{
__asm
{
// 当前线程保存寄存器到自己的栈顶
push ebp;
mov ebp,esp;
push edi;
push esi;
push ebx;
push ecx;
push edx;
push eax;

mov esi,OldGMThreadp; // mov esi, [ebp + 0x08]
mov edi,NewGMThreadp; // mov edi, [ebp + 0x0C]

mov [esi + GMThread_t.KernelStack], esp; // 保存旧ESP
mov esp,[edi + GMThread_t.KernelStack]; // 设置新ESP

// 从新线程的栈里恢复寄存器的值
pop eax;
pop edx;
pop ecx;
pop ebx;
pop esi;
pop edi;
pop ebp;

// 返回到新线程之前调用 SwitchContext 的地方;如果是第一次调度,则跳转到 GMThreadStartup
ret;
}
}

// 此函数在 SwitchContext 的 ret 指令执行时调用,功能是调用线程入口函数
void GMThreadStartup(GMThread_t *GMThreadp)
{
GMThreadp->func(GMThreadp->lpParameter);
GMThreadp->Flags = GMTHREAD_EXIT;
Scheduling();
printf("这句永远不会执行,因为修改线程状态为退出,Scheduling 永远不会返回到这里.\n");
return;
}

// 空闲线程,没事做就调用它
void IdleGMThread(void *lpParameter)
{
printf("IdleGMThread-------------------\n");
Scheduling();
return;
}

// 模拟压栈
void PushStack(unsigned int **Stackpp, unsigned int v)
{
*Stackpp -= 1;
**Stackpp = v;

return;
}

// 初始化线程结构体和线程栈,设置状态为“就绪”
void InitGMThread (GMThread_t *GMThreadp, char *name, void (*func)(void *lpParameter), void *lpParameter)
{
unsigned char *StackPages;
unsigned int *ESP;
// 结构初始化赋值
GMThreadp->Flags = GMTHREAD_CREATE;
GMThreadp->name = name;
GMThreadp->func = func;
GMThreadp->lpParameter = lpParameter;
// 申请栈空间
StackPages = (unsigned char*)VirtualAlloc(NULL,GMTHREADSTACKSIZE, MEM_COMMIT, PAGE_READWRITE);
// 清零
memset(StackPages,0,GMTHREADSTACKSIZE);
// 栈初始化地址
GMThreadp->InitialStack = (StackPages + GMTHREADSTACKSIZE);
// 栈限制
GMThreadp->StackLimit = StackPages;
// 栈地址
ESP = (unsigned int *)GMThreadp->InitialStack;

// 初始化线程栈
PushStack(&ESP, (unsigned int)GMThreadp); // 通过这个指针来找到:线程函数、函数参数
PushStack(&ESP, (unsigned int)0); // 平衡堆栈,此值无意义,详见 SwitchContext 函数注释
PushStack(&ESP, (unsigned int)GMThreadStartup); // 线程入口函数,这个函数负责调用线程函数
PushStack(&ESP, (unsigned int)0); // push ebp,此值无意义,是寄存器初始值
PushStack(&ESP, (unsigned int)0); // push edi,此值无意义,是寄存器初始值
PushStack(&ESP, (unsigned int)0); // push esi,此值无意义,是寄存器初始值
PushStack(&ESP, (unsigned int)0); // push ebx,此值无意义,是寄存器初始值
PushStack(&ESP, (unsigned int)0); // push ecx,此值无意义,是寄存器初始值
PushStack(&ESP, (unsigned int)0); // push edx,此值无意义,是寄存器初始值
PushStack(&ESP, (unsigned int)0); // push eax,此值无意义,是寄存器初始值

GMThreadp->KernelStack = ESP;

GMThreadp->Flags = GMTHREAD_READY;

return;
}

// 添加新线程到调度队列,然后初始化线程
int RegisterGMThread(char *name, void (*func)(void *lpParameter), void *lpParameter)
{
int i;

// 找一个空位置,或者是name已经存在的那个项
// 下标0是当前正在运行的线程,所以从1开始遍历
for (i = 1; GMThreadList[i].name; i++)
{
if (0 == stricmp(GMThreadList[i].name, name))//不区分大小写
{
break;
}
}
// 初始化线程结构体
InitGMThread(&GMThreadList[i], name, func, lpParameter);

return (i | 0x55AA0000);
}

// 线程调度函数,功能是遍历调度队列,找到“就绪”线程,然后切换线程
void Scheduling()
{
int i;
int TickCount;
GMThread_t *OldGMThreadp;
GMThread_t *NewGMThreadp;

TickCount = GetTickCount(); // GetTickCount 返回操作系统启动到目前为止经过的毫秒
// 正在调度的线程,第一次是 GMThreadList[0],这个表示主线程
OldGMThreadp = &GMThreadList[CurrentThreadIndex];

// 遍历线程调度队列,找第一个“就绪”线程
// 如果找不到,就回到主函数,模拟时钟中断
NewGMThreadp = &GMThreadList[0];
for (i = 1; GMThreadList[i].name; i++)
{
// 如果达到“等待时间”,就修改状态为“就绪”
if (GMThreadList[i].Flags & GMTHREAD_SLEEP)
{
if (TickCount > GMThreadList[i].SleepMillisecondDot)
{
GMThreadList[i].Flags = GMTHREAD_READY;
}
}
// 找到“就绪”线程
if (GMThreadList[i].Flags & GMTHREAD_READY)
{
NewGMThreadp = &GMThreadList[i];
break;
}
}
// 更新当前调度线程下标
CurrentThreadIndex = NewGMThreadp - GMThreadList;
// 线程切换
SwitchContext(OldGMThreadp, NewGMThreadp);
return;
}

// 正在运行的线程主动调用此函数,将自己设置成“等待”状态,然后让调度函数调度其他线程
void GMSleep(int Milliseconds)
{
GMThread_t *GMThreadp;
GMThreadp = &GMThreadList[CurrentThreadIndex];

if ((GMThreadp->Flags) != 0)
{
GMThreadp->SleepMillisecondDot = GetTickCount() + Milliseconds;
GMThreadp->Flags = GMTHREAD_SLEEP;
}

Scheduling();
return;
}

void Thread1(void *lpParameter)
{
int i;
for (i = 0; i < 3; i++)
{
printf("Thread1\n");
GMSleep(100); // 主动切换,模拟WIN32 API
}

return;
}

void Thread2(void *lpParameter)
{
int i = 0;
while (++i)
{
printf(" Thread2(%d)\n", i);
GMSleep(200); // 主动切换,模拟WIN32 API
}

return;
}

void Thread3(void *lpParameter)
{
int i = 0;
while (++i)
{
printf(" Thread3(%d)\n", i);
GMSleep(200); // 主动切换,模拟WIN32 API
}

return;
}

void Thread4(void *lpParameter)
{
int i = 0;
while (++i)
{
printf(" Thread4(%d)\n", i);
GMSleep(400); // 主动切换,模拟WIN32 API
}

return;
}

2.2 线程切换

  1. 关键结构体

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    //线程结构体(仿EHREAD)
    typedef struct
    {
    char *name; //线程名 相当于线程TID
    int Flags; //线程状态
    int SleepMillisecondDot;//休眠时间

    void *InitialStack; //线程堆栈起始位置,栈底
    void *StackLimit; //线程堆栈界限,申请内存块的顶
    void *KernelStack; //线程堆栈当前位置,也就是ESP

    void *lpParameter; //线程函数的参数
    void (*func)(void *lpParameter);//线程函数

    } GMThread_t;
  2. 线程链表

    在之前已经讲解过,正在运行的线程在KPCR中,另外还有1个等待链表和32个就绪链表。我们在模拟线程切换时就简单定义一个数组来存储所有的链表,其中第一个(下标为0)的表示正在运行的线程

    所谓创建线程,就是创建一个结构体,并且挂到这个数组中。此时的线程状态为:创建。

    34.png

  3. 初始化线程堆栈

    35.png

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 初始化线程栈
    PushStack(&ESP, (unsigned int)GMThreadp); // 通过这个指针来找到:线程函数、函数参数
    PushStack(&ESP, (unsigned int)0); // 平衡堆栈,此值无意义,详见 SwitchContext 函数注释
    PushStack(&ESP, (unsigned int)GMThreadStartup); // 线程入口函数,这个函数负责调用线程函数
    PushStack(&ESP, (unsigned int)0); // push ebp,此值无意义,是寄存器初始值
    PushStack(&ESP, (unsigned int)0); // push edi,此值无意义,是寄存器初始值
    PushStack(&ESP, (unsigned int)0); // push esi,此值无意义,是寄存器初始值
    PushStack(&ESP, (unsigned int)0); // push ebx,此值无意义,是寄存器初始值
    PushStack(&ESP, (unsigned int)0); // push ecx,此值无意义,是寄存器初始值
    PushStack(&ESP, (unsigned int)0); // push edx,此值无意义,是寄存器初始值
    PushStack(&ESP, (unsigned int)0); // push eax,此值无意义,是寄存器初始值
  4. 1)线程不是被动切换的,而是主动让出CPU。
    2)线程切换并没有使用TSS来保存寄存器,而是使用堆栈
    3)线程切换的过程就是堆栈切换的过程。

3 逆向分析-KiSwapContext函数

我们要带着问题开始逆向:

  1. SwapContext 有几个参数,分别是什么?
  2. SwapContext 在哪里实现了线程切换?
  3. 线程切换的时候,会切换CR3吗?切换CR3的条件是什么?
  4. 中断门提权时,CPU会从TSS得到ESP0和SS0,TSS中存储的一定是当前线程的ESP0和SS0吗?如何做到的?
  5. FS:[0]在3环指向TEB,但是线程有很多,FS:[0]指向的是哪个线程的TEB,如何做到的?
  6. 0环的 ExceptionList 在哪里备份的?
  7. IdleThread是什么?什么时候执行?找到这个函数。
  8. 如何找到下一个就绪线程?
  9. 模拟线程切换与Windows线程切换有哪些区别?

线程切换是操作系统的核心内容,几乎所有的内核API都会调用切换线程的函数。

KiSwapContext函数调用了 SwapContext,我们通过逆它可以判断出 SwapContext 有几个参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
.text:0046E969 ; ---------------------------------------------------------------------------
.text:0046E96B align 4
.text:0046E96C
.text:0046E96C ; =============== S U B R O U T I N E =======================================
.text:0046E96C
.text:0046E96C
.text:0046E96C ; __fastcall KiSwapContext(x)
.text:0046E96C @KiSwapContext@4 proc near ; CODE XREF: KiSwapThread()+85↑p
.text:0046E96C
.text:0046E96C var_10 = dword ptr -10h //看到这几个值都是负数,说明是局部变量而不是参数
.text:0046E96C var_C = dword ptr -0Ch
.text:0046E96C var_8 = dword ptr -8
.text:0046E96C var_4 = dword ptr -4
.text:0046E96C //调用约定__fastcall,ecx和edx传递1、2参数。类似stdcall内平栈。
.text:0046E96C sub esp, 10h //使用到寄存器,因此要将使用到的寄存器暂时保存到堆栈中
.text:0046E96F mov [esp+10h+var_4], ebx //这里和 push 是等效的
.text:0046E973 mov [esp+10h+var_8], esi
.text:0046E977 mov [esp+10h+var_C], edi
.text:0046E97B mov [esp+10h+var_10], ebp
.text:0046E97E mov ebx, large fs:1Ch //ebx = KPCR.SelfPcr
.text:0046E985 mov esi, ecx //esi = ecx = KPRCB.NextThread(KTHREAD)
.text:0046E987 mov edi, [ebx+124h] //edi = KPRCB.CurrentThread(KTHREAD)
.text:0046E98D mov [ebx+124h], esi //修改 KPCR,更新当前线程
.text:0046E993 mov cl, [edi+58h] //ecx = KTHREAD.WaitIrql(UChar),旧线程中断请求等级
.text:0046E996 call SwapContext //参数有4个,均通过寄存器保存
//ebx: _KPCR,esi: 新线程 _ETHREAD,edi: 旧线程 _ETHREAD
//cl: 旧线程的 WaitIrql,这个参数用来控制是否执行APC
//调用 SwapContext 后,已经完成了线程切换
//后面就是新线程从它自己的堆栈里恢复寄存器的值的过程
.text:0046E99B mov ebp, [esp+10h+var_10]
.text:0046E99E mov edi, [esp+10h+var_C]
.text:0046E9A2 mov esi, [esp+10h+var_8]
.text:0046E9A6 mov ebx, [esp+10h+var_4]
.text:0046E9AA add esp, 10h
.text:0046E9AD retn
.text:0046E9AD @KiSwapContext@4 endp

4 逆向分析-SwapContext函数

主要解决:SwapContext 在哪里实现了线程切换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
.text:0046EA81 ; ---------------------------------------------------------------------------
.text:0046EA82 align 10h
.text:0046EA90
.text:0046EA90 ; =============== S U B R O U T I N E =======================================
.text:0046EA90 //ebx: _KPCR,esi: 新线程 _ETHREAD,edi: 旧线程 _ETHREAD
.text:0046EA90 //cl: 旧线程的 WaitIrql,这个参数用来控制是否执行APC,貌似用不到,直接覆盖了
.text:0046EA90 SwapContext proc near ; CODE XREF: KiUnlockDispatcherDatabase(x)+99↑p
.text:0046EA90 ; KiSwapContext(x)+2A↑p ...
.text:0046EA90 or cl, cl //类似 mov edi,edi
.text:0046EA92 mov byte ptr es:[esi+2Dh], 2 //将新线程的状态修改为运行(Running)
//Ready = 1,Running = 2,Waiting = 5
.text:0046EA97 pushf //pushfd
.text:0046EA98 lea ecx, [ebx+540h] //KPRCB._KSPIN_LOCK_QUEUE.Lock,自旋锁
.text:0046EA9E call @KeAcquireQueuedSpinLockAtDpcLevel@4 ; KeAcquireQueuedSpinLockAtDpcLevel(x)
//上锁,目前还不了解具体功能,猜测是处理线程互斥、同步
.text:0046EAA3 lea ecx, [ebx+538h]
.text:0046EAA9 call @KeReleaseQueuedSpinLockFromDpcLevel@4 ; KeReleaseQueuedSpinLockFromDpcLevel(x)
//解锁
.text:0046EAAE
/*----------------------
判断是否有DPC,备份异常链表
----------------------*/
.text:0046EAAE loc_46EAAE: ; CODE XREF: KiIdleLoop()+7C↓j
.text:0046EAAE mov ecx, [ebx] //ecx = ExceptionList异常链表
.text:0046EAB0 cmp dword ptr [ebx+994h], 0 //DpcRoutineActive,是否有DPC,非零就跳去蓝屏
.text:0046EAB7 push ecx //备份指向ExceptionList异常链表的指针,
//保存本线程切换时的内核seh链表
.text:0046EAB8 jnz loc_46EC41 //蓝屏
.text:0046EABE cmp ds:_PPerfGlobalGroupMask, 0//LOG用的 Windows自己调试用的,别的地方没有用
.text:0046EAC5 jnz loc_46EC18 //说明_PPerfGlobalGroupMask == 0
.text:0046EACB
/*----------------------
判断旧线程是否支持浮点寄存器
----------------------*/
.text:0046EACB loc_46EACB: ; CODE XREF: SwapContext+190↓j
.text:0046EACB ; SwapContext+1A1↓j ...
.text:0046EACB mov ebp, cr0 //cr0 控制寄存器可以判断当前环境是实模式还是保护模式,是否开启分页模式,写保护
.text:0046EACE mov edx, ebp //edx = ebp = cr0
.text:0046EAD0 cmp byte ptr [edi+31h], 0 //KTHREAD.NpxState,判断旧线程是否支持浮点寄存器
.text:0046EAD4 jz loc_46EBF3 //KTHREAD.NpxState == 0,不支持
.text:0046EADA
/*---------------------------------------------------
初始化0环线程堆栈,eax指向栈底,减去210h字节浮点寄存器占用空间
---------------------------------------------------*/
.text:0046EADA loc_46EADA: ; CODE XREF: SwapContext+183↓j
.text:0046EADA mov cl, [esi+2Ch] //若KTHREAD.DebugActive == -1,不能使用调试寄存器:Dr0-Dr7
.text:0046EADD mov [ebx+50h], cl //更新_KPCR中当前线程的调试状态位,此时存的是新线程的值
.text:0046EAE0 cli //屏蔽中断, 不被I/O事件、时钟干扰
.text:0046EAE1 mov [edi+28h], esp //***保存旧线程的KernelStack = esp***
.text:0046EAE4 mov eax, [esi+18h] //eax = 新线程KTHREAD.InitialStack栈底
.text:0046EAE7 mov ecx, [esi+1Ch] //ecx = 新线程KTHREAD.StackLimit
.text:0046EAEA sub eax, 210h //线程堆栈的前0x210字节是浮点寄存器(TrapFrame)
//此时eax指向_KTRAP_FRAME.V86Gs
.text:0046EAEF mov [ebx+8], ecx //更新KPCR.StackLimit = 新线程KTHREAD.StackLimit
.text:0046EAF2 mov [ebx+4], eax //更新KPCR.StackBase = 新线程KTHREAD.InitialStack栈底
.text:0046EAF5 xor ecx, ecx //ecx == 0
.text:0046EAF7 mov cl, [esi+31h] //ecx = 新线程KTHREAD.NpxState
.text:0046EAFA and edx, 0FFFFFFF1h //cr0 & 0xFFFFFFF1
.text:0046EAFD or ecx, edx //判断新线程NpxState是否支持浮点
.text:0046EAFF or ecx, [eax+20Ch]
.text:0046EB05 cmp ebp, ecx //ebp == cr0,根据判断结果决定是否更新cr0
//如果上一个线程和要替换的线程对浮点支持不一样那就需要更新cr0
.text:0046EB07 jnz loc_46EBEB
.text:0046EB0D lea ecx, [ecx+0]
.text:0046EB10
/*------------
判断虚拟8086模式
-------------*/
.text:0046EB10 loc_46EB10: ; CODE XREF: SwapContext+15E↓j
.text:0046EB10 test dword ptr [eax-1Ch], 20000h//SegCS & 0x20000
//判断是否是虚拟8086模式,如果不是,直接减掉
// +0x07c V86Es : Uint4B
// +0x080 V86Ds : Uint4B
// +0x084 V86Fs : Uint4B
// +0x088 V86Gs : Uint4B
.text:0046EB17 jnz short loc_46EB1C //是虚拟8086模式则跳转
.text:0046EB19 sub eax, 10h //栈底eax - 0x10,eax就指向了0环栈底,接下来就会存储到TSS里
//此时eax指向TrapFrame的+0x078 HardwareSegSs: Uint4B
.text:0046EB1C
/*------------------------
线程切换,更新CPU编号,更新Teb
------------------------*/
.text:0046EB1C loc_46EB1C: ; CODE XREF: SwapContext+87↑j
.text:0046EB1C mov ecx, [ebx+40h] //ecx = TSS
.text:0046EB1F mov [ecx+4], eax //更新TSS.esp0 = TrapFrame.HardwareSegSs
.text:0046EB22 mov esp, [esi+28h] //esp = 新线程KTHREAD.KernelStack
//**************此处是切换线程,切换线程本质是切换堆栈
//**************将esp0修改为新线程的栈顶,然后就可以从堆栈里取数据恢复现场了
.text:0046EB25 mov eax, [esi+20h] //eax = 新线程的Teb
.text:0046EB28 mov [ebx+18h], eax //更新KPCR.NtTib.Self = Teb
//当前线程中有很多状态,一个在ETHREAD中,一个在FS中
//这样的好处就是可以在3环通过FS获取当前线程的状态
.text:0046EB2B sti //允许中断发生
.text:0046EB2C mov eax, [edi+44h] //eax = 旧线程KTHREAD.ApcState.KPROCESS结构体指针
.text:0046EB2F cmp eax, [esi+44h] //新旧线程的KPROCESS结构体指针做比较
//如果发生了跨进程切换线程则更换Cr3,否则不更换Cr3
.text:0046EB32 mov byte ptr [edi+50h], 0 //旧线程KTHREAD.IdleSwapBlock = 0
.text:0046EB36 jz short loc_46EB78 //如果是同一个进程内的线程切换,就跳转
//如果不是同一个进程的,那么就要做额外的工作,主要就是切换CR3
.text:0046EB38 mov edi, [esi+44h] //edi = 新线程KTHREAD.ApcState.KPROCESS结构体指针
.text:0046EB3B mov ecx, [ebx+48h] //ecx = KPCR.SetNumber。当前CPU编号,从1开始。
.text:0046EB3E xor [eax+34h], ecx //旧线程KPROCESS.ActiveProcessors ⊕ CPU编号
.text:0046EB41 xor [edi+34h], ecx //新线程KPROCESS.ActiveProcessors ⊕ CPU编号
.text:0046EB44 test word ptr [edi+20h], 0FFFFh//新线程KPROCESS.LdtDescriptor ⊕ -1
//判断LdtDescriptor是否为-1,win95之前用Ldt,之后不用
.text:0046EB4A jnz short loc_46EBBD
.text:0046EB4C xor eax, eax
.text:0046EB4E
/*-------------------
切换CR3、更新TSS中的CR3
-------------------*/
.text:0046EB4E loc_46EB4E: ; CODE XREF: SwapContext+156↓j
.text:0046EB4E lldt ax //LDTR寄存器清0,SLDT用于保存LDTR
.text:0046EB51 lea ecx, [ebx+540h] //KPRCB._KSPIN_LOCK_QUEUE.Lock,自旋锁,下面解锁
.text:0046EB57 call @KeReleaseQueuedSpinLockFromDpcLevel@4 ; KeReleaseQueuedSpinLockFromDpcLevel(x)
.text:0046EB5C xor eax, eax
.text:0046EB5E mov gs, eax //将GS寄存器置为0,这就是Windows不使用gs的依据
.text:0046EB60 assume gs:nothing
.text:0046EB60 mov eax, [edi+18h] //eax = 新线程KPROCESS.DirectoryTableBase(CR3)
.text:0046EB63 mov ebp, [ebx+40h] //ebp = KPCR.TSS
.text:0046EB66 mov ecx, [edi+30h] //ecx = 新线程KPROCESS.IopmOffset
//指定IOPM(I/O权限表)的位置,内核通过IOPM可控制进程的用户模式I/O访问权限
.text:0046EB69 mov [ebp+1Ch], eax //更新TSS中的CR3
.text:0046EB6C mov cr3, eax //切换CR3
.text:0046EB6F mov [ebp+66h], cx //更新TSS.IOMap=cx,windows2000以后没用了
.text:0046EB73 jmp short loc_46EB83
.text:0046EB73 ; ---------------------------------------------------------------------------
.text:0046EB75 align 4
.text:0046EB78
.text:0046EB78 loc_46EB78: ; CODE XREF: SwapContext+A6↑j
.text:0046EB78 lea ecx, [ebx+540h]
.text:0046EB7E call @KeReleaseQueuedSpinLockFromDpcLevel@4 ; KeReleaseQueuedSpinLockFromDpcLevel(x)
.text:0046EB83
/*--------------------------------------
ebp:KPCR.TSS,edi:新线程KPROCESS结构体指针
ebx:KPCR,esi: 新线程 KTHREAD
使用KPCR.NtTib.Self(TEB)修改KPCR.NtTib.GDT
的FS = 0x3B对应的地址0x8003f038
恢复新线程异常链表的地址
--------------------------------------*/
.text:0046EB83 loc_46EB83: ; CODE XREF: SwapContext+E3↑j
.text:0046EB83 mov eax, [ebx+18h] //eax = KPCR.NtTib.Self
//此时eax指向了TEB(地址0x0046EB28处更新的)
.text:0046EB86 mov ecx, [ebx+3Ch] //ecx = KPCR.GDT
//假设GDT表在0x8003f000,ecx = 0x8003f000
//3环FS = 0x3B,所以FS在GDT表里的地址是0x8003f000+0x8*0x7
//(Index == 0x7)= 0x8003f038
//0x8003f038为FS[0]_R3的GDT段描述符
//下面的操作是修改FS的段描述符,这样3环FS就能找到TEB了
.text:0046EB89 mov [ecx+3Ah], ax //0x8003f03A为段描述符BaseAddress起始地址
//更新GDT描述符BaseAddress的低2字节(0~15bit)
.text:0046EB8D shr eax, 10h //eax = TEB:16~31bit
.text:0046EB90 mov [ecx+3Ch], al //更新GDT描述符BaseAddress的16~23bit
.text:0046EB93 mov [ecx+3Fh], ah //更新GDT描述符BaseAddress的23~31bit
.text:0046EB96 inc dword ptr [esi+4Ch] //新线程KTHREAD.ContextSwitches加1
.text:0046EB99 inc dword ptr [ebx+61Ch] //KPCR.KernelReserved数组中对应的值加1
.text:0046EB9F pop ecx //ECX将指向_KPCR.Nt_Tib?这里查阅资料是这么解释,不太懂,因为
//esp已经切换了(0x0046EB22),为什么是0x0046EAB7处的push ecx?
//是因为每个线程切换前最后压栈的都是0x0046EAB7处的push ecx?
//是的,通过0x0046EAB7和0x0046EAE1知道,线程切换前栈顶已经保存好了。
.text:0046EBA0 mov [ebx], ecx //恢复新线程异常链表的地址
.text:0046EBA2 cmp byte ptr [esi+49h], 0 //KTHREAD.ApcState.KernelApcPending - 0
.text:0046EBA6 jnz short loc_46EBAC
.text:0046EBA8 popf //说明0x0046EB9F的猜测是正确的
.text:0046EBA9 xor eax, eax
.text:0046EBAB retn
.text:0046EBAC ; ---------------------------------------------------------------------------
.text:0046EBAC
.text:0046EBAC loc_46EBAC: ; CODE XREF: SwapContext+116↑j
.text:0046EBAC popf
.text:0046EBAD jnz short loc_46EBB2
.text:0046EBAF mov al, 1
.text:0046EBB1 retn
.text:0046EBB2 ; ---------------------------------------------------------------------------
.text:0046EBB2
.text:0046EBB2 loc_46EBB2: ; CODE XREF: SwapContext+11D↑j
.text:0046EBB2 mov cl, 1
.text:0046EBB4 call ds:__imp_@HalRequestSoftwareInterrupt@4 ; HalRequestSoftwareInterrupt(x)
.text:0046EBBA xor eax, eax
.text:0046EBBC retn
.text:0046EBBD ; ---------------------------------------------------------------------------
.text:0046EBBD
.text:0046EBBD loc_46EBBD: ; CODE XREF: SwapContext+BA↑j
.text:0046EBBD mov ebp, [ebx+3Ch]
.text:0046EBC0 mov eax, [edi+20h]
.text:0046EBC3 mov [ebp+48h], eax
.text:0046EBC6 mov eax, [edi+24h]
.text:0046EBC9 mov [ebp+4Ch], eax
.text:0046EBCC mov eax, 48h
.text:0046EBD1 mov ebp, [ebx+38h]
.text:0046EBD4 mov ecx, [edi+28h]
.text:0046EBD7 mov [ebp+108h], ecx
.text:0046EBDD mov ecx, [edi+2Ch]
.text:0046EBE0 mov [ebp+10Ch], ecx
.text:0046EBE6 jmp loc_46EB4E
.text:0046EBEB ; ---------------------------------------------------------------------------
.text:0046EBEB
.text:0046EBEB loc_46EBEB: ; CODE XREF: SwapContext+77↑j
.text:0046EBEB mov cr0, ecx
.text:0046EBEE jmp loc_46EB10
.text:0046EBF3 ; ---------------------------------------------------------------------------
.text:0046EBF3
.text:0046EBF3 loc_46EBF3: ; CODE XREF: SwapContext+44↑j
.text:0046EBF3 and edx, 0FFFFFFF1h
.text:0046EBF6 mov ecx, [ebx+4]
.text:0046EBF9 cmp ebp, edx
.text:0046EBFB jz short _ScPatchFxb
.text:0046EBFD mov cr0, edx
.text:0046EC00 mov ebp, edx
.text:0046EC02
.text:0046EC02 _ScPatchFxb: ; CODE XREF: SwapContext+16B↑j
.text:0046EC02 ; DATA XREF: KiInitMachineDependent()+23A↓w ...
.text:0046EC02 fxsave dword ptr [ecx]
.text:0046EC05
.text:0046EC05 _ScPatchFxe: ; DATA XREF: KiInitMachineDependent():loc_5CD548↓o
.text:0046EC05 ; KiInitMachineDependent()+235↓o ...
.text:0046EC05 mov byte ptr [edi+31h], 0Ah
.text:0046EC09 mov dword ptr [ebx+5C0h], 0
.text:0046EC13 jmp loc_46EADA
.text:0046EC18 ; ---------------------------------------------------------------------------
.text:0046EC18
.text:0046EC18 loc_46EC18: ; CODE XREF: SwapContext+35↑j
.text:0046EC18 mov eax, ds:_PPerfGlobalGroupMask
.text:0046EC1D cmp eax, 0
.text:0046EC20 jz loc_46EACB
.text:0046EC26 mov edx, esi
.text:0046EC28 mov ecx, edi
.text:0046EC2A test dword ptr [eax+4], 4
.text:0046EC31 jz loc_46EACB
.text:0046EC37 call @WmiTraceContextSwap@8 ; WmiTraceContextSwap(x,x)
.text:0046EC3C jmp loc_46EACB
.text:0046EC41 ; ---------------------------------------------------------------------------
.text:0046EC41
.text:0046EC41 loc_46EC41: ; CODE XREF: SwapContext+28↑j
.text:0046EC41 push 0B8h ; BugCheckCode
.text:0046EC46 call _KeBugCheck@4 ; KeBugCheck(x)
.text:0046EC46 SwapContext endp
.text:0046EC46
.text:0046EC4B ; ---------------------------------------------------------------------------
.text:0046EC4B retn
.text:0046EC4C

通过

1
2
3
4
5
.text:0046EAB7	push    ecx			//备份指向ExceptionList异常链表的指,esp指向存储ExceptionList地址
...
.text:0046EAE1 mov [edi+28h], esp //***保存旧线程的KernelStack = esp***
...
.text:0046EB22 mov esp, [esi+28h] //esp = 新线程KTHREAD.KernelStack

可以得知:线程切换时新线程的ESP来自于新线程的KTHREAD.KernelStack

之前学习的API3环进0环的ESP0来自于TSS.ESP0。而TSS.ESP0是在SwapContext这一行代码更新的:

1
2
.text:0046EB1C	mov     ecx, [ebx+40h]		//ecx = TSS
.text:0046EB1F mov [ecx+4], eax //更新TSS.esp0 = TrapFrame.HardwareSegSs

关于0环线程堆栈的使用情况,可以参考 5.4 TSS处理。

总结如下(摘自《Windows内核原理与实现3.5章节》):

  • 等待将要被切换过去的新线程的切换标志(KTHREAD的SwapBusy域)被清除。
  • 当前处理器上的线程切换计数器增1。
  • 原来线程的异常链表头保存到栈中。
  • 根据需要保存协处理器的状态。
  • 保存原来线程的栈指针,保存到KTHREAD的KernelStack域中。
  • 检查新线程的协处理器状态,判断CR0寄存器与原来线程的CR0寄存器是否匹配,如果不是,则重新加载新线程的CR0寄存器。
  • 设置新线程的栈指针,即新线程的KTHREAD对象的KernelStack域。
  • 判断新线程是否与原来的线程同属于一个进程。如果不是,则先维护一下进程对象的ActiveProcessors域,然后判断LDT是否匹配,如果不匹配,则加载LDT,同时更新IDT的INT 21项。接下来,让CR3寄存器指向新的目录表,这样把地址空间切换到新进程中。
  • 清除原来线程的切换标志(KTHREAD的SwapBusy域)。
  • 设置当前处理器KPRCB中的TEB,指向新线程的TEB,并且设置GDT(全局描述符表)中的TEB项也指向新线程的TEB。
  • 调整初始的内核栈地址,并设置到TSS的Esp0域。
  • 新线程的环境切换次数增1(即KTHREAD的ContextSwitches域)。
  • 恢复线程的异常链表头。
  • 判断当前是否在一个DPC中进行线程切换,如果是,则调用BugCheck,蓝屏,在Windows中这是不允许的。
  • 判断在新线程中是否有内核模式的AP℃正在等待处理,如果有,则调用HAL函数HalRequestSoftwareInterrupt以请求一个APC_LEVEL的软件中断,然后返回。如果没有APC在等待处理,则直接返回。

5 线程切换

有两种情况会发生线程切换,分为主动切换和被动切换。

  • 主动切换是通过调用一些系统提供的API,在这些API中会调用KiSwapThread,而该函数通过调用SwapContext来实现线程切换。
  • 被动切换则发生在线程分配的时限用完或者被抢占的时候,此时KiDispatchInter会处理这两种情况,而无论哪种情况,最后都会通过调用函数SwapContext来切换线程,具体情况如下图所示:

38.png

上图摘自《Windows内核原理与实现》。

线程切换的三种情况:

  1. 当前线程主动调用API:KiSwapThread --> KiSwapContext --> SwapContext
  2. 当前线程时间片到期:KiDispatchInterrupt --> KiQuantumEnd --> SwapContext。或者有备用线程(KPCR.PrcbData.NextThread != NULL):KiDispatchInterrupt --> SwapContext
  3. 异常处理。

5.1 主动切换

绝大部分系统内核函数都会调用SwapContext函数,来实现线程的切换,那么这种切换是线程主动调用的

主动切换API调用的大致流程: xxxx() --> KiSwapThread --> KiSwapContext --> SwapContext

可以在IDA中使用交叉引用查看哪些函数调用了SwapContext,逐级往上交叉引用向上查看发现会有许多API会调用,从而导致线程切换。

  1. Windows中绝大部分API都调用了SwapContext函数。也就是说,当线程只要调用了API,就是导致线程切换。
  2. 线程切换时会比较是否属于同一个进程,如果不是,切换Cr3。Cr3换了,进程也就切换了。

具体的细节可以参考:Windows内核学习笔记之线程(下)

5.2 时钟中断切换

如何中断一个正在执行的程序?

  • 异常:比如缺页,或者 INT N 指令。
  • 中断:比如时钟中断。

系统时钟中断:时钟中断发生时,会转化为INT 0x30中断号。

39.png

Windows系列操作系统:10 - 20 毫秒。
如要获取当前的时钟间隔值,可使用Win32 API:GetSystemTimeAdjustment

⚠️说明:并不是每次时钟中断发生都会导致线程切换,时钟中断导致线程切换的条件是:

  • 时间片用完
  • 有备用线程(KPCR.PrcbData.NextThread != NULL

这两种方式都是因为时钟中断触发的,将在下面逆向分析函数KiSwapThread时详细解释。

下面将跟一下时钟中断触发int 0x30调用过程:

  1. 在IDA Pro快捷键Alt+T输入_IDT搜索,找到int 0x30对应的地址为函数KiStartUnexpectedRange()
  2. 有两条路线:
    • KiStartUnexpectedRange() --> KiEndUnexpectedRange() --> _KiUnexpectedInterruptTail() --> Hal!HalBeginSystemInterrupt(x,x,x) --> retn
    • KiStartUnexpectedRange() --> KiEndUnexpectedRange() --> _KiUnexpectedInterruptTail() --> Hal!HalEndSystemInterrupt(x,x) --> ntkrnlpa!KiDispatchInterrupt() --> SwapContext()

总结:线程切换的几种情况:

  1. 主动调用API函数

  2. 时钟中断

  3. 异常处理

如果一个线程不调用API,在代码中屏蔽中断(CLI指令),并且不会出现异常,那么当前线程将永久占有CPU,单核占有率
100% ,2核就是50%。

5.3 时间片管理

在上一节中我们讲过了,时钟中断会导致线程进行切换,但并不是说只要有时钟中断就一定会切换线程,时钟中断时,两种情况会导致线程切换:
1、当前的线程CPU时间片到期
2、有备用线程(KPCR.PrcbData.NextThread)

关于CPU时间片:

1、当一个新的线程开始执行时,初始化程序会在_KTHREAD.Quantum赋初始值,该值的大小由_KPROCESS.ThreadQuantum决定。
2、每次时钟中断会调用KeUpdateRunTime函数,该函数每次将当前线程Quantum减少3个单位,如果减到0,则将KPCR.PrcbData.QuantumEnd的值设置为非0,表示时间片到期。
3、KiDispatchInterrupt判断时间片到期:调用KiQuantumEnd(重新设置时间片、找到要运行的线程)。

  1. 查看_KPROCESS.ThreadQuantum

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    kd> !process 0 0
    **** NT ACTIVE PROCESS DUMP ****
    PROCESS 86085020 SessionId: 0 Cid: 0b20 Peb: 7ffd9000 ParentCid: 03f0
    DirBase: 06e401c0 ObjectTable: e22db780 HandleCount: 145.
    Image: wuauclt.exe

    kd> dt _KPROCESS 86085020
    ntdll!_KPROCESS
    ...
    +0x063 ThreadQuantum : 6 ''
    ...
  2. 分析KeUpdateRunTime函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    ...
    .text:0046E268 loc_46E268: ; CODE XREF: KeUpdateRunTime(x)+FA↑j
    .text:0046E268 ; KeUpdateRunTime(x)+103↑j ...
    .text:0046E268 sub [ebx+_ETHREAD.Tcb.Quantum], 3 //Quantum -= 3
    .text:0046E26C jg short loc_46E287 //Quantum > 0 --> pop ebx;retn 4;
    .text:0046E26E cmp ebx, [eax+_KPCR.PrcbData.IdleThread] //Quantum == 0,
    //ebx == CurrentThread == IdleThread
    .text:0046E274 jz short loc_46E287 //如果当前线程为空闲线程则直接返回不切换线程
    .text:0046E276 mov [eax+_KPCR.PrcbData.QuantumEnd], esp //QuantumEnd = 非0值,表示时间片用完
    .text:0046E27C mov ecx, 2 //ecx为参数
    .text:0046E281 call ds:__imp_@HalRequestSoftwareInterrupt@4 ; HalRequestSoftwareInterrupt(x)
  3. 分析KiDispatchInterrupt(x)函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    .text:0046E9DF loc_46E9DF:                             ; CODE XREF: KiDispatchInterrupt()+10↑j
    .text:0046E9DF sti
    .text:0046E9E0 cmp [ebx+_KPCR.PrcbData.QuantumEnd], 0 //QuantumEnd != 0表示时间片已用完
    .text:0046E9E7 jnz loc_46EA6E //发生跳转:时间片已用完
    .text:0046E9ED cmp [ebx+_KPCR.PrcbData.NextThread], 0
    .text:0046E9F4 jz short locret_46EA65 //NextThread == NULL
    .text:0046E9F6 cli //如果时间片没用完,但是有备用线程,会向下执行
    ...
    .text:0046EA18 sti
    .text:0046EA19 mov eax, [ebx+_KPCR.PrcbData.NextThread]
    ...
    .text:0046EA6E loc_46EA6E: ; CODE XREF: KiDispatchInterrupt()+37↑j
    .text:0046EA6E mov [ebx+_KPCR.PrcbData.QuantumEnd], 0 //QuantumEnd = 0设置时间片为未用完
    .text:0046EA78 call _KiQuantumEnd@0 ; KiQuantumEnd()
    .text:0046EA7D or eax, eax
    .text:0046EA7F jnz short loc_46EA1F
    .text:0046EA81 retn
    .text:0046EA81 _KiDispatchInterrupt@0 endp
    ...
    KiQuantumEnd()
    ...
    .text:0046E9DF loc_46E9DF: ; CODE XREF: KiDispatchInterrupt()+10↑j
    .text:0046E9DF sti
    .text:0046E9E0 cmp [ebx+_KPCR.PrcbData.QuantumEnd], 0
    .text:0046E9E7 jnz loc_46EA6E //如果时间片用完就跳转过去将时间片设置为0
    .text:0046E9ED cmp [ebx+_KPCR.PrcbData.NextThread], 0
    .text:0046E9F4 jz short locret_46EA65 //如果时间片用完,且NextThread == NULL
    .text:0046E9F6 cli
    ...
    .text:0046EA18 sti
    .text:0046EA19 mov eax, [ebx+_KPCR.PrcbData.NextThread]//eax = NextThread
    .text:0046EA1F
    .text:0046EA1F loc_46EA1F: ; CODE XREF: KiDispatchInterrupt()+CF↓j
    .text:0046EA1F sub esp, 0Ch
    .text:0046EA22 mov [esp+0Ch+var_4], esi
    .text:0046EA26 mov [esp+0Ch+var_8], edi
    .text:0046EA2A mov [esp+0Ch+var_C], ebp
    .text:0046EA2D mov esi, eax ; esi = NextThread
    .text:0046EA2F mov edi, [ebx+_KPCR.PrcbData.CurrentThread]//edi = CurrentThread
    .text:0046EA35 mov [ebx+_KPCR.PrcbData.NextThread], 0
    .text:0046EA3F mov [ebx+_KPCR.PrcbData.CurrentThread], esi
    .text:0046EA45 mov ecx, edi //ecx = edi = CurrentThread
    .text:0046EA47 mov [edi+_KTHREAD.IdleSwapBlock], 1
    .text:0046EA4B call @KiReadyThread@4 ; KiReadyThread(x)
    .text:0046EA50 mov cl, 1
    .text:0046EA52 call SwapContext
    .text:0046EA57 mov ebp, [esp+0Ch+var_C]
    .text:0046EA5A mov edi, [esp+0Ch+var_8]
    .text:0046EA5E mov esi, [esp+0Ch+var_4]
    .text:0046EA62 add esp, 0Ch
    ...
    .text:0042B041 loc_42B041: ; CODE XREF: KiQuantumEnd()+31↑j
    .text:0042B048 mov al, [eax+_KPROCESS.ThreadQuantum]
    .text:0042B04B mov [esi+_KTHREAD.Quantum], al
    ...
    .text:0042B081 call @KiFindReadyThread@8 ; KiFindReadyThread(x,x)

    KiDispatchInterrupt(x)函数会判断时间片是否用完,

    • 如果已经用完:则_KPCR.PrcbData.QuantumEnd = 0,继续调用KiQuantumEnd(),该函数会更新KPCR当前线程和下一个线程的值,然后调用KiReadyThread(x)将当前线程挂到就绪链表中,至于挂到哪一个级别的就绪链表中,代码会判断比对线程的优先级,由于32个不同级别的链表都有对应的链表头,所以只要使用相应链表头即可进行挂链。
    • 如果没有用完:会判断是否有备用线程(cmp [ebx+_KPCR.PrcbData.NextThread], 0),如果有备用线程会继续执行,然后进行线程切换。(即使当前线程的CPU时间片没有到期,仍然会被切换)

总结:线程切换的三种情况:

  1. 当前线程主动调用API:KiSwapThread --> KiSwapContext --> SwapContext
  2. 时钟切换
    • 当前线程时间片到期:KiDispatchInterrupt --> KiQuantumEnd --> SwapContext
    • 有备用线程(KPCR.PrcbData.NextThread != NULL):KiDispatchInterrupt --> SwapContext
  3. 异常处理:异常会触发int 0xIndex中断号,从而经过函数调用进行线程切换。

5.4 TSS处理

SwapContext这个函数是Windows线程切换的核心,无论是主动切换还是系统时钟导致的线程切换,最终都会调用这个函数。

Intel设计TSS的目的是为了任务切换(线程切换),但Windows与Linux并没有使用。而是采用堆栈来保存线程的各种寄存器。

Intel设计TSS在任务(线程)切换时起着重要的作用,通过它保存CPU中各寄存器的值,实现任务的挂起和恢复。但是微软使用线程切换来实现任务的切换,仅使用到TSS中的ESP0CR3

76.png

通过逆向分析SwapContext函数可知,线程在0环的堆栈如下:

40.png

SwapContext函数用到TSS的3个地方:

  • 更新TSS中ESP0,从3环通过API进入0环时的ESP0就从TSS中拿。(可解决:一个CPU只有一个TSS,但是线程很多,如何用一个TSS来保存所有线程的ESP0呢?)

    1
    .text:0046EB1F	 mov [ecx+4], eax	//更新TSS.esp0 = TrapFrame.HardwareSegSs
  • 更新CR3和IO权限位图(Windows2000以后没用了)。

    1
    2
    3
    .text:0046EB69	mov     [ebp+1Ch], eax		//更新TSS中的CR3
    .text:0046EB6C mov cr3, eax //切换CR3
    .text:0046EB6F mov [ebp+66h], cx //更新TSS.IOMap=cx,Windows2000以后没用了

分析3环调用API进0环用TSS.ESP0:

1、中断调用:通过TSS.ESP0得到0环堆栈。(还会使用到TSS.SS)
2、快速调用:从MSR得到一个临时0环栈,代码执行后仍然通过TSS.ESP0得到当前线程0环堆栈。

5.5 FS寄存器

  • 3环:FS:[0]指向TEB
  • 0环:FS:[0]指向KPCR

在3环,可以通过FS:[0]找到当前线程的Teb,FS == 0x3B -- 00111 0 11 -- Index == 0x7,即3环的FS:[0]对应的是GDT表的第8项。

在逆向分析SwapContext函数时,会将GDT表中第8项的基地址(BaseAddress)进行更新,使在3环通过FS:[0]即可获得Teb的地址。

GDT表的第8项:0x8003f038

1
2
3
kd> r gdtr
gdtr=8003f000
8003f000+0x8*0x7 == 0x8003f038
  1. SwapContext函数中首先会更新KPCR.NtTib.Self = Teb

    1
    2
    .text:0046EB25	mov     eax, [esi+20h]		//eax = 新线程的Teb
    .text:0046EB28 mov [ebx+18h], eax //更新KPCR.NtTib.Self = Teb
  2. 然后更新GDT表的第8项:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    .text:0046EB83 loc_46EB83:                      ; CODE XREF: SwapContext+E3↑j
    .text:0046EB83 mov eax, [ebx+18h] //eax = KPCR.NtTib.Self
    //此时eax指向了TEB(地址0x0046EB28处更新的)
    .text:0046EB86 mov ecx, [ebx+3Ch] //ecx = KPCR.GDT
    //假设GDT表在0x8003f000,ecx = 0x8003f000
    //3环FS = 0x3B,所以FS在GDT表里的地址是0x8003f000+0x8*0x7
    //(Index == 0x7) = 0x8003f038
    //0x8003f038为FS[0]_R3的GDT段描述符
    //下面的操作是修改FS的段描述符,这样3环FS就能找到TEB了
    .text:0046EB89 mov [ecx+3Ah], ax //0x8003f03A为段描述符BaseAddress起始地址
    //更新GDT描述符BaseAddress的低2字节(0~15bit)
    .text:0046EB8D shr eax, 10h //eax = TEB:16~31bit
    .text:0046EB90 mov [ecx+3Ch], al //更新GDT描述符BaseAddress的16~23bit
    .text:0046EB93 mov [ecx+3Fh], ah //更新GDT描述符BaseAddress的23~31bit

    8003f038 0040f300`00000fff

    • 0000KPCR.NtTib.Self低16位
    • 00KPCR.NtTib.Self高2字节的低8位
    • 00KPCR.NtTib.Self高2字节的高8位

    GDT段描述符如下:

    9.png

⚠️注意:在KiFastCallEntry / KiSystemService中 FS 值由0x3B变成0x30;在KiSystemCallExit / KiSystemCallExitBranch / KiSystemCallExit2中再将 RING3 的 FS 恢复。

5.6 线程优先级

之前讲过有三种情况会导致线程切换,在KiSwapThreadKiQuantumEnd函数中都是通过KiFindReadyThread来找下一个要切换的线程,KiFindReadyThread是根据什么条件来选择下一个要执行的线程呢?
调度链表有32个,每次都从头开始查找效率太低,所以Windows都过一个DWORD类型变量的变量来记录,正好是32位,一个位代表一个链表,当向调度链表.中挂入或者摘除某个线程时,会判断当前级别的链表是否为空,为空将.变量对应位置0,否则置1,这个变量就是_kiReadySummary。多CPU会随机寻找KiDispatcherReadyListHead指向的数组中的线程,线程可以绑定某个CPU,可以使用APISetThreadAffinityMask进行设置。

  1. 使用 KiDispatcherReadyListHead 查看处理32个不同等级的就绪线程双向链表,每个等级的双向链表有两项(_LIST_ENTRY)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    kd> dd KiDispatcherReadyListHead L50
    8055df80 8055df80 8055df80 8055df88 8055df88
    8055df90 8055df90 8055df90 8055df98 8055df98
    8055dfa0 8055dfa0 8055dfa0 8055dfa8 8055dfa8
    8055dfb0 8055dfb0 8055dfb0 8055dfb8 8055dfb8
    8055dfc0 8055dfc0 8055dfc0 8055dfc8 8055dfc8
    8055dfd0 8055dfd0 8055dfd0 8055dfd8 8055dfd8
    8055dfe0 8055dfe0 8055dfe0 8055dfe8 8055dfe8
    8055dff0 8055dff0 8055dff0 8055dff8 8055dff8
    8055e000 8055e000 8055e000 8055e008 8055e008
    8055e010 8055e010 8055e010 8055e018 8055e018
    8055e020 8055e020 8055e020 8055e028 8055e028
    8055e030 8055e030 8055e030 8055e038 8055e038
    8055e040 8055e040 8055e040 8055e048 8055e048
    8055e050 8055e050 8055e050 8055e058 8055e058
    8055e060 8055e060 8055e060 8055e068 8055e068
    8055e070 8055e070 8055e070 8055e078 8055e078
    8055e080 00000000 00000000 00000000 00000000
    8055e090 00000000 00000000 00000000 00000000
    • 空链表:Flink == Blink == 地址,0x8055df80 == 0x8055df80 == 0x8055df80
    • 链表有一个线程:Flink == Blink != 地址。
  2. KiFindReadyThread查找方式:按照优先级别进行查找:31..30..29..28…..也就是说,在本次查找中,如果级别31的链表里面有线程,那么就不会查找级别为30的链表!地址越高,优先级越高。(具体查找请见 7 逆向分析KiFindReadyThread函数)。

  3. 高效查找:通过这个变量:_kiReadySummary来记录当向调度链表(32个)中挂入或者摘除某个线程时,会判断当前级别的链表是否为空,为空将DWORD变量对应位置0,否则置1

    44.png

  4. 多CPU会随机寻找KiDispatcherReadyListHead指向的数组中的线程。线程可以绑定某个CPU(使用api:setThreadAffinityMask)。

5.7 逆向分析-KiReadyThread函数

KiReadyThread(x)将当前线程挂到就绪链表中,至于挂到哪一个级别的就绪链表中,代码会判断比对线程的优先级,由于32个不同级别的链表都有对应的链表头,所以只要使用相应链表头即可进行挂链。

内核 API 函数 KiReadyThread 实现将旧线程的 _ETHREADECX 传参)添加到就绪链表中。

主要逻辑如下:

1、根据你的当前老线程,亲核,你最喜欢的哪个核SoftAffinity,闲置核去计算当前老的线程要挂在哪一个KPCR中的nextThread上。 2、如果Affinity、SoftAffinity闲置核计算没有找到,那么就会去下一个处理器KPCR挂上。

3、如果KPCR 中有下一个了,那么就对比优先级,看是挂老线程,还是用KPCR 以前的 。如果都挂不上,那就挂在WAITLIST 里面。 如果我当前老线程的优先级大于KPCR中当前线程优先级,那么在WAITLIST 那么就挂在当前优先级链表第一个,否则是当前优先级链表的最后一个。

45.png

具体过程可参考:线程被动切换(时间碎片) - KiReadyThread函数详细分析Windows线程调度学习(一)NT分发调度Windows内核学习笔记之线程(下

  • _KiIdleSummary:空闲位图(KiIdleSummary),Windows2000还维护一个称为空闲位图(KildleSummary)的32位量。空闲位图中的每一位指示一个处理机是否处于空闲状态。

  • _KiFindFirstSetLeft:查找_kiReadySummary中下标为1的位的位置。

  • _KiProcessorBlock:每个CPU都有一个KPCR结构体,那也就有KPCB结构体,在一个多核环境下就会有多个KPCB结构体,而全局变量KiProcessorBlock保存的就是不同的核对应的KPCB结构体地址。

  • _KiReadySummary:_kiReadySummary来记录当向调度链表(32个)中挂入或者摘除某个线程时,会判断当前级别的链表是否为空,为空将DWORD变量对应位置0,否则置1

  • _KiProcessInSwapListHead:单链表项,当一个进程被换入内存时,它通过此域加入到以KiProcessInSwapListHead为链头的单链表中。

  • _KiProcessOutSwapListHead:单链表项,当一个进程要被换出内存时,它通过此域加入到KiProcessOutSwapListHead为链头的单链表中

  • _KiStackInSwapListHead:将内核堆栈交换进来(需要交换进来的线程存放在KiStackInSwapListHead中)。

    • 交换事件由KiSwapEvent触发。有四种不同类型的事件。

        - 将内核堆栈交换出去(由BOOLEAN KiStackOutSwapRequest指定);

        - 将进程交换出去 (需要交换出去的进程存放在KiProcessOutSwapListHead中)

          - 将进程交换进来 (需要交换出去的进程存放在KiProcessInSwapListHead中)
      
              - 将内核堆栈交换进来(需要交换进来的线程存放在KiStackInSwapListHead中).
      

6 逆向分析-KiSwapThread函数

KiSwapThread/KiSwapContex/SwapContex 直接的调用关系如下:

41.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
.text:0040AB8A ; =============== S U B R O U T I N E =======================================
.text:0040AB8A
.text:0040AB8A
.text:0040AB8A ; _DWORD __cdecl KiSwapThread()
.text:0040AB8A @KiSwapThread@0 proc near ; CODE XREF: KeDelayExecutionThread(x,x,x):loc_40A5F3↑p
.text:0040AB8A ; KeWaitForMultipleObjects(x,x,x,x,x,x,x,x):loc_40AB6E↑p ...
.text:0040AB8A mov edi, edi //热备
.text:0040AB8C push esi
.text:0040AB8D push edi
.text:0040AB8E mov eax, large fs:20h //eax = _KPRCB
.text:0040AB94 mov esi, eax //esi = eax = _KPRCB
.text:0040AB96 mov eax, [esi+_KPRCB.NextThread] //eax = 新线程_KTHREAD
.text:0040AB99 test eax, eax
.text:0040AB9B mov edi, [esi+_KPRCB.CurrentThread] //edi = 旧线程_KTHREAD
/*----------------------------
如果有NextThread则直接进行线程切换
-----------------------------*/
.text:0040AB9E jnz loc_4109B9 //已获得NextThread,将_KPCR._KPRCB.NextThread的值清空
//并调用KiSwapContext(x)进行线程切换
/*---------------------
没有NextThread就找就绪线程
---------------------*/
.text:0040ABA4 push ebx //未获取到NextThread的_KTHREAD
//说明当前的CPU中没有其他就绪线程
.text:0040ABA5 movsx ebx, [esi+_KPRCB.Number] //将当前CPU编号进行有符号扩展
//将_KPRCB.Number扩展为ebx(32位),高位补符号
.text:0040ABA9 xor edx, edx //edx == 0(edx:参数1)
.text:0040ABAB mov ecx, ebx //ecx = ebx = 当前CPU编号(ebx:参数2)
.text:0040ABAD call @KiFindReadyThread@8 ; //在线程就绪队列中寻找就绪线程,KiFindReadyThread(x,x)
/*--------------------
没有就绪线程就调用空闲线程
--------------------*/
.text:0040ABB2 test eax, eax //eax是指向就绪线程_KTHREAD的指针
.text:0040ABB4 jz loc_4107BE //跳转:没有就绪线程,将会默认执行空闲线程
//eax将会获取到空闲线程的_KTHREAD
.text:0040ABBA
.text:0040ABBA loc_40ABBA: ; CODE XREF: KiSwapThread()+5C54↓j
.text:0040ABBA ; KiSwapThread()+5C78↓j ...
.text:0040ABBA pop ebx
.text:0040ABBB
/*-----------------------
已经找到就绪线程,进行线程切换
-----------------------*/
.text:0040ABBB loc_40ABBB: ; CODE XREF: KiSwapThread()+5E33↓j
.text:0040ABBB mov ecx, eax //ecx = eax = 就绪线程_KTHREAD(ecx:参数)
.text:0040ABBD call @KiSwapContext@4 ; KiSwapContext(x)
.text:0040ABC2 test al, al
.text:0040ABC4 mov cl, [edi+_KTHREAD.WaitIrql] ; NewIrql
.text:0040ABC7 mov edi, [edi+_KTHREAD.WaitStatus]
.text:0040ABCA mov esi, ds:__imp_@KfLowerIrql@4 ; KfLowerIrql(x)
.text:0040ABD0 jnz loc_41BC56
.text:0040ABD6
.text:0040ABD6 loc_40ABD6: ; CODE XREF: KiSwapThread()+110DC↓j
.text:0040ABD6 call esi ; KfLowerIrql(x) ; KfLowerIrql(x)
.text:0040ABD8 mov eax, edi
.text:0040ABDA pop edi
.text:0040ABDB pop esi
.text:0040ABDC retn
.text:0040ABDC @KiSwapThread@0 endp

接着分析,如果没有就绪线程,将会默认执行空闲线程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.text:004107BE loc_4107BE:                             ; CODE XREF: KiSwapThread()+2A↑j
.text:004107BE mov eax, [esi+_KPRCB.IdleThread] //eax = 空闲线程
.text:004107C1 xor edx, edx //edx == 0
.text:004107C3 inc edx //edx == 1
.text:004107C4 mov ecx, ebx //ecx = 当前CPU编号
//如果不存在就绪线程则把当前CPU添加到KiIdleSummary
.text:004107C6 shl edx, cl //edx = 0010
.text:004107C8 or ds:_KiIdleSummary, edx //_KiIdleSummary | 0010
.text:004107CE mov ecx, [esi+_KPRCB.MultiThreadProcessorSet]
.text:004107D4 mov edx, ecx
.text:004107D6 and edx, ds:_KiIdleSummary
.text:004107DC cmp edx, ecx
.text:004107DE jnz loc_40ABBA
.text:004107E4 or ds:_KiIdleSMTSummary, ecx
.text:004107EA lea ecx, [esi+_KPRCB.MultiThreadSetMaster]
.text:004107F0 mov edx, [ecx]
.text:004107F2 mov byte ptr [edx+4C8h], 0
.text:004107F9 mov ecx, [ecx]
.text:004107FB cmp byte ptr [ecx+0BCDh], 1
.text:00410802 jnz loc_40ABBA
.text:00410808 jmp loc_44B8CE //经过一波设置后,就去进行线程切换(_KPRCB.IdleThread)
  • 就绪位图(KiReadySummary)
    为了提高调度速度,Windows 2000维护了一个称为就绪位图(KiReadySummary)的32位量。就绪位图中的每一位指示一个
    调度优先级的就绪队列中是否有线程等待运行。B0与调度优先级0相对应,B1与调度优先级1相对应,等待。
  • 空闲位图(KiIdleSummary)
    Windows2000还维护一个称为空闲位图(KildleSummary)的32位量。空闲位图中的每一位指示一个处理机是否处于空闲状态。
  • 调度器自旋锁(KiDispatcherLock)
    为了防止调度器代码与线程在访问调度器数据结构时发生冲突,处理机调度仅出现在DPC调度层次。但在多处理机系统中,修
    改调度器数据结构需要额外的步骤来得到内核调度器自旋锁(KiDispatcherLock),以协调各处理机对调度器数据结构的访问。

6.1 空闲线程-KiIdleLoop函数

在函数KiSwapThread通过以下两行代码可以找到空闲线程执行的函数:

1
2
.text:0046ED34		lea     ecx, [ebx+_KPCR.PrcbData.PowerState.IdleFunction]
.text:0046ED3A call dword ptr [ecx]

而这里循环调用的地址IdleFunction可以在ETHREAD.StartAddress(+0x224)得到,但是使用如下方法看不到空闲线程的ETHREAD.StartAddress

  1. 得到KPCR.KPRCB:

    1
    2
    3
    4
    5
    6
    kd> r fs
    fs=00000030
    kd> r gdtr
    gdtr=8003f000
    kd> dq 8003f000+0x8*0x6
    8003f030 ffc093df`f0000001 0040f300`00000fff

    FS[0].Base = 0xFFDFF000,查看IdleThread地址:0x8055ce60

    1
    2
    3
    4
    5
    6
    7
    kd> dt _KPRCB 0xFFDFF120
    ntdll!_KPRCB
    +0x000 MinorVersion : 1
    +0x002 MajorVersion : 1
    +0x004 CurrentThread : 0x8055ce60 _KTHREAD
    +0x008 NextThread : (null)
    +0x00c IdleThread : 0x8055ce60 _KTHREAD

    接着查看IdleThread.StartAddress(+0x224):NULL。

    1
    2
    3
    4
    5
    kd> dt _ETHREAD 0x8055ce60
    ntdll!_ETHREAD
    ...
    +0x224 StartAddress : (null)
    ...

则只能通过空闲线程的堆栈来查看了,可以这样查看的原因:当前运行的线程正是空闲线程0x8055ce60

  1. 分析函数SwapContext可知,线程切换(mov esp, [esi+28h])后,堆栈操作如下:

    1
    2
    3
    4
    5
    .text:0046EB9F               pop     ecx	//ecx = ExceptionList 异常链表
    ...
    .text:0046EBA8 popf //pop eflag
    ...
    .text:0046EBAB retn //跳转至新线程的ETHREAD.StartAddress(+0x224)去执行
  2. 经过上面分析,线程切换后0环堆栈中应该如下:

    1
    2
    3
    esp0:		--> ExceptionList(KTHREAD.KernelStack)
    esp0+0x4: --> eflag
    esp0+0x8: --> retn(ETHREAD.StartAddress)

    ⚠️这里需要特别注意:线程切换后的堆栈是上面这样的,而线程从3环进0环使用的TrapFrame并不是线程切换,不要弄混了

  3. 查看IdleThread的堆栈:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    kd> dt _KTHREAD 0x8055ce60
    ntdll!_KTHREAD
    ...
    +0x028 KernelStack : 0x8055244c Void
    ...

    kd> dd 0x8055244c
    8055244c 00000000 ffdff980 80546d3c 00000000

    kd> u 80546d3c
    nt!KiIdleLoop+0x10:
    80546d3c f390 pause
    80546d3e fb sti
    80546d3f 90 nop
    80546d40 90 nop
    80546d41 fa cli
    80546d42 3b6d00 cmp ebp,dword ptr [ebp]
    80546d45 740d je nt!KiIdleLoop+0x28 (80546d54)
    80546d47 b102 mov cl,2
  4. 可以看到IdleThreadKiIdleLoop函数

43.png

7 逆向分析-KiFindReadyThread函数

  1. 该函数做了三件事:

        ① 解析KiReadySummary,找到从左起第一个为1的位数;

        ② 用该位获取从KiDispatchReadListHead中的第一个_KTHREAD线程,将其从链表中摘除;

        ③ 如果摘除后该链表为空,则找到相应的KiReadySummary位将其置0。

  2. 函数作用:当线程切换发生时,要调用KiFindReadyThread函数从调度链表里找到下一个就绪线程并通过EAX寄存器返回。

  3. KiFindReadyThread 的参数个数。

    1
    __fastcall KiFindReadyThread(x, x)

    快速调用:

    • ECX传参数1:CPU编号。

      1
      2
      movsx   ebx, [esi+_KPRCB.Number]
      mov ecx, ebx
    • EDX传参数2:0(是最低优先级)。

      1
      xor     edx, edx

    通过IDA的分析函数有两个参数,阅读XP的源码(目录/NT/base/ntos/ke/thredsup.c),发现 KiFindReadyThread 的声明是这样的:grep --color=auto -rns "KiFindReadyThread" *

    1
    2
    3
    4
    PKTHREAD FASTCALL KiFindReadyThread (
    IN ULONG ProcessorNumber,
    IN KPRIORITY LowPriority
    );
    • ProcessorNumber:是 CPU 编号,从 KPCR 里获取 ,单核模式下这个参数是没有用的;
    • LowPriority:是最低优先级,KiSwapThread 里调用,传的是0。举例说明,如果这个参数是8,那么等价于 _KiReadySummary 的低8位置0,也就是忽略优先级0-7的线程。
  4. KiFindFirstSetLeft:

    KiFindFirstSetLeft 是一个全局的字节数组,大小是256字节,在/NT/base/ntos/ke/kernldat.c:328定义如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    const CCHAR KiFindFirstSetLeft[256] = {
    0, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3,
    4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
    5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
    5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
    6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
    6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
    6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
    6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
    7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
    7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
    7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
    7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
    7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
    7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
    7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
    7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7};

    这个数组配合下面的宏,可以高效的找到32位里左起第一个置1位的位置:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // 一个比较关键的宏函数,作用是找到32位整型变量 Set 里左起第一个置1的位的下标,存储到 Member 里
    // 算法分析:
    // 把32位分成4字节,两轮二分,确定了左起第一个“有1”的字节的偏移,记录在 _Offset
    // Set >> _Offset 是把第一个有1的字节移到低8位
    // KiFindFirstSetLeft[Set >> _Offset] 得到的是8位里左起第1个置1位的位置,如 0000 0001 得到的是0,0011 0000 得到的是5
    // KiFindFirstSetLeft[Set >> _Offset] + _Offset 得到的是在整个32位里,左起第一个置1的位的位置
    #define KeFindFirstSetLeftMember(Set, Member) { \
    ULONG _Mask; \
    ULONG _Offset = 16; \
    if ((_Mask = Set >> 16) == 0) { \
    _Offset = 0; \
    _Mask = Set; \
    } \
    if (_Mask >> 8) { \
    _Offset += 8; \
    } \
    *(Member) = KiFindFirstSetLeft[Set >> _Offset] + _Offset; \
    }
  5. 使用二分法查找从左边起下标为1的位:

    46.png

  6. 之前我们介绍过,其存在32个就绪链表,0-31序号,在Windows操作系统中,其存在一个全局变量KiReadySummary,32位,如果哪个存在就绪链表,其该位被置1。

    这样,我们直接检测其KiReadySummary全局变量是否为0就判断是否存在就绪链表。

    其从低位到高位优先级依次升高,因此需要找从左边数第一个为1的位数下标,该位下标作为索引从KiDispatcherReadyListHead数组中寻找对应优先级的线程链表

    找对应优先级的线程链表:$PKTHREAD\ Kthread = (PKTHREAD)(KiDispatcherReadyListHead[eax*8] - 60h)$

    47.png

7.1 KiFindReadyThread源码分析

将多核相关的预处理删除了,整理如下,添加了注释的 KiFindReadyThread 源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
const CCHAR KiFindFirstSetLeft[256] = {
0, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3,
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7};


// 一个比较关键的宏函数,作用是找到32位整型变量 Set 里左起第一个置1的位的下标,存储到 Member 里
// 算法分析:
// 把32位分成4字节,两轮二分,确定了左起第一个“有1”的字节的偏移,记录在 _Offset
// Set >> _Offset 是把第一个有1的字节移到低8位
// KiFindFirstSetLeft[Set >> _Offset] 得到的是8位里左起第1个置1位的位置,如 0000 0001 得到的是0,0011 0000 得到的是5
// KiFindFirstSetLeft[Set >> _Offset] + _Offset 得到的是在整个32位里,左起第一个置1的位的位置
#define KeFindFirstSetLeftMember(Set, Member) { \
ULONG _Mask; \
ULONG _Offset = 16; \
if ((_Mask = Set >> 16) == 0) { \
_Offset = 0; \
_Mask = Set; \
} \
if (_Mask >> 8) { \
_Offset += 8; \
} \
*(Member) = KiFindFirstSetLeft[Set >> _Offset] + _Offset; \
}


PKTHREAD FASTCALL KiFindReadyThread (
IN ULONG ProcessorNumber,
IN KPRIORITY LowPriority
)
{

ULONG HighPriority;
PRLIST_ENTRY ListHead;
PRLIST_ENTRY NextEntry;
ULONG PrioritySet;
KAFFINITY ProcessorSet;
PKTHREAD Thread;
PKTHREAD Thread1;
PKTHREAD Thread2 = NULL;
ULONG WaitLimit;
CCHAR Processor;

Processor = (CCHAR)ProcessorNumber;
PrioritySet = (~((1 << LowPriority) - 1)) & KiReadySummary; // _KiReadySummary 将低 LowPriority 位清0的值

KeFindFirstSetLeftMember(PrioritySet, &HighPriority); // HighPriority 等于左起第一个置1位的下标,表示该优先级有就绪线程
ListHead = &KiDispatcherReadyListHead[HighPriority]; // 找到该优先级的调度链表头
PrioritySet <<= (31 - HighPriority); // 此时最高位是左起第一个置1的位,如果值是0,说明没有就绪线程
while (PrioritySet != 0) {

//
// If the next bit in the priority set is a one, then examine the
// corresponding dispatcher ready queue.
//

// 如果最高位是1,则遍历这个优先级调度链表
if ((LONG)PrioritySet < 0) {
NextEntry = ListHead->Flink; // NextEntry 指向当前优先级调度链表里的第一个线程

ASSERT(NextEntry != ListHead); // 当前优先级置1,链表里却没有值,是不可能的

Thread = CONTAINING_RECORD(NextEntry, KTHREAD, WaitListEntry); // 计算 KTHREAD
RemoveEntryList(&Thread->WaitListEntry); // 从链表里删除该线程
if (IsListEmpty(ListHead)) {
ClearMember(HighPriority, KiReadySummary); // 如果该优先级的调度链表已经是空的,那么 KiReadySummary 相应的位清零
}

return Thread;
}

HighPriority -= 1;
ListHead -= 1;
PrioritySet <<= 1;
};

//
// No thread could be found, return a null pointer.
//

return NULL;
}

7.2 KiFindReadyThread逆向分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
.text:0042BFBC ; =============== S U B R O U T I N E =======================================
.text:0042BFBC
.text:0042BFBC ; Attributes: bp-based frame
.text:0042BFBC
.text:0042BFBC ; __fastcall KiFindReadyThread(x, x)
.text:0042BFBC @KiFindReadyThread@8 proc near ; CODE XREF: KiAdjustQuantumThread(x)+63↑p
.text:0042BFBC ; KeDelayExecutionThread(x,x,x)+12F↑p ...
.text:0042BFBC
.text:0042BFBC var_14 = dword ptr -14h
.text:0042BFBC var_10 = dword ptr -10h
.text:0042BFBC var_C = dword ptr -0Ch
.text:0042BFBC var_8 = dword ptr -8
.text:0042BFBC var_4 = dword ptr -4
.text:0042BFBC
.text:0042BFBC mov edi, edi //热补丁
.text:0042BFBE push ebp
.text:0042BFBF mov ebp, esp
.text:0042BFC1 sub esp, 14h
.text:0042BFC4 and [ebp+var_C], 0
.text:0042BFC8 push ebx
.text:0042BFC9 push esi
.text:0042BFCA xor esi, esi
.text:0042BFCC inc esi //esi == 1
.text:0042BFCD mov [ebp+var_10], ecx //参数4 = ecx = CPU编号
.text:0042BFD0 mov ecx, edx //ecx = edx = 0,LowPriority
.text:0042BFD2 shl esi, cl //1 << 0 == esi == 1
.text:0042BFD4 movsx ecx, byte ptr [ebp+var_10]//ecx = CPU编号
.text:0042BFD8 dec esi //esi = 0
.text:0042BFD9 push edi
.text:0042BFDA mov edi, _KeTickCount.LowPart//Windows8以前,Windows内核Security Cookie/ASLR使用系统时钟
//KeTickCount或RDTSC作为随机数源
/*----------------------------------------------
忽略优先级低于当前线程的就绪线程,KiReadySummary低位清0
----------------------------------------------*/
.text:0042BFE0 not esi //按位取反(翻转)
.text:0042BFE2 and esi, _KiReadySummary //全局变量 _KiReadySummary 有32位,对应32个就绪队列
//esi = (~((1 << LowPriority) - 1)) & _KiReadySummary
//此时 esi 存的是 _KiReadySummary 将低 LowPriority 位清0的值
.text:0042BFE8 xor edx, edx //下面是利用二分+位图实现了查找左起第一个置1位
.text:0042BFEA inc edx //edx == 1
.text:0042BFEB shl edx, cl //1<<CPU编号
.text:0042BFED mov eax, esi //eax = esi = 低位清0的_KiReadySummary
.text:0042BFEF sub edi, 3 //edi随机数 -= 3
.text:0042BFF2 shr eax, 10h //取KiReadySummary高16位
//如果结果是0,说明高16位全是0
//如果不是0,说明高16位至少有一个置1位
.text:0042BFF5 push 10h
.text:0042BFF7 mov [ebp+var_14], ecx //CPU编号
.text:0042BFFA pop ecx //ecx = 0x10
//ecx 在这里的作用是记录偏移,初始化为16,
//意思是假设高16位至少有一个置1的位
/*-----------
第一轮查找
跳转:高16位有1
-----------*/
.text:0042BFFB jnz short loc_42C001 //跳转:KiReadySummary高16位有1
/*-----------
低16位有1
-----------*/
.text:0042BFFD xor ecx, ecx
.text:0042BFFF mov eax, esi
.text:0042C001
/*-------------------------
第一轮查找
判断高/低16位中的1在高8位还是低8位
-------------------------*/
.text:0042C001 loc_42C001: ; CODE XREF: KiFindReadyThread(x,x)+3F↑j
.text:0042C001 test eax, 0FFFFFF00h //测试KiReadySummary高/低16位中的低8位
.text:0042C006 jz short loc_42C00B //跳转:说明1在KiReadySummary高/低16位中的低8位
.text:0042C008 add ecx, 8 //否则,否则偏移 +8
//此时 ecx 存储的是左起第一个置1位所在的字节内的偏移
//举例说明,假如 _KiReadySummary 是 0x30000000
//因为0x30 == 0011 0000,左起第一个1下标index = ecx = 5
.text:0042C00B
/*----------------------------
第一轮查找
1 在KiReadySummary高16位中的低8位
----------------------------*/
.text:0042C00B loc_42C00B: ; CODE XREF: KiFindReadyThread(x,x)+4A↑j
.text:0042C00B mov eax, esi //eax = esi = 低位清0的_KiReadySummary
.text:0042C00D shr eax, cl //KiReadySummary >> 10h,取KiReadySummary高16位
.text:0042C00F push 1Fh
.text:0042C011 movsx eax, ds:_KiFindFirstSetLeft[eax]//eax = KiReadySummary中有1的下标索引
.text:0042C018 add eax, ecx //eax += 基数
//若1在高16位,则ecx == 10h,16 <= eax <= 31
//若1在低16位,则ecx == 0h,0 <= eax <= 15
.text:0042C01A lea ecx, _KiDispatcherReadyListHead[eax*8]//ecx = 对应优先级的线程KTHREAD + 0x60
.text:0042C021 mov [ebp+var_4], ecx
.text:0042C024 pop ecx //ecx = 1Fh = 31
.text:0042C025 sub ecx, eax //ecx = 左起第 ecx 位是1所在的下标索引
.text:0042C027 shl esi, cl //esi = 低位清0的_KiReadySummary << ecx
//正常来说若KiReadySummary有其中某位是1,假设下标是M
//KiReadySummary左移M位 <= 31
.text:0042C029 test esi, esi //如果结果是0,表示没有就绪线程
.text:0042C02B jmp short loc_42C056
.text:0042C02D ; ---------------------------------------------------------------------------
.text:0042C02D
/*------
有就绪线程
------*/
.text:0042C02D loc_42C02D: ; CODE XREF: KiFindReadyThread(x,x)+9D↓j
.text:0042C02D test esi, esi
.text:0042C02F jge short loc_42C04F //esi >= 0
.text:0042C031 mov ecx, [ebp+var_4] //ecx = 找到第一就绪线程KTHREAD + 0x60
.text:0042C034 mov ebx, [ecx] //ebx = 第一个就绪线程.FLink = 下一个就绪线程KTHREAD + 0x60
.text:0042C036 cmp ebx, ecx
.text:0042C038 jz short loc_42C04F
.text:0042C03A
.text:0042C03A loc_42C03A: ; CODE XREF: KiFindReadyThread(x,x)+8E↓j
.text:0042C03A lea eax, [ebx-60h] //eax = 找到的就绪线程的KTHREAD
.text:0042C03D test [eax+_KTHREAD.Affinity], edx//CPU编号,测试亲核性
.text:0042C043 mov ebx, [ebx] //ebx = 下一个就绪线程
.text:0042C045 jnz short loc_42C062
.text:0042C047 cmp ebx, [ebp+var_4]
.text:0042C04A jnz short loc_42C03A
.text:0042C04C mov eax, [ebp+var_8]
.text:0042C04F
.text:0042C04F loc_42C04F: ; CODE XREF: KiFindReadyThread(x,x)+73↑j
.text:0042C04F ; KiFindReadyThread(x,x)+7C↑j
/*-----------
循环查找就绪线程
-----------*/
.text:0042C04F sub [ebp+var_4], 8 //将第一个就绪线程.FLink上移,使得第一个线程 = 下一个线程
.text:0042C053 dec eax //索引 -= 1
.text:0042C054 shl esi, 1 //KiReadySummary
.text:0042C056
.text:0042C056 loc_42C056: ; CODE XREF: KiFindReadyThread(x,x)+6F↑j
.text:0042C056 mov [ebp+var_8], eax //eax = 加基数的索引
.text:0042C059 jnz short loc_42C02D //有就绪线程
.text:0042C05B xor eax, eax //没有就绪线程,返回NULL
.text:0042C05D
.text:0042C05D loc_42C05D: ; CODE XREF: KiFindReadyThread(x,x)+137↓j
.text:0042C05D ; KiFindReadyThread(x,x)+14D↓j
.text:0042C05D pop edi
.text:0042C05E pop esi
.text:0042C05F pop ebx
.text:0042C060 leave
.text:0042C061 retn
.text:0042C062 ; ---------------------------------------------------------------------------
.text:0042C062
.text:0042C062 loc_42C062: ; CODE XREF: KiFindReadyThread(x,x)+89↑j
.text:0042C062 movzx ecx, [eax+_KTHREAD.IdealProcessor]//指明了在多处理器上该线程的理想处理器编号
.text:0042C069 mov esi, [ebp+var_14] //esi = CPU编号
.text:0042C06C cmp ecx, esi
.text:0042C06E jz short loc_42C0DA
.text:0042C070 movzx ecx, [eax+_KTHREAD.NextProcessor]
.text:0042C077 cmp ecx, esi
.text:0042C079 jnz short loc_42C083
.text:0042C07B test [eax+_KTHREAD.SoftAffinity], edx
.text:0042C081 jnz short loc_42C0DA
.text:0042C083
.text:0042C083 loc_42C083: ; CODE XREF: KiFindReadyThread(x,x)+BD↑j
.text:0042C083 cmp edi, [eax+_KTHREAD.WaitTime]
.text:0042C086 jnb short loc_42C0DA
.text:0042C088 cmp [ebp+var_8], 19h
.text:0042C08C jnb short loc_42C0DA
.text:0042C08E jmp short loc_42C0D1
.text:0042C090 ; ---------------------------------------------------------------------------
.text:0042C090
.text:0042C090 loc_42C090: ; CODE XREF: KiFindReadyThread(x,x)+118↓j
.text:0042C090 lea ecx, [ebx-60h]
.text:0042C093 test [ecx+124h], edx
.text:0042C099 mov ebx, [ebx]
.text:0042C09B mov [ebp+var_14], ebx
.text:0042C09E jz short loc_42C0D1
.text:0042C0A0 movzx ebx, [ecx+_KTHREAD.IdealProcessor]
.text:0042C0A7 cmp ebx, esi
.text:0042C0A9 jz short loc_42C0B6
.text:0042C0AB movzx ebx, [ecx+_KTHREAD.NextProcessor]
.text:0042C0B2 cmp ebx, esi
.text:0042C0B4 jnz short loc_42C0C9
.text:0042C0B6
.text:0042C0B6 loc_42C0B6: ; CODE XREF: KiFindReadyThread(x,x)+ED↑j
.text:0042C0B6 test [ecx+_KTHREAD.SoftAffinity], edx
.text:0042C0BC jnz short loc_42C0D8
.text:0042C0BE cmp [ebp+var_C], 0
.text:0042C0C2 jnz short loc_42C0C9
.text:0042C0C4 mov [ebp+var_C], ecx
.text:0042C0C7 mov eax, ecx
.text:0042C0C9
.text:0042C0C9 loc_42C0C9: ; CODE XREF: KiFindReadyThread(x,x)+F8↑j
.text:0042C0C9 ; KiFindReadyThread(x,x)+106↑j
.text:0042C0C9 cmp edi, [ecx+_KTHREAD.WaitTime]
.text:0042C0CC jnb short loc_42C0D8
.text:0042C0CE mov ebx, [ebp+var_14]
.text:0042C0D1
.text:0042C0D1 loc_42C0D1: ; CODE XREF: KiFindReadyThread(x,x)+D2↑j
.text:0042C0D1 ; KiFindReadyThread(x,x)+E2↑j
.text:0042C0D1 cmp ebx, [ebp+var_4]
.text:0042C0D4 jnz short loc_42C090
.text:0042C0D6 jmp short loc_42C0DA
.text:0042C0D8 ; ---------------------------------------------------------------------------
.text:0042C0D8
.text:0042C0D8 loc_42C0D8: ; CODE XREF: KiFindReadyThread(x,x)+100↑j
.text:0042C0D8 ; KiFindReadyThread(x,x)+110↑j
.text:0042C0D8 mov eax, ecx
.text:0042C0DA
/*------------------------------------------
将找到的就绪线程从链表中摘出,并判断链表是否为空,
若为空,则在_KiReadySummary中将该优先级对应的位置0
------------------------------------------*/
.text:0042C0DA loc_42C0DA: ; CODE XREF: KiFindReadyThread(x,x)+B2↑j
.text:0042C0DA ; KiFindReadyThread(x,x)+C5↑j ...
.text:0042C0DA mov cl, byte ptr [ebp+var_10] //ecx = CPU编号
.text:0042C0DD mov [eax+_KTHREAD.NextProcessor], cl //NextProcessor指明了该线程在哪个处理器开始运行
//将当前线程从链表中摘下
//eax = KTHREAD
.text:0042C0E3 mov ecx, [eax+_KTHREAD.___u24.Flink] //ecx = WaitListEntry.FLink,下一个
.text:0042C0E6 mov edx, [eax+_KTHREAD.___u24.Blink] //edx = WaitListEntry.BLink,上一个
.text:0042C0E9 mov [edx], ecx //WaitListEntry.BLink.FLink = WaitListEntry.FLink
.text:0042C0EB mov [ecx+4], edx //WaitListEntry.FLink.BLink = WaitListEntry.BLink
.text:0042C0EE mov ecx, [ebp+var_4] //ecx = 被摘下的线程
.text:0042C0F1 cmp [ecx], ecx //判断该链表是否为空
.text:0042C0F3 jnz loc_42C05D //跳转:链表不为空
.text:0042C0F9 mov ecx, [ebp+var_8] //链表为空:则将KiReadySummary有关位置为0
.text:0042C0FC xor edx, edx
.text:0042C0FE inc edx
.text:0042C0FF shl edx, cl
.text:0042C101 not edx
.text:0042C103 and _KiReadySummary, edx //如果当前优先级调度链表为空,则修改
//_KiReadySummary 相应的位
.text:0042C109 jmp loc_42C05D
.text:0042C109 @KiFindReadyThread@8 endp
.text:0042C109
.text:0042C109 ; ---------------------------------------------------------------------------

可参考:

8 进程挂靠

进程挂靠涉及到APC相关的知识,目前还没有学到APC,先浅写一下,后面学了APC再回来总结。

进程与线程的关系:

  1. 一个进程可以包含多个线程
  2. 一个进程至少要有一个线程
  3. 进程为线程提供资源,也就是提供Cr3的值,Cr3中存储的是页目录表基址
  4. Cr3确定了,线程能访问的内存也就确定了

:CPU解析线程代码 mov eax,dword ptr ds:[0x12345678]

CPU解析线性地址时要通过页目录表来找对应的物理页,页目录表基址存在Cr3寄存器中。
当前的Cr3的值来源于当前的进程(_KPROCESS.DirectoryTableBase(+0x018))。

进程CR3与线程的关联:

资源提供者(养父母):_ETHREAD.Tcb.ApcState.Process(+0x44)
线程创建者(亲生父母):_ETHREAD.ThreadsProcess(+0x220)
一般情况下,_ETHREAD.Tcb.ApcState.Process_ETHREAD.ThreadsProcess 指向的是同一个进程。
将当前CR3的值改为其它进程的CR3,称为进程挂靠

养父母负责提供CR3:线程切换的时候,会比较切换前后两个线程_KTHREAD结构体0x044处指定的EPROCESS是否为同一个,如果不是同一个,会将0x044处指定的EPROCESSDirectoryTableBase的值取出,赋值给CR3。所以,线程需要的CR3的值来源于0x044处偏移指定的EPROCESS

Cr3的值可以随便改吗?

正常情况下,Cr3的值是由养父母提供的,但Cr3的值也可以改成和当前线程毫不相干的其他进程的DirectoryTableBase

1
2
3
4
5
6
7
8
线程代码:

mov cr3,A.DirectoryTableBase
mov eax,dword ptr ds:[0x12345678] //A进程的0x12345678内存
mov cr3,B.DirectoryTableBase
mov eax,dword ptr ds:[0x12345678] //B进程的0x12345678内存
mov cr3,C.DirectoryTableBase
mov eax,dword ptr ds:[0x12345678] //C进程的0x12345678内存

将当前Cr3的值改为其他进程,称为“进程挂靠”。

:追一下NtReadVirtualMemory调用过程:NtReadVirtualMemory(x,x,x,x,x) --> MmCopyVirtualMemory(x,x,x,x,x,x,x) --> MiDoMappedCopy(x,x,x,x,x,x,x) --> KeStackAttachProcess(x,x) --> KiAttachProcess(x,x,x,x)修改养父母 --> KiSwapProcess(x,x)进程挂靠CR3

  1. NtReadVirtualMemory(x,x,x,x,x)函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    ; NTSTATUS __stdcall NtReadVirtualMemory(HANDLE ProcessHandle, PVOID BaseAddress, 
    PVOID Buffer, SIZE_T NumberOfBytesToRead,
    PSIZE_T NumberOfBytesRead)
    PAGE:004DD28A _NtReadVirtualMemory@20 proc near ; DATA XREF: .text:0042D738↑o
    ...
    PAGE:004DD317 loc_4DD317: ; CODE XREF: NtReadVirtualMemory(x,x,x,x,x)+4A↑j
    PAGE:004DD317 ; NtReadVirtualMemory(x,x,x,x,x)+67↑j
    PAGE:004DD317 xor eax, eax
    PAGE:004DD319 mov [ebp+var_28], eax
    PAGE:004DD31C mov [ebp+var_1C], eax
    PAGE:004DD31F cmp esi, eax // esi = NumberOfBytesToRead
    PAGE:004DD321 jz short loc_4DD366
    PAGE:004DD323 push eax ; HandleInformation
    PAGE:004DD324 lea eax, [ebp+Object] // Object = 要读取内存所属的进程KPROCESS
    PAGE:004DD327 push eax ; Object
    PAGE:004DD328 push dword ptr [ebp+AccessMode] ; AccessMode
    PAGE:004DD32B push _PsProcessType ; ObjectType
    PAGE:004DD331 push 10h ; DesiredAccess
    PAGE:004DD333 push [ebp+ProcessHandle] ; Handle
    PAGE:004DD336 call _ObReferenceObjectByHandle@24 ; ObReferenceObjectByHandle(x,x,x,x,x,x)
    PAGE:004DD33B mov [ebp+var_1C], eax // 获取进程的句柄Handle,eax = 返回码
    PAGE:004DD33E test eax, eax
    PAGE:004DD340 jnz short loc_4DD366 // eax = 0,句柄获取成功
    PAGE:004DD342 lea eax, [ebp+var_28]
    PAGE:004DD345 push eax ; int
    PAGE:004DD346 push dword ptr [ebp+AccessMode] ; AccessMode
    PAGE:004DD349 push esi ; Length
    PAGE:004DD34A push [ebp+Buffer] ; Address
    PAGE:004DD34D push dword ptr [edi+44h] ; ULONG_PTR
    PAGE:004DD350 push [ebp+BaseAddress] ; int
    PAGE:004DD353 push [ebp+Object] ; BugCheckParameter1
    PAGE:004DD356 call _MmCopyVirtualMemory@28 // 读取内存,继续跟进这个函数
  2. MmCopyVirtualMemory(x,x,x,x,x,x,x) --> MiDoMappedCopy(x,x,x,x,x,x,x) --> KeStackAttachProcess(x,x)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    KeStackAttachProcess(x,x)
    ...
    .text:00421C8E loc_421C8E: ; CODE XREF: KeStackAttachProcess(x,x)+5B↑j
    .text:00421C8E lea eax, [esi+_KTHREAD.SavedApcState.ApcListHead.Flink]
    .text:00421C94 push eax
    .text:00421C95 push [ebp+BugCheckParameter1]
    .text:00421C98 push edi // 参数二:edi = 要读取进程的KPROCESS
    .text:00421C99 push esi // 参数一:esi = CurrentThread
    .text:00421C9A call _KiAttachProcess@16 ; KiAttachProcess(x,x,x,x)
  3. KiAttachProcess(x,x,x,x)修改养父母

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    KiAttachProcess(x,x,x,x)
    ...
    .text:00421A2C mov [esi+_KTHREAD.ApcState.Process], edi // 修改养父母
    .text:00421A2F mov [esi+_KTHREAD.ApcState.KernelApcInProgress], 0
    .text:00421A33 mov [esi+_KTHREAD.ApcState.KernelApcPending], 0
    .text:00421A37 mov [esi+_KTHREAD.ApcState.UserApcPending], 0
    ...
    .text:00421A7D push dword ptr [eax+10h]
    .text:00421A80 push edi // edi = 要读取进程的KPROCESS
    .text:00421A81 call _KiSwapProcess@8 ; KiSwapProcess(x,x)
  4. KiSwapProcess(x,x)修改CR3

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    .text:0046ECF0 loc_46ECF0:                             ; CODE XREF: KiSwapProcess(x,x)+2D↑j
    .text:0046ECF0 lldt ax
    .text:0046ECF3 mov ecx, large fs:20h
    .text:0046ECFA lea ecx, [ecx+(_KPRCB.LockQueue.Next+8)]
    .text:0046ED00 call @KeReleaseQueuedSpinLockFromDpcLevel@4 ; KeReleaseQueuedSpinLockFromDpcLevel(x)
    .text:0046ED05 mov ecx, large fs:40h
    .text:0046ED0C mov edx, [esp+arg_0] // edx = 要读取进程的_KPROCESS
    .text:0046ED10 xor eax, eax
    .text:0046ED12 mov gs, ax // 在Windows中没有使用到 GS 段寄存器,GS = 0
    .text:0046ED15 mov eax, [edx+_KPROCESS.DirectoryTableBase]
    .text:0046ED18 mov [ecx+_KPRCB.ProcessorState.ContextFrame.ContextFlags], eax
    .text:0046ED1B mov cr3, eax // 切换CR3
    .text:0046ED1E mov ax, [edx+_KPROCESS.IopmOffset]
    .text:0046ED22 mov word ptr [ecx+(_KPRCB.ProcessorState.ContextFrame.FloatSave.RegisterArea+12h)], ax
    .text:0046ED26 retn 8
    .text:0046ED26 _KiSwapProcess@8 endp

思考:可不可以只修改Cr3而不修改养父母?
答案:不可以,假设刚刚修改完Cr3,还没读取内存时,发生了线程切换,当再次切换回来时,会根据养父母的值为Cr3赋值,Cr3又变回了原来的值,此时将变成自己读自己。如果我们自己来写这个代码,在切换Cr3后关闭中断,并且不调用会导致线程切换的API,就可以不用修改养父母的值

总结

  1. 正常情况下,当前线程使用的Cr3是由其所属进程提供的(_ETHREAD.Tcb.ApcState.Process),正是因为如此,A进程中的线程只能访问A的内存。
  2. 如果要让A进程中的线程能够访问B进程的内存,就必须要修改Cr3的值为B进程的页目录表基址(B.DirectoryTableBase),这就是所谓的“进程挂靠”。

9 跨进程读写内存

描述:跨进程的本质是“进程挂靠”,正常情况下,A进程的线程只能访问A进程的地址空间,如果A进程的线程想访问B进程的地址空间,就要修改当前的Cr3的值为B进程的页目录表基值(KPROCESS.DirectoryTableBase)。即:mov cr3, B.DirectoryTableBase。

跨进程读写错误写法:

1
2
3
4
5
6
7
8
9
A进制中的线程代码:

mov cr3,B.DirectoryTableBase //切换Cr3的值为B进程
mov eax,dword ptr ds:[0x12345678] //将进程B 0x12345678的值存的eax中
mov dword ptr ds:[0x00401234],eax //将数据存储到0x00401234中
mov cr3,A.DirectoryTableBase //切换回Cr3的值

此时0x00401234中的数据还有吗?
此时在A进程0x00401234读到的数据是不正确的。

以下内存成功读写的原因:这是因为高2G地址可让所有进程共用

正确的流程可以参考NtReadVirtualMemory执行流程

  1. 当前线程的Cr3切换至目标进程的Cr3,并修改线程养父母;
  2. 将要读的数据复制到高2G(暂存区);
  3. 当前线程的Cr3切换至原本进程的Cr3;
  4. 将要读的数据从高2G复制到目标位置

48.png

NtWriteVirtualMemory执行流程

  1. 将当前线程的数据复制到高2G(暂存区)
  2. 当前线程的Cr3切换至目标进程的Cr3
  3. 将要写入的数据从高2G复制到目标位置
  4. 当前线程的Cr3切换至原本进程的Cr3

49.png

总结

每个进程的高2G内存空间的线性地址对应的物理页几乎是相同的,可以通过对高2G内存空间的利用,实现跨进程内存读写的操作。