在8月份举办的世界黑帽大会BlackHat USA 2022上,公布了网络安全领域最受关注的两大榜单:素有“安全界奥斯卡”之称的Pwnie Awards ,及微软安全响应中心(MSRC)公布的年度最具价值研究员榜单。两大榜单中,Pwnie Awards 作为一项网络安全领域殿堂级的荣耀,由全球权威安全研究专家担任奖项评委,经过严格筛选才能入围提名,任何安全研究员被成功提名已是实力的象征。蚂蚁安全光年实验室研究员林性伟(@xwlin_roy)针对虚拟化设备漏洞挖掘的工作 V-SHUTTLE 获得 Pwnie Awards 2022 “Most Innovative Research”提名,连续两年获得“Most Innovative
Research”提名。
第二份重量级榜单,来自于微软安全响应中心(MSRC)发布的2022 年度全球“最具价值研究员”榜单。每年度,微软根据全球安全研究人员对微软产品安全的贡献,严格甄选TOP100,以展示他们全球领先的漏洞挖掘能力。
今年,蚂蚁安全光年实验室研究员洪祯皓(@rthhh17)名列其中。在 BlackHat USA 2022 会议正式议题部分,洪祯皓和光年实验室研究员张子明(@ezrak1e)以《DirectX: The New Hyper-V
Attack Surface》为题进行了深度分享,介绍了微软 Azure 云的虚拟化解决方案 Hyper-V 的新型攻击面——DirectX。
本文将对《DirectX: The New Hyper-V Attack Surface》这一议题进行深入解读。
全文首先介绍 Hyper-V DirectX 组件的基本架构,并讲解为了在虚拟机中使用这个虚拟设备,如何配置虚拟机参数。然后参考 WSL2 linux kernel 代码和逆向工程,本文将详细介绍 Hyper-V DirectX 组件的攻击面。最后会披露4个 Hyper-V DirectX 组件的漏洞,以便于读者更好的理解这个攻击面。
1 Hyper-V DirectX 组件架构
2 如何配置虚拟 GPU
3 DirectX 攻击面
4 漏洞细节
4.1 CVE-2022-21918
4.2 CVE-2021-43219
4.3 CVE-2022-21912
4.4 CVE-2022-21898
2020 年,Hyper-V 引入了 GPU-Paravirtualization 的新特性,这个特性基于
GPU 虚拟化技术。这个 GPU 虚拟化技术已经集成在 WDDM(Windows Display Driver Model)中,并且所有版本大于 2.5 的
WDDM 驱动都可以原生支持该虚拟化技术。然而,新的特性意味着新的攻击面。图1. WDDM 架构 [dx10arch.png]Hyper-V DirectX 组件架构图如图 2 所示:Hyper-V DirectX 组件架构中的数据流如图 4 所示:图4. Hyper-V DirectX 组件数据流
通过 powershell 命令,Add-VMGpuPartitionAdapter 命令来添加一个虚拟
GPU 到一个指定的虚拟机中。并且可以通过 Get-VMGpuPartitionAdapter 命令来查看指定虚拟机的虚拟 GPU 配置信息。如图 5 所示。当完成了虚拟机的配置后,可以通过查看 Linux 虚拟机 Kernel Log 来确定虚拟 GPU 是否被启用。如图 6 所示。比如说,图 6 中的虚拟机内核版本是
4.15,从图 6 中得知,这里有两个未知的 GUID,分别是虚拟 GPU DXGK channel GUID 和 global DXGK channel
GUID。如果虚拟机是
Linux 虚拟机,只有 WSL2 Linux Kernel 源代码树(https://github.com/microsoft/WSL2-Linux-Kernel/tree/linux-msft-wsl-5.10.y/drivers/hv/dxgkrnl)能原生支持 DirectX 虚拟设备。但是,Linux驱动 dxgkrnl.ko 很容易被编译并根据需求定制化。DirectX的 Linux 驱动在用户态生成了一个 /dev/dxg 设备文件,这个设备文件有很多 IOCTL 可以使用,这些 IOCTL 模拟了 WDDM 的 Windows 内核服务层。
下面着重展示
Hyper-V DirectX 的攻击面。首先要介绍的是在 Linux 虚拟机中如何初始化 Hyper-V DirectX 组件。
如图 7 所示,dxgvmbuschannel_init 函数调用 vmbus_open 函数去初始化一个
DXGK channel。这里有两个函数分别调用了 dxgvmbuschannel_init 函数去初始化 DXGK
channel,它们分别是dxgglobal_init_global_channel 函数和 dxgadapter_set_vmbus 函数。这两个函数分别初始化一个
global DXGK channel 和一个虚拟 GPU DXGK channel。Linux 内核驱动
dxgkrnl.ko 使用 dxgvmb_send_sync_msg/dxgvmb_send_async_msg 函数发送 DXGKRNL command 到宿主机。并且,dxgvmbuschannel_receive
函数被用来接收从宿主机发来的消息或者 command。通过参考 WSL2
Linux kernel 源代码,可以看到这里有许多的 command 可以被使用。如图 8,图 9。所有的 command
消息都由一个大小为 0x18 的头和消息体构成。消息的头包含四个成员,channel_type 成员用来决定是发送到虚拟 GPU DXGK channel 还是一个
global DXGK channel 中。Command_type 被用来决定发送哪个类型的 command 到宿主机。图11. 发送 dxgkrnl Command 到 Host图 11 中的代码是
dxgvmb_send_lock2 函数发送 DXGK_VMBCOMMAND_LOCK2 command 到宿主机,在
command_vgpu_to_host_init2 函数中,可以看到 DXGK_VMBCOMMAND_LOCK2 command 是一个虚拟 GPU
DXGK channel command,并且 dxgkvmb_command_lock2 结构体定义了 DXGK_VMBCOMMAND_LOCK2
command 消息体的格式。当 Guest 给
Host 发送 DXGK_VMBCOMMAND 数据后,VMBUS 组件会根据特定的 channel 调用
DXG_HOST_VIRTUALGPU_VMBUS::VmBusChannelProcessPacket 或者
DXG_HOST_GLOBAL_VMBUS::VmBusChannelProcessPacket 函数。然后 VmBusProcessPacket 函数根据
channel_type 来决定在表 DXG_HOST_GLOBAL_VMBUS::VmBusCommandTableVmToHost 还是在表
DXG_HOST_VIRTUALGPU_VMBUS::VmBusCommandTableVgpuToHost 中查询 command_type 对应的处理函数。最后,调用特定的处理函数处理 Guest 发来的数据。宿主机中
dxgkrnl.sys 驱动中的 CastToVmBusCommand 函数用来获取 Guest 发来的数据。VmBusCompletePacket 函数是
Host 用来将数据发送的 Guest 的函数,这个函数的第二参数是 databuffer,第三参数是 databuflength。Hyper-V
DirectX 组件的主要的攻击面存在于三个驱动文件中,它们分别是:dxgkrnl.sys dxgmms1.sys dxgmms2.sys。如图 13 所示,这里一共有 87 个 command,并且每个 command 都有一个对应的结构体。可以看到,Hyper-V DirectX
组件有很大的攻击面。
4.1 CVE-2022-21918
下面通过四个漏洞来进一步了解 Hyper-V DirectX 组件的攻击面。第一个要介绍的是 CVE-2022-21918,这是一个空指针引用漏洞。这个漏洞发生在
DXGK_VMBCOMMAND_SIGNALSYNCOBJECT command 的处理流程中。这个漏洞发生的核心原因是
VidSchiSignalSyncObjectsFromCpu 函数的第五参数引用了空指针,图 14 是漏洞触发时的栈回溯。图14. CVE-2022-21918 漏洞栈回溯VidSchiSignalSyncObjectsFromCpu
函数的第五参数由 VidSchSignalSyncObjectsFromCpu 函数第四参数赋值。VidSchSignalSyncObjectsFromCpu
函数的第四参数是 SignalSynchronizationObjectFromCpu 函数中的 v5.pfence_values,如图 15。图15. SignalSynchronizationObjectFromCpu 函数SignalSynchronizationObjectFromCpu
函数被 DXG_HOST_VIRTUALGPU_VMBUS::VmBusSignalSyncObject 函数调用,并且
SignalSynchronizationObjectFromCpu 函数的第一参数的 pfence_values 成员被 v24 赋值,如图 16。图16. DXG_HOST_VIRTUALGPU_VMBUS::VmBusSignalSyncObject 函数节选如图 17 所示,如果
v22 比 v17 小,v24 为 0。图17. DXG_HOST_VIRTUALGPU_VMBUS::VmBusSignalSyncObject 函数节选DXGK_VMBCOMMAND_SIGNALSYNCOBJECT
command 消息的内存布局如图 18。图18. DXGK_VMBCOMMAND_SIGNALSYNCOBJECT command 消息内存布局内存布局中的
ObjectHandleArray,ContextArray 和 MonitoredFenceValueArray 都是可变大小的数组,它们的大小由
object_count 和 context_count 控制。如图 19,如果
v10_ObjectCount 为 1,Context_Count 为 0,并且 v16_buffer_length 为 0x3C,那么
v17_Length_Of_MonitoredFenceValueArray 为 8,v18_Offset_Of_MonitoredFenceValueArray
为 0x3C。在图中代码的 151 行处,v22 = v16 - v18,所以 v22 等于 0,v17 等于 8,满足了 v22 < v17 的条件,所以v24 等于 0,在随后的处理流程中,会引用一个空指针。PoC 代码包含两个部分,第一部分是创建
sync_handle,第二部分是发送 DXGK_VMBCOMMAND_SIGNALSYNCOBJECT command。如 20 图设置
signalsyncobject 结构体会触发这个漏洞。4.2 CVE-2021-43219
下一个漏洞是 CVE-2021-43219,这也是一个空指针引用漏洞。这个漏洞发生在
DXGK_VMBCOMMAND_SUBMITCOMMAND command 的数据处理流程中。如 21 图是这个漏洞触发时候的栈回溯。图21. CVE-2021-43219 漏洞栈回溯在
DxgkSubmitCommandInternal 函数中,DXGCONTEXT::HandleVistaBltStub 函数的第二参数是
v37_present_history_token,并且 v37_present_history_token 可以从虚拟机中被控制。在 DXGCONTEXT::HandleVistaBltStub 函数中,如图 22。图22. DXGCONTEXT::HandleVistaBltStub 函数节选如果
a2_present_history_token 小于 0,代码流程会进入到如图 22 的这个分支中,并会调用函数 CWin32kLocks::Lock 函数,CWin32kLocks::Lock
函数的第一个参数是 CWin32kLocks 结构体指针。当这个漏洞被触发,CWin32kLocks 结构体中的成员还尚未初始化,造成 BSOD。图 24 是这个漏洞的调试过程,首先设置断点在
DXGKSubmitCommandInternal+0xA689D。当这个断点被触发,检查 present_history_token 的值,可以看到现在是小于
0 的。单步到 CWin32kLocks::Lock 函数,并查看第一参数内存,可以看到在 CWin32kLocks 结构体中的许多成员都未初始化,这些未初始化的值会在
CWin32kLocks::Lock 函数被引用并造成崩溃。4.3 CVE-2022-21912
下一个漏洞是 CVE-2022-21912,这是一个任意地址读漏洞。这个漏洞发生在
DXGK_VMBCOMMAND_WAITFORSYNCOBJECTFROMGPU command 数据处理流程中,图 25 是这个漏洞触发时的栈回溯。图25. CVE-2022-21912 漏洞栈回溯图 26 是 DXGK_VMBCOMMAND_WAITFORSYNCOBJECTFROMGPU command 消息的内存布局。图26. DXGK_VMBCOMMAND_WAITFORSYNCOBJECTFROMGPU command 消息内存布局在
DXG_HOST_VIRTUALGPU_VMBUS::VmBusWaitForSyncObjectFromGpu 函数中,如果
legacy_fence_object 等于 1,syncgpu.fence_value 会赋值给 Dst.pFenceValue,并且 v23 会赋值为 0。如图
27。现在,DxgkWaitForSynchronizationObjectFromGpuInternal
函数的第四参数是 0。在图 28 中的红色标记处,因为 v5 等于 0,Src.pFenceValue 会被赋值给 v5,并且 Src.pFenceValue
可以从虚拟机中控制。在之后的代码流程中,v5 将作为 WaitForSynchronizationObjectFromGpu 函数的第三参数。在
WaitForSynchronizationObjectFromGpu 函数中,如图 29。V10 等于 a3,v10 直接解引用指针,并造成任意地址读取。4.4 CVE-2022-21898
最后一个漏洞是 CVE-2022-21898,这个漏洞是任意地址写漏洞。这个漏洞发生在
DXGK_VMBCOMMAND_SUBMITVAILPRESENTHISTORYTOKEN command 数据处理流程中,图 32 是这个漏洞的栈回溯。图32. CVE-2022-21898 漏洞栈回溯如图 33,DXGK_VMBCOMMAND_SUBMITVAILPRESENTHISTORYTOKEN
command 消息内存布局如下。在
DXG_HOST_VIRTUALGPU_VMBUS::VmBusSubmitVailPresentHistoryToken 函数中,present.unkown4_off18
被用于 DXGADAPTER::SubmitPresentHistoryTokenFromVm 函数的第七个参数。在
DXGADAPTER::SubmitPresentHistoryTokenFromVm 函数中,a7_unknown4_off18 被写入到
v29+0x300 地址的内存中。VidSchSubmitCommandContextless 函数的第一参数是 v29。如图 34。图34. DXGADAPTER::SubmitPresentHistoryTokenFromVm 函数VidSchSubmitCommandContextless
函数的第一参数是 VidSchiRedirectedFlipWaitOnSyncObject 函数的第二参数。图 35 中,VidSchiRedirectedFlipWaitOnSyncObject
函数的第一参数加上 0x238 是 VidSchiAcquirePrivateDataReference 的第二参数。图35. VidSchiRedirectedFlipWaitOnSyncObject 函数如图 36,在
VidSchiAcquirePrivateDataReference 函数中,当 v2 等于 0,v4=*(_QWORD *)((char *)a2 +
0xC8)。a2 的值是 VidSchiRedirectedFlipWaitOnSyncObject 函数中的 v6+0x238,0x238+0xC8=0x300,所以
*(_QWORD *)((char *)a2 + 0xC8) 的值就是 DXGADAPTER::SubmitPresentHistoryTokenFromVm
函数中的 a7_unknown4_off18 的值。在之后的代码流程中,在 v4+0x0C 的地址处加一会造成任意地址写。图36. VidSchiAcquirePrivateDataReference 函数