一个EPYC逃生:案例研究KVM突破

2021-06-30 01:39:17

KVM(基于内核的虚拟机)是基于Linux的云环境的De-Facto标准虚拟机管理程序。在Azure之外,几乎所有大型云和托管提供商都在KVM之上运行,将其转化为云中的一个基本安全边界之一。

在本博客文章中,我描述了特定于kVM的AMD代码中的漏洞,并讨论了如何将此错误变成完整的虚拟机转义。据我所知,这是第一个公开写作KVM访客到主机突破,不依赖于QEMU等用户空间组件中的错误。讨论的错误被分配了CVE-2021-29657,影响了内核版本V5.10-RC1至V5.12-RC6,并在2021年3月底被修补。由于该错误仅在V5.10中被解释,并且在大约5个月后发现,大多数真实的KVM部署不应受到影响。我仍然认为这个问题是一个有趣的案例研究,可以在为KVM建立稳定的客人到主管逃生的工作中,希望这个写作能够加强虚拟机管理程序妥协不仅是理论问题的情况。

在潜入错误和剥削之前,我开始简短概述KVM的体系结构。

KVM是一种基于Linux的开源管理程序,支持X86,ARM,PowerPC和S / 390上的硬件加速虚拟化。与其他大开源管理程序Xen相比,KVM与Linux内核深度集成,并在其调度,内存管理和硬件集成上构建,以提供有效的虚拟化。

KVM实现为一个或多个内核模块(kvm.ko plus kvm-intel.ko或kvm-amd.ko上的x86),将低级IOCTL的API公开到/ dev / kvm设备上的用户空间进程。使用此API,用户空间进程(通常称为VMM for Virtual Machine Manager)可以创建新的VM,分配VCPU和内存,以及拦截内存或IO访问,以提供对模拟D或虚拟化感知硬件设备的访问。 QEMU一直是基于KVM的虚拟化的标准用户空间选择很长时间,但在过去的几年中,LKVM,CROSVM或鞭炮等替代方案已经开始变得流行。

虽然KVM对单独的用户空间组件的依赖可能似乎是一个非常好的好处:KVM主机上运行的每个VM都有1:1映射到Linux过程,使其使用标准的Linux工具可管理。

例如,这意味着嘉宾' s存储器可以通过转储其用户空间过程的分配内存或可以轻松地应用CPU时间和内存的资源限制来检查。此外,KVM可以将大多数与设备仿真相关的工作卸载到Userspace组件。在与中断处理相关的几个性能敏感设备之外,可以在用户佩纳中实现所有复杂的低级代码,用于提供虚拟磁盘,网络或GPU访问权限。

在查看与KVM相关的公众撰写相关的漏洞并利用它变得清楚,这种设计是一个明智的决定。大多数披露的漏洞和所有公共利用影响QEMU及其对模拟/半虚拟化设备的支持。

尽管KVM的内核攻击表面明显小于由默认的QEMU配置或类似的用户空间VMMS暴露的攻击,但KVM漏洞具有使其对攻击者非常有价值的优势:

虽然用户空间VMMS可以是Sandboxed以减少VM Breakout的影响,但没有此类选项可用于KVM本身。一旦攻击者能够在主机内核的上下文中实现代码执行(或类似强大的基语),系统完全泄露。

由于QEMU的安全历史稍差,新的用户空间VMMS,如CROSVM或Firecracker都以Rust,一种内存安全语言编写。当然,由于KVM API的错误或错误使用,仍然存在非记忆安全漏洞或问题,但使用RECT有效地防止了过去在基于C的用户空间VMMS中发现的大多数错误。

最后,纯KVM利用可以针对使用专有或重型用户空间VMM的目标。虽然大云提供商公开的虚拟化堆栈没有详细描述,但可以安全地假设它们不依赖于未修改的QEMU版本,以获得其生产工作负载。相比之下,KVM的较小的代码基础使得重大修改不太可能(并且KVM的贡献者列表点在存在时,在上游的上游修改时)。

考虑到这些优势,我决定花一些时间为kvm漏洞进行狩猎,这可能变成客人到主机逃生。在过去,我取得了一些成功,在英特尔CPU上找到了KVM支持嵌套虚拟化的漏洞,因此回顾了AMD的相同功能似乎是一个很好的起点。这更为真实,因为近期AMD的市场份额的增加意味着KVM的AMD实施突然成为一个比过去几年更有趣的目标。

