搞定PatchGuard:利用KPTI绕过内核修改保护

原文链接:https://blog.ensilo.com/meltdown-patchguard

0x00 前言

2018年年初暴露了一类新的漏洞:推测执行(speculative execution)。具体漏洞为Spectre以及Meltdown,这两个漏洞都源自于CPU本身测信道方面的缺陷。虽然这些漏洞位于硬件中,但一些缓解措施可以通过软件来实现。内核页表隔离(Kernel Page Table Isolation,KPTI)就是其中非常复杂的一种缓解措施,这种方法需要大幅修改操作系统内核,由于其重要地位,之前的老版本操作系统甚至也会部署这种方案。

在本文中,我们将介绍绕过微软PatchGuard的一种新方法,这种方法利用了KPTI来hook系统服务调用以及用户模式中断。

注意:在开发出PoC样例后,我们向微软安全响应中心(MSRC)反馈了我们的研究成果。微软确认该问题存在,并根据我们的建议在Windows 10 RS5中引入了修复补丁。

0x01 背景介绍

推测执行是现代CPU的一种特性,可以用来提高计算性能。CPU在内部会采用无序方式提前执行指令,如果某些指令不应该被执行,那么就会在影响CPU最终状态之前被丢弃。当指令被弃用时,就会影响缓存(cache),因此我们有可能使用这一特性,通过侧信道方式来评估内存的访问时间。

Meltdown(也称为Rogue Data Cache Load,恶意数据缓存加载)利用的正是指令执行以及权限检查之间存在的CPU竞争条件,用户模式代码可以利用该漏洞,以可靠且相对快速的方式读取内核空间内存。

0x02 KPTI

Kernel Virtual Address Shadow(内核虚拟地址影子)是微软在Windows系统上对KPTI的具体实现方式。部署这种机制后,进程虚拟地址空间会被分割为两个单独的页表:运行在内核以及用户模式下的代码分别使用其中一个表。内核模式页面会映射完整的地址空间(用户及内核空间),而用户模式页表只映射了ring 3及ring 0转换所需的最小内核空间。

执行转换过程的代码位于ntoskrnl.exe中,落在名为KVASCODE的特定section中。该section被映射到用户及内核页表的相同虚拟地址以及内核处理器控制块(Kernel Processor Control Block,KPRCB)结构末尾的栈空间及专用区域,因此不会出现敏感内核信息泄露问题。

用户地址空间在每个进程的两个页表之间共享。映射到用户页表中的最小内核地址空间在不同进程之间共享,这种情况同样适用于常规的内核地址空间。操作系统将不同页表中的不同PML4(页表的最顶层)表项指向相同的PDP表来实现这种机制。

图1. 启用KVAS的页表样例

为了使性能最大化的同时降低开销,KVAS会应用于较低权限的应用,使其充当完整权限的进程,通过加载驱动来访问内核进程。

与TLB管理有关的PTE还有其他改动,但我们不会深入讨论这方面内容,因为这与本文的主题关系不大。

0x03 系统服务调用及中断

ring 3及ring 0之间的转换现在会涉及一个新的步骤,及用户地址空间中映射的最小内核空间。我们可以将这种情况看成原先的转换过程中插入了一个间接跳转环节。

图2. 启用KVAS时ring 3到ring 0的转换过程

新的系统调用及终端转换代码会检查用户页表是否被使用,如果条件满足,则切换到内核页表,然后在内核中恢复正常操作,切换回ring 3时则执行相反的操作。

图3. KVAS syscall入口点反汇编代码

0x04 可以预料的后果

Patch Guard或Kernel Patch Protection(内核修改保护)的目的是避免操作系统在运行时被篡改,会监控对ntoskrnlHALNDIS中代码以及关键结构(如IDT、SSDT)的修改。

现在ring 3到ring 0转换的第一条指令以及最后一条指令已不在操作系统内核中执行,而是在单独的地址空间中执行,因此我们现在面对的是在内核模式下运行代码的一个新环境,可能没有得到充足的保护。

由于内核没有映射到用户页表中,因此在内核模式和用户模式来回转换期间PatchGuard无法运行。简而言之,用户页表中的内核地址空间对PatchGuard透明,因此我们可以拦截并操控在用户模式下触发的ring 0系统服务调用及中断。

