This page looks best with JavaScript enabled

Windows KVAS

 ·  ☕ 5 min read · 👀... views

因为一些任务需要,接触了Windows KVAS机制,并做了一些逆向分析,可以说是挺复杂的,这里非常感谢 @gmh5225 @kanren3 的帮助

什么是KVAS

内核虚拟地址影子(Kernel Virtual Address Shadow,由微软提出的一个术语,简称KVAS),类似于Linux上的KPTI(Kernel page-table isolation)内核页表隔离机制,即把进程页表按照成用户态、内核态独立的分割成两份,为了杜绝用户态通过一些旁路漏洞来窃取内核态的数据。KVAS在Windows10/11上是默认开启的。

如果开启KVAS的话,应用程序会有两个CR3,即有PCB.DirectoryTableBase和PCB.UserDirectoryTableBase两个域。其中DirectoryTableBase域可以理解为内核CR3,能够访问内核物理页,而三环的Cr3(UserDirectoryTableBase)只映射了内核的KVASCODE区段的物理页(少数r3进入r0的入口),而没有映射其他区段的,因此通过3环的Cr3寻找内核TEXT段的物理页,最多只能找到PPE,而从PDE开始就没有映射了。

分析KVAS初始化过程

分析KVAS的初始化过程,可以追溯到操作系统初始化函数 KiSystemStartup(Windows操作启动入口点,由操作系统加载器(OS Loader)调用), 其中调用了KilnitializeBootStructures,会初始化操作系统所需的一些结构,其中就会通过PRCB判断是否初始化KVAS而调用KiEnableKvaShadowing,并设置 KiKvaShadow = 1,并随后配置系统调用处理器为KiSystemCall32Shadow/KiSystemCall64Shadow,而若没有开启KVAS则发生系统调用时调用的处理器是KiSystemCall32/KiSystemCall64。

KiEnableKvaShadowing

也就是当开启KVAS后syscall发生时会走KiSystemCall64Shadow交换页表

KiSystemCall64Shadow

PS:实际上,不仅是系统调用函数,很多中断处理例程(如KiBreakPointTrap等),也有shadow版本

dual CR3

先说结论:在Windows10下当特权级身份运行程序的时候,不会出现dual CR3情况,只有一个CR3(只有PCB.DirectoryTableBase,PCB.UserDirectoryTableBase为空)

为了分析Windows内核页表隔离的操作流程,可以看Windows各中断例程代码。我们知道当中断发生时,需要从Ring3进入Ring0执行代码,这个过程肯定是要处理KVAS的问题的,以Int3中断处理例程KiBreakpointTrapShadow为例

KiBreakpointTrapShadow

v6.OffsetLow是PreviousMode,即发生中断时是属于哪个Mode。如果是UserMode,则会进入if体。在if体内,先执行了swapgs指令,交换GS寄存器与IA32_KERNEL_GS_BASE。其中,IA32_KERNEL_GS_BASE寄存器是一个MSR寄存器,用了保存kernel级别的数据结构指针,因为在这个位置,还没有映射内核内存(只映射了少数入口点),所以需要通过GS获得必要的信息。gs[0x9000]包含PML4基址的物理地址(KernelDirectoryTableBase),而gs[0x9018]则是一个标志位(ShadowFlags),标志是否开启KVAS,若开启则将KernelDirectoryTableBase写入CR3,以完成CR3的切换。那么决定是否转换页表的就是这个ShadowFlags,需要看一下这个位置是在哪里设置的。

KVAS这个东西是和进程相关的,也就是说gs[0x9018]这个标志位会在进/线程发生切换的时候根据上下文的状态被设置。ntoskrnl中,切换上下文的函数 KiSwapContext 调用 SwapContext,其中会根据上下文设置该标志位以决定这条线程发生中断时是否需要转化页表。
SwapContext

在这部分代码中,会将PCB.DirectoryTableBase写入PRCB.KernelDirectoryTableBase,也就是前面看到的GS[0x9000]位置;并会依据KPROCESS->AddressPolicy来设置ShadowFlags(GS[0x9018]), KPROCESS->AddressPolicy在进程创建时依据进程是否由管理员身份启动而设置(NtCreateProcess->PspAllocateThread->PspUserThreadStartup->PspDisablePrimaryTokenExchange->SeTokenIsAdmin/SecureHandle->Process->Pcb.AddressPolicy = 1),如果是管理员进程,则KPROCESS->AddressPolicy为1,ShadowFlags会被设置为2,若是普通进程,则ShadowFlags为1。 在中断发生时,会依据ShadowFlags判断是否要切换CR3到PCB.DirectoryTableBase。PCB.DirectoryTableBase中映射了内核内存,而PCB.UserDirectoryTableBase没有映射内核内存,而非特权级应用程序在三环中使用的时UserDirectoryTableBase,只有在中断时才会被切换到DirectoryTableBase从而达到内核页表隔离的目的。

关于非特权级应用程序具有dual CR3而特权级应用程序只有一个CR3(PCB.UserDirectoryTableBase为空)还在 KiKernelSysretExit 中有体现,该函数是系统调用完成后从Ring0返回三环的函数。

可以看到对进程类型做了判断,如果是非特权级进程会获取到KPROCESS->UserDirectoryTableBase并填入CR3以恢复内核页表隔离。

参考资料

  1. 内核页表隔离补丁
Share on

Qfrost
WRITTEN BY
Qfrost
CTFer, Anti-Cheater, LLVM Committer