深入解析Hyper-V虚拟机工作进程攻击面与安全研究

本文详细分析了微软Hyper-V虚拟机工作进程(VMWP)的内部架构、组件交互和安全漏洞,涵盖指令模拟、设备仿真、通信机制及实际CVE案例,为安全研究人员提供深入的技术洞察和调试方法。

攻击虚拟机工作进程 | MSRC博客

在过去一年中,我们投入了大量时间使Hyper-V研究对所有人更加开放。我们的第一篇博客文章《Hyper-V研究的第一步》描述了调试管理程序的工具和设置,并检查了虚拟化堆栈组件的有趣攻击面。随后我们发布了《在Hyper-V中模糊测试半虚拟化设备》,这一直是我们虚拟化安全团队朋友们关注的焦点。在这篇博客中,他们深入探讨了通过VMBus进行的VSPs-VSCs通信,并描述了根分区内核中vPCI VSP的一个有趣的客户机到主机漏洞(vpcivsp.sys)。八月,Joe Bialek在Black Hat上做了一个精彩的演讲,描述了他如何利用IDE模拟器中的另一个漏洞,该漏洞位于虚拟机工作进程(VMWP)中。因此,现在是时候更深入地研究VMWP的内部结构,并考虑那里可能存在的其他漏洞了。

什么是虚拟机工作进程?

我们虚拟化堆栈中最大的攻击面之一是在根分区的用户空间中实现的,并位于虚拟机工作进程(VMWP.exe)中。在运行Hyper-V时,每个虚拟机都有一个VMWP.exe进程实例。正如我们在第一篇博客文章中所说,以下是位于VMWP中的一些组件示例:

  • vSMB服务器
  • Plan9FS
  • IC(集成组件)
  • 虚拟设备(模拟器、非模拟设备)

您可以将VMWP视为我们的“QEMU”式进程。我们需要一个组件来实现模拟/非模拟设备,并且我们强烈倾向于在用户空间而不是内核空间中实现它。这样的组件通常非常复杂,而复杂的东西很难正确实现……这就是您发挥作用的地方。VMWP似乎是一个寻找漏洞的好地方:它相当容易调试,具有巨大的攻击面,并实现了复杂的驱动程序。哦,您还可以使用公共符号。

在这篇博客中,我想谈谈VMWP的内部结构:类、接口、职责及其工作方式。

VMWP内部结构

让我们深入探讨工作进程的内部结构,查看其一些组件以及不同操作的有趣流程。首先,让我们看一下一般的虚拟化架构:

工作进程在根分区中运行,并通过虚拟化基础设施驱动程序(vid.sys)与管理程序通信。该驱动程序还将管理程序与虚拟机管理服务(VMMS)连接起来。VID使用超级调用向管理程序发送管理命令,用于以下操作:

  • 创建/删除分区
  • 暂停/恢复分区
  • 动态内存
  • 添加/删除虚拟处理器

此外,VID还模拟MMIO(和ROM)。由于工作进程需要与VID的接口,它使用一个用户空间库,该库具有与驱动程序的接口,称为vid.dll(与vid.sys相对)。这个接口可以从VMWP的导入表中清楚地看到。

VID通知分发器(VND)是这种通信的关键组件。VND允许客户端注册并接收由客户机执行的操作生成的VID通知。大多数最终进入工作进程的流程都是从VND通知开始的。以下是x64的可能VID通知:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
typedef enum _VID_MESSAGE_TYPE
{
        VidMessageInvalid = 0x0000,

        //
        // Message types corresponding to generic hypervisor intercepts.
        //
        VidMessageMbpAccess = 0x0001,
        VidMessageException = 0x0002 | VID_MESSAGE_TYPE_FLAG_INTERCEPT,
        VidMessageTripleFault = 0x0003 | VID_MESSAGE_TYPE_FLAG_INTERCEPT,
        VidMessageExecuteInstruction = 0x0004 | VID_MESSAGE_TYPE_FLAG_INTERCEPT,
        VidMessageMmio = 0x0005 | VID_MESSAGE_TYPE_FLAG_INTERCEPT,
        VidMessageRegister = 0x0006 | VID_MESSAGE_TYPE_FLAG_INTERCEPT,

        //
        // Message types corresponding to other monitored events.
        //
        VidMessageHandlerUnregistered = 0x0007,
        VidMessageStopRequestComplete = 0x0008,
        VidMessageMmioRangeDestroyed = 0x0009,
        VidMessageTerminateVm = 0x000A,
        VidMessageGuestControlForPageCopy = 0x000B,

        //
        // Message types for minimal partitions
        //
        VidMessagePassthroughIntercept = 0x000C | VID_MESSAGE_TYPE_FLAG_INTERCEPT,
        VidMessageStopRequestCompleteDirect = 0x000D | VID_MESSAGE_TYPE_FLAG_INTERCEPT,

        VidMessageApicEoi = 0x000E | VID_MESSAGE_TYPE_FLAG_INTERCEPT,
        VidMessageCpuid = 0x000F | VID_MESSAGE_TYPE_FLAG_INTERCEPT,
        VidMessageExecuteIoInstruction = 0x0010 | VID_MESSAGE_TYPE_FLAG_INTERCEPT,
        VidMessageMsr = 0x0011 | VID_MESSAGE_TYPE_FLAG_INTERCEPT,

        VidMessageTypeCount = 0x0012,

} VID_MESSAGE_TYPE;

在本文的后面,我们将看到理解这些消息及其含义的重要性。VID是连接虚拟化堆栈中组件的粘合剂,允许客户机内部的组件与主机中的工作进程通信。工作进程的另一个重要接口是VMB(虚拟主板)接口,用于VDEV与工作进程通信。

提示:您可以在vmwp!TraceVidMessageDetails中找到此枚举的名称和值。有时候有跟踪器为您完成所有逆向工程是很好的(剧透——这不是该领域唯一的)。

有了对如何通过VID消息进行通信的更好理解,我们可以深入研究一些组件并开始设置断点来检查通知和数据。稍后我们将看到如何跟踪从VID到vmwp组件中发生的逻辑的流程,然后再返回。

工作进程的关键组件

让我们从工作进程中一些关键组件的概述开始。

指令解码器

工作进程中有一个x86模拟器,在一个名为EmulatorVP的类中实现。该类模拟x86和x64指令的执行。其目的是通过在拦截之间保持在工作进程上下文中来加速子分区中拦截密集型代码的情况,因为vmentries/vmexits具有严重的性能开销。这个类不小(约250个函数),但有些流程很容易理解。让我们讨论它的一些子组件:

  • EmulatorVp::ExecuteInstruction和EmulatorVP::ExecuteOp。两者都是旨在模拟许多指令执行的函数集合。ExecuteInstruction函数系列具有不同级别的抽象。在执行基本的一般操作/检查后,它们调用ExecuteOp函数系列,其中包含更具体的函数。

  • EmulatorVp::ExecuteIs*是一组用于检索RFLAGS值的函数。许多指令需要检查某些条件(如溢出/零/符号/进位)。所有标志都存储在一个常量联合RFLAGS中,可以在其中一个处理程序中查看:

  • EmulatorVP::Decode* - 如果我们进行模拟,我们也必须解码指令。为此,有许多形式为EmulatorVP::decode*的函数,负责解析指令并将流程重定向到每个模拟指令的相关处理程序。

注意:还有更多此类示例,我将留给您去查找!

为了更好地理解该逻辑如何融入大局,让我们看一个逻辑流程。例如,以下是EmulatorVP::DecodePrimaryOp运行时的调用堆栈:

VndCompletionThread从VID消息队列中拉取消息(现在您明白为什么我们在文章开头介绍VID了)。一旦VndCompletionThread获得这样的消息,它会以适当的方式处理它。此流程是VidMessageExecuteIoInstruction VID消息类型的一个示例,我们之前在VID_MESSAGE_TYPE枚举定义中看到过。所有消息的switchcase在VndCompletionHandler::HandleVndCallback中,从VndCompletionThread::RunSelf调用。

注意:记住这些函数。大多数有趣的流程都源于它们。

设备

既然您已经看到了拦截如何最终进入VMWP指令模拟器,让我们谈谈设备。任何虚拟化平台都需要执行设备模拟。在基于KVM的PC平台上,通常是QEMU进程。

您可能听说过“模拟设备”和“半虚拟化设备”。我们可以将虚拟化设备分为以下3类:

  • 模拟设备:硬件设备的完整软件模拟。这在我们希望运行客户机而无需任何修改使其意识到它不是在物理硬件上运行的情况下非常有用(例如,当主机NIC不同时模拟e1000 NIC)。
  • 半虚拟化:半虚拟化是修改客户机使其意识到它在虚拟环境中运行的概念,要么是为了减少虚拟化复杂性,要么是为了实现更高的性能。在设备上下文中,管理程序通常实现一些不模拟任何真实存在的物理硬件但设计为仅以虚拟形式存在的虚拟设备。
  • 直接访问(直通):顾名思义。管理程序简单地让客户机直接访问硬件中的设备并与之通信。这会影响性能,虚拟化平台对某些设备这样做。有时需要硬件支持,否则设备将仅在单个客户机分区中可用。

上一篇关于vPCI的博客文章中描述的所有VSP都是半虚拟化(PV)的。对于存储、网络和vPCI,主机内核中有驱动程序为来自客户机内核的请求提供服务。现在,是时候看一些模拟设备了。

以PCI-ISA总线为例。在vmemulateddevices.dll中实现了一个名为IsaBusDevice的类。开始逆向该类的方法和逻辑,您将看到实际实现模拟了一个称为PIIX(用于PCI-to-ISA / IDE Xcelerator)的Intel芯片组。或者,在VmEmulatedNic.dll中实现的EthernetCard类,实现了以下PCI以太网卡规范:DEC 21041、DEC 21140A和DEC 21143。列表还在继续。在工作进程二进制文件或其加载的dll中实现了许多模拟/非模拟设备:

DLLs 职责 设备/逻辑范围
VmEmulatedDevices.dll 模拟超过1250和1500个函数的许多设备 PitDevice PicDevice PciBusDevice SpeakerDevice IsaBusDevice Ps2Keyboard I8042Device VideoS3Device VideoDevice DmaControllerDevice
vmuidevices.dll 模拟超过1500个函数的许多设备 VideoDevice VideoDirt InputManager SynthMouseDevice SynthKeyboardDevice HidDevice SynthRdpServerConnection VideoSynthDevice
vmEmulatedNic.dll 网卡设备 Class EthernetDevice,模拟以下卡:DEC 21041, DEC 21140A, DEC 21143
VmEmulatedStorage.dll 存储设备 IdeControllerDevice IdeOpticalDrive IdeHardDrive IdeChannel DiskMetricDevice FloppyDrive
winHvEmulation.dll 在UM进行x86指令模拟(EmulatorVP函数) N/A
Vmchipset.dll 模拟许多芯片组 IoApicEmulator PowerManagementEmulator

让我们看看到该代码区域的完整流程是什么样的。从客户机执行需要某些模拟的特定指令开始,直到提到的库中的相关函数处理所需模拟,会发生什么?

首先,让我们考虑通用设计,这将有助于探索该领域的其他组件。考虑PCI配置函数NotifyPciConfigAccess:

1
2
3
4
5
6
7
8
STDMETHOD(NotifyPciConfigAccess)(
        _In_ PCI_BUS_NUMBER         Bus,
        _In_ PCI_DEVICE_NUMBER      DeviceNumber,
        _In_ PCI_FUNCTION_NUMBER    FunctionNumber,
        _In_ UINT16                 InRegAddress,
        _In_ BOOLEAN                InIsWrite,
        _Inout_ UINT32*             IoData
        );

这是一个接口中函数的定义,用于描述具有PCI配置访问处理程序的设备(工作进程是用C++编写的)。一些类实现此接口并使用它们自己的实现重写该方法:

1
2
3
4
5
0:035> x *!*NotifyPciConfigAccess
00007ffc`1fdafeb0 vmchipset!VmEmu::WCL::PciHandler::NotifyPciConfigAccess
00007ffc`477047c0 VmEmulatedStorage!IdeControllerDevice::NotifyPciConfigAccess
00007ffc`4c56c5e0 vmemulateddevices!VideoS3Device::NotifyPciConfigAccess
00007ffc`4c556300 vmemulateddevices!IsaBusDevice::NotifyPciConfigAccess

这是针对PCI配置的。我们还可以看到读/写访问的处理程序,它们可能解析更多客户机控制的数据。处理程序提取总线号、设备号、功能号、寄存器地址和类型代码。所有这些都需要清理和边界检查;查看PciBusDevice::PciConfigIoWrite作为示例。由于调试跟踪带有解释其目的的字符串,那里的代码应该相当容易逆向工程。例如:

1
2
3
4
5
if ((unsigned int)VmlIsDebugTraceEnabled(0xC0C5i64))
VmlDebugTrace(
        0xC0C5i64,
        L"Write to PCI Data Port %04X [bus=%02X, unit=%02X, func=%02X, reg=%02X]: %08X",
        ioAddr, );

