Hyper-V 半虚拟化设备模糊测试技术解析

本文深入探讨了在Hyper-V中对半虚拟化设备进行模糊测试的方法,重点介绍了VMBus架构、VPCI设备通信机制,以及如何通过VMBus通道发送恶意数据来发现安全漏洞,并以CVE-2018-0965为例进行了详细分析。

Hyper-V 半虚拟化设备模糊测试 | MSRC 博客

引言

Hyper-V 是 Azure 的支柱,运行在其主机上以提供高效、公平的资源共享和隔离。这就是为什么我们在 Windows 漏洞研究团队多年来一直在幕后帮助保护 Hyper-V。Microsoft 邀请全球安全研究人员通过 Hyper-V 赏金计划提交漏洞,最高可获得 25 万美元的奖励。

为了帮助人们参与 Hyper-V 安全领域,去年 Microsoft 的内部团队发布了一些工作。在 BlackHat 2018 USA 上,Joe Bialek 和 Nicolas Joly 介绍了“深入 Hyper-V 架构与漏洞”。他们涵盖了面向安全研究人员的 Hyper-V 架构概述,并讨论了一些在 Hyper-V 中看到的有趣漏洞。在同一会议上,Jordan Rabet 介绍了“通过进攻性安全研究强化 Hyper-V”,他详细讨论了 VMSwitch(Hyper-V 组件)中 CVE-2017-0075 的利用过程。去年 12 月,Saar Amar 发布了一篇详细的博客,介绍了 Hyper-V 安全研究的基础知识。

继他们的工作之后,我们想分享一个与 Hyper-V 安全相关的新故事,供任何有兴趣了解 Hyper-V 安全或学习更多的人参考。最近我们一直在研究 Virtual PCI (VPCI),这是 Hyper-V 中可用的半虚拟化设备之一,用于向虚拟机暴露硬件。与其他半虚拟化设备一样,它使用 VMBus 进行分区间通信。

在本博客中,我们想分享一些我们的学习成果,介绍 VMBus 和 VPCI,分享一种模糊测试 VPCI 使用的 VMBus 通道的策略,并讨论我们的一个发现。这里的一些概念和策略可用于处理 Hyper-V 中使用 VMBus 的其他虚拟设备。

VMBus 概述

VMBus 是 Hyper-V 提供半虚拟化的机制之一。简而言之,它是一个虚拟总线设备,在客户机和主机之间建立通道。这些通道提供了在分区之间共享数据和设置合成设备的能力。

在本节中,我们将介绍 VMBus 架构,了解通道如何提供给分区,以及如何设置合成设备。根分区(或主机)托管虚拟化服务提供者(VSP),它们通过 VMBus 通信以处理来自子分区的设备访问请求。另一方面,子分区(或客户机)使用虚拟化服务消费者(VSC)通过 VMBus 将设备请求重定向到 VSP。子分区需要 VMBus 和 VSC 驱动程序来使用半虚拟化设备栈。

VMBus 通道允许 VSC 和 VSP 主要通过两个环形缓冲区传输数据:上游和下游。这些环形缓冲区通过 hypervisor 映射到两个分区,hypervisor 还提供合成中断以在有数据可用时驱动分区之间的通知。

架构可以总结为下图:

更详细的 VMBus 介绍可以在之前链接的演示中找到:

由于 VMBus 允许在潜在恶意的客户机和主机中的 VSP 驱动程序之间传输 I/O 相关数据,后者是漏洞狩猎和模糊测试的主要候选。模糊测试虚拟设备的一般想法是找到可用于 VSC 的 VMBus 通道,并使用它向 VSP 发送畸形数据。

为此,我们需要广泛了解 VMBus 通道如何提供给 VSC。让我们首先介绍 VMBus 设备如何提供给客户机。从实践的角度来看,如果你部署一个 Windows 第二代虚拟机(启用的客户机),你可以在设备管理器中找到暴露的 VMBus 设备:

设备管理器中的连接视图还显示 VMBus 通过 ACPI 暴露给客户机。实际上,其描述可以在 Differentiated System Description Table (DSDT) 中找到:

 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
