avatar

Catalog
进程挂靠

在学习进程挂靠之前,先回顾一下进程与线程相关的知识

进程与线程的关系

基本关系

  • 一个进程可以包含多个线程
  • 一个进程至少要有一个线程

进程为线程提供资源,也就是提供Cr3的值,Cr3中存储的是页目录表基址,Cr3确定了,线程能访问的内存也就确定了。

代码分析

来看这样一行代码:

Code
1
mov eax,dword ptr ds:[0x12345678]

CPU如何解析0x12345678这个地址呢?

  1. CPU解析线性地址时,需要通过页目录表(PDT)来找到对应的物理页,页目录表基址存在Cr3寄存器中,这些都是保护模式的内容,已经很熟悉了
  2. 当前的Cr3的值来源于当前的进程(_KPROCESS.DirectoryTableBase(+0x018))

线程找进程

线程找进程有两种情况:

  1. KHTREAD.ApcState.Process(+0x44)
  2. ETHREAD.ThreadProcess(+0x220)

所以,从KTHREAD以及ETHREAD均能找到当前线程的进程,这里引用海哥的叫法,把KTHREAD找到的Process(+0x44)叫做养父母,把ETHREAD找到的ThreadProcess(+0x220)叫做亲生父母

养父母负责提供Cr3

线程切换的时候,会比较KTHREAD结构体0x044处指定的EPROCESS是否为同一个,如果不是同一个,会将0x044处指定的EPROCESS的DirectoryTableBase的值取出,赋值给Cr3。这部分在分析SwapContext的Part3部分提到过,这里不多赘述。可以跳转或者参考下图的紫色部分

所以,线程所需要的Cr3的值,来源于0x044偏移处指定的EPROCESS,所以得出如下结论:

  • 0x220:亲生父母,这个线程谁创建的
  • 0x044:养父母,谁在为这个线程提供资源(也就提供Cr3)。一般情况下,0x220与0x044指向的是同一个进程

进程挂靠

有了上述概念后,我们知道了,正常情况下,Cr3的值是由养父母提供的,但是Cr3的值也可以改成和当前线程毫不相干的其它进程的DirectoryTableBase。

观察下面的代码:

Code
1
2
3
4
5
6
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的值改为其它进程,称为“进程挂靠”。

进程挂靠存在的意义是什么呢,上面的代码,分别将不同进程的DirectoryTableBase的值写入Cr3。这时,每次读入的0x12345678这个线性地址上的值,分别是对应进程上0x12345678线性地址所对应物理页的内容。有了进程挂靠,就意味着可以读取其它进程的内存。

分析NtReadVirtualMemory

我们知道,ReadProcessMemory这个三环API是可以读取其它进程的内存的,该函数在0环的实现是NtReadVirtualMemory,来分析一下这个函数,看看它是如何读取其它进程内存的:

  1. 首先,进入NtReadVirtualMemory,由于这个函数非常复杂,就直接挑重点来说了 这里调用了一个_MmCopyVirtualMemory函数,看名字就感觉,这个和别的进程的内存可能有点关系,毕竟是Copy来的….

  2. 进入_MmCopyVirtualMemory继续查看 这个函数不大,有一个函数很关键MiDoPoolCopy,这个函数Push了一大堆参数,内部应该实现了重要的功能,继续更近

  3. 跟进_MiDoPoolCopy函数 往下翻,有一个KeStackAttachProcess,由名字可知,这个函数和进程挂靠有关

  4. 再进一步,进入_KeStackAttachProcess函数 看到这里,就是真正的挂靠函数,进入分析看看Windows到底是如何实现进程挂靠的

  5. 直接看图 这里面进行了两个主要的操作:

    1)修改养父母,即KTHREAD.ApcState.Process的值,修改为将要访问的进程的进程结构体

    2)调用进程切换函数KiSwapProcess(本质是切换Cr3

  6. 进入KiSwapProcess看看这个函数具体做了什么 来看最关键的部分,KiSwapProcess函数,先从外部参数,获取到了将要访问的进程的Cr3,然后分别修改TSS.Cr3和KPROCESS+0x18(DirectoryTableBase)处的值,然后便完成了进程切换。可以发现,进程切换,实际上就是切换了Cr3

小结

简要分析完了NtReadVirtualMemory函数后可以发现,这个函数主要做了两件事,第一件事,修改线程养父母,第二件事,修改进程Cr3。随后就可以访问和读取另一个进程的内存了。

那么小盆友要问了,可不可以只修改Cr3,而不修改养父母呢?当然是不可以的,如果不修改养父母的值,一旦发生线程切换,再切回来的时候,读取的内存,是由养父母提供的Cr3,而养父母没有修改,因此读取的还是自己线程所在的进程,即变成了自己读自己了。

总结

正常情况下,当前线程使用的Cr3是由其所属进程提供的(KTHREAD+0x44偏移处指定的EPROCESS),正是因为如此,A进程中的线程只能访问A的内存。

如果要让A进程中的线程能够访问B进程的内存,就必须要修改Cr3的值为B进程的页目录表基址(B.DirectoryTableBase),这就是所谓的“进程挂靠

参考教程:https://www.bilibili.com/video/BV1NJ411M7aE?p=54

参考文章:

  1. https://blog.csdn.net/weixin_42052102/article/details/83268680
  2. https://blog.csdn.net/qq_38474570/article/details/104286261
  3. https://blog.csdn.net/qq_41988448/article/details/103435464
Author: cataLoc
Link: http://cata1oc.github.io/2020/04/05/%E8%BF%9B%E7%A8%8B%E6%8C%82%E9%9D%A0/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
Donate
  • 微信
    微信
  • 支付寶
    支付寶