本文是关于基于虚拟化的安全和设备保护功能的系列文章的第一篇。这些文章的目的是从技术角度分享对这些特征的更好理解。第一篇文章将介绍从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的演示,在这项工作中,给我们提供了很多帮助。
您还可以阅读Hyper-V Internals博客的两篇文章,了解Hyper-V更多相关技术信息:
"Hyper-V debugging for beginners" (also covers Hyper-V startup);
“初学者的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 variables |
C:\Windows\System32\CodeIntegrity\ SIPolicy.p7b | Si |
\EFI\Microsoft\Boot\ SIPolicy.p7b | SiPolicyVersionSiPolicyUpdateSigners |
C:\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 | 0 |
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超级调用实际如何工作。
感谢魏星对本文的审校。