一个进程可以包含多个线程
一个进程至少要有一个线程
进程为线程提供资源,也就是提供CR3的值,CR3中存储的是页目录表的基址,CR3确定了线程能访问的内存也就确定了
mov eax,dowrd ptr ds:[0x12345678]
CPU如何解析0x12345678这个地址呢?
_KPROCESS.DirectoryTableBase(+0x018)
)ETHREAD结构体:
+0x034 ApcState
+0x000 ApcListHead
+0x010 Process
+0x014 KernelApcInProgress
+0x015 KernelApcPending
+0x016 UserApcPending
+0x220 ThreadsProcess
ETHREAD结构体+0x220的位置存储的就是当前线程所属的进程。
另外在KTHREAD结构体0x34的位置是子结构体ApcState,ApcState也有一个成员Process指向了当前线程所属的进程。
这就存在一个问题,同一个线程结构体里存了两份指针,这两份指针代表什么?
下面分析SwapContext函数:
这里首先取出目标线程的ApcState.Process存到eax里,然后比较当前线程的ApcState.Process和目标线程的这个成员是否相同,如果不相同就说明不属于同一个进程
代码继续往下走,就会切换CR3的值
线程切换的时候,会比较KTHREAD结构体0x044处指定的EPROCESS是否为同一个,如果不是同一个,会将eax的值取出,赋给CR3。eax此时存储的是目标线程的ApcState.Process。这个时候就发生了进程切换
所以,线程需要的CR3的值来源于0x44处偏移指定的EPROCESS
总结:
0x220亲生父母:这个线程谁创建的
0x44 养父母:谁在为这个线程提供资源,也就是提供CR3
一般情况下,0x220与0x44指向的是同一个进程
正常情况下,CR3的值是由养父母提供的,但CR3的值也可以改成和当前线程毫不相关的其他进程的DirectoryTableBase
线程代码:
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的值改为其他进程,称为进程挂靠
接下来就通过分析NtReadVirtualMemory函数,来看看是怎么读取其他进程的内存。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hprSWzoq-1581511027410)(assets/1581507589446.png)]
首先找到NtReadVirtualMemory函数
这个函数在内部调用了MmCopyVirtualMemory
,继续跟进
真正COPY的函数是MiDoMappedCopy,继续跟进
在开始读取之前先调用了KeStackAttachProcess,也就是之前说过的进程挂靠,继续
这个函数又继续调用了KiAttachProcess,继续跟进
这里先将该线程的+0x44位置的ApcState.Process修改为要读取的进程的KPROCESS
然后又调用KiSwapProcess,真正的挂靠是通过这个函数实现的,继续跟进
真正关键的代码是上面两行,首先取出要读取进程的CR3,+0x18的位置是DirectoryTableBase页目录表基址,然后修改CR3为要读取进程的CR3
NtReadVirtualMemory流程总结
既然修改CR3就可以读取目标进程的内存,那么NtReadVirtualMemory可不可以只修改CR3,不修改当前线程的ApcState.Process为要挂靠的进程。
答案是不可以。
回顾一下之前的为什么需要ApcState.Process的问题就会发现,当调用SwapContext进行线程切换的时候,给CR3赋值的时候赋的是ApcState.Process的值。
如果没有修改ApcState.Process,那就意味着ApcState.Process指向的不是挂靠的进程,而是自己的父进程,一旦这个时候发生线程切换并且在线程切换回来的时候,NtReadVirtualMemory读取的就是自己进程的内存了。
如果我们自己来写这个代码,在切换CR3后关闭中断,并且不调用会导致线程切换的API,就可以不用修改养父母的值
正常情况下,当前线程使用的CR3是由其所属进程提供的(ETHREAD 0x44偏移处指定的EPROCESS),正因为如此,A进程中的线程只能访问A进程的内存
如果要让A进程中的内容能够访问B进程的内存,就必须修改CR3的值为B进程的页目录表基址(DirectoryTableBase),这就是所谓的进程挂靠
跨进程的本质就是进程挂靠,也就是修改CR3的值为目标进程的页目录表基址
A进程中的线程代码
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的值
这段代码的问题在于,当我切换CR3为B进程的页目录基址时,读取的是B进程的内存,那么读取的这段数据该如何传递给A进程呢?不管将这段数据放在哪个位置,始终都是B进程的内存空间。
这里需要回顾一下进程的地址空间管理,低2GB是每个进程私有的,而高2GB的操作系统共享的。如果在B进程将读取的数据放到高2GB共享的内核空间,然后在切回CR3的时候,从高2GB取数据,就解决了这个问题
我们来看一下NtReadVirtualMemory是如何解决这个问题的:
NtWriteVirtualMemory和NtReadVirtualMemory的执行流程类似