Device(_SB.VMOD.VMBS)
{
    Name(STA, 0x0F)
    Name(_ADR, Zero)
    Name(_DDN, "VMBUS")
    Name(_HID, "VMBus")
    Name(_UID, Zero)
    Method(_DIS, 0, NotSerialized)
    {
        And(STA, 0x0D, STA)
    }
    Method(_PS0, 0, NotSerialized)
    {
        Or(STA, 0x0F, STA)
    }
    Method(_STA, 0, NotSerialized)
    {
        Return(STA)
    }
    Name(_PS3, Zero)
    Name(_CRS, ResourceTemplate()
    {
        IRQ(Edge, ActiveHigh, Exclusive) {5}
    })
}

一旦 VMBus 准备就绪,对于根分区提供的每个通道,客户机将在设备树中构建一个新节点。总结(和通用)流程是:

  1. 根分区提供一个通道。
  2. 通过合成中断将提供传递给客户机。
  3. 在客户机中,由于中断,PnP 系统中注入了一个总线关系查询。
  4. 在客户机中,VMBus 驱动程序为设备栈创建一个新的物理设备对象(PDO)。提供的信息保存在 PDO 上下文中。
  5. 设备驱动程序(例如 VPCI)为设备栈创建一个新的功能设备对象(FDO)。用于创建 FDO 对象的例程,例如即插即用驱动程序的 AddDevice,是找到分配和打开新 VMBus 通道的代码的好地方。

内核调试器和命令“!devnode”可用于列出客户机内 VMBus 顶部可用的设备:

 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
38
39
40
41
0: kd> !devnode 0 1
Dumping IopRootDeviceNode (= 0xffffe28c76fbd9e0)
DevNode 0xffffe28c76fbd9e0 for PDO 0xffffe28c76e6b830
  InstancePath is "HTREE\ROOT\0"
  State = DeviceNodeStarted (0x308)
  Previous State = DeviceNodeEnumerateCompletion (0x30d)
  .
  .
  .
  DevNode 0xffffe28c76ed19b0 for PDO 0xffffe28c76ecfd80
    InstancePath is "ROOT\ACPI_HAL\0000"
    State = DeviceNodeStarted (0x308)
    Previous State = DeviceNodeEnumerateCompletion (0x30d)
    DevNode 0xffffe28c76f17c00 for PDO 0xffffe28c76eeed30
      InstancePath is "ACPI_HAL\PNP0C08\0"
      ServiceName is "ACPI"
      State = DeviceNodeStarted (0x308)
      Previous State = DeviceNodeEnumerateCompletion (0x30d)
      DevNode 0xffffe28c76e9e8b0 for PDO 0xffffe28c76f52900
        InstancePath is "ACPI\ACPI0004\0"
        State = DeviceNodeStarted (0x308)
        Previous State = DeviceNodeEnumerateCompletion (0x30d)
        DevNode 0xffffe28c76f5b8b0 for PDO 0xffffe28c76f54d60
          InstancePath is "ACPI\PNP0003\3&fdac00f&0"
          State = DeviceNodeInitialized (0x302)
          Previous State = DeviceNodeUninitialized (0x301)
        DevNode 0xffffe28c76f5bbe0 for PDO 0xffffe28c76f59c30
          InstancePath is "ACPI\VMBus\0"
          ServiceName is "vmbus"
          State = DeviceNodeStarted (0x308)
          Previous State = DeviceNodeEnumerateCompletion (0x30d)
          .
          .
          .
          DevNode 0xffffe28c78629340 for PDO 0xffffe28c78625c90
            InstancePath is "VMBUS\{44c4f61d-4444-4400-9d52-802e27ede19f}\{7f7e8f36-7342-4531-a380-d3a9911f80bf}"
            ServiceName is "vpci"
            State = DeviceNodeStarted (0x308)
            Previous State = DeviceNodeEnumerateCompletion (0x30d)
            .
            .

既然我们已经确立了 VMBus 作为一个有趣的攻击向量并学会了如何使用它,我们可以讨论使用它的一个虚拟设备:VPCI。

用例:VPCI

VPCI 是一个虚拟化总线驱动程序,用于向虚拟机暴露硬件。使用 VPCI 的场景包括 SR-IOV 和 DDA。重要的是要指出,只有在有虚拟设备需要时(并且必须由主机配置),VPCI 才会暴露给客户机。在本节中,我们将学习如何找到 VPCI 使用的 VMBus 通道,以及如何使用它向 VSP 发送任意数据。我们还提供了一个 Windows 驱动程序的骨架来说明这个想法。

如前所述,每个半虚拟化设备都需要一个 VSC 和 VSP 对。在 VPCI 的情况下,我们将 VSC 组件识别为 VPCI,VSP 组件识别为 VPCIVSP。VPCI 由客户机中的 vpci.sys 驱动程序管理。另一方面,vpcivsp.sys 管理主机中的 VPCIVSP 组件。对于当前分析,我们使用 vpci.sys 版本 10.0.17134.228。

找到 VMBus 通道

如前所述,新 FDO 的初始化是开始搜索 VMBus 通道分配的好地方。由于 VPCI 是一个内核模式驱动程序框架(KMDF)驱动程序,我们对 WdfDriverCreate 的调用感兴趣,特别是 DriverConfig 参数:

1
2
3
4
5
6
7
NTSTATUS WdfDriverCreate(
  PDRIVER_OBJECT         DriverObject,
  PCUNICODE_STRING       RegistryPath,
  PWDF_OBJECT_ATTRIBUTES DriverAttributes,
  PWDF_DRIVER_CONFIG     DriverConfig,
  WDFDRIVER              *Driver
);

DriverConfig 参数很有趣,因为它是一个指向 WDF_DRIVER_CONFIG 结构的指针,我们可以在其中找到 EvtDriverDeviceAdd 回调函数:

1
2
3
4
5
6
7
typedef struct _WDF_DRIVER_CONFIG {
  ULONG                     Size;
  PFN_WDF_DRIVER_DEVICE_ADD EvtDriverDeviceAdd;
  PFN_WDF_DRIVER_UNLOAD     EvtDriverUnload;
  ULONG                     DriverInitFlags;
  ULONG                     DriverPoolTag;
} WDF_DRIVER_CONFIG, *PWDF_DRIVER_CONFIG;

EvtDriverDeviceAdd 由 PnP 管理器调用,以在找到新设备时执行设备初始化。在 VPCI 的情况下,它是 FdoDeviceAdd:

在 FdoDeviceAdd 期间,VPCI 将通过调用 VmbChannelAllocate 分配新的 VMBus 通道:

VmbChannelAllocate 原型可以在 vmbuskernelmodeclientlibapi.h 公共头文件中找到。分配的通道指针在第三个参数中返回:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
/// \page VmbChannelAllocate VmbChannelAllocate
/// Allocates a new VMBus channel with default parameters and callbacks. The
/// channel may be further initialized using the VmbChannelInit* routines before
/// being enabled with VmbChannelEnable. The channel must be freed with
/// VmbChannelCleanup.
///
/// \param ParentDeviceObject A pointer to the parent device.
/// \param IsServer Whether the new channel should be a server endpoint.
/// \param Channel Returns a pointer to an allocated channel.
_IRQL_requires_(PASSIVE_LEVEL)
NTSTATUS
VmbChannelAllocate(
    _In_ PDEVICE_OBJECT ParentDeviceObject,
    _In_ BOOLEAN IsServer,
    _Out_ _At_(*Channel, __drv_allocatesMem(Mem)) VMBCHANNEL *Channel
    );

为了更好地理解通道如何分配和引用存储,让我们首先回顾从 FdoDeviceAdd 调用 FdoCreateVmBusChannel:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
__int64 __fastcall FdoDeviceAdd(__int64 a1, __int64 a2)
{
  __int64 v5; // rbx
  signed int v6; // esi
  .
  .
  .
  // WdfObjectGetTypedContextWorker, similar to WdfObjectGetTypedContext
  v5 = (*(__int64 (__fastcall ** )(__int64))(WdfFunctions_01015 + 1616))(WdfDriverGlobals);
  .
  .
  .
  v6 = FdoCreateVmbusChannel((_QWORD *)v5);
  .
  .
  .
 }

