导语:从 Windows 10 和 Windows Server 2016 开始,Microsoft 引入了虚拟安全模式 (VSM) ,其中包括一组安全功能,例如 Credential Guard、Device Guard、TPM 和 shielded VMs。
0x01 基本介绍
从 Windows 10 和 Windows Server 2016 开始,Microsoft 引入了虚拟安全模式 (VSM) ,其中包括一组安全功能,例如 Credential Guard、Device Guard、TPM 和 shielded VMs。
通过创建用于保存机密和安全运行敏感代码的隔离内存区域,VSM 构建了一个新的安全边界,也会限制系统特权代码访问受保护的内存区域。这种架构有助于限制恶意软件的的攻击范围,即使当攻击者发现并利用内核漏洞时也无法对VSM内的隔离内存区域造成影响,因为 NT 内核代码被排除在信任链之外。
由于虚拟机管理程序利用了处理器级别的虚拟化技术,这使得新的安全边界的创建成为可能。如果 Rootkit 能够篡改页表条目以更改其对物理页的访问权限,则虚拟化会在地址转换中添加一个新层,包括虚拟化操作系统无法访问的新表,以此来阻止rootkit访问虚拟化系统。
我通过这篇文章分析了 Hyper-V 如何通过创建与rings正交的新信任级别(虚拟信任级别 (VTL))来增强操作系统安全性,以及这些级别如何在最低级别表示。
我将首先讨论硬件虚拟化扩展,重点是英特尔虚拟化技术,称为英特尔 VT-x,然后我将分析 Hyper-V 如何利用硬件虚拟化来加强操作系统的安全性。通过基于逆向和调试 Hyper-V 的方法发现了 Hyper-V 的内部结构以及如何使用它们来实现同一虚拟机内不同 VTL 之间的内存隔离的目标。
最后,我提供了一个 Windbg 脚本,可帮助在 Hyper-V 版本 19041.21 上逆向分析出这些结构。
0x02 虚拟模式扩展 (VMX)
硬件虚拟化是 Intel 于 2005 年推出的 Intel VT-x,AMD 紧随其后,于 2006 年发布了 SVM(后命名为 AMD-V),目标是辅助软件实现虚拟化,后来的几代它允许用于性能提升改进。
在这篇文章中,我重点介绍了 Intel VT-x 及其 Virtual Mode Extensions 的扩展,它们增加了 13 条新指令,其中我引用了VMXON和VMXOFF,它们允许分别进入和退出虚拟执行模式。
1.VMX 操作和转换
由于操作系统中存在rings以分隔在ring 0 中运行的程序和在ring 3 中运行的其他程序之间的特权级别,因此虚拟化定义了区分两种不同执行模式的VMX 模式操作:
◼Hypervisor(主机)进程运行时的VMX root操作;
◼虚拟机(guest)中的进程正在运行时的VMX非 root操作。
两种模式之间的转换称为VMX 转换,并在发生定义类型的事件时触发。从非 root 到 root 模式的 VMX 转换,因此从虚拟机到管理程序的退出称为VM Exit。另一方面,从管理程序到虚拟机的 VMX 转换称为VM Entry。
转换由管理程序管理和控制,该管理程序严重依赖虚拟机控制结构 (VMCS) 。
2.虚拟机控制结构(VMCS)
VMX 转换需要在退出 VMX 模式之前保存处理器状态并恢复它正在进入的适当的处理器状态。VMCS 就是在做这个事情,VMCS 包含一个主机隔离域,保存管理程序/主机的处理器状态,root据它是执行 VM 入口还是 VM 出口,该区域被保存或用于恢复。
VMCS 中也存在一个host区域,其目的相同,不同之处在于它保存 VMX 非root模式的处理器状态,特别是导致 VM 退出的进程的内存状态。因此,跟据虚拟机拥有的虚拟处理器数量,可以有多个 VMCS。
当 VM 退出发生时,执行返回到 VMX root操作,特别是管理程序代码中的 VM Exit handler 程序。此处理程序负责跟据触发 VM 退出的原因采取适当的操作。VM 退出的原因可能大不相同,但幸运的是,它们的代码可以在文档中找到,反汇编代码如下:
FFFFFFFF ; enum VMExit_reason, mappedto_14 FFFFFFFF EXIT_REASON_EXCEPTION_NMI = 0 FFFFFFFF EXIT_REASON_EXTERNAL_INTERRUPT = 1 FFFFFFFF EXIT_REASON_TRIPLE_FAULT = 2 FFFFFFFF EXIT_REASON_INIT = 3 FFFFFFFF EXIT_REASON_SIPI = 4 FFFFFFFF EXIT_REASON_IO_SMI = 5 FFFFFFFF EXIT_REASON_OTHER_SMI = 6 FFFFFFFF EXIT_REASON_PENDING_VIRT_INTR = 7 FFFFFFFF EXIT_REASON_PENDING_VIRT_NMI = 8 FFFFFFFF EXIT_REASON_TASK_SWITCH = 9 FFFFFFFF EXIT_REASON_CPUID = 0Ah FFFFFFFF EXIT_REASON_GETSEC = 0Bh FFFFFFFF EXIT_REASON_HLT = 0Ch
这些值用于 VM 退出处理程序的代码中,其中一个很大的 switch 将它们与导致虚拟机中 VM 退出的实际原因进行比较。
该函数vmcs_get_vmexit_reason调用在FFFFF80000304106是负责读取VMCS VM退出的原因。以下摘录显示了此函数的代码,其中代表 VM_EXIT_REASON 的代码 与 VT-x 指令vmread 一起使用。在 Intel VT-x 中,VMCS 是处理器的内部结构,软件的布局未知。因此,英特尔提供了vmread / vmwrite VT-x 指令,用于在 VMX root操作中从当前 VMCS 读取和写入各个字段,提供与 VMCS 字段相关联的代码。
.text:FFFFF80000302892 .text:FFFFF80000302892 loc_FFFFF80000302892: ; CODE XREF: vmcs_get_vmexit_reason+D↑j .text:FFFFF80000302892 mov eax, VM_EXIT_REASON .text:FFFFF80000302897 vmread rax, rax .text:FFFFF8000030289A mov [rcx], eax .text:FFFFF8000030289C retn .text:FFFFF8000030289C vmcs_get_vmexit_reason endp
所述VM_EXIT_REASON代码在英特尔文档中被定义为0x4402,并且作为 vmread/vmwrite 指令的操作数是必要的:
FFFFFFFF VM_INSTRUCTION_ERROR = 4400h ; XREF: vmcs_get_VM_INSTRUCTION_ERROR FFFFFFFF VM_EXIT_REASON = 4402h ; XREF: vmcs_get_VM_EXIT_REASON+E/s
但是,这两条 VT-x 指令都不需要执行访问的 VMCS 的地址,因为它们都在当前 VMCS 上运行,在 VMX root操作中。
为了知道读/写访问发生在哪个 VMCS 上,Intel VT-x 提供了vmptrst指令来获取当前 VMCS 的物理地址。
loc_FFFFF8000030BD1C: MOV [ RSP + 48 ħ + var_28 ], R14 vmptrst [ RSP + 48 ħ + var_28 ] MOV RAX , [ RSP + 48 ħ + var_28 ]
切换到另一个 VMCS 时需要将其物理地址作为vmptrld VT-x 指令的操作数。
.text:FFFFF80000300993 loc_FFFFF80000300993: ; CODE XREF: sub_FFFFF800003008F8+67↑j .text:FFFFF80000300993 vmptrld qword ptr [rcx+188h]
在调试管理程序时,在vmptrld指令上设置断点表明可以有几个不同的 VMCS 物理地址作为指令的操作数。即使只有一个虚拟机在运行,情况也可能如此。
虽然人们可能认为 VMCS 适合虚拟机,但它实际上附属于其虚拟处理器。每个虚拟处理器至少有一个 VMCS,因此当运行具有 X 个虚拟处理器的虚拟机时,至少存在 X 个 VMCS。
在某些情况下,与一个虚拟处理器关联的 VMCS 可能不止一个。
总而言之,VMCS 是一个内存区域,通常在 4KB 内存页面中,管理程序可以通过其物理地址访问它,以控制和管理正在运行的虚拟机。
它在 VMX 转换中起着关键作用,以便跟踪 VMX root操作和非root操作中的执行情况,并可进一步用于控制虚拟机内的执行。
VMCS 中存在一些附加区域,它们负责修改虚拟机行为,例如添加新的VM Exit reasons,这将强制从虚拟机退出 VM,而其他一些字段将强制使 VM 进入虚拟机机。
3.二级地址转换
除了在 VMCS 的帮助下进行 VMX 操作和处理虚拟机之外,硬件虚拟化在分页机制中增加了一个扩展层。通过分页虚拟内存抽象了物理硬件内存,因为程序只能看到它们的虚拟地址空间。两个进程可以访问指向不同物理内存地址的相同虚拟地址。
由于处理器中的地址转换,其中每个虚拟地址都是一个位域,用于在页表遍历中对用于地址转换的表中的偏移量进行编码。表中读取的每个条目都引用下一个表基址的物理地址。当最后一个表被命中时,读取的条目称为页表条目 (PTE),包含生成的物理页的帧号及其访问权限。
页面遍历从PML4表开始,其地址由 CR3 寄存器在尝试执行访问的进程的上下文中引用。
硬件虚拟化通过在表层次结构中添加一个新级别,在地址转换过程中引入了一个新层。这种硬件虚拟化功能称为“二级地址转换 (SLAT)”或嵌套分页,可以虚拟化host机器的物理内存。基于扩展页表机制 (EPT),它定义了 4 个不同的地址空间:
◼GVA (Guest Virtual Addresses) - 虚拟机中运行的进程所看到的虚拟地址;
◼GPA (Guest Physical Addresses) - 被虚拟化操作系统视为物理内存的物理地址;
◼SVA(系统虚拟地址) - 管理程序进程看到的虚拟地址;
◼SPA(系统物理地址)-指向底层硬件中有效物理内存的物理地址;
系统虚拟地址代表运行在硬件之上的管理程序进程看到的虚拟内存地址空间。因此,它不受第二级地址转换的约束,而是按照从进程的 PML4 表开始的正常页表遍历进行转换,在其 CR3 寄存器中找到。这种地址转换的结果是一个带有帧号的 PTE,可用于访问硬件内存中的系统物理地址。
但是,host物理地址和虚拟地址的概念是特定于虚拟化的,因为在运行虚拟机时会添加一个抽象级别。EPT 增加了层次表并定义了一个新的表层。在虚拟机级别,GVA 是一个虚拟地址,它通过正常的页表遍历转换为 GPA。即使被视为guest级别的物理地址,页表中每个访问的guest物理地址本身也是一个位域,需要将其转换为系统物理地址才能访问真正的物理内存。
除了地址转换中使用的表之外,从guest物理地址到OS物理地址的地址转换遵循从虚拟地址到物理地址的正常地址转换的相同过程。从 GPA 到 SPA 的转换所需的第一个表名为EPT PML4表,他的基地址由Extended Page Table Pointer - EPTP指针引用,该指针可在虚拟机的虚拟处理器的 VMCS 中找到。
EPTP 从第 12 位开始编码 EPT PML4 表的物理地址。
EPTP format 2:0 : Memory type = 6 => Uncacheable (UC) 5:3 : Ept table walk -1 6 : Dirty flags 11:7 : Reserved N-1:12 : Physical address of the 4-KByte aligned PML4 Table
页表遍历发生在 EPT 表中,生成的 EPT 页表条目包含页面的系统物理地址及其有效访问权限。如果虚拟化操作系统中的 ring 0 进程无法访问 EPT PTE,则无法对其访问权限产生影响。
就像两个进程在启用分页的情况下运行有两个不同的 PML4 表,因此使用相同虚拟地址的物理内存存在不同视图,使用指向不同 EPT PML4 表的不同 EPTP,相同的客户物理地址可以转换为不同的系统具有不同访问权限的物理地址。
考虑到操作系统访问的表中的每个条目都是需要转换为 SPA 的 GPA,因此 GVA 到 SPA 的地址转换涉及遍历 2 个级别的表,如下所示:
0x03 Hyper-V
现在我已经描述了硬件虚拟化的基础知识,现在转向逆向和调试 Microsoft 的虚拟机管理程序Hyper-V。Hyper-V 是一种裸机管理程序,因为它直接在硬件上运行。它是微软操作系统架构的重要组成部分,因为 Windows 的大量安全功能依赖于虚拟化。几年前,可以使用 Mimikatz 读取lsass.exe 内存轻松执行凭据窃取。现在,即使在启用了 VBS 的 Windows 10 上使用管理权限也将失败。在这一部分中,我旨在了解 Hyper-V 如何实现这一点。
1.分区(Partitions)
Hyper-V 跟据分区管理其虚拟机的隔离区域。在 Microsoft 术语中,虚拟机称为分区,它们以两种不同的形式存在。第一个是root分区。它是运行在 Hyper-V 之上的主要 Windows 操作系统。它包括虚拟化堆栈的很大一部分,以最大限度地减少管理程序内的代码数量并减少其攻击面。通过向管理程序发出特定调用,root分区可以创建和管理其他分区,这就给我带来了第二种分区:子分区. 子分区是运行在root分区之上的host虚拟机,它们在访问虚拟机管理程序资源方面的特权较低,与大量参与虚拟化任务的root分区不同。这些资源包括超级调用和特定模型的寄存器,可以在 Microsoft 的 Hyper-V TLFS 中找到。一个分区可以有一个或多个虚拟处理器,由它的分区标识符和它的处理器索引来标识。每个虚拟处理器都有自己的寄存器、中断和拦截。
2.虚拟信任级别
启用 VSM 后,在同一分区内,会出现一个新区域,称为安全区域。它在rings 0 中运行一个安全内核,在rings 3 中运行的用户态进程数量有限,称为 trustlets。虚拟信任级别的创建直接来自创建隔离的需要,该隔离设置了不同内存区域之间的界限:normal worlds和secure worlds。
VTL归属于一个虚拟处理器(简称VP)。总共可以有 16 个 VTL,并且它们是分层的。目前微软选择只实现 2 个 VTL:VTL 0 和 VTL 1。VTL 越低,它的特权就越少。较低的 VTL 可以使用较高的 VTL 来存储其秘密或进行敏感操作。
在 Windows 体系结构中,带有用户态进程的普通 NT 内核在 VTL 0 中运行,安全功能在 VTL 1 中运行,即 SecureKernel 和 trustlets。在这个模型中,NT 内核处于信任链之外。secure worlds运行最少的代码,本质上是像 RPC 一样的加密和通信协议,而常规的 OS 功能保留在普通的 NT 内核中。两个内核相互通信,需要协同工作才能正常运行。secure worlds为normal worlds提供安全的方式来保存秘钥和运行敏感的安全算法检查,而普通内核为secure worlds提供通常的操作系统功能,如内存分配等。
3.Hyper-V 结构
Hyper-V 会跟踪当前状态,例如当前正在运行的分区、当前虚拟处理器、当前 VMCS 等。这些信息表示为主要 gs 结构指向的内存中的对象,正如 Saar Amar 在 Microsoft 的博客文章中所述逆向 Hyper-V 。
大多数超级调用会通过检查其权限flag来检查是否允许调用分区执行调用。
HvCallDeletePartition proc near arg_0= qword ptr 8 mov [rsp+arg_0], rbx push rdi sub rsp, 20h mov rax, gs:103A8h mov rdi, rcx mov rdx, [rax+128h] mov rax, 100000000h test rax, rdx jnz short loc_FFFFF800002900D3
在上面的代码段中,HvCallDeletePartition函数首先检查gs:103A8指向的当前 Partition 结构的偏移量128处的权限是否允许执行调用。
0:kd> rdmsr 0xc0000101 msr[c0000101] = fffff87d`63fa6000 0:kd> dq fffff87d`63fa6000 + 0x103A8 fffff87d`63fb63a8 ffffe800`00001000 ffffe800`00669050
当前运行的分区结构位于0xffffe80000001000。而当前的虚拟处理器由gs:103A0指向。
0:kd> dq fffff87d`63fa6000 + 0x103A0 fffff87d`63fb63a0 ffffe800`00669050 ffffe800`00001000 :KD> DQ ffffe800`00001000 L100 ffffe800`00001000 00000000`00000000 00000000`00000000 ffffe800`00001010 00000000`00000000 ffffe800`00001018 ffffe800`00001020 ffffe800`00001018 00000000`00000000 ffffe800`00001030 00000000`00000000 ffffe800`00001038 ffffe800`00001040 ffffe800`00001038 00000000`00000000 ffffe800`00001050 00000000`00000000 ffffe800`00001058 ffffe800`00001060 ffffe800`00001058 00000000`00000000 ffffe800`00001070 00000000`00000000 ffffe800`00001078 ffffe800`00001080 ffffe800`00001078 00000003`00000000 ffffe800`00001090 00000000`00000000 00000001`00000010 ffffe800` 000010a0 00000000`00000000`00000000`00000000 ffffe800`000010b0 00000000`00000000`00000000`00000000 ffffe800`000010c0 00000000`00000000 00000000`00000000 ffffe800`000010d0 00000000`00000000 00000000`00000000 ffffe800`000010e0 00000000`00000000 00000000`00000000 ffffe800`000010f0 00000000`00000000 00000000`00000000 ffffe800`00001100 00000000`00000000 00000000`00000000 ffffe800`00001110 00000000` 00000000 00000000`00000000 ffffe800`00001120 00000000`01017843 002bb9ff`00003fff ffffe800`00001130 ffffe800`00000000 00000000`00000000 ffffe800`00001140 ffffe800`0063c7f0 00000003`00000004 ffffe800`00001150 00000000`00000000 ffffe800`00669050 ffffe800`00001160 ffffe800`00770050 ffffe800`00793050 ffffe800 `00001170 ffffe800`007b1050 00000000`00000000 ffffe800`00001180 00000000`00000000 00000000`00000000
在Partition 结构的偏移量0x128处,可以提取分区的权限,值为 002bb9ff00003fff。它的二进制表示对允许分区访问的超级调用和 MSR 进行编码。
>>> bin(0x002bb9ff00003fff) 1 0 1 0 1 1 1 0 1 1 1 00 111111111 000000000000000000 11111111111111 Access to Hypercalls | Reserved |Access to MSRs
跟据允许的管理程序资源,我可以确定发现的分区是root分区。(有关更多信息,请查看Hyper-v TLFS的HV_PARTITION_PRIVILEGE_MASK结构)。
将此值与在0xffffe80200001000处找到的另一个分区的权限 ( 0038803000002e7f ) 进行比较
2:kd> dq 0xffffe80200001000 + 0128 ffffe802`00001128 00388030`00002e7f
第二个分区似乎特权较低,因为它可以访问非常有限数量的超级调用。
>>> bin(0x38803000002e7f) 1 1 1 0 0 0 1 0 0 0 0 00 000110000 000000000000000000 10111001111111 Access to Hypercalls | Reserved |Access to MSRs
可以确定这是一个子分区。
除了特权flag外,分区结构还保存有关其拥有的虚拟处理器数量以及与其关联的每个虚拟处理器结构的地址的信息。
在虚拟处理器创建期间,对HvCallCreateVp超级调用的分析显示对sub_FFFFF8000029FA64函数的调用。它通过增加虚拟处理器数量、最大虚拟处理器 ID 来更新分区结构,由以下代码中的 RDI 寄存器引用,并影响添加到VP ID*8的偏移量0x158的虚拟处理器结构地址。
.text:FFFFF8000029FBA3 mov [rdi+14Ch], r15d ; max VP id .text:FFFFF8000029FBAA .text:FFFFF8000029FBAA loc_FFFFF8000029FBAA: ; CODE XREF: sub_FFFFF8000029FA64+13D↑j .text:FFFFF8000029FBAA mov rcx, rdi .text:FFFFF8000029FBAD call sub_FFFFF800002A20DC .text:FFFFF8000029FBB2 mov rax, [rbp+4Fh+var_28] .text:FFFFF8000029FBB6 mov rcx, rdi .text:FFFFF8000029FBB9 mov [rdi+r15*8+158h], rax ; r15= VP number, rdi= partition .text:FFFFF8000029FBC1 call sub_FFFFF800002A20DC .text:FFFFF8000029FBC6 inc dword ptr [rdi+148h] ; number of VPs
因此,可以将以下信息添加到分区结构中:
◼offset 0x148 : 虚拟处理器的数量;
◼offset 0x14C : 虚拟处理器的最大标识符,第一个在 ID = 0 处;
◼offset 0x158:第一个虚拟处理器结构地址(id=0)。
调试器调试记录:
2:KD> DQ ffffe800`00001000 + 0x140 ffffe800`00001140 ffffe800`0063c7f0 00000003`00000004 ffffe800`00001150 00000000`00000000 ffffe800`00669050 ffffe800`00001160 ffffe800`00770050 ffffe800`00793050 ffffe800`00001170 ffffe800`007b1050 00000000`00000000
可以确认:
◼0xffffe80000001000 + 0x148:分区有4个不同的虚拟处理器;
◼0xffffe80000001000 + 0x14C:最大虚拟处理器索引为3;
◼0xffffe80000001000 + 0x158:虚拟处理器 0 位于0xffffe80000669050;
◼0xffffe80000001000 + 0x160:虚拟处理器 1 位于0xffffe80000770050;
◼0xffffe80000001000 + 0x168:虚拟处理器 2 位于0xffffe80000793050;
◼0xffffe80000001000 + 0x170:虚拟处理器 3 位于 0xffffe800007b1050。
4.虚拟机控制结构的研究
由于 VMCS 在管理分区方面起着至关重要的作用,需要分析分区和虚拟处理器结构,我的目标是找到对 VP 的 VMCS 物理地址的引用。
跟据得到的 VMX 信息,针对操作 VMCS 的 VT-x 指令似乎是一个比较好的入口点。
vmptrld将 VMCS 物理地址作为操作数。指令在hvix64.exe二进制文件的反汇编代码中可以提取出来,目的是找到管理程序存储这种地址的位置,以及引用函数等信息。
VMCS 物理地址通常在添加到指针的偏移量0x188处。如果 RCX 保存了Struct1结构的地址,则在该结构的偏移量0x188处就可以找到 VMCS 物理地址。
.text:FFFFF80000300993 vmptrld qword ptr [rcx+188h] hv+0x30c848: fffff846`eb70c848 0fc7b188010000 vmptrld qword ptr [rcx+188h] kd> r rcx rcx=ffffe8000066d188 kd> dd [rcx+188] ffffe800`0066d310 0e32f000 00000001 00678000 ffffe800 Breakpoint 3 hit hv+0x22697f: fffff846`eb62697f 0fc7b188010000 vmptrld qword ptr [rcx+188h] kd> dd rcx+188 ffffe800`0066b310 0e32b000 00000001 00674000 ffffe800
Struct1的地址是从我选择命名为Struct2的第二个结构的偏移量0xe90中提取的。
.text: FFFFF80000300955 mov rcx , [ rdi + 0 E90h ]
按照相同的过程,Struct2被第三个结构Struct3引用,在偏移量0x180处添加到一个乘以 8 的索引。
.text: FFFFF80000300939 mov rdi , [ rbx + r11 * 8 + 180 h ]
Struct3的基址依次由 GS 寄存器引用的结构指向,偏移量为0x103B0。
.text: FFFFF8000030090C mov rax , gs : 0 ... .text: FFFFF8000030091B mov rbx , [ rax + 103 B0h ]
这个地址指向一个虚拟处理器结构,在代码看到这个地址与当前虚拟处理器结构指针之一进行比较:
.text:FFFFF80000258907 mov rdi, [rsi+103B0h] .text:FFFFF8000025890E test rdi, rdi .text:FFFFF80000258911 jz loc_FFFFF80000258B18 .text:FFFFF80000258917 cmp rdi, [rsi+103A0h] .text:FFFFF8000025891E jnz loc_FFFFF80000258B18 .text:FFFFF80000258924 mov eax, gs:8
现在可以确定以下内容:
◼GS在偏移量0x103B0 处是Virtual Processor;
◼Virtual Processor 在偏移量 0x180 + index*8处是 Struct2;
◼Struct2 在偏移量 0xe90处是 Struct1;
◼Struct1 在偏移量 0x180处是 VMCS Physical Address。
至此,似乎发现了VP和VMCS地址之间的关联。但是需要定义两个结构Struct1和Struct2。
如果回顾一下 Virtual Processor 结构布局,特别是在偏移量0x180处:
0: kd> dq ffffe800`00669050 +0x180 L80 ffffe800`006691d0 ffffe800`0066a000 ffffe800`0066c000 ffffe800`006691e0 000ffe0000000000000000000000000000000
如下:
◼索引 0, Struct2在ffffe8000066a000 ;
◼索引 1, Struct2在ffffe8000066c000。
我注意到偏移量0x180和0x198包含相同的地址,因此偏移量0x198也指向Struct2。
以下调试信息显示了这些结构的内存布局,注意到虚拟处理器在偏移量 0 处被引用。
0:kd> dq ffffe800`0066a000 ffffe800`0066a000 ffffe800`00669050 ffffe800`0066ae80 0:kd> dq ffffe800`0066c000 ffffe800`0066c000 ffffe800`00669050 ffffe800`0066ce80
然而,这两个结构在它们的偏移量0xe90处有不同的Struct1指针,这些指针又包含 2 个不同的 VMCS 物理地址,因此有两个不同的内存状态。
为了找到Struct1和Struct2的正确定义,我深入研究了 Hyper-V TLFS,资料显示:“Virtual Processor为每个活动 VTL 维护单独的状态。每个 VTL 保留的状态(也称为私有状态) ) 由管理程序跨 VTL 转换保存。如果启动了 VTL 切换,管理程序会保存活动 VTL 的当前私有状态,然后切换到目标 VTL 的私有状态。”。
我假设Struct2代表虚拟信任级别,索引 0 处的那个是 VTL 0,而索引 1 处的那个是 VTL 1。因此,Struct1表示 VTL 的内存状态,包括 VMCS 地址。
5.VTL switch
VP可以切换VTL;取决于它是进入更高还是更低的 VTL,分别是 VTL Call和 VTL Return。
(1)VTL Return
VTL Return是指较高的 VTL 启动到较低 VTL 的切换。对HvCallVtlReturn的静态分析表明,该函数在执行切换之前检查 VTL 的索引,如果它等于 0,则跳转到函数末尾。
在我的版本中,包含 VTL 索引的字节位于偏移量0x14处,而当前 VTL 结构在虚拟处理器结构的偏移量0x198处被引用。
使用调试器,在超级调用函数HvCallVtlReturn的开始处设置一个断点,以分析上述偏移量处的数据。
以下调试信息显示了HvCallVtlReturn超级调用开始时的内存上下文:
============ Partition ============ gs:[103a8] = Current Partition: ffffe80000001000 Current Partition Privileges: 2bb9ff00003fff ============ Virtual Processor ============ gs:[103a0] = Current VP: ffffe80000669050 VP:[1d4] = VP ID: 0 ============ Current VTL ============ VP:[198] = Current VTL: ffffe8000066c000 VTL:[14] = Current VTL ID : 1 STATE:[188] = VMCS physical Address: 10e32e000 STATE:[180] = VMCS virtual Address: ffffe80000676000
在超级调用结束时设置的断点显示 ID 为 0 的当前 VP 已从 VTL 1 切换到 VTL 0,并切换了内存上下文。
============ Partition ============ gs:[103a8] = Current Partition: ffffe80000001000 Current Partition Privileges: 2bb9ff00003fff ============ Virtual Processor ============ gs:[103a0] = Current VP: ffffe80000669050 VP:[1d4] = VP ID: 0 ============ Current VTL ============ VP:[198] = Current VTL: ffffe8000066a000 VTL:[14] = Current VTL ID : 0 STATE:[188] = VMCS physical Address: 10e32a000 STATE:[180] = VMCS virtual Address: ffffe80000672000
(2)VTL Call
VTL Call是指较低的 VTL 启动到较高 VTL 的切换。这可以通过HvCallVtlCall超级调用将secrets存储到 VTL 1 中。超级调用首先分析当前虚拟处理器的 VTL 索引值,因为 VTL 1 是允许的最高值。
基于提取的偏移量,使用 Windbg 脚本分析HvCallVtlCall超级调用开始和结束时的不同值 。
以下内存显示启动 VTL Return 的 VP 是 VP 2,VTL 0。当前 VTL 结构地址为ffffe80000794000,索引为0。
============ Partition ============ gs:[103a8] = Current Partition: ffffe80000001000 Current Partition Privileges: 2bb9ff00003fff ============ Virtual Processor ============ gs:[103a0] = Current VP: ffffe80000793050 VP:[1d4] = VP ID: 2 ============ Current VTL ============ VP:[198] = Current VTL: ffffe80000794000 VTL:[14] = Current VTL ID : 0 STATE:[180] = VMCS virtual Address: ffffe8000079c000 STATE:[188] = VMCS physical Address: 430c67000
在HvCallVtlCall结束时,Current VTL 的值变为ffffe80000796000,其索引为 1。因此,虚拟处理器 2 的 Virtual Trust Level 从 0 切换到下一个 Virtual Trust Level = 1。虚拟处理器的内存上下文被切换并由系统物理地址0x430c6a000处的 VMCS 控制。
============ Partition ============ gs:[103a8] = Current Partition: ffffe80000001000 Current Partition ID: 0 Current Partition Privileges: 2bb9ff00003fff ============ Virtual Processor ============ gs:[103a0] = Current VP: ffffe80000793050 VP:[1d4] = VP ID: 2 VP:[1c4] = VP VTL: 1 ============ Current VTL ============ VP:[198] = Current VTL: ffffe80000796000 VTL:[14] = Current VTL ID : 1 STATE:[180] = VMCS virtual Address: ffffe8000079f000 STATE:[188] = VMCS physical Address: 430c6a000
因此,之前命名为Struct1的结构(在Struct2的偏移量0xe90处引用)保存有关 VTL 私有状态的信息,包括 VMCS 属性:偏移量0x188和0x180处的虚拟和物理地址。
回到在ffffe80000669050发现的 VP 结构:
0xffffe8000066a000 : 引用 VTL 0 结构;
0xffffe8000066c000:引用VTL 1结构;
0xffffe8000066a000:引用当前 VTL 结构(VTL 0)。
对于 VP0 的 VTL 0,VMCS 结构位于0xffffe8000066b188。
0: kd> dq ffffe800`0066a000+e90 ffffe800`0066ae90 ffffe800`0066b188
一旦进入 VTL 状态(在其中找到 VMCS 属性)结构,偏移量0x180和0x188 分别提供 VMCS 的系统虚拟和物理地址。
0:kd> dq ffffe800`0066b188+180 ffffe800`0066b308 ffffe800`00672000 00000001`0e586000
VMCS 的 (VP 0,VTL 0) 物理地址为0x10e586000,而其系统虚拟地址为0xffffe80000672000。
对于单一虚拟化目的,每个 VP 有一个 VMCS 就足够了,但是 Microsoft 在其 Hyper-V 的实现中选择了在启用 VSM 时将 VMCS 影响到 VTL。此选择允许在同一分区内具有不同的虚拟信任级别。在 VP 上切换 VTL 会通过 VMCS 的切换引起处理器状态的切换。每个 VTL 都有自己的拦截、中断和内存布局,从 VMCS 中的不同 EPTP 开始。
因此,VMCS 在启用了 VSM 的 Hyper-V 架构中受到影响,成为一对(VP、VTL)。
0x04 从 VTL 0 访问 VTL 1 内存
探索VMCS布局,使用的处理器之后,可以得到EPTP偏移量0xe8,而guest CR3在偏移0x2e0。提供这两个值,可以从虚拟机管理程序上下文对任何host虚拟地址和物理地址(无论是在 VTL0 还是 VTL1 中)执行第二层地址转换。
下面,我将提取 SecureKernel 的基地址,并将其转换为它的 SPA。接下来,我将生成的 SPA 映射到普通内核的地址空间。
1.VTL 1中内存地址的 SLAT
在引导部分,root分区的调试器显示 SecureKernel 虚拟映像基址0xFFFFF80773600000。
SecureKernel virtual image base = 0xFFFFF80773600000 Image size = 0xfe000 Entry point = 0xFFFFF80773633668
首先,我从 (VP 0, VTL 1) 对的 VMCS 中提取 EPTP:
EPTP:0x11070d01e;
Guest CR3 是从 VTL 1 的 VMCS 中提取的,它必须是导致 VM 退出的 SecureKernel,才能获得其 CR3:
guestCR3:0x6c00002。
(1)EPT 转换:GPA 到 SPA
地址转换的第一步是将guest CR3 GPA 转换为其 SPA,GPA 0x6c00000用于提取将在页表遍历期间使用的偏移量:
PML4 偏移量:0x0;
PDPT 偏移量:0x0;
PD 偏移 0x36;
PT偏移0x0;
物理页偏移量 0x0。
从EPTP引用的EPT PML4开始,我们提取将用于页表遍历的偏移量。
2:KD> DQ / P 11070d000 L1 00000001`1070d000 80000001`10713163 2:KD> DQ / P 80000001`10713000 L1 80000001`10713000 00000001`1073c507 2:KD> DQ / P 00000001`1073c000 L1 00000001`1073c000 00000001`1072d507 2 :KD> DQ / p 00000001`1073c000 + 0x36 * 8 L1 00000001`1073c1b0 00000001`10775507 2:KD> DQ / p 00000001`10775000 L1 00000001`10775000 08100000`06c00230
我注意到PML4 SPA = PML4 GPA = 0x6c00000,身份映射。对于root分区,每个 GPA 将其自身转换为 SPA。从虚拟机的表中读取的每个条目都是一个 GPA,考虑到身份映射,我将跳过从 GPA 到 SPA 的转换,因为 GPA = SPA。
(2)从 GVA 到 GPA 的地址转换
从0xfffff80773600000 GVA 中,我提取将用于页表遍历的偏移量:
PML4 offset:0x1f0;
PDPT offset:0x1d;
PD:0x19b;
PT:0x0;
OFFSET:0x0。
由于运行分区具有身份映射,GPA = SPA,因此仅从 GVA 计算 GPA。
2:KD> DQ / P 000`06c00000 + 0x1f0 * 8 L1 00000000`06c00f80 00000000`06c03063 2:KD> DQ / P 000000`06c03000 + 0x1d * 8 L1 00000000`06c030e8 00000000`06c02063 2:KD> DQ / P 0000 `06c02000 + 0x19b*8 L1 00000000`06c02cd8 00000000`06c01063 2:kd> dq /p 0000000`06c01000 L1 000000000002000000000002083c
SecureKernel 的系统物理地址是0x4239000:
2: kd> db /p 000`04239000 00000000`04239000 4d 5a 90 00 03 00 00 00-04 00 00 00 ff ff 00 00 MZ.............. 00000000`04239010 b8 00 00 00 00 00 00 00-40 00 00 00 00 00 00 00 ........@....... 00000000`04239020 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 00000000`04239030 00 00 00 00 00 00 00 00-00 00 00 00 08 01 00 00 ................ 00000000`04239040 0e 1f ba 0e 00 b4 09 cd-21 b8 01 4c cd 21 54 68 ........!..L.!Th 00000000`04239050 69 73 20 70 72 6f 67 72-61 6d 20 63 61 6e 6e 6f is program canno 00000000`04239060 74 20 62 65 20 72 75 6e-20 69 6e 20 44 4f 53 20 t be run in DOS 00000000`04239070 6d 6f 64 65 2e 0d 0d 0a-24 00 00 00 00 00 00 00 mode....$.......
然而,从 VTL 0 上下文中的root分区调试器访问 SecureKernel 的物理地址是受到限制的,因为它没有映射到 NT 内核的地址空间中。由于 VTL 0 和 VTL 1 具有不同的 VMCS 和不同的 EPTP,因此它们具有不同的内存布局视图。
3:kd> db /p 000`04239000 00000000`04239000 ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ?? ?????????????????? 00000000`04239010 ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ?? ?????????????????? 00000000`04239020 ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ?? ?????????????????? 00000000`04239030 ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ?? ?????????????????? 00000000`04239040 ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ?? ?????????????????? 00000000`04239050 ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ?? ?????????????????? 00000000`04239060 ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ?? ?????????????????? 00000000`04239070 ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ?? ??????????????????
(3)将内存页从 VTL 1 映射到 VTL 0
为了能够从 VTL 0 访问 SecureKernel 内存,需要将其地址映射到 VTL 0 的进程物理地址空间。因此,我将通过创建从 VTL 0 的 EPTP 开始的正确页表条目,将其 SPA 0x4239000映射到 NT 内核的物理地址空间。
我可以选择将其映射到任何 GPA。由于 GPA 被视为从中提取表偏移量的位域,因此我选择一个简单的 GPA,例如0x4000,不需要创建新表。
0x4000 [EPT_PML4_idx] 0x0 0x4000 [EPT_PDPT_idx] 0x0 0x4000 [EPT_PD_idx] 0x0 0x4000 [EPT_PT_idx] 0x4 0x4000 [EPT_0x0SET]
使用提取索引,我使用从 EPTP 指向的 EPT PML4 表库开始的页表遍历。
EPTP = 0x1109d201e
EPT PML4E:
3:kd> dq /p 1109d2000 + 0*8 L1 00000001`109d2000 80000001`109d8163
EPT PDPTE:
3:kd> dq /p 001`109d8000 + 0*8 L1 00000001`109d8000 00000001`10601507
EPT PDE:
3:kd> dq /p 00001`10601000 + 0*8 L1 00000001`10601000 00000001`109f2507
最后,在 EPT PTE 处写入值 0x4239637。
3:kd> dq /p 01`109f2000 + 4*8 L1 00000001`109f2020 00000000`04239637
为了确保访问被授予,我从root分区的内核调试器中检查 GPA 0x4000处的内存。
成功访问SecureKernel 的物理地址,因为对 SecureKernel(在 VTL1 中运行)的系统物理内存内容的访问被授予 VTL0。
3: kd> db /p 000`00004000 00000000`00004000 4d 5a 90 00 03 00 00 00-04 00 00 00 ff ff 00 00 MZ.............. 00000000`00004010 b8 00 00 00 00 00 00 00-40 00 00 00 00 00 00 00 ........@....... 00000000`00004020 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 00000000`00004030 00 00 00 00 00 00 00 00-00 00 00 00 08 01 00 00 ................ 00000000`00004040 0e 1f ba 0e 00 b4 09 cd-21 b8 01 4c cd 21 54 68 ........!..L.!Th 00000000`00004050 69 73 20 70 72 6f 67 72-61 6d 20 63 61 6e 6e 6f is program canno 00000000`00004060 74 20 62 65 20 72 75 6e-20 69 6e 20 44 4f 53 20 t be run in DOS 00000000`00004070 6d 6f 64 65 2e 0d 0d 0a-24 00 00 00 00 00 00 00 mode....$.......
SecureKernel SPA 可通过 GPA: 0x4000从 NT 内核进程访问。为了将其映射到 NT 内核的虚拟地址空间,必须提取其 CR3 寄存器。然后通过创建在地址转换过程中隐含的不同表条目来继续相同的操作。
2.用于调试 Hyper-V 的 Windbg 脚本
我写了一个小脚本来在调试 Hyper-V 时检索上述结构和信息。该脚本适用于 Hyper-V 版本 19041.21,可以在以下 URL 中找到:
https://github.com/quarkslab/windbg-vtl
转储在 Hyper-V 上运行的现有分区。
1: kd> dx -r1 @$scripts.hv.Contents.hvExtension.HvInternals.getPartitions() @$scripts.hv.Contents.hvExtension.HvInternals.getPartitions() : [object Generator] [0x0] : _HV_PARTITION *[0xffffe80000001000] [0x1] : _HV_PARTITION *[0xffffe80200001000]
列出分区的属性
1: kd> dx -r1 @$scripts.hv.Contents.hvExtension.HvInternals.getPartitions()[0] @$scripts.hv.Contents.hvExtension.HvInternals.getPartitions()[0] : _HV_PARTITION *[0xffffe80000001000] PartitionProperty : 0x2bb9ff00003fff VirtualProcessorCount : 0x4 VirtualProcessorList : Array of _HV_VP PartitionId : 0x1
列出分区的虚拟处理器
1: kd> dx -r1 @$scripts.hv.Contents.hvExtension.HvInternals.getPartitions()[0].VirtualProcessorList @$scripts.hv.Contents.hvExtension.HvInternals.getPartitions()[0].VirtualProcessorList : Array of _HV_VP [0x0] : _HV_VP * [0xffffe80000669050] [0x1] : _HV_VP * [0xffffe80000770050] [0x2] : _HV_VP * [0xffffe80000793050] [0x3] : _HV_VP * [0xffffe800007b1050]
列出虚拟处理器的 VTL
1: kd> dx -r1 @$scripts.hv.Contents.hvExtension.HvInternals.getPartitions()[0].VirtualProcessorList[0].VtlList @$scripts.hv.Contents.hvExtension.HvInternals.getPartitions()[0].VirtualProcessorList[0].VtlList : Array of _HV_VTL [0x0] : _HV_VTL * [0xffffe8000066a000] [0x1] : _HV_VTL * [0xffffe8000066c000]
找到保存与分区的 VP-VTL 关联的 VMCS 属性的结构:
1: kd> dx -r1 @$scripts.hv.Contents.hvExtension.HvInternals.getPartitions()[0].VirtualProcessorList[0].VtlList[0] @$scripts.hv.Contents.hvExtension.HvInternals.getPartitions()[0].VirtualProcessorList[0].VtlList[0] : _HV_VTL * [0xffffe8000066a000] VmcsProperties : _HV_VMCS_PROPS * [0xffffe8000066b188]
查找 VMCS 的物理和虚拟地址
0: kd> dx -r1 @$scripts.hv.Contents.hvExtension.HvInternals.getPartitions()[0].VirtualProcessorList[0].VtlList[0].VmcsProperties.VmcsAddress @$scripts.hv.Contents.hvExtension.HvInternals.getPartitions()[0].VirtualProcessorList[0].VtlList[0].VmcsProperties.VmcsAddress : 0x10e586000,_HV_VPVTL_VMCS * [0xffffe80000672000] length : 0x2 [0x0] : 0x10e586000 [0x1] : _HV_VPVTL_VMCS * [0xffffe80000672000]
0x05 分析总结
微软增强Windows安全性的方式是将权力委托给虚拟机监控程序而不是Windows内核。通过虚拟化创建隔离的内存区域,虚拟安全模式强制敏感操作在正常 Windows 系统无法访问的隔离地址空间(secure worlds)内进行。例如,Hypervisor-Protected Code Integrity 通过代码完整性检查确保只有经过授权的代码才能在内核模式下执行。负责完整性检查的函数在 VTL 1 中运行,无法从 VTL 0 访问。在同一行中,Credential Guard 通过保护 VTL 1 中的身份验证secrets来防止针对本地安全机构子系统内存进行凭据窃取的攻击。作为直接结果, 在系统受到威胁的情况下, 即使 ring 0 中的特权代码也无法访问 VTL 1 中的内存空间,也无法修改其页表,因为它只能查看内存页表的受限视图,而管理程序则持有该系统内核的密钥。随着 Windows 11 的发布,微软希望在默认情况下启用 VBS,并表示 VSM 在统计上将内核攻击减少了 60% 。虽然这听起来很厉害,但它仍然是一个挑战,因为更新需要特定的硬件来支持新功能。
0x06 参考文献
[1] https://docs.microsoft.com/en-us/virtualization/hyper-v-on-windows/tlfs/vsm
[2] (1, 2) https://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-software-developer-vol-3c-part-3-manual.pdf
[3] (1, 2, 3) TLFS: https://docs.microsoft.com/en-us/virtualization/hyper-v-on-windows/reference/tlfs
[4] https://msrc-blog.microsoft.com/2018/12/10/first-steps-in-hyper-v-research/
[5] https://blogs.windows.com/windows-insider/2021/06/28/update-on-windows-11-minimum-system-requirements/
[6] https://www.microsoft.com/security/blog/2021/01/11/new-surface-pcs-enable-virtualization-based-security-vbs-by-default-to-empower-customers-to-do-more-securely/
[7] Basics about building a hypervisor: https://rayanfam.com/topics/hypervisor-from-scratch-part-1/
[8] Debugging Hyper-V using VMWare: http://hvinternals.blogspot.com/2021/01/hyper-v-debugging-for-beginners-2nd.html
本文翻译自:https://blog.quarkslab.com/a-virtual-journey-from-hardware-virtualization-to-hyper-vs-virtual-trust-levels.html如若转载,请注明原文地址