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准备就绪,对于根分区提供的每个通道,客户机将在设备树中构建一个新节点。总结的(和通用的)流程是:
- 根分区提供一个通道
- 通过合成中断将提供传递给客户机
- 在客户机中,由于中断,在PnP系统中注入总线关系查询
- 在客户机中,VMBus驱动程序为设备栈创建一个新的物理设备对象(PDO)。提供的信息保存在PDO上下文中
- 设备驱动程序(例如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
|