0x05 访问用户页表

在进一步深入研究之前,我们需要访问这个新的用户页表。由于地址转换由MMU配合物理地址来实现,因此我们当前所处的虚拟地址空间不一定能访问不同地址空间的页表。

以内核模式运行时我们能够访问物理内存,因此,我们首先需要找到用户页表的物理地址,我们可以从线程的用户模式上下文中的EPROCESS.Pcb.UserDirectoryTableBase或者CR3寄存器找到这个信息。

掌握这个信息后,PTE重映射和页表遍历就变得非常简单,这样我们就可以完全控制用户页表中的内核地址空间。我们可以使PTE有效来分配内存,修改PTE的PFN(Page Frame Number,页帧号)来交换页面。如果需要读/写,我们可以通过相同的方式将页面映射回内核页表。

0x06 Hook

由于ring 0入口点逻辑发生变化,因此hook方法也需要相应改变。

内存管理

kvascode section的页面会映射到所有页表(用户及内核空间)中的相同物理页面。我们首先修改用户页表,这样KVASCODE虚拟地址就会被映射到不同的物理页面。这样就能腾出相关函数为我们所用,因为现在不再需要检查哪个页表正在被使用。通过这种方式,我们也能阻止PatchGuard检测代码更改,因为这里并没有更改ntoskrnl的任何页面。

代码执行

接下来我们要解决的就是如何真正运行我们的代码。KVASCODE section的大小非常有限,我们可以在用户页表中映射其他页面。这并不能让我们使用操作系统提供的服务,但可能可以让我们切换到内核页表。

由于我们的代码没有映射到用户页表中,因此在切换页表前我们无法直接跳转到我们的代码中。如果简单切换页表,就会返回到我们无法控制的原始KVASCODE页面。此时我们可以做的就是利用ROP。

在我们在用户页表中重映射的页面中,我们将创建一个CR3分配gadget,其ret指令在两个虚拟地址空间中重合。通过这种方式,即使修改了页表,ret指令也会被执行。接下来,我们将修改代码逻辑,压入我们驱动的地址,并且跳转到我们创建的CR3分配gadget上。

图4. 原始及hook后nt!KiSystemCall64Shadow的汇编代码

处理VBS

VBS并不会让这种技术失效,但的确会带来一些麻烦。

在启用HyperVisor代码完整性(HVCI)的情况下,我们再也无法在内核中运行未签名的代码。HVCI会阻止动态创建hook,因此我们需要在驱动中编译代码。这会给我们造成很大的困难,因为这意味着我们必须预先知道每个页面中函数的具体位置。由于这些函数并不会发生太大变化,并且KVASCODE section的大小实际上没有改变(并且还有很大的空间),因此并非完全不可能找到解决办法。如果我们需要在运行时确定数据结构的偏移值,就可以将它们存储在数据页表中,而我们会将数据页表映射到用户页表中距离KVASCODE页面固定偏移值的某个位置。

还有另一件事情需要注意,在启用了VBS的主机上,root分区将使用int 2e指令,而不会使用syscall或者sysenter指令。

0x07 缓解措施

解决方法非常简单:PatchGuard应该检查内核和用户页表中KVASCODE页面PTE的PFN是否相同。这样将确保最小内核地址空间中的代码与实际内核空间中的代码相匹配。一旦PatchGuard验证了内核中的代码,就不会发生篡改现象。

0x08 总结

对现代操作系统主要架构的修改将会给攻击者和研究人员带来新的复杂性和攻击途径。研究人员可以利用这种新方法来监控用户模式进程中的系统调用,无需依赖虚拟化或者其他硬件功能,也不用受到操作系统安全机制的干扰。由于KVAS的性能优化,这种修改可能涉及到较低权限应用,而这些对象通常更有可能带来安全风险。

我们的研究依托于Windows 10 RS4 x64系统,然而,由于KVAS已经被移植到所有支持的Windows版本上,因此这种技术也适用于Windows 7、8.1以及相应的服务器版本。

虽然本文重点介绍的是Windows系统,但相关技术同样适用于使用KPTI的其他主流操作系统,如Linux以及macOS。

0x09 时间线

源链接

Hacking more

...