Windows进程与线程学习笔记(九)—— 线程优先级/进程挂靠/跨进程读写
要点回顾
三种情况会导致线程切换:
- 当前线程主动调用API:
KiSwapThread -> KiSwapContext -> SwapContext - 当前线程时间片到期:
KiDispatchInterrupt -> KiQuantumEnd -> SwapContext - 存在备用线程(KPCR.PrcbData.NextThread)
KiDispatchInterrupt -> SwapContext
思考:在KiSwapThread与KiQuantumEnd函数中都是通过KiFindReadyThread来找下一个要切换的线程,KiFindReadyThread是根据什么条件来选择下一个要执行的线程呢?
线程优先级
调度链表
描述:
- 在Windows 32位操作系统中,共有32个双向链表(调度链表)
- 在Windows 64位操作系统中,共有64个双向链表(调度链表)
- 线程在调度链表的中下标表示线程优先级(0~31/64)
分析 KiFindReadyThread
查找方式:
按照优先级别进行查找:31…30…29…28…
也就是说,在本次查找中,如果级别31的链表里面有线程,那么就不会查找级别为30的链表
注意:
- Windows 32位操作系统中调度链表有32个,由于每次都从头开始查找效率太低,所以Windows通过一个DWORD类型的变量来记录:_KiReadySummary
- 当向调度链表(32个)中挂入或者摘除某个线程时,会判断当前级别的链表是否为空,为空则将 _KiReadySummary 对应位置0,否则置1
- 若当前级别链表的链表头和链表尾的值相同,并且等于它们的地址,说明不存在等待调度的线程
若当前级别链表的链表头和链表尾的值相同,但不等于它们的地址,说明存在一个等待调度的线程 - 多cpu会随机寻找 KiDispatcherReadyListHead 指向的数组中的线程。线程可以绑定某个cpu(API:setThreadAffinityMask)
- 若当前CPU不存在就绪线程,则会执行空闲线程,每一个CPU都会指定一个空闲线程
nt!_KPRCB +0x004 CurrentThread : Ptr32 _KTHREAD //当前线程 +0x008 NextThread : Ptr32 _KTHREAD //就绪线程 +0x00c IdleThread : Ptr32 _KTHREAD //空闲线程
分析 KiSwapThread
总结
- 调度链表的下标即线程优先级
- CPU通过遍历调度链表判断是否存在需要调度的线程
- 若不存在需要调度的线程,则会执行空闲线程(_KPRCB.IdleThread)
进程挂靠
进程与线程的关系
- 一个进程可以包含多个线程
- 一个进程至少要有一个线程
- 进程为线程提供资源,也就是提供Cr3的值,Cr3中存储的是页目录表基址
- Cr3确定了,线程能访问的内存也就确定了
例:CPU解析线程代码 mov eax,dword ptr ds:[0x12345678]
- CPU解析线性地址时要通过页目录表来找对应的物理页,页目录表基址存在
Cr3寄存器中 - 当前的Cr3的值来源于当前的进程(_KPROCESS.DirectoryTableBase(+0x018))
进程与线程的关联
- 资源提供者(养父母):_ETHREAD.Tcb.ApcState.Process
- 线程创建者(亲生父母):_ETHREAD.ThreadsProcess
- 一般情况下,_ETHREAD.Tcb.ApcState.Process 和 _ETHREAD.ThreadsProcess 指向的是同一个进程
- 将当前Cr3的值改为其它进程的Cr3,称为“进程挂靠”
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?
答案:线程切换的时候,会比较两个线程的EPROCESS是否为同一个,若不是同一个,则会将 _ETHREAD.Tcb.ApcState.Process 指向的 EPROCESS的DirectoryTableBase 取出,赋值给Cr3
分析 SwapContext
分析 NtReadVirtualMemory
MmCopyVirtualMemory:
MiDoPoolCopy:
KeStackAttachProcess:
KiAttachProcess:
KiSwapProcess:
思考:可不可以只修改Cr3而不修改养父母?
答案:不可以,假设刚刚修改完Cr3,还没读取内存时,发生了线程切换,当再次切换回来时,会根据养父母的值为Cr3赋值,Cr3又变回了原来的值,此时将变成自己读自己。如果我们自己来写这个代码,在切换Cr3后关闭中断,并且不调用会导致线程切换的API,就可以不用修改养父母的值
总结
- 正常情况下,当前线程使用的Cr3是由其所属进程提供的(_ETHREAD.Tcb.ApcState.Process),正是因为如此,A进程中的线程只能访问A的内存
- 如果要让A进程中的线程能够访问B进程的内存,就必须要修改Cr3的值为B进程的页目录表基址(B.DirectoryTableBase),这就是所谓的“进程挂靠”
跨进程读写
描述:跨进程的本质是“进程挂靠”,正常情况下,A进程的线程只能访问A进程的地址空间,如果A进程的线程想访问B进程的地址空间,就要修改当前的Cr3的值为B进程的页目录表基值(KPROCESS.DirectoryTableBase)。即:mov cr3, B.DirectoryTableBase
分析代码:
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的值
问题:以上代码是否存在问题?
答案:存在问题。当读取B进程内存之后,由于Cr3并未改变,写入的地址仍为B进程的地址。当Cr3切换回A进程后,A进程中并不存在读出来的值
思考:如何解决以上问题?
跨进程读
NtReadVirtualMemory执行流程:
- 将当前线程的Cr3切换至目标进程的Cr3
- 将要读的数据复制到高2G(暂存区)
- 将当前线程的Cr3切换至原本进程的Cr3
- 将要读的数据从高2G复制到目标位置
跨进程写
NtWriteVirtualMemory执行流程:
- 将当前线程的数据复制到高2G(暂存区)
- 将当前线程的Cr3切换至目标进程的Cr3
- 将要写入的数据从高2G复制到目标位置
- 将当前线程的Cr3切换至原本进程的Cr3
总结
每个进程的高2G内存空间的线性地址对应的物理页几乎是相同的,可以通过对高2G内存空间的利用,实现跨进程内存读写的操作