嵌套虚拟化,VM(名为L1)的能力将嵌套的访客(L2)(L2),也是很长一段时间的利基特征。但是,由于硬件改进,减少了其开销和越来越多的客户需求,它变得更加广泛。例如,Microsoft严重推动基于虚拟化的安全性,作为较新的Windows版本的一部分,需要嵌套虚拟化来支持云托管的Windows安装。 KVM默认支持AMD和Intel上的嵌套虚拟化,因此如果管理员或用户空间VMM未明确禁用它,则它是恶意或受损VM的攻击曲面的一部分。

AMD的虚拟化扩展名为SVM(对于安全虚拟机),为了支持嵌套虚拟化,主机管理程序需要拦截其访客执行的所有SVM指令,模拟其行为并将其与底层硬件同步保持状态。正如您可能想象的那样,对复杂逻辑缺陷的巨大潜力来实现这一正确的困难,使其成为手动代码审查的完美目标。

在进入KVM CodeBase和我发现的错误之前,我想快速介绍AMD SVM如何使遗迹的其余部分更容易理解。 (对于彻底的文档,请参阅AMD64架构程序员手册,第2卷:系统编程第15章。)SVM如果通过在EFER MSR中设置SVME位,请支持6个新指令到X86-64。这些指令最有趣的是VMRUN(因为它的名字表明)负责运行Guest VM。 VMRUN通过rax寄存器指向名为“虚拟机控制块”(vmcb)的数据结构的页面对齐的物理地址,该rax寄存器采用隐式参数,该数据结构(vmcb)描述了VM的状态和配置。

VMCB分为两部分:首先,状态保存区域存储所有访客寄存器的值,包括段和控制寄存器。其次,描述VM配置的控制区域。控制区域描述为VM启用的虚拟化特征,设置截取的VM动作以触发VM退出,并存储一些基本配置值,例如用于嵌套分页的页面表地址。

如果VMCB正确准备(并且我们尚未在VM中运行),则VMRUN将首先将主机状态保存在名为HOSS SAVE区域的内存区域中,通过将物理地址写入VM_HSAVE_PA MSR来配置其地址。保存主机状态后,将CPU切换到VM上下文,只有在一个原因或另一个原因触发VM Exit后,vmrun仅返回。

SVM的一个有趣方面是VM Exit后的大量状态恢复必须由虚拟机管理程序完成。发生VM退出后,仅恢复RIP,RSP和RAX,恢复到先前的主机值,并且所有其他通用寄存器仍包含访客值。此外,完整的上下文交换机需要手动执行VMSAVE / VMLOAD指令,该指令保存/加载额外的系统寄存器(FS,SS,LDTR,Star,LSTAR ...)。

对于嵌套虚拟化工作,KVM拦截VMRUN指令的执行,并根据VMCB准备的L1访客(称为kVM术语中的VMCB12)创建自己的VMCB。当然,KVM不能相信客户提供vmcb12,并且需要仔细验证最终以来传递给硬件的真实VMCB的所有字段(称为vmcb02)。

AMD上的大多数KVM用于嵌套虚拟化的代码在ARCH / X86 / KVM / SVM /嵌套中实现.C,拦截嵌套访客的代码在Nested_svm_vmrun中实现:

*保存旧的VMCB,所以我们不需要选择我们的保存,但可以

该功能首先在1(编码样本中标记)中的当前活动的VMCB(SVM-> VCMB)中的rax的值。对于使用嵌套分页的访客(现在是现在唯一的相关配置)Rax包含一个客户物理地址(GPA),它需要首先将其转换为主机物理地址(HPA)。 KVM_VCPU_MAP(2)负责此转换,并将底层页面映射到可通过KVM直接访问的主机虚拟地址(HVA)。

vmcb映射后,将indested_vmcb_checks调用3中的某些基本验证。之后,将存储在SVM-&gt中的L1 Guest上下文。VMCB被复制到主机保存区域SVM->嵌套。在KVM之前通过调用Enter_svm_guest_mode(4)进入嵌套的guatern never之前。

查看Enter_svm_guest_Mode,我们可以看到KVM将VMCB12控制区域直接复制到SVM->嵌套.DCL,并且不会对复制值进行任何进一步检查。

