KPTI对VMI的影响

Reading time ~1 minute

问题

最近在从事和VMI相关的一些工作,在过程中遇到了一个问题.一般来说,在X86体系下,CR3寄存器可以用来唯一的标识一个进程,所以非常重要.而最近在使用CR3标识进程的时候,发现一个进程在mm_struct.pgd中的值不是其CR3实际的值.

CR3值的由来

在Linux内核中,每个线程都有一个task_struct结构,这个结构体里mm成员是mm_struct结构体的指针,代表着该线程的内存地址布局.一个线程组(进程)中所有的线程共享一个mm_struct. mm_struct结构体中pgd变量即是指向该线程的顶级页表目录. 对于一个内核线程,它没有自己的页表,在调度算法调度到内核线程时,将复用上一个用户态线程的页表项,并将上一个线程的mm_struct指针赋给该内核线程task_struct结构体的active_mm成员.

现象

借助qemu+kvm,可以观察到虚拟机中的CR3赋值.一般来说,CR3的序列是随机的,每个进程都有一个特有的CR3.而在对系统较新的虚拟机(Ubuntu 1804)观察CR3可以看到,CR3的值成对出现,会出现一大一小的CR3值,两者相差0x1000,而且两者被装载到CR3的次数相近. 通过内存搜索的方法,对两个CR3进行回溯发现,小的那个CR3有对应的task_struct,而大的那个却没有.

KPTI

在2017年的时候,有一篇论文讲到如何实现内核空间和用户态空间的隔离以加固KASLR.之后出现了Meltdown和Spectre漏洞.而这周隔离机制正好可以用来抵御Meltdown,被并入内核主线.

实现

KPTI实现非常简单,具体可以看原文.大致来说是在进程创建时候会alloc8K对齐的2个page,一个是正常的页表,一个是没有映射内核地址空间的页表.在从内核态切换到用户态时候,会将CR3+0x1000. 这样一来一个进程实际上有两个CR3,一个是跑在内核态时,值即使进程的mm_struct.pgd,另一个跑在用户态时,值为之前的值加0x1000.