Windows XP 进程线程(二)
ʕ •ᴥ•ʔ ɔ:
1 线程链表
进程结构体EPROCESS(0x50和0x190)是2个链表,里面圈着当前进程所有的线程。
对进程断链,程序可以正常运行,原因是CPU执行与调度是基于线程的,进程断链只是影响一些遍历系统进程的API,并不会影响程序执行。
对线程断链也是一样的,断链后在Windbg、任务管理器或者OD中无法看到被断掉的线程,但并不影响其执行(仍然再跑)。
线程的三种状态:等待(Wait),运行(Running),就绪(Ready)。每种状态下的线程实际上都由相应的双向循环链表串起来,操作系统通过这些链表来调度线程。
正在运行中的线程存储在KPCR中,就绪和等待的线程全在另外的33个链表中。一个等待链表,32个就绪链表。
1.1 等待链表(Wait)
等待链表(一个):Wait,挂起,阻塞。
线程调用了Sleep()
、WaitForSingleObject()
、SuspendThread()
或者以挂起状态CreateThread()
等函数时,就将线程挂到这个链表。
在
KTHREAD
偏移0x60
处的成员WaitListEntry
串起来的链表即位系统中所有处于等待的线程。等待线程存储在等待链表头
KiWaitListHead
中,KiWaitListHead是一个全局变量,可以dd
查看。1
2kd> 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
6kd> 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 | kd> dt _ETHREAD 0x860f0988-0x60 |
然后看看镜像名:
1 | kd> dt _EPROCESS 0x863d5380 |
1.2 运行线程(Running)
一个核只有一个运行中的线程,运行中的线程存储在 KPCR 中。
1 | kd> r fs |
KPCR == FS:[0] == 0xFFDFF000。
1 | kd> dt _KPCR 0xFFDFF000 |
可以看到:
- 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 | kd> dd KiDispatcherReadyListHead L70 |
每两个4字节就构成了一个LIST_ENTRY
,我们发现这里32个链表都是空的(BLINK == FLINK == 当前地址,说明链表是空的。),原因是现在Windbg把系统挂起了,所有线程都处于等待状态,不能被调度了。
说明:等待链表和调度链表都是在_KTHREAD + 0x60
,是一个共用体union
,线程处于等待Wait或者调度Ready状态就会存到这个位置的链表里,如果是等待状态,这个地方就是等待链表;如果是调度状态,这里就是调度链表。
1 | +0x060 WaitListEntry : _LIST_ENTRY |
1.4 总结
- XP只有一套这样的33个链表,也就是说上面这个数组只有一个,多核也只有一个。Win7也是一样的只有一个组,如果是64位的,那就有64个链表。
服务器版本:KiWaitListHead整个系统只有一个,但KiDispatcherReadyListHead这个数组有几个CPU就有几组。 - 正在运行的线程在KPCR中。
- 准备运行的线程在32个调度链表中(0 - 31级),KiDispatcherReadyListHead 是个数组存储了这32个链表头。
- 等待状态的线程存储在等待链表中,KiWaitListHead存储链表头。
- 等待链表都挂一个相同的位置:_KTHREAD(0x060)。
2 模拟线程切换
2.1 代码模版
1 |
|
2.2 线程切换
关键结构体
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;线程链表
在之前已经讲解过,正在运行的线程在KPCR中,另外还有1个等待链表和32个就绪链表。我们在模拟线程切换时就简单定义一个数组来存储所有的链表,其中第一个(下标为0)的表示正在运行的线程。
所谓创建线程,就是创建一个结构体,并且挂到这个数组中。此时的线程状态为:创建。
初始化线程堆栈
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,此值无意义,是寄存器初始值1)线程不是被动切换的,而是主动让出CPU。
2)线程切换并没有使用TSS来保存寄存器,而是使用堆栈。
3)线程切换的过程就是堆栈切换的过程。
3 逆向分析-KiSwapContext函数
我们要带着问题开始逆向:
- SwapContext 有几个参数,分别是什么?
- SwapContext 在哪里实现了线程切换?
- 线程切换的时候,会切换CR3吗?切换CR3的条件是什么?
- 中断门提权时,CPU会从TSS得到ESP0和SS0,TSS中存储的一定是当前线程的ESP0和SS0吗?如何做到的?
- FS:[0]在3环指向TEB,但是线程有很多,FS:[0]指向的是哪个线程的TEB,如何做到的?
- 0环的 ExceptionList 在哪里备份的?
- IdleThread是什么?什么时候执行?找到这个函数。
- 如何找到下一个就绪线程?
- 模拟线程切换与Windows线程切换有哪些区别?
线程切换是操作系统的核心内容,几乎所有的内核API都会调用切换线程的函数。
KiSwapContext
函数调用了 SwapContext,我们通过逆它可以判断出 SwapContext 有几个参数。
1 | .text:0046E969 ; --------------------------------------------------------------------------- |
4 逆向分析-SwapContext函数
主要解决:SwapContext 在哪里实现了线程切换。
1 | .text:0046EA81 ; --------------------------------------------------------------------------- |
通过
1 | .text:0046EAB7 push ecx //备份指向ExceptionList异常链表的指,esp指向存储ExceptionList地址 |
可以得知:线程切换时新线程的ESP来自于新线程的KTHREAD.KernelStack。
之前学习的API3环进0环的ESP0来自于TSS.ESP0。而TSS.ESP0是在SwapContext这一行代码更新的:
1 | .text:0046EB1C mov ecx, [ebx+40h] //ecx = TSS |
关于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来切换线程,具体情况如下图所示:
上图摘自《Windows内核原理与实现》。
线程切换的三种情况:
- 当前线程主动调用API:
KiSwapThread --> KiSwapContext --> SwapContext
。 - 当前线程时间片到期:
KiDispatchInterrupt --> KiQuantumEnd --> SwapContext
。或者有备用线程(KPCR.PrcbData.NextThread != NULL
):KiDispatchInterrupt --> SwapContext
。 - 异常处理。
5.1 主动切换
绝大部分系统内核函数都会调用SwapContext
函数,来实现线程的切换,那么这种切换是线程主动调用的。
主动切换API调用的大致流程: xxxx() --> KiSwapThread --> KiSwapContext --> SwapContext
。
可以在IDA中使用交叉引用查看哪些函数调用了SwapContext
,逐级往上交叉引用向上查看发现会有许多API会调用,从而导致线程切换。
- Windows中绝大部分API都调用了SwapContext函数。也就是说,当线程只要调用了API,就是导致线程切换。
- 线程切换时会比较是否属于同一个进程,如果不是,切换Cr3。Cr3换了,进程也就切换了。
具体的细节可以参考:Windows内核学习笔记之线程(下)
5.2 时钟中断切换
如何中断一个正在执行的程序?
- 异常:比如缺页,或者 INT N 指令。
- 中断:比如时钟中断。
系统时钟中断:时钟中断发生时,会转化为INT 0x30
中断号。
Windows系列操作系统:10 - 20 毫秒。
如要获取当前的时钟间隔值,可使用Win32 API:GetSystemTimeAdjustment
。
⚠️说明:并不是每次时钟中断发生都会导致线程切换,时钟中断导致线程切换的条件是:
- 时间片用完
- 有备用线程(
KPCR.PrcbData.NextThread != NULL
)
这两种方式都是因为时钟中断触发的,将在下面逆向分析函数KiSwapThread
时详细解释。
下面将跟一下时钟中断触发int 0x30
调用过程:
- 在IDA Pro快捷键
Alt+T
输入_IDT
搜索,找到int 0x30
对应的地址为函数KiStartUnexpectedRange()
。 - 有两条路线:
KiStartUnexpectedRange() --> KiEndUnexpectedRange() --> _KiUnexpectedInterruptTail() --> Hal!HalBeginSystemInterrupt(x,x,x) --> retn
。KiStartUnexpectedRange() --> KiEndUnexpectedRange() --> _KiUnexpectedInterruptTail() --> Hal!HalEndSystemInterrupt(x,x) --> ntkrnlpa!KiDispatchInterrupt() --> SwapContext()
。
总结:线程切换的几种情况:
主动调用API函数
时钟中断
异常处理
如果一个线程不调用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
(重新设置时间片、找到要运行的线程)。
查看
_KPROCESS.ThreadQuantum
。1
2
3
4
5
6
7
8
9
10
11kd> !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 ''
...分析
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)分析
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时间片没有到期,仍然会被切换)
- 如果已经用完:则
总结:线程切换的三种情况:
- 当前线程主动调用API:
KiSwapThread --> KiSwapContext --> SwapContext
。 - 时钟切换
- 当前线程时间片到期:
KiDispatchInterrupt --> KiQuantumEnd --> SwapContext
; - 有备用线程(
KPCR.PrcbData.NextThread != NULL
):KiDispatchInterrupt --> SwapContext
。
- 当前线程时间片到期:
- 异常处理:异常会触发
int 0xIndex
中断号,从而经过函数调用进行线程切换。
5.4 TSS处理
SwapContext这个函数是Windows线程切换的核心,无论是主动切换还是系统时钟导致的线程切换,最终都会调用这个函数。
Intel设计TSS的目的是为了任务切换(线程切换),但Windows与Linux并没有使用。而是采用堆栈来保存线程的各种寄存器。
Intel设计TSS在任务(线程)切换时起着重要的作用,通过它保存CPU中各寄存器的值,实现任务的挂起和恢复。但是微软使用线程切换来实现任务的切换,仅使用到TSS中的ESP0和CR3。
通过逆向分析SwapContext
函数可知,线程在0环的堆栈如下:
在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 | kd> r gdtr |
在
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然后更新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~31bit8003f038 0040f300`00000fff
- 0000:
KPCR.NtTib.Self
低16位 - 00:
KPCR.NtTib.Self
高2字节的低8位 - 00:
KPCR.NtTib.Self
高2字节的高8位
GDT段描述符如下:
- 0000:
⚠️注意:在KiFastCallEntry / KiSystemService
中 FS 值由0x3B
变成0x30
;在KiSystemCallExit / KiSystemCallExitBranch / KiSystemCallExit2
中再将 RING3 的 FS 恢复。
5.6 线程优先级
之前讲过有三种情况会导致线程切换,在KiSwapThread
与KiQuantumEnd
函数中都是通过KiFindReadyThread
来找下一个要切换的线程,KiFindReadyThread
是根据什么条件来选择下一个要执行的线程呢?
调度链表有32个,每次都从头开始查找效率太低,所以Windows
都过一个DWORD
类型变量的变量来记录,正好是32位,一个位代表一个链表,当向调度链表.中挂入或者摘除某个线程时,会判断当前级别的链表是否为空,为空将.变量对应位置0
,否则置1
,这个变量就是_kiReadySummary
。多CPU
会随机寻找KiDispatcherReadyListHead
指向的数组中的线程,线程可以绑定某个CPU
,可以使用API
:SetThreadAffinityMask
进行设置。
使用 KiDispatcherReadyListHead 查看处理32个不同等级的就绪线程双向链表,每个等级的双向链表有两项(
_LIST_ENTRY
)。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19kd> 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 != 地址。
- 空链表:Flink == Blink == 地址,
KiFindReadyThread查找方式:按照优先级别进行查找:31..30..29..28…..也就是说,在本次查找中,如果级别31的链表里面有线程,那么就不会查找级别为30的链表!地址越高,优先级越高。(具体查找请见 7 逆向分析KiFindReadyThread函数)。
高效查找:通过这个变量:_kiReadySummary来记录当向调度链表(32个)中挂入或者摘除某个线程时,会判断当前级别的链表是否为空,为空将DWORD变量对应位置0,否则置1。
多CPU会随机寻找KiDispatcherReadyListHead指向的数组中的线程。线程可以绑定某个CPU(使用api:setThreadAffinityMask)。
5.7 逆向分析-KiReadyThread函数
KiReadyThread(x)
将当前线程挂到就绪链表中,至于挂到哪一个级别的就绪链表中,代码会判断比对线程的优先级,由于32个不同级别的链表都有对应的链表头,所以只要使用相应链表头即可进行挂链。
内核 API
函数 KiReadyThread
实现将旧线程的 _ETHREAD
( ECX
传参)添加到就绪链表中。
主要逻辑如下:
1、根据你的当前老线程,亲核,你最喜欢的哪个核SoftAffinity,闲置核去计算当前老的线程要挂在哪一个KPCR中的nextThread上。 2、如果Affinity、SoftAffinity闲置核计算没有找到,那么就会去下一个处理器KPCR挂上。
3、如果KPCR 中有下一个了,那么就对比优先级,看是挂老线程,还是用KPCR 以前的 。如果都挂不上,那就挂在WAITLIST 里面。 如果我当前老线程的优先级大于KPCR中当前线程优先级,那么在WAITLIST 那么就挂在当前优先级链表第一个,否则是当前优先级链表的最后一个。
具体过程可参考:线程被动切换(时间碎片) - 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
直接的调用关系如下:
1 | .text:0040AB8A ; =============== S U B R O U T I N E ======================================= |
接着分析,如果没有就绪线程,将会默认执行空闲线程:
1 | .text:004107BE loc_4107BE: ; CODE XREF: KiSwapThread()+2A↑j |
- 就绪位图(KiReadySummary)
为了提高调度速度,Windows 2000维护了一个称为就绪位图(KiReadySummary)的32位量。就绪位图中的每一位指示一个
调度优先级的就绪队列中是否有线程等待运行。B0与调度优先级0相对应,B1与调度优先级1相对应,等待。 - 空闲位图(KiIdleSummary)
Windows2000还维护一个称为空闲位图(KildleSummary)的32位量。空闲位图中的每一位指示一个处理机是否处于空闲状态。 - 调度器自旋锁(KiDispatcherLock)
为了防止调度器代码与线程在访问调度器数据结构时发生冲突,处理机调度仅出现在DPC调度层次。但在多处理机系统中,修
改调度器数据结构需要额外的步骤来得到内核调度器自旋锁(KiDispatcherLock),以协调各处理机对调度器数据结构的访问。
6.1 空闲线程-KiIdleLoop函数
在函数KiSwapThread
通过以下两行代码可以找到空闲线程执行的函数:
1 | .text:0046ED34 lea ecx, [ebx+_KPCR.PrcbData.PowerState.IdleFunction] |
而这里循环调用的地址IdleFunction
可以在ETHREAD.StartAddress(+0x224)
得到,但是使用如下方法看不到空闲线程的ETHREAD.StartAddress
。
得到
KPCR.KPRCB
:1
2
3
4
5
6kd> r fs
fs=00000030
kd> r gdtr
gdtr=8003f000
kd> dq 8003f000+0x8*0x6
8003f030 ffc093df`f0000001 0040f300`00000fffFS[0].Base = 0xFFDFF000,查看IdleThread地址:
0x8055ce60
。1
2
3
4
5
6
7kd> 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
5kd> dt _ETHREAD 0x8055ce60
ntdll!_ETHREAD
...
+0x224 StartAddress : (null)
...
则只能通过空闲线程的堆栈来查看了,可以这样查看的原因:当前运行的线程正是空闲线程0x8055ce60
。
分析函数
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)去执行经过上面分析,线程切换后0环堆栈中应该如下:
1
2
3esp0: --> ExceptionList(KTHREAD.KernelStack)
esp0+0x4: --> eflag
esp0+0x8: --> retn(ETHREAD.StartAddress)⚠️这里需要特别注意:线程切换后的堆栈是上面这样的,而线程从3环进0环使用的TrapFrame并不是线程切换,不要弄混了。
查看
IdleThread
的堆栈:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19kd> 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可以看到
IdleThread
是KiIdleLoop
函数
7 逆向分析-KiFindReadyThread函数
该函数做了三件事:
① 解析KiReadySummary,找到从左起第一个为1的位数;
② 用该位获取从KiDispatchReadListHead中的第一个_KTHREAD线程,将其从链表中摘除;
③ 如果摘除后该链表为空,则找到相应的KiReadySummary位将其置0。
函数作用:当线程切换发生时,要调用
KiFindReadyThread
函数从调度链表里找到下一个就绪线程并通过EAX寄存器返回。KiFindReadyThread 的参数个数。
1
__fastcall KiFindReadyThread(x, x)
快速调用:
ECX传参数1:CPU编号。
1
2movsx ebx, [esi+_KPRCB.Number]
mov ecx, ebxEDX传参数2:0(是最低优先级)。
1
xor edx, edx
通过IDA的分析函数有两个参数,阅读XP的源码(目录
/NT/base/ntos/ke/thredsup.c
),发现 KiFindReadyThread 的声明是这样的:grep --color=auto -rns "KiFindReadyThread" *
1
2
3
4PKTHREAD FASTCALL KiFindReadyThread (
IN ULONG ProcessorNumber,
IN KPRIORITY LowPriority
);- ProcessorNumber:是 CPU 编号,从 KPCR 里获取 ,单核模式下这个参数是没有用的;
- LowPriority:是最低优先级,KiSwapThread 里调用,传的是0。举例说明,如果这个参数是8,那么等价于 _KiReadySummary 的低8位置0,也就是忽略优先级0-7的线程。
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
17const 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的位的位置
ULONG _Mask; \
ULONG _Offset = 16; \
if ((_Mask = Set >> 16) == 0) { \
_Offset = 0; \
_Mask = Set; \
} \
if (_Mask >> 8) { \
_Offset += 8; \
} \
*(Member) = KiFindFirstSetLeft[Set >> _Offset] + _Offset; \
}使用二分法查找从左边起下标为1的位:
之前我们介绍过,其存在32个就绪链表,0-31序号,在Windows操作系统中,其存在一个全局变量KiReadySummary,32位,如果哪个存在就绪链表,其该位被置1。
这样,我们直接检测其KiReadySummary全局变量是否为0就判断是否存在就绪链表。
其从低位到高位优先级依次升高,因此需要找从左边数第一个为1的位数下标,该位下标作为索引从
KiDispatcherReadyListHead
数组中寻找对应优先级的线程链表。找对应优先级的线程链表:$PKTHREAD\ Kthread = (PKTHREAD)(KiDispatcherReadyListHead[eax*8] - 60h)$
7.1 KiFindReadyThread源码分析
将多核相关的预处理删除了,整理如下,添加了注释的 KiFindReadyThread 源码。
1 | const CCHAR KiFindFirstSetLeft[256] = { |
7.2 KiFindReadyThread逆向分析
1 | .text:0042BFBC ; =============== S U B R O U T I N E ======================================= |
可参考:
8 进程挂靠
进程挂靠涉及到APC相关的知识,目前还没有学到APC,先浅写一下,后面学了APC再回来总结。
进程与线程的关系:
- 一个进程可以包含多个线程
- 一个进程至少要有一个线程
- 进程为线程提供资源,也就是提供Cr3的值,Cr3中存储的是页目录表基址
- 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
处指定的EPROCESS
的DirectoryTableBase
的值取出,赋值给CR3。所以,线程需要的CR3的值来源于0x044处偏移指定的EPROCESS
。
Cr3的值可以随便改吗?
正常情况下,Cr3的值是由养父母提供的,但Cr3的值也可以改成和当前线程毫不相干的其他进程的DirectoryTableBase
。
1 | 线程代码: |
将当前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
。
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 // 读取内存,继续跟进这个函数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
9KeStackAttachProcess(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)KiAttachProcess(x,x,x,x)
,修改养父母。1
2
3
4
5
6
7
8
9
10KiAttachProcess(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)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,就可以不用修改养父母的值
总结:
- 正常情况下,当前线程使用的Cr3是由其所属进程提供的(_ETHREAD.Tcb.ApcState.Process),正是因为如此,A进程中的线程只能访问A的内存。
- 如果要让A进程中的线程能够访问B进程的内存,就必须要修改Cr3的值为B进程的页目录表基址(B.DirectoryTableBase),这就是所谓的“进程挂靠”。
9 跨进程读写内存
描述:跨进程的本质是“进程挂靠”,正常情况下,A进程的线程只能访问A进程的地址空间,如果A进程的线程想访问B进程的地址空间,就要修改当前的Cr3的值为B进程的页目录表基值(KPROCESS.DirectoryTableBase)。即:mov cr3, B.DirectoryTableBase。
跨进程读写错误写法:
1 | A进制中的线程代码: |
以下内存成功读写的原因:这是因为高2G地址可让所有进程共用。
正确的流程可以参考NtReadVirtualMemory执行流程:
- 将当前线程的Cr3切换至目标进程的Cr3,并修改线程养父母;
- 将要读的数据复制到高2G(暂存区);
- 将当前线程的Cr3切换至原本进程的Cr3;
- 将要读的数据从高2G复制到目标位置。
NtWriteVirtualMemory执行流程:
- 将当前线程的数据复制到高2G(暂存区)
- 将当前线程的Cr3切换至目标进程的Cr3
- 将要写入的数据从高2G复制到目标位置
- 将当前线程的Cr3切换至原本进程的Cr3
总结
每个进程的高2G内存空间的线性地址对应的物理页几乎是相同的,可以通过对高2G内存空间的利用,实现跨进程内存读写的操作。