熟悉双重获取或使用时间使用时间漏洞的读者可能已经看到了一个潜在的问题:indested_svm_vmrun开头的呼叫对vmcb的副本执行所有检查的indapt_vmcb_check存储在访客内存中。这意味着具有多个CPU内核的访客可以在indested_vmcb_checks验证后修改VMCB中的字段,但在它们被复制到SVM-&gt之前;嵌套。在load_nested_vmcb_control中嵌套。

让我们来看看Nested_vmcb_checks,看看我们可以用这种方法绕过什么样的检查:

乍一看,这看起来很无害。 Control-> ASID未使用任何位置,最后一次检查仅对不支持嵌套分页的系统相关。但是,第一个检查结果是非常有趣的。

出于ME未知的原因,SVM VMCB包含一位位,其在客座内部执行时启用或禁用VMRUN指令的拦截。清除该位实际上并非实际支持硬件并导致立即vmexit,因此indested_vmcb_check_controls中的校验只会复制此行为。当我们通过反复翻转拦截_VmRun位的值来竞争和绕过检查时,我们最终可以在SVM->嵌套.ctl中包含0代替拦截_Vmrun位的情况。要了解我们首次需要了解嵌套vmexit的影响,请参见kvm:

主SVM退出处理程序是Arch / x86 / kvm / svm.c中的函数handle_exit,只要发生vmexit就会调用。当KVM正在运行嵌套的访客时,首先必须检查退出是否应由自身或L1虚拟机管理程序处理。为此,它调用unteed_svm_exit_handled(5)whi ch将返回nested_exit_done,如果vmexit将由L1虚拟机管理程序处理,并且不需要通过L0虚拟机管理程序进行进一步处理:

indested_svm_exit_handled首次调用nested_svm_intercept(6)以查看是否应该处理退出。当我们通过在L2 Guest中执行VMRUN触发EXIT时,执行默认情况(7)以查看SVM-&gt中的拦截器_Vmrun位是否设置。通常,这应该始终是这种情况,函数返回Nested_exit_done以触发从L2到L1的嵌套VM退出,并让L1虚拟机管理程序处理出口(8)。 (以这种方式KVM支持无限嵌套的虚拟机管理程序)。

但是,如果L1客户利用上述SVM-&gt所描述的竞争条件;嵌套.ctl不会有拦截器,并且VM Exit将由KVM本身处理。这导致第二次调用indest_svm_vmrun,同时仍在L2 Guest上下文中运行。 Nested_svm_vmrun未写入以处理这种情况,并盲目地覆盖存储在SVM-&gt中的L1上下文;嵌套.HSAVE使用当前活动的SVM-&GT的数据; VMCB包含L2 Guest的数据:

*保存旧的VMCB,所以我们不需要选择我们的保存,但可以

这成为一个安全问题,因为型号特定寄存器(MSR)拦截被嵌套的访客处理:

SVM使用权限位图来控制VM可以访问哪些MSR。位图是一个8KB数据结构,每个MSR两个位,其中一个控制读取访问和另一个写访问。在该位置中的1位意味着接入被拦截并触发VM出口,0比特表示VM具有直接访问MSR。位图的HPA地址存储在VMCB控制区域中,对于正常的L1 KVM Guest,一旦创建VCPU,页面就会分配并固定到内存中。

对于嵌套的访客,MSR权限位图存储在SVM-&gt中;嵌套.msrpm及其物理地址被复制到活动的VMCB中(在SVM-> vmcb-> control.msrpm_base_pa)中,而嵌套guest虚拟机运行。使用所描述的双重调用inested_svm_vmrun,恶意客户机可以将此值复制到svm->执行Copy_vmcb_Control_area时,将此值复制到SVM->嵌套.hsave vmcb。这很有趣,因为KVM的HSAVE区域通常仅包含L1 Guest上下文所以SVM->嵌套.hsave.msrpm_base_pa通常指向Pinned VCPU的MSR位图页面。

自提交“2FCF4876:KVM:NSVM:从去年10月开始嵌套状态的需求分配”,SVM->当客户更改MSR_EFER寄存器的SVME位时,动态分配并释放嵌套.MSRPM:

对于“禁用SVME”案例,KVM将首先调用svm_leave_,以强制留下潜力

嵌套客人然后释放SVM->嵌套数据结构(包括MSR权限位图的备份页面)在SVM_FREE_已被中。正如SVM_LEAVE_NESTED认为svm->嵌套.hsave包含l1 guest虚拟机的保存上下文,它只是将其控件区域复制到真实的vmcb:

svm->嵌套 - > msrpm。一旦完成了SVM_FREE_已完成并且KVM通过控制返回给客户端,CPU将使用释放页面进行其MSR权限检查。如果页面重复使用并将其部分覆盖为零,则这为客户提供了对主机MSR的访问权限。

为了总结,恶意客人可以使用以下方法获得主机MSRS:

反复尝试使用VMRUN指令启动L2访客,同时在第二个CPU核心上翻转Intercept_vmrun位。

如果vmrun成功,请尝试使用vmrun的另一个调用来启动“L3”来宾。如果这失败,我们在步骤2中失去了比赛,并且必须再试一次。如果vmrun成功,我们已成功覆盖SVM->嵌套.HSAVE与我们的L2上下文。

清除MSR_efer中的SVME位,同时仍在“L3”上下文中运行。这使得现在正在执行的L2 Guest虚拟机使用的MSR权限位图备份页面。

等到KVM主机重用备份页面。这将可能清除全部或某些位,使访客访问主机MSR。

当我最初发现并报告这种漏洞时,我感到非常相信,这种类型的MSR访问应该或多或少等于主机上的完整代码执行。虽然我的感觉是正确的,但到达那里仍然花了我多周的利用发展。在下一节中,我将描述将此原语转为主机转义的步骤。

假设我们的访客可以获得对任何MSR的完全不受限制的访问权限(这只是凭借Init_On_Alloc = 1是最现代化分布的默认值),我们如何在KVM主机的上下文中升级它在运行任意代码中?要回答这个问题,我们首先需要查看现代AMD系统支持哪种MSR。查看最近AMD处理器的BIOS和Kernel开发人员指南我们可以找到广泛的MSR,以众所周知的和广泛使用的诸如EFER(扩展功能启用寄存器)或LSTAR(SYSCALL目标地址)以很少使用与SMI_ON_IO_TRAP(可用于在访问特定IO端口范围时生成系统管理模式中断)。

查看列表,像LSTAR或Kernel_GSBase这样的多个寄存器似乎是有趣的目标,用于重定向主机内核的执行。默认情况下,实际上启用了对这些寄存器的不受限制的访问,但是在vmexit之后,它们会自动将其恢复到有效状态,以便修改它们不会导致主机行为中的任何更改。

此前,我们之前提到的一个MSR似乎为我们提供了一种直接实现代码执行的方式:存储主机保存区域物理地址的VM_HSAVE_PA,用于在发生vmexit时恢复主机上下文。如果我们可以在我们的控件下将此MSR指向内存位置,我们应该能够伪造恶意主机上下文并在VMexit后执行自己的代码。

AMD非常清楚的是,软件不应该以任何方式触摸主机保存区域,并且存储在该区域中的数据依赖于CPU:“处理器实现可以在指向的内存区域中仅存储部分或者主状态通过vm_hsave_pa msr,可以在隐藏的片上存储器中存储一些或所有主机状态。不同的实现可以选择保存主机段寄存器的隐藏部分以及选择器。由于这些原因,软件不得依赖主机状态保存区域的格式或内容,也不会通过修改主机保存区域的内容来尝试更改主机状态。 “(AMD64架构程序员手册,第2卷:系统编程,第477页)。为了加强重点,主机保存区域的格式无证。

随着任何问题导致立即处理器关闭,调试涉及无效主机状态的问题非常乏味。更糟糕的是,我不确定是否在VM内部运行时重写VM_HSAVE_PA MSR甚至可以工作。在正常运行期间,在最坏的情况下,这并不是真正的事情,因此覆盖了MSR,只会导致立即崩溃。

即使我们可以在我们的客人中创建有效(但恶意)主机保存区域,我们仍然需要某种方式来识别其主机物理地址(HPA)。由于我们的访客使用嵌套分页启用,我们可以在访客中看到的物理地址(GPAS)仍然是一个源于HPA等效的地址。

在花一些时间滚动到AMD的文档之后,我仍然决定VM_HSAVE_PA似乎是最佳的前进方向,并决定一个接一个地解决这些问题。

在倾倒在AMD EPYC 7351P CPU上运行的普通KVM Guest的主机保存区域后,第一个问题很快就会消失:事实证明,主机保存区域具有与普通VMCB相同的布局,只有几个相关字段初始化。甚至更好,初始化的字段包括在AMD手册中记录的所有已保存的主机信息,因此担心A

......