在学习进程挂靠之前,先回顾一下进程与线程相关的知识
进程与线程的关系
基本关系
- 一个进程可以包含多个线程
- 一个进程至少要有一个线程
进程为线程提供资源,也就是提供Cr3的值,Cr3中存储的是页目录表基址,Cr3确定了,线程能访问的内存也就确定了。
代码分析
来看这样一行代码:
1 | mov eax,dword ptr ds:[0x12345678] |
CPU如何解析0x12345678这个地址呢?
- CPU解析线性地址时,需要通过页目录表(PDT)来找到对应的物理页,页目录表基址存在Cr3寄存器中,这些都是保护模式的内容,已经很熟悉了
- 当前的Cr3的值来源于当前的进程(_KPROCESS.DirectoryTableBase(+0x018))
线程找进程
- KHTREAD.ApcState.Process(+0x44)
- 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。
观察下面的代码:
1 | mov cr3,A.DirectoryTableBase |
将当前Cr3的值改为其它进程,称为“进程挂靠”。
进程挂靠存在的意义是什么呢,上面的代码,分别将不同进程的DirectoryTableBase的值写入Cr3。这时,每次读入的0x12345678这个线性地址上的值,分别是对应进程上0x12345678线性地址所对应物理页的内容。有了进程挂靠,就意味着可以读取其它进程的内存。
分析NtReadVirtualMemory
我们知道,ReadProcessMemory这个三环API是可以读取其它进程的内存的,该函数在0环的实现是NtReadVirtualMemory,来分析一下这个函数,看看它是如何读取其它进程内存的:
首先,进入NtReadVirtualMemory,由于这个函数非常复杂,就直接挑重点来说了 这里调用了一个_MmCopyVirtualMemory函数,看名字就感觉,这个和别的进程的内存可能有点关系,毕竟是Copy来的….
进入_MmCopyVirtualMemory继续查看 这个函数不大,有一个函数很关键MiDoPoolCopy,这个函数Push了一大堆参数,内部应该实现了重要的功能,继续更近
跟进_MiDoPoolCopy函数 往下翻,有一个KeStackAttachProcess,由名字可知,这个函数和进程挂靠有关
再进一步,进入_KeStackAttachProcess函数 看到这里,就是真正的挂靠函数,进入分析看看Windows到底是如何实现进程挂靠的
-
1)修改养父母,即KTHREAD.ApcState.Process的值,修改为将要访问的进程的进程结构体
2)调用进程切换函数KiSwapProcess(本质是切换Cr3)
进入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
参考文章: