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

本文深入探讨了在Hyper-V环境中对半虚拟化设备进行模糊测试的技术方法,重点分析了VMBus通信机制、VPCI设备的安全漏洞挖掘过程,以及CVE-2018-0965漏洞的详细技术细节和利用方式。

Fuzzing para-virtualized devices in Hyper-V

引言

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

为了帮助人们参与Hyper-V安全领域,去年微软内部团队发布了一些他们的工作。在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安全或学习更多的人。最近我们一直在研究虚拟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主要通过两个环形缓冲区传输数据:上游和下游。这些环形缓冲区通过管理程序映射到两个分区中,管理程序还提供合成中断来在数据可用时驱动分区间的通知。

架构可以总结为以下图表:

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

深入Hyper-V架构与漏洞",幻灯片15-19。 “通过进攻性安全研究强化Hyper-V”,幻灯片5-16。

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

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

设备管理器中的连接视图还显示VMBus通过ACPI暴露给客户机。实际上,它的描述可以在差异化系统描述表(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通道

完整的骨架可以在这个repo中找到,可以构建和玩耍。在客户机中安装驱动程序后,VPCI栈显示我们的过滤器驱动程序:

1
2
3
0: kd> !devstack ffff8407f64cbad0
  !DevObj           !DrvObj            !DevExt           ObjectName
  ffff8407f2379de
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计