深入分析微软新引入的内核虚拟地址影子(KVAS)特性

前言

目前,针对Meltdown和Spectre,各方仍然在努力修复这两个漏洞,包括苹果、甲骨文和微软在内的大型厂商,已经陆续发布了补丁。英特尔发布的补丁存在一些潜在的问题,可能导致用户主机的频繁异常重启,这些潜在问题从Sandy Bridge架构(2011年)开始就一直存在,直至如今的Kaby Lake(2016年,即第七代酷睿处理器)。
在之前的博客文章( https://blog.fortinet.com/2018/01/17/into-the-implementation-of-spectre )中,我们详细介绍过Spectre漏洞的原理。此外,我们还在另一篇文章( https://blog.fortinet.com/2018/01/12/dr-strangepatch-or-how-i-learned-to-stop-worrying-about-meltdown-and-spectre-and-love-security-advisory-adv180002 )中,对微软安全公告ADV180002所涉及的补丁进行了技术分析。
该补丁主要引入的一个特性就是内核虚拟地址影子(Kernel Virtual Address Shadow,由微软提出的一个术语,简称KVAS),由于该特性仅允许用户模式代码访问有限的内核内存,因此能有效防范Meltdown攻击。在本文,我们将对内核虚拟地址影子这一特性进行深入分析。
关于Meltdown和Spectre漏洞,请参见: https://spectreattack.com/
苹果发布的通告及补丁,请参见: https://support.apple.com/en-us/HT208394
甲骨文发布的通告及补丁,请参见: http://www.zdnet.com/article/meltdown-spectre-oracles-critical-patch-update-offers-fixes-against-cpu-attacks/
微软发布的通告及补丁,请参见: http://www.zdnet.com/article/meltdown-spectre-oracles-critical-patch-update-offers-fixes-against-cpu-attacks/
微软安全公告ADV180002,请参见: https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/ADV180002

 

整体设计

在深入挖掘细节之前,我们有必要先对其有一个整体上的认识。下表由美国互联网应急中心提供,对Spectre和Meltdown的攻击方式进行了概括:

根据反病毒测试研究团队对新发现的119个恶意样本的分析,证实了Meltdown攻击实现的难度确实较低(主要是成本非常低),因此在漏洞被爆出的两周之内,极有可能在实际中被大量利用。所以,微软首先专注于对Meltdown漏洞的修补,是完全合理的。
我们假定读者已经对操作系统的概念和原理有基本的了解,包括虚拟内存、x86与x64、MSR、内核模式与用户模式等。如果你对上述的某些概念不太了解,我们建议你首先查阅《英特尔64和IA-32架构软件开发人员手册》(Intel® 64 and IA-32 Architectures Software Developer’s Manuals),这是一本权威并且系统的指南,任何新手都可以从这里起步。
下图是内核虚拟地址影子特性的总体设计思路:

从概念上看,该特性分为三部分:入口、中间的任意控制流、出口。这里的关键之处在于,通过巧妙的分页结构,将内核空间与用户空间分隔开。在用户空间和内核空间中,只有少量的页面会被映射。正因如此,即使攻击者成功实现了Meltdown攻击,也无法再进一步泄漏内核内存。入口和出口的swap地址空间会来回交换,使得只有内核代码才有权访问内核内存空间。这样的设计还有一个好处,以后就可以仅对分页结构进行操作,不必再依靠任何硬件级别上提供的额外支持(例如:微代码更新)来实现。
如下图所示,NtOpenProcess的代码区域不可访问,因为它没有使用UserDirectoryTableBase进行映射。

详细分析

在这里,重点介绍我们对内核虚拟地址影子(KVAS)分析过程中的主要发现。以下是NT内核自身的关键细节:
Windows 7 x64系统版本号:6.1.7601.24000
大小:5581544字节
Sha256:9A6C19B29EBB8D9399C771F2B570E6DCDDF75AC7F2A5F4E8013F4EC7A31F7CA8
我们发现,KVAS在启动(Boot)过程中就被初始化,会在操作系统初始化过程中被启用。NT内核的入口点称为KiSystemStartup,由操作系统加载器(OS Loader)调用。KiSystemStartup依次执行一些基础的初始化步骤,其中的一个步骤就是调用KilnitializeBootStructures。

KiInitializeBootStructures将会调用KiEnableKvaShadowing。KiEnableKvaShadowing的功能是负责启用KVAS,并设置KiKvaShadow = 1。

我们还观察到,基于进程上下文的标识符(即PCID,用于性能优化,以避免刷新整个转换检测缓冲区TLB,也就是俗称的快表)中,全局标志KiKvaShadowMode将被相应地设置为1或2。如果KiFlushPcid != 0,则该值为1;如果KiFlushPcid == 0,则该值为2。

整个过程的最关键环节,是建立并配置好系统调用处理器。

在此之前,首先我们迅速介绍一下用于执行系统调用的机制。当执行SYSCALL指令(例如在ntdll.dll中)时,代码执行将会切换到内核模式例程,特殊模块寄存器(Model Specific Register)指向该例程的地址。MSR是一个比较特殊的寄存器,必须通过rdmsr和wrmsr这两个CPU指令来借助索引进行访问。例如,IA32_STAR的索引是C0000081,IA32_LSTAR的索引是C0000082等等。
IA32_STAR(0xC0000081):Ring 0和Ring 3段基址,以及SYSCALL的EIP。在较低的32位中存储的是SYSCALL的EIP,在第32-47位存储内核段基址,在第48-63为存储用户段基址。
IA32_CSTAR(0xC0000083):兼容模式下,SYSCALL的内核RIP相对寻址。
IA32_LSTAR(0xC0000082):长模式(Long Mode,即64位)下,SYSCALL的内核RIP相对寻址。
基本上,全局标志KiKvaShadow中的所有结果都会被检查。如果对KiKvaShadow的检查结果为TRUE,就不会使用正常的系统调用处理程序(KiSystemCall32和KiSystemCall64),而是使用影子版本。如果该处理器是AMD的,则会使用AMD专用版本。
当SYSCALL指令被执行时,控制权会立即转移到系统调用处理器,这也就意味着这些处理器也必须被映射到UserDirectoryTableBase中。

同样地,中断服务例程(ISR)也有影子版本:

并且,这些中断服务例程序必须要容易被读取:

重点示例:系统调用

为了说明地址空间是如何来回交换的,我们将聚焦于一个重要的示例——系统调用。和以前一样,我们不对其基础原理做过多讲解,如有需要,大家可以从《英特尔系统编程指南(第三卷)》(Intel’s System Programming Guide (Volume 3))中进行参考和学习。
KiSystemCall64Shadow如下:

首先,SWAPGS用于将当前GS基址寄存器的值与MSR地址C0000102H(IA32_KERNEL_GS_BASE)中的值进行交换。在最初阶段,映射的内核内存非常少(因此非常安全),所以就必须通过GS段来获取所有重要的信息。从GS:6000h开始,依次包含PML4基址的物理地址(也就是x64系统中的页目录)。
GS段中的关键参数如下:

位于gs:6018h的标志将会被检查,如果需要进行交换(Swap),则PML4基址(对应于内核地址空间)将被移动到CR3之中。移动后,便可以访问内核栈,一切都进入了正轨。我们注意到,不一定每次都需要进行交换,因为该交换很可能已经进行过(比如,在使用系统调用过程中发生了中断)。
需要注意的一点时,当新的PML4移动到CR3之后,地址空间会立即切换,并且紧接着的下一个指令会在新的地址空间(内核私有空间)上发生。但由于KiSystemCall64Shadow被映射到相同的虚拟地址,所以一切还都保持着正常工作。
KiSystemCall64AmdShadow:

从这里可以看出,英特尔版本和AMD版本的系统调用处理器差别非常小:二者最终都调用KiSystemCall64ShadowCommon,并且依次使用KeServiceDescriptorTable(又称系统服务描述符/调度表,简称为SSDT)或KeServiceDescriptorTableShadow(包含win32k.sys中的例程地址)。
系统调用返回如下:

在返回到用户模式之前,地址空间会通过sysret指令得到保护。

安全分析

我们对内核的入口和出口进行了相同的分析,这里以中断服务例程(ISR)为例。
中断服务例程:

内核出口:

我们可以看到,同样的设计也适用于ISR。这是一个非常明智的设计,我们可以推断出其中的安全策略。由于只有入口和出口(以及少量的支持数据)被映射,所以内核能够抵御像Meltdown这样的内核攻击。
当然,世界上没有免费的午餐。以前,应用程序对内核进行系统调用或者接收到中断时,内核页表总是存在,所以不需要进行TLB刷新、页表交换等,耗费的资源也比较少。但现在,由于使用了KVAS特性,在系统调用或中断时(例如:NVM Express SSD等繁重的输入/输出),性能可能会有所下降。在云服务中,这一现象尤为明显,例如EpicGames的例子:

总结

总体来说,虚拟内核地址影子是一个非常棒的特性。该特性以一种合理的方式,对系统性能进行了权衡。
本分析报告由FortiGuard Lion团队撰写。

(完)