AMOSSYS Security 团队的 Adrien Chevalier 撰写了一系列文章,讲述了基于虚拟化的安全。
作者在文章中使用了 CC 协议,InfoQ 翻译本文。
本文是关于基于虚拟化的安全和设备保护功能的系列文章的第一篇。这些文章的目的是从技术角度分享对这些特征的更好理解。第一篇文章将介绍从 Windows 引导加载程序到 VTL0 启动的系统引导过程。
基于虚拟化的安全
基于虚拟化的安全(Virtualization Based Security,VBS)是 Microsoft Windows 的主要安全特色,随 Windows 10 和 Windows Server 2016 一起提供。例如,DeviceGuard 和 CredentialGuard 都依赖它。对于那些不知道 Windows 10 的这两个关键的安全创新,DeviceGuard 允许系统阻止任何东西,包括受信任的应用。对于 CredentialGuard,它允许系统隔离 lsass.exe 进程,以阻止密码收集器(如 Mimikatz)的内存读取尝试。
这个新功能的主要思想是使用硬件虚拟化技术,如 Intel VT-X,以便在两个虚拟机(VM)之间提供强大的隔离,并且,在将来可能会更多。这些技术允许虚拟机管理器(Virtual Machine Manager,VMM)使用扩展页表(Extended Page Tables,EPT)在物理页上设置不同的权限。换句话说,VM 可以在其页表项(Page Table Entry,PTE)中设置物理页可写(+W),并且 VMM 可以通过在其 EPT 中设置适当的访问权限来静默地授权或阻止这一点。
基于虚拟化的安全性依赖于 Hyper-V 技术,这将产生不同虚拟信任级别(Virtual Trust Levels,VTL)的虚拟机。Hyper-V 的构成,包括一个管理程序 hypervisor 以及运行着任何操作系统的 VM,包括物理机的操作系统 Windows 本身都被视为一个组件。也就是说,Hyper-V 的架构是 CPU 之上级别的,然后才是它的核心思想——VTL 的分层,每一层的权限严格限制和区分。Hyper-V 信任它并接受管理订单,例如控制其他 VM。其他 VM 可以是“开明的”,如果是这样的话,那么就向 Hyper-V 发送受限消息以用于它们自己的管理。
VTL 被编号,编号较高的是最可信的。现在,有两个 VTL:
- VTL0,这是正常的环境,基本上都在标准的 Windows 操作系统。
- VTL1,它是安全的环境,包含一个最小化的内核和安全的应用程序,称为 trustlet。
图 1:基于虚拟化的安全性概述
CredentialGuard 安全功能利用此技术隔离 VTL1 信任单(lsaiso.exe,上图中的“隔离 LSA”)中的关键 lsass.exe 进程,甚至使 VTL0 内核不能访问其内存。只有消息可以从 VTL0 转发到隔离的进程,有效地阻止内存密码和散列收集器(如 Mimikatz)。
DeviceGuard 安全功能允许在 VTL0 内核地址空间实现 W^X 内存缓解(物理页不能同时处于可执行和可写的状态),并接受包含授权代码签名的策略。如果 VTL0 内核想要使物理页可执行,它必须要求 VTL1 进行改变(图中的“HVCI”),这将根据其策略检查签名。对于 usermode 代码,目前还没有完成,VTL0 内核仅仅要求签名验证。策略在引导启动期间加载,并且不能在之后修改,这强制用户重新启动以加载新策略。
策略也可以写死:在这种情况下,在 UEFI 变量中设置授权签名者,并将针对这些签名者检查新策略。UEFI 变量包括 Setup 和 Boot 标志设置,这意味着它们不能在启动后访问或修改。为了清除这些变量,本地用户必须使用访客账号的 Microsoft EFI 引导加载程序重新启动,这将在用户交互(通过按键)后删除它们。为了清除这些变量,本地用户必须使用自定义的 Microsoft EFI 引导加载程序重新启动,这将在用户交互(通过按键)后删除它们。
因此,VBS 主要依赖 SecureBoot :必须检查引导加载程序的完整性,因为它负责加载策略、Hyper-V、VTL1 等等。
如果您对详细的设备保护(Device Guard)概述感兴趣,可以阅读 MSDN 的这篇文章。
您还可以看看 2015 年、2016 年黑客大会上 Alex Ionescu 和 Rafal Wojtczuk 的演示,在这项工作中,给我们提供了很多帮助。
- “Battle of SKM and IUM”, Alex Ionescu BlackHat 2015 (slides) ;
- “Battle of SKM and IUM”, Alex Ionescu BlackHat 2015 (video) ;
- “Analysis of the attack surface of windows 10”, Rafal Wojtczuk BlackHat 2016 (slides) ;
- “Analysis of the attack surface of windows 10”, Rafal Wojtczuk BlackHat 2016 (whitepaper) ;
- “Analysis of the attack surface of windows 10”, Rafal Wojtczuk BlackHat 2016 (video) .
您还可以阅读 Hyper-V Internals 博客的两篇文章,了解 Hyper-V 更多相关技术信息:
“Hyper-V debugging for beginners” (also covers Hyper-V startup) ;
“ Hyper-V internals ”。
“初学者的 Hyper-V 调试”(也包括 Hyper-V 启动); “Hyper-V 内部”。
在本文中,我们将介绍从 Windows 引导加载程序到 VTL0 启动的系统引导过程。为了分析 VBS 在引导过程中如何初始化,我们对 Windows 10 1607 版本的以下文件进行了逆向工程:
- bootmgr.efi:EFI 引导加载程序(它的一小部分);
- winload.efi: EFI Windows 加载器;
- hvix.exe: Hyper-V(真的很小);
- ntoskrnl.exe: NTOS 内核;
- securekernel.exe: 安全内核;
- ci.dll: 检测 VTL0 代码完整性;
- skci.dll: 检测 VTL1 代码完整性。
因此,让我们进入 VBS 引导过程,从执行 winload.efi 到 ntoskrnl.exe 入口点执行。
引导过程
引导过程可以总结为以下五个基本步骤:
- bootmgr.efi 是要加载的第一个组件。它由 SecureBoot 验证,然后执行;
- bootmgr.efi 加载并检查 winload.efi,主要的 Windows 加载器;
- winload 加载并检查 VBS 配置;
- winload 加载并检查 Hyper-V 和 VTL0/VTL1 内核组件;
- winload 退出 EFI 模式,启动 Hyper-V。
Bootmgr.efi
当系统启动时,Bootmgr.efi 是第一个执行的 Microsoft 组件。其完整性和签名已事先由 Secure Boot UEFI 代码验证。为了能够识别撤销的签名,检查包含已撤销的签名的 DBX 数据库(截止 2016 年底,该数据库包含 71 个黑名单和未知的 SHA256 哈希值)。在 bootmgr.efi 代码结束时,执行将传递到 winload.efi 入口点:OslpMain/OslMain。
OslpMain 首先调用 OslpPrepareTarget,这是 winload.efi 的“核心”函数:它将启动管理程序、内核等。但是首先,它使用 OslSetVsmPolicy 启动 VBS 配置。
VBS 策略负载
OslSetVsmPolicy 首先检查 VbsPolicyDisabledEFI 变量值(Microsoft 命名空间的值,请参见下文)。如果设置,则清除此变量(设置为 0),这意味着不会加载 Credential Guard 配置。因此,此 EFI 变量允许仅禁用单引导的凭据保护(并且可以通过来自 VTL0 ring3 的特权调用设置)。如果不存在,则从 SYSTEM 注册表配置单元加载配置,并对 BlVsmSetSystemPolicy 执行调用,BlVsmSetSystemPolicy 将根据需要读取和更新 VbsPolicyEFI 变量。相应的值然后存储在 BlVsmpSystemPolicyglobal 变量中。如果启用 UEFI 锁,则设置此 EFI 变量,并且不能由 winload.efi 禁用(仅仅只是没有删除它的代码,必须使用自定义 EFI 代码)。
函数 OslpPrepareTarget 也调用 OslpProcessSIPolicy(它被调用两次,第一次直接调用,然后从函数 OslInitializeCodeIntegrity 调用)。OslpProcessSIPolicy 使用三个 EFI 变量“池”检查 SI 策略签名。每个池包含三个 EFI 变量,第一个包含策略,第二个包含其版本,第三个包含授权的策略更新签名者。例如,对于 C:\Windows\System32\CodeIntegrity\SIPolicy.p7b,变量是 Si,SiPolicyVersion 和 SiPolicyUpdateSigners。如果设置了“版本”和“更新签名者”变量,系统将强制执行 SI 策略签名:它必须存在并且正确签名,否则引导过程将失败。验证本身由 BlSiPolicyIsSignedPolicyRequired 函数执行。
三个策略和相关联的变量总结如下:
Policy file EFI variablesC:\Windows\
System32\CodeIntegrity\
SIPolicy.p7b Si\EFI\Microsoft\Boot\
SIPolicy.p7b SiPolicyVersionSiPolicyUpdateSignersC:\Windows\
System32\
CodeIntegrity\
RevokeSiPolicy.p7b RevokeSiRevokeSiPolicyVersionRevokeSiPolicyUpdateSigners\EFI\Microsoft\Boot\
SkuSiPolicy.p7b SkuSiSkuSiPolicyVersionSkuSiPolicyUpdateSigners
表 1:SI 政策和相应的 EFI 变量
我们没有确定“revokeSiPolicy”和“skuSiPolicy”的目的,但它们似乎与常规的“SiPolicy”类似。
Hyper-V 和内核组件负载
接下来系统将跳转并执行一个参数预先设置为 0 的 OslArchHypervisorSetup 函数。第一次,它将启动 Hyper-V(加载 hvloader.efi 并通过 HvlpLaunchHvLoader 执行它)。然后通过 OslInitializeCodeIntegrity 检查安全引导设置。
OslpPrepareTarget 然后加载 NTOS 内核(ntoskrnl.exe),并使用 OslpLoadAllModules 函数加载 hal.dll 和 mcupdate.dll 模块。它们的签名验证在加载过程中执行(在 ImgpLoadPEImage 和 OslLoadImage 中)。然后通过 OslVsmProvisionLKey 和 OslVsmProvisionIdk 函数从 EFI 变量加载“本地密钥”和“标识密钥”。
此时,NTOS 内核开始初始化但还未启动。然后使用“0”参数调用 OslVsmSetup(与 OslArchHypervisorSetup 相同:它需要一个“step”参数),它首先检查 Hyper-V 是否已经启动,然后初始化 OslVsmLoaderBlock 全局变量(在初始化期间赋值)。然后,OslVsmSetup 通过 OslpVsmLoadModules 函数(OslLoadImage 再次用于检查其签名)加载安全内核(securekernel.exe)及其依赖(skci.dll)。然后将 EFI 变量 OsLoaderIndications 的第一位设置为 1。
最后,再次调用 OslVsmSetupfunction,但此时该函数的参数为 1。这将触发几个 OslVsmLoaderBlock 参数的初始化。
当函数 OslpPrepareTarget 返回时,VBS 参数已验证,并且加载 NTOS 和安全内核。它们的入口点地址已存储在 OslpVsmSystemStartup 和 OslEntryPoint 全局变量(分别为 securekernel.exe 和 ntoskrnl.exe 入口点)中,以便进一步重用。
Microsoft EFI 变量
VBS EFI 变量(以及更常见的微软变量)属于命名空间:{0x77FA9ABD, 0x0359, 0x4D32, 0xBD, 0x60, 0x28, 0xF4, 0xE7, 0x8F, 0x78, 0x4B}。这些变量的“Boot”和“Setup”属性已设置,因此不允许在 EFI 引导阶段后访问或修改它们。
然而,可以转储它们以便在分析期间帮助逆向。与 VBS 相关的 EFI 变量及其相应的用法总结如下: >
EFI variable name
Usage
VbsPolicy VBS settings VbsPolicyDisabled Disable “magic” variable VsmLocalKeyProtector VsmLocalKey VsmLocalKey2 WindowsBootChainSvn RevocationList Kernel_Lsa_Cfg_Flags_Cleared VsmIdkHash Si First CodeIntegrity policy SiPolicyUpdateSigners Update signers SiPolicyVersion Version RevokeSi Second CodeIntegrity policy RevokeSiPolicyVersion Update signers RevokeSiPolicyUpdateSigners Version SkuSi Third CodeIntegrity policy SkuSiPolicyUpdateSigners Update signers SkuSiPolicyVersion Version 表 2:Microsoft 命名空间 EFI 变量列表
为了转储这些变量的内容,可以关闭安全启动和使用一个简单的 EFI 自定义启动加载程序(gnu-efi 和 VisualStudio 工作相当完美)。下面给出一些变量转储作为示例:
Name
Value
CurrentActivePolicy
CurrentPolicy
2
BootDebugPolicyApplied
0x2a
WindowsBootChainSvn
0x00000001
VsmLocalKey2 4c 4b 45 89 50 4b 47 31 96 00 00 00 01 00 01 00 2c 00 00 00 01 00 01 00 01 00 00 00 b2 21 ae a7 12 86 07 a8 15 28 d5 49 33 ac 09 ac 93 c8 e0 12 61 8f 10 d6 4c 68 d1 5a 5f 00 90 0c 5a 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 50 6c 1a 00 00 00 00 00 00 00 00 00 00 00 00 00 c2 0f 94 00 00 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 03 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 表 3:EFI 变量转储示例
Hyper-V 和安全内核的启动
从 OslpPrepareTarget 返回,执行流程现在已启动 Hyper-V 并单独切分 VTL0 和 VTL1 空间。这个过程可以总结为以下几点:
- winload 在“第一个 / 最底层”的 Hyper-V 虚拟机里启动;
- winload 唤醒 securekernel;
- securekernel 初始化,并根据写死的策略,向 Hyper-V 申请内存保护;
- securekernel 激活 VTL1 层;
- Hyper-V 允许 securekernel 激活 VTL1,并返回 ShvlpVtl1Entry 函数;
- 通过 ShvlpVtlReturn 函数,Hyper-V 把 VTL1 层的 securekernel 状态告诉 VTL0 层的 winload(自从它唤醒 securekernel 之后等待了很久);
- 在得到 securekernel 已经完成安全检查(启用内存保护等动作)的消息后,winload 才开始唤醒 ntoskrnl。
这些是在 securekernel 初始化之前和之后的状态(VTL0 VM 是蓝色块,VTL1 是绿色块,而 Hyper-V 是橙色块):
图 2:securekernel 初始化之前和之后的状态
当遵循执行流程时,OslpMain 通过调用 OslFwpKernelSetupPhase1 退出 EFI 模式,并通过步骤“1”的 OslArchHypervisorSetup 启动 Hyper-V。Hvix64 通过将 RSP 保存到 HvlpSavedRsp 全局中并通过将 HvlpReturnFromHypervisor 传递给 hvix64 来启动。当命中 HvlpReturnFromHypervisor 时,使用 cpuid 指令检查启动,并恢复 RSP。我们实际上是在第一个虚拟机,这将很快成为 VTL1。
OslVsmSetup 最后一次被调用(步骤“2”),这将会发生:
- 检查 VBS 参数;
- 验证 Hyper-V 是否正确运行;
- 修改 OslVsmLoaderBlock 设置;
- 在同一块中复制 OslVsmLKeyArray(Local Key)和 OslVsmIdk(“idk”用于“Identification Key”);
- 调用已存储到 OslpVsmSystemStartup 全局中的安全内核入口点,指定 OslVsmLoaderBlock 及其大小作为参数。
然后,安全内核将执行初始化,安全内核通过 SkmiProtectSecureKernelPages 这一特殊函数的调用来申请独占内存(以确保安全性),同时安全内核还注册了 Hyper-V 的事件监听例程(HyperGuard 及其 Skpg * 前缀例程)。根据特殊模块寄存器的说明文献对以下 MSR 的操作,由函数 SkpgxInterceptMsr 拦截和处理:
- 0x1B(APIC_BASE);
- 0x1004(?);
- 0x1005(?);
- 0x1006(?);
- 0x100C(?);
- 0xC0000080(EFER);
- 0xC0000081(STAR);
- 0xC0000082(LSTAR);
- 0xC0000083(CSTAR);
- 0xC0000084(FMASK);
- 0xC0000103(TSC_AUX);
- 0x174(SEP_SEL);
- 0x175(SEP_RSP);
- 0x176(SEP_RIP);
- 0x1a0(MISC_ENABLE)。
我们的假设是这些处理程序设置为捕获 VTL0 中的 CPL 转换和阻止关键的 MSR 修改。还有两个其他例程,SkpgxInterceptRegisters 和 SkpgInterceptRepHypercall。一种可能性是,第一个可能能够拦截 CRXXX 寄存器操作(例如,CR4 写入 SMEP 禁用),第二个可以拦截未授权的超级调用(这仅仅是一个假设)。
关于 HyperGuard,似乎 VTL0 完整性检查由 SkpgVerifyExtents 执行。这个特定的函数由 SkpgHyperguardRuntime 调用,它可以被定期执行(使用 SkpgSetTimer)。HyperGuard 的执行和回调函数被复制到了 SkpgContext 的全局函数中(由 SkpgAllocateContext 和 SkpgInitializeContext 初始化)。
请记住,前面的讨论只是假设,可能是错误的,因为我们现在没有在 VTL1 HyperGuard/PatchGuard 例程花时间研究。
在其初始化结束时,安全内核将最终执行两个超级调用:
- 0x0F,进入 ShvlEnableVpVtl,指定一个 ShvlpVtl1Entry 函数指针;
- 0x12,进入 ShvlpVtlCall,它不在代码的任何其他部分使用,并且使用它自己的超级调用 trampoline(我们将在下一篇文章中给出关于这些超级调用 trampolines 的更多细节)。
ShvlpVtl1Entry 结束了 SkpPrepareForReturnToNormalMode,似乎这个过程实际上使 Hyper-V 启用 VTL1 和 VTL0,返回到 ShvlpVtl1Entry,然后返回到 winload.efi 到 VTL0 上下文。
最后,当回到 winload.efi 主程序时,它将通过 OslArchTransferToKernel 执行 NTOS 入口点,它使用 OslEntryPoint 全局调用其入口点。
然后执行下一个操作,就像 Windows 在正常环境中启动一样,只是 NTOS 内核现在知道与 VBS 相关的组件(如 Device Guard)。
结论
基于虚拟化的安全性是 Microsoft Windows 10 安全功能的关键组成部分。通过覆盖 VBS 的安全内核初始化,我们希望本文将给予更多资源,以便更深入地了解这些功能。
在第二篇中,我们将介绍 VTL0 和 VTL1 之间的内核通信以及 Hyper-V 超级调用实际如何工作。
感谢魏星对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ , @丁晓昀),微信(微信号: InfoQChina )关注我们。
评论