Windows进程与线程学习笔记(九)—— 线程优先级/进程挂靠/跨进程读写

前言

一、学习自滴水编程达人中级班课程,官网:https://bcdaren.com
二、海东老师牛逼!

要点回顾

三种情况会导致线程切换

  1. 当前线程主动调用API
    KiSwapThread -> KiSwapContext -> SwapContext
  2. 当前线程时间片到期
    KiDispatchInterrupt -> KiQuantumEnd -> SwapContext
  3. 存在备用线程(KPCR.PrcbData.NextThread)
    KiDispatchInterrupt -> SwapContext

思考:在KiSwapThread与KiQuantumEnd函数中都是通过KiFindReadyThread来找下一个要切换的线程,KiFindReadyThread是根据什么条件来选择下一个要执行的线程呢?

线程优先级

调度链表

描述

  1. Windows 32位操作系统中,共有32个双向链表(调度链表)
  2. Windows 64位操作系统中,共有64个双向链表(调度链表)
  3. 线程在调度链表的中下标表示线程优先级(0~31/64)

调度链表

分析 KiFindReadyThread

查找方式

按照优先级别进行查找:31…30…29…28…
也就是说,在本次查找中,如果级别31的链表里面有线程,那么就不会查找级别为30的链表

注意

  1. Windows 32位操作系统中调度链表有32个,由于每次都从头开始查找效率太低,所以Windows通过一个DWORD类型的变量来记录:_KiReadySummary
  2. 当向调度链表(32个)中挂入或者摘除某个线程时,会判断当前级别的链表是否为空,为空则将 _KiReadySummary 对应位置0,否则置1
    在这里插入图片描述
  3. 若当前级别链表的链表头链表尾的值相同,并且等于它们的地址,说明不存在等待调度的线程
    若当前级别链表的链表头链表尾的值相同,但不等于它们的地址,说明存在一个等待调度的线程
  4. 多cpu会随机寻找 KiDispatcherReadyListHead 指向的数组中的线程。线程可以绑定某个cpu(API:setThreadAffinityMask
  5. 若当前CPU不存在就绪线程,则会执行空闲线程,每一个CPU都会指定一个空闲线程
    nt!_KPRCB
       +0x004 CurrentThread    : Ptr32 _KTHREAD		//当前线程
       +0x008 NextThread       : Ptr32 _KTHREAD		//就绪线程
       +0x00c IdleThread       : Ptr32 _KTHREAD		//空闲线程
    

分析 KiSwapThread

KiSwapThread
loc_41073e
线程切换

扫描二维码关注公众号,回复: 8534684 查看本文章

总结

  1. 调度链表的下标即线程优先级
  2. CPU通过遍历调度链表判断是否存在需要调度的线程
  3. 若不存在需要调度的线程,则会执行空闲线程(_KPRCB.IdleThread

进程挂靠

进程与线程的关系

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

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

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

进程与线程的关联

  1. 资源提供者(养父母):_ETHREAD.Tcb.ApcState.Process
  2. 线程创建者(亲生父母):_ETHREAD.ThreadsProcess
  3. 一般情况下,_ETHREAD.Tcb.ApcState.Process_ETHREAD.ThreadsProcess 指向的是同一个进程
  4. 将当前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

SwapContext

分析 NtReadVirtualMemory

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

总结

  1. 正常情况下,当前线程使用的Cr3是由其所属进程提供的(_ETHREAD.Tcb.ApcState.Process),正是因为如此,A进程中的线程只能访问A的内存
  2. 如果要让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执行流程

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

NtReadVirtualMemory

跨进程写

NtWriteVirtualMemory执行流程

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

跨进程写内存

总结

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

发布了45 篇原创文章 · 获赞 2 · 访问量 1827

猜你喜欢

转载自blog.csdn.net/qq_41988448/article/details/103435464