FdoCreateVmbusChannel 的第一个参数是 FDO 设备的上下文。FdoCreateVmbusChannel 将调用 VmbChannelAllocate 并将分配的 VMBCHANNEL 的引用保存在堆栈(局部变量)中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
__int64 __fastcall FdoCreateVmbusChannel(_QWORD *FdoContext)
{
  v1 = FdoContext;
.
.
.
  __int64 vpciChannel; // [rsp+70h] [rbp+10h]
.
.
.
  v5 = VmbChannelAllocate(v3, 0i64, &vpciChannel);

此时通道已分配,但仍不能使用,因为它必须先打开。客户端 VSC 通过调用 VmbChannelEnable 打开提供的通道。函数原型也包含在 vmbuskernelmodeclientlibapi.h 头文件中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
/// \page VmbChannelEnable VmbChannelEnable
/// Enables a channel that is in the disabled state by connecting to vmbus and
/// offering or opening a channel (whichever is appropriate for the endpoint
/// type).
///
/// See \ref state_model.
///
/// \param Channel A handle for the channel.  Allocated by \ref VmbChannelAllocate.
_Must_inspect_result_
NTSTATUS
VmbChannelEnable(
    _In_    VMBCHANNEL  Channel
    );

在 Windows 10 Redstone 4 (1803) 中,对 VmbChannelEnable 的调用也发生在 FdoCreateVmbusChannel。之后,通道的引用保存在 FDO 上下文中:

1
2
3
4
5
6
  v5 = VmbChannelEnable(vpciChannel);
  if ( v5 >= 0 )
  {
    v1[3] = vpciChannel;
    return 0i64;
  }

通过 VMBus 通道发送数据

现在我们了解了 VPCI 如何设置其 VMBus 通道,一个获取引用并使用它进行模糊测试的简单策略是为 VPCI 使用上层过滤器驱动程序。当 VPCI FDO 设备栈创建时,我们的驱动程序将被 PnP 管理器调用。此时,VMBus 通道已经被 FdoDeviceAdd 分配和启用,我们可以通过 VPCI FDO 上下文访问它。

让我们看看如何用驱动程序实现。第一步是提供一个 INF 文件来为 VPCI 设备安装我们的过滤器驱动程序。INF 的重要部分已高亮显示。考虑到:

  • wvpci.inf 是 VPCI 驱动程序的 INF。
  • VPCI 硬件 ID 是 VMBUS{44C4F61D-4444-4400-9D52-802E27EDE19F}
 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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
;
; BlogDriver.inf
;

[Version]
Signature="$WINDOWS NT$"
Class=System
ClassGuid={4d36e97d-e325-11ce-bfc1-08002be10318}
Provider=%ManufacturerName%
DriverVer=
CatalogFile=BlogDriver.cat

[DestinationDirs]
DefaultDestDir = 12

[SourceDisksNames]
1 = %DiskName%,,,""

[SourceDisksFiles]
BlogDriver.sys  = 1

[Manufacturer]
%ManufacturerName%=Standard,NT$ARCH$

[Standard.NT$ARCH$]
%BlogDriver.DeviceDesc%=Install_Section, VMBUS\{44C4F61D-4444-4400-9D52-802E27EDE19F}

[Install_Section.NT]
Include=wvpci.inf
Needs=Vpci_Device_Child.NT
CopyFiles=BlogDriver_Files

[BlogDriver_Files]
BlogDriver.sys

[Install_Section.NT.HW]
Include=wvpci.inf
Needs=Vpci_Device_Child.NT.HW
AddReg=BlogDriver_AddReg

[BlogDriver_AddReg]
HKR,,"UpperFilters",0x00010000,"BlogDriver"

[Install_Section.NT.Services]
Include=wvpci.inf
Needs=Vpci_Device_Child.NT.Services
AddService=BlogDriver,,BlogDriver_Service_Child

[BlogDriver_Service_Child]
DisplayName    = %BlogDriver.SvcDesc%
ServiceType    = 1               ; SERVICE_KERNEL_DRIVER
StartType      = 3               ; SERVICE_DEMAND_START
ErrorControl   = 1               ; SERVICE_ERROR_NORMAL
ServiceBinary  = %12%\BlogDriver.sys

[Strings]
ManufacturerName="TestManufacturer"
ClassName=""
DiskName="BlogDriver Source Disk"
BlogDriver.DeviceDesc="Microsoft Hyper-V Virtual PCI Bus (With Filter)"
BlogDriver.SvcDesc="Microsoft Hyper-V Virtual PCI Bus (With Filter)"

现在让我们看看过滤器驱动程序的初始骨架。首先澄清一些事项:

  • AddDevice 例程创建过滤器设备对象并将其附加到 VPCI FDO。VPCI VMBus 通道的引用保存在设备扩展中以方便访问。
  • 在这个骨架中,所有 IRP 都只是通过设备栈传递,我们不想修改 VPCI 行为,只想访问其 VMBus 通道。

完整的骨架准备构建

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