如果您看过Joe Bialek在Blackhat 2019上的演讲,您就会看到正确清理通过WriteIOPort处理程序到达IDE模拟器(存储)内部的数据是多么重要 - IDEControllerDevice::NotifyIoPortWrite。让我们看看如何调试从客户机分区到主机上工作进程的整个流程。

为了设定基线,让我们回顾一下CVE-2018-0959。该漏洞存在于存储模拟器中,根本原因是处理读/写IO端口的代码缺乏对客户机控制数据的验证。参见Joe演讲中的这张图片进行说明:

看到从客户机到主机上工作进程中该函数的整个流程将有助于理解全貌,并可能帮助您发现新的错误!所以让我们调试一下。考虑以下指令,在客户机分区中运行:

1
out 0x1f0, 0x41414141

这是一个特权指令,因此我们不能直接从用户空间执行它。我们有两个选项:

  1. 编写我们自己的内核驱动程序,向用户空间公开接口,并为我们读/写IO端口
  2. 打开内核调试器并在那里执行它

如果我们要编写完整的漏洞利用,我们会选择第一个选项。对于仅运行指令并在用户模式下调试工作进程,第二个选项将起作用。

注意:在以下屏幕截图中,vmwp调试器在左侧,kd在右侧。

假设我们想在IDE存储模拟器中设置一个写入io端口的断点。相关函数是vmEmulatedStorage!IDEControllerDevice::NotifyIoPortWrite。因此,通过设置以下断点:

1
bp vmemulatedstorage!IdeControllerDevice::NotifyIoPortWrite ".printf \"write to port: 0x%x, accessSize==0x%x, data==0x%x\r\n\", @rdx, @r8, @r9;g"

我们可以获得所有通过IDE模拟器的IO端口写入。在所有这些中,我们可以在左下角看到我们的,带有受控数据。请注意,跟踪已停止,因为我进入了内核调试器。此时客户机内核中没有任何运行,但我们通过调试器进行的写入仍然传递到主机用户模式,因为管理程序和主机仍在运行。

现在,假设我们想检查和分析到我们特定流程的调用堆栈。我们可以如下更改断点:

1
bp vmemulatedstorage!IdeControllerDevice::NotifyIoPortWrite ".printf \"write to port: 0x%x, accessSize==0x%x, data==0x%x\\r\\n\", @rdx, @r8, @r9; .if(@r9 != 41414141) {g;};"

在这里我们可以看到调用堆栈,类似于指令模拟示例:通过VND到模拟存储设备。

这可能是一个好时机指出一些设置断点的好目标。以下是有趣函数及其在游戏中的角色的列表。

函数 描述
VndCompletionHandler::HandleVndCallback 当我们收到IO完成事件时调用。这是处理来自客户机的请求/数据的大多数流程的根点
EmulatorVp::TryIoEmulation 模拟简单的IO指令(包括字符串IO指令)
VmbCallback::NotifyIoPortWrite IO端口写入的通用处理程序。它调用相关设备的处理程序
EmulatorVp::ExecuteInstruction和EmulatorVP::ExecuteOp 模拟许多指令的执行
EmulatorVp::GenerateEvent 在客户机中生成事件。事件可能是异常、客户机嵌套页面错误等。此函数很重要,因为大多数事件注入到客户机都通过这里。例如,将页面错误和保护错误注入到客户机中使用此函数完成。

半虚拟化设备

现在,让我们谈谈工作进程如何为客户机分配新的PCI设备。工作进程为此实现了一个称为VpciBus的接口。此代码包装了vPCI VSP公开的ioctl API(我们需要以某种方式与设备通信)。正如我们在上一篇博客文章中讨论的,VSP是在根的内核空间中运行的驱动程序。它们通过VMBus与在客户机内核空间中运行的虚拟化服务消费者(VSC)通信。您可以在VMWP内部发现打开到vpcivsp.sys公开设备的HANDLE的代码:

使用该HANDLE,VMWP发出ioctl,要求vPCI VSP设备执行许多任务,通过ioctl暴露给vPCI VSP。其中一个例子是IOCTLVPCI_ASSIGN_DEVICE,映射到内核驱动程序vpcipvsp.sys中的函数_vpcivsp!VpciIoctlAssignDevice,并负责将设备分配给客户机。我们可以从函数VMWP!VpciBus::AssignDevice中看到该调用。

注意:这是针对PCI设备的。另一个例子是模拟PC

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计