原文链接:https://revers.engineering/patchguard-detection-of-hypervisor-based-instrospection-p1/
翻译:看雪翻译小组-OsWalker
校对:看雪翻译小组-八企鹅
勘误?
在过去2-3年,微软在patchguard中引入了各种虚拟机内省(令人耳目一新)方法。随着攻击者代码运行在更高特权层,破坏内核补丁的保护很容易,所以微软的做法并不令人惊讶。Windows在hypervisor上运行效果很好,并且有开放的半虚拟化(paravirtualization)接口,pacchguard在寻找vmm被篡改成虚拟机必须功能以外的状态的特征。例如,尝试通过隐藏MSRs真实值来hook系统调用以控制系统调用分发目标,或者利用嵌套的分页机制在关键控制路径中获得代码执行。
Patchguard中包含的检测这些内省的机制比本文中介绍的更多,作者根据自己的兴趣选择了一些特殊的进行介绍。读者可以尝试查找更多的内容。本文目标是帮助安全软件、反病毒软件和内省工具与内核补丁保护机制进行交互。
第一个要介绍的是KiErrata704Present。初看,这个函数的命名很无规则,对不熟悉的人,看起来像是某种错误类型的合法性检查。让我们来分析一下这个函数:
一些背景知识:某些古老的特权切换形式,如SYSENTER和门调用,准许调用者单步跳过操作码。这不是最佳选择,因为单步信号#DB(Debug Breakpoint)在任务分发到处理代码(转移到系统调用处理函数、中断门处理函数)后会被提交。内核需要注意这种情况,以便可以通过IRET返回调用者,最终在处理完系统调用后继续执行单步操作。后来引入SYSCALL/SYSRET指令通过FMASK MSR来解决系统调用的问题。这个MSR让开发者对SYSCALL指令执行时如何处理RFLAGS有更好地控制。任何现代操作系统都会保证这个MSR的IF标志位和TF标志位置为0。并且SYSRET会进行特殊处理以确保如果在TF置位时加载了一个RFLAGS镜像,就在下一个指令边界处增加#DB信号,与IRET在分发的处理函数执行后在边界的做法相反。单步跳过SYSCALL指令时,这可提供流畅的调试体验。我们希望对此有更好的理解,可以看到KiErrata704Present函数首先保存FMASK MSR内容,然后设置MSR值,使得TF标志位不会被SYSCALL指令修改。
之后我们看到一串PUSHFQ/POPFQ,它们修改追踪标志(trap flag)然后将改后的MSR加载回RFLAGS寄存器。你可能已经意识到,这会引起前面的指令执行时将TF置位,并在执行后引起#DB信号。如果指令是软件异常,软件中断或特权软件异常,或者指令产生一个硬件异常。
你可能意识到,一旦SYSCALL执行完成,会产生一个#DB信号,这就像在其它分支指令处执行单步跳过一样。所以,如果LSTAR的目标是下面类似的指令序列:
0x40000: SWAPGS
0x40001: MOV GS:[0x8], RSP
0x40002: MOV RSP, GS:[0x10]
#DB处理函数中断包含0x40000的堆栈段,因为这是尚未执行的syscall操作分支的目标。
你可能已经意识到了,patchguard可以在中断处理中通过内省#DB来产生IP(指令指针)间接发现LSTAR MSR的真实值。这是一种能发现在RDMSR/WRMSR时是否存在恶意的虚拟机,并给操作系统预期望的值的方法。
接下来是我最喜欢的,KiErrataSkx55Present。回顾CVE-2018-8897,这个函数在该脆弱性被修复后不久被加入patchguard。要理解这个检测如何工作,需要看一下POP SS/MOV SS vulnerability whitepaper。
如果你读了这篇文章,会发现它只是在说这个漏洞。所以在上面我们给出SYSCALL处理函数,这个#DB也会在中断栈上放入0x40000.
客户代码能够获得RDMSR/WRMSR相关的更多信息时,经验不多的hypervisor开发者会怎么做?非常简单,设置异常位图使得在#DB异常时退出,并检查客户状态IP来处理上面提到的两类指令边界#DB,如果不匹配,合适的做法是基于向量事件注入返回到客户代码。检测退出条件而不仅检查客户状态中的TF标记是明智的选择。
这里我给大家讲一个流行的反病毒hypervisor在这里发生错误的例子,因此当它往回注入#DB到客户的代码到自定义的syscall处理函数的RIP时,KiDebugTraps缓解做的不好,导致这个hypervisor使你的系统又变得可被CVE-2018-8897攻击。
最后,这非但不是一个锦上添花的东西,这个可靠性检查反而使你的hypervisor在#DB异常退出时暴露,或者,你应该友好一点不要去检查?进入KiErrata361Present函数。
这里还有一些要解释的东西。在通常的指令周期中,通过POPF的某个变体给RFLAGS寄存器加载TF标记,随后的SS(Stack Segment堆栈段寄存器)加载操作将引起单步在SS 加载指令后面的指令的指令边界被发现。当临时被一个加载SS的操作限制时,这与#DB遇到被破坏的调试寄存器的情形一样。在上面的情况,一个INTn(软件中断),或专门设置的INT3操作码(软件异常)并不关心之前通过TF挂起的#DB信号,并一定会被丢弃。
尽管没有被文档化,这也是ICEBP的正常行为,在Intel手册中这个信号描述为特权软件异常。在这种情况,#DB信号没有置位DR6.BS,尽管处于挂起状态,基于这个操作码的功能这个信号将被丢弃。当ICEBP引入一个#DB VMEXIT时,它实际上带有这个警告。在常规体系结构的指令循环中,VMCS的挂起的调试异常域的BS位会被置位,因为这正是当时的真实状态,但当特权软件异常引起退出时,该位将被清除。
由于这些情况下VMCS的状态不可恢复,将引起VMRESUME错误,引起大多数hypervisors草率地中止运行。该体系结构要求,如果虚拟cpu处于中断过程中,并且启用了MOV SS/POP SS阻塞,并且设置TF标志位,那么必须存在一个基于BS的挂起的#DB,因为没有其他方法可以获得此机器状态。修复这个问题也很简单:在符合条件的退出点检测特权软件异常,如果MOV SS引起阻塞时TF==1,就确保挂起的调试异常中BS被置位。
KiErrata361Present函数的思路实际上来自于CVE-2018-1087漏洞,在该漏洞被大众知道前特权软件异常就是ICEBP,并且这个异常会在patchguard中显示直到被KVM修复。Intel SDM就在那时进行了更新指明特权软件异常是什么,但仍然没有出现这种情况。
如果你觉得本文不无聊,就继续看看第二部分,我们讨论了另一个Patchguard检测机制,用一些批判性思维来想出我们自己的妙招。
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课