一、前 言
二、VTL机制和IUM进程介绍
三、必要的硬件虚拟化知识(Intel)
四、VTL内部细节研究
五、IUM进程调试
六、总 结
现今的Windows操作系统已经融合了虚拟化技术作为自身的核心功能,Windows平台下的虚拟化技术的应用也不仅限于Hyper-V虚拟化软件的应用,在一些用于保护Windows系统的安全功能中,虚拟化技术的使用也变得尤为重要。现在我们就来简单探索下Windows的VTL机制和IUM进程,这两种基于虚拟化技术的安全功能。
Virtual Secure Mode(VSM)最早在Windows10和Windows Server 2016中被引入,是基于Windows平台下的虚拟化技术的安全功能,使用的是Hyper-V的虚拟化组件。
VSM的主要作用是,在这个安全功能开启的情况下,即使主机上的内核或者驱动程序(即Ring0层)受到攻击,受到VSM体系保护的数据依然可以保持安全,保证数据不被篡改或者无法被访问,甚至攻击者处于Ring0权限下。
与我们熟知的VMware或者QEMU虚拟化软件不同,Hyper-V的虚拟化实现是裸金属架构的,即Hypervisor层的组件运行在实体的硬件上,而无论是所谓的Guest(Child Parition)或者Host(Root Parition)都是Hypervisor层之上抽象出来的。
那么,Hyper-V的技术架构是VSM功能的基础,无论是Guest还是Host,实际上都可以通过Hypervisor来控制不同分区对硬件资源的使用能力从而实现在VSM中提及的安全功能。
VSM功能的引入同时引入了一个新的概念——VTL。
Virtual Trust Levels(VTL),VSM通过VTL来实现和维护隔离。VTL是有层级概念的,例如熟知的Ring0~3层级权限。但是与Ring0~3概念不同的是,VTL的层级权限是级别高的权限要高于级别低的权限。举个例子:VTL0的权限 < VTL1的权限;VTL1的权限 < VTL2的权限。在微软对VTL的代码实现里,最多支持16个级别VTL,但是截止到目前的关于VTL的代码中,微软只实现了2个级别的VTL。
为了更直观地展示VTL的作用,这里使用了微软官方给出的IUM架构体系图。
图片来源:参考链接[2]
从图中可以看到,我们所熟知的Windows内核态和用户态都属于VTL0级别下,而SecureKernel和运行在同VTL层级下的用户态进程属于VTL1级别下。此时,假如我们在VTL0中进行内核调试,都无法修改VTL1层级下的用户态进程空间内存。这个例子说明了低级别VTL无法影响到高级别VTL内存空间,不同的VTL级别之间有着明显的隔离边界。
有了VTL这个概念之后,下面我们来介绍下什么是Isolated User Mode(IUM)进程。通俗来讲,IUM进程就是运行在VTL1层级下的用户态进程。最简单的例子就是LSAIso.exe进程,当这个进程运行在VTL1层级下时,无论是在用户态还是内核态都无法修改这个进程的内存空间,同样地,对其进行调试也是无法进行的。
下面,我们一起来探索VTL机制和IUM进程(以及如何调试IUM进程)的一些内部细节。
在探索之前,我们还需要简单地了解下CPU硬件虚拟化知识,这里我们以Intel处理器(Intel VT-x)加以介绍。
这里首先要介绍的是Intel VT-x和VMX是什么。Intel VT-x的全称是Intel Virtualization Technology for x86,这个东西就是所谓的Intel硬件虚拟化技术,而VMX是其实现的架构,全称为Virtual-Machine Extensions。在VMX下引入了两个概念:VMX root operation和VMX non-root operation模式。
VMX的root和non-root operation模式可以简单地理解成,Hypervisor也就是虚拟化层,即VM管理者(VMM, virtual machine monitor)和Guest所使用的环境。这两种模式可以互相转换,当从VMX的root模式转换到non-root模式时,这个行为被称作VM-entry。那么当VMX的non-root模式切换到root模式,这个行为被称作VM-Exit。
假设目前VMX处于non-root状态,此时是在执行Guest中的代码,如果执行时遇到了比如CPUID,读写MSR寄存器等操作时,Guest操作系统会被暂停,并产生Vm-Exit事件,同时陷落入VMM,即root operation状态中。VMM根据不同的VM-Exit原因来处理(模拟)此时Guest的执行指令,处理数据并返回结果,最后vmm通过执行vmresume指令重新让Guest系统继续运行,此时的VMX状态又变回了non-root状态。
在VMX进行root和non-root operation状态切换时,VMCS(Virtual Machine Control Structure)用来配置此时发生切换的处理器状态和执行的环境。在Hyper-V的虚拟化环境中,每个虚拟处理器都对应着一个或者多个VMCS。
VMCS中有很多字段,对应着当前虚拟处理器的状态信息,比如用于记录当前VM-Exit信息的”Exit reason”字段,通过阅读Intel手册发现这个字段对应的ID是0x4402。因为VMCS中的字段信息无法直接通过读取物理内存的方式读取到,所以这里必须使用Intel给出的指令集vmread/vmwrite来读写对应的字段内容。
通过vmread读取0x4402
id字段的内容,就可以得到VM-Exit的原因,VMM根据上图中这些若干的原因进行处理,完成处理后,将结果改写到例如Guest中的寄存器中,此时也需要通过vmwrite改写其中关于Guest寄存器信息的字段,最后通过vmresume将控制权交还Guest。
这里还要介绍一对重要的指令:vmptrld/vmptrst,vmptrld这个指令是用来从内存中加载一个64位的物理地址作为当前VMCS指针;vmptrst是用来将当前的VMCS指针保存到内存中。vmptrld是用来切换当前VMCS的指令,而vmread/vmwrite所读写的也是当前VMCS。
下面要介绍的是SLAT(Second Level Address Translation),上面介绍了通过VMX root、non-root operation和VMCS来控制虚拟处理器,那么在虚拟机中的内存也需要和宿主机/其他虚拟机的内存做隔离。广为人知的四级页表转换已经无法满足这个需求,所以在此Intel提出了二级地址转换这个功能。
如quarkslab博客中文章中使用的图片所示,下图展现了SLAT完整的转换流程。
图片来源:参考链接[3]
首先,在Guest中的虚拟地址通过四级页表转化成了Guest中的物理地址(GPA),然后GPA通过相似的四级页表转换,转换成了Host中的物理地址(SPA)。这样就实现了Guest和Host以及其他Guest之间的内存隔离,并且在Guest操作系统中是完全透明的,因为在GPA到SPA转换中,CPU是处于VMX root operation模式下的。
这里要提到EPT这个东西,EPT(Extend Page Table)的作用和CR3寄存器是一样的,用来确定页目录基址,完成GPA到SPA转换。我们可以通过读取当前VMCS中的EPT pointer字段(ID:0x201a)获取到EPT的指针。
这里举个简单的例子:首先我们准备一个Guest,这里我使用的是Linux,编写一个驱动,驱动主要是打印了一个buffer所在的物理地址的信息,如下:
通过对Hyper-V的Hypervisor进行逆向和调试,获得当前Guest所在的VMCS中的ept指针信息。
下面我们可以使用!vtop这个windbg命令快速求出Guest中我们的驱动的buffer的GPA对应SPA。
可以看到GPA:0x96eee000
对应的SPA:0x1972ee000
。
这里先看一下一个简单的vmcall处理函数:HvDeletePartition
,由于创建或者删除分区的权限只有root分区才有,所以在调用vmcall指令对应的功能时,要先检测当前陷入VM-Exit的分区的权限,以保证Child分区不会影响到Root分区。
从这个HvDeletePartition
函数中可以看到,代码先从gs:[0x103E0]
处拿到了一个指针,然后对这个指针offset 0xF0
处的64bit的数据进行比特位检测运算,检测的比特位是第32位。通过逆向得知,这个64位数据就是Hyper-V Hypervisor TLFS中的HV_PARTITION_PRIVILEGE_MASK
数据,比较幸运的是,微软公开了这个数据的数据结构,下面是它的参考。
通过windbg结果可以看到此时的分区权限拥有比如创建分区的权限等较多的权限,它目前就是root分区。
可以看到,HvDeletePartition
函数先检测了当前的分区是否拥有创建分区权限,再进行下一步操作。这一步给我们的逆向思路提供了经验,gs:[103e0]
中存储的很有可能就是代表partition的大结构体,其中offset 0xF0
处代表这个分区的权限信息。通过对HvCreateVp
这个函数的逆向,我们得出了进一步的信息。这里省略对HvCreateVp
的逆向过程,感兴趣的小伙伴可以自行研究,很简单。
逆向得出,vmcall_0x4e_HvCreateVp_sub_FFFFF80000294100
–> sub_FFFFF800002A1D28
,sub_FFFFF800002A1D28
函数中会更新目前的partition中的虚拟处理器的信息。如下所示。
上述代码中的rbx指向的就是当前partition的指针,offset 0x120
代表着当前partition index为0的虚拟处理器的指针,假设我有8个虚拟处理器,那么0x120
代表index为0的虚拟处理器指针,0x128
代表index为1的虚拟处理器指针,剩下的以此类推。
通过逆向还发现partition指针offset 0x110
处的4bytes代表着虚拟处理器的数量,0x114
处的4bytes代表着虚拟处理器的最大index值。windbg中表现为:
由于这里演示用的partition是root分区,而且我的被调试机中硬件CPU的核心数量为24个,所以此时:
根据微软的TLFS文档中的内容,HvVtlCall
这个函数的作用是将当前虚拟处理器的VTL权限升高。通过逆向这个函数内部实现,我们发现在后续的调用中出现了VTL实现的相关结构,如下代码所示。
这个函数包含三个参数,其中的第二参数是我们上面提到的Viurtal Process结构体指针,第三参数是VTL等级。通过代码得知,VP offset 0x328
处使用了VTL级别来进行引索,随后取出一个指针,我们暂时称这个指针为struc1。然后从struc1 offset 0x10d0
处取出了一个指针,这个指针我们暂时称为struc2。后续又从struc2 offset 0x188
中取出一个64bits的地址,这个地址就是我们上文提到过的VMCS的地址。代码运行到vmptrld这里,就实现了VMCS的切换,同时抽象理解成为VTL也完成了切换。
可以看到,VTL从0升为1时,并执行过vmptrld进行VMCS切换后,典型的NT内核的CR3从0x1aa002
变为0x4c00000
,并且EPT指针从0x12382301e
变为0x12382501e
。
这里用一个更直观的例子来验证VTL0 VTL1之间的差别。vmsp.exe进程也属于IUM进程,所以我们即使在内核调试环境下,都无法对vmsp.exe进程空间进行修改。
通过上面的调试发现,在VTL0环境下,即使拥有ring0权限,依然无法对物理地址0x49b36e7d0
进行修改。
下面再回到Hypervisor调试的场景,我们来看看到底为什么VTL0下Ring0无法访问到这片内存。
这里发现,root分区的物理地址0x49b36e7d0
经过二级地址转换后依然还是0x49b36e7d0
,因为所处的分区是root分区的原因,所以转换前后的物理地址都是一致的。但是差别出现在权限上。
如果我们修改最后PDE的读写权限,就可以实现VTL的隔离操作了,下面调试验证一下。
那么,我们照猫画虎,把VTL0环境下的PTE条目给改成0x4`9b2007b7
,看看VTL0环境下是否还会无法写入数据。
内存修改得极度丝滑!vmsp进程的VTL内存隔离已经被打破,我们可以对IUM进程空间内存进行任意修改。
简单地总结一下:
介绍了完了VTL机制,我们来介绍下比较实际的技巧,因为IUM进程无法在用户态被正常attach,这种情况无疑对安全研究造成了些许阻碍,下面我们就来介绍如何开启IUM进程的调试。
首先,我们要找到securekenel.exe的基址,然后patch掉securekernel!SkpsIsProcessDebuggingEnabled
中关于调试attach判断的代码。
到了这里我们可以使用IDA的内存搜索,搜索48 89 04 24 48 89 6c 24
字节码,得到了如下地址:
知道了目前的函数偏移,我们可以轻松的推断出securekernel.exe的基物理地址是:0x2e40000
我们要进行patch的函数SkpsIsProcessDebuggingEnabled
偏移是0x9EDD3
,这里把mov al, bl
patch成mov al, 1
。对应的字节码就是8a c3
–> B0 01
。
最后回到用户态用windbg尝试attach一下。
本作品著作权归HongZhenhao所有
未经作者同意,不得转载
天工实验室安全研究员
专注于Windows、云、虚拟化领域漏洞挖掘。
曾多次获得微软 MSRC 最高漏洞赏金,荣登2019,2020,2022,2024 微软MSRC全球最具价值安全精英榜。
连续两年BlackHat USA世界黑帽大会Speaker。
原文始发于微信公众号(奇安信天工实验室):深入解析Windows VTL机制 & IUM进程