这篇博客着眼于普渡大学暑期实习生Abdulellah Alsaheel 的Pwn2Own获奖项目。这是该获奖漏洞研究系列的第二部分。您可以在这里阅读该系列的第一部分。
在今年的温哥华Pwn2Own比赛期间,Fluoroacetate团队展示了他们如何在VMware Workstation中实现从客户操作系统权限逃逸至宿主机操作系统权限。他们利用了虚拟USB 1.1 UHCI(通用主机控制器接口)中的越界读/写漏洞(ZDI-19-421)。
虽然此漏洞影响了各种VMware产品,但本博客的分析主要基于Workstation 15.0.3,采用Fluoroacetate的exp。该漏洞在VMware Workstation 15.0.4中被修补,编号为VMSA-2019-0005.1。
为了让VMware guest虚拟机访问USB设备,VMware guest虚拟机需安装名为uhci_hcd
的内核设备驱动程序。“hcd”代表“主机控制器驱动程序”。此驱动程序允许guest虚拟机与主机端的主机控制器接口(HCI)进行通信,主机端通过该硬件接口与物理USB端口进行通信。通过向USB设备定义的各种端点发送或接收USB请求块(URB)分组来完成通信。USB设备通过各端点发送或接收数据包传送实现通信,这些端点或从主机接收数据包(OUT),或向主机发送数据包(IN)。我们通过将特制的OUT数据包发送到一个名为Bulk的特定端点触发漏洞。
uhci_hcd
驱动程序处理的数据包在内存中使用uhci_td
(传输描述符)结构表示:
<center>传输描述符(TD)结构
请注意,token
字段包含一些未标明的位对齐子段。尤其注意的是,最低8位表示的是“分组ID”,定义了分组的类型。前10位是名为MaxLen
的长度字段。
为了触发此漏洞,guest虚拟机必须发送精心设计的TD结构,将Packet ID设置为OUT(0xE1)。此外,由MaxLen
子字段指示的TD的缓冲区长度必须大于0x40字节才能溢出堆上的对象。使用windbg attach调试vmware-vmx.exe,然后触发漏洞,我们会得到以下访问冲突:
调用堆栈显示了一系列处理UHCI请求的函数:
程序在调用memcpy
从TD的缓冲区复制数据的过程中发生崩溃:
这是memcpy
从TD缓冲区复制到堆中的内容:
让我们看看目标缓冲区大小是多少:
缓冲区的大小为0x58,vmware-vmx
通过[number_of_TD_structures]*0x40+0x18
计算目标缓冲区的大小。因为这一次我们只发送了一个TD结构,缓冲区大小是1*0x40+0x18=0x58
字节。
在调用memcpy
的过程中,我们可以精确指定要复制的字节数。为此,我们将OUT TD的token
字段的子字段MaxLen
(21位到31位)设置为所需的memcpy
大小减1。
很明显,有了这个,我们就可以溢出堆。但是,除了溢出堆之外,漏洞利用作者还能利用此漏洞执行其他越界写入。函数NewURB()
(位于vmware_vmx+0x165710
)用来处理传入的URB数据包。每次函数NewURB()
接收TD时,它都会将TD的MaxLen
值添加到称为光标的变量中。光标变量指向函数接收TD结构时应该写入的位置。通过这种方式,该MaxLen
字段可用于在处理后续TD时部分地控制目的地址。
为了利用此漏洞,必须先对vmware-vmx
进程进行堆布局。为了执行堆布局工作,漏洞利用主要依赖于前端(客户端)上的SVGA3D协议,它用于通过SVGA FIFO与主机通信。在后端(主机端),VMware使用DX11Renderer组件处理请求。漏洞利用代码从初始化阶段开始,先初始化SVGA FIFO内存,然后分配SVGA3D对象表。
堆布局的总体策略如下。exp首先创建hole
或未分配内存的island
,每个都是0x158字节大小,正是分配一定数量的TD加缓冲区头所需的大小。TD可能会在某个hole
内进行分配。之后,exp创建一个名为资源容器
的结构,大小为0x150字节,表示与图形界面相关的数据。这样做是为了破坏紧跟在TD之后的资源容器
。
漏洞利用代码使用以下步骤布局堆:
- 定义并绑定大小为0x5000的Context内存对象。
- 定义大小为0x1000 的内存对象(SPRAY_OBJ
),用于重复地绑定结构(例如,着色器
)。
- 定义大小为0x158的2400个着色器
,将它们绑定到SPRAY_OBJ
。之后,使用SVGA_3D_CMD_SET_SHADER
在主机中喷射着色器
。
- 迭代喷射着色器
并执行以下操作:
---释放偶数编号的着色器
。
---创建一个surface,分配一个大小为0x150的资源容器
。通常是在着色器
空出的hole
中进行分配。此外,主机将分配大小为0x160的关联数据缓冲区。由于大小不同,这些数据缓冲区将位于低碎片堆(LFH)的单独区域中。每个0x150字节的资源容器
将包含指针指向其关联的0x160字节数据缓冲区。
---再创建两个surface,分配两个大小为0x160的资源容器
。由于它们大小为0x160字节,在此步骤中分配的资源容器
在内存中位于上一步骤的0x160字节数据缓冲区附近。出于这个原因,这些资源容器
被称为“相邻”资源容器
。下面将解释这些“相邻”资源容器
的目的。
- 释放所有剩余着色器
,释放大小为0x158的块。这些大小为0x158的hole
将与大小为0x150的资源容器
交替。
在强调漏洞利用的一般结构之前,先介绍触发漏洞的WriteOOB
函数。在整个漏洞利用期间,为了不同的目的我们需要多次调用WriteOOB
,例如泄漏vmware-vmx.exe
和kernel32.dll
基址,以及最终的代码执行步骤。函数的参数如下:
WriteOOB()(void * data, size_t data_size, uint32_t offset)
data
参数是一个指向缓冲区的指针,该缓冲区包含我们打算写入主机堆栈的数据。size
参数指定数据的长度。最后,offset
参数指定要写入数据的位置,表示相对被损坏的资源容器
头的偏移。
该函数首先分配和初始化帧列表和五个TD结构。回想一下,在堆布局过程中,我们创建了大小为0x158的hole
。此函数发送五个TD结构,因此堆上分配的缓冲区大小为5*0x40+0x18=0x158
。我们希望这能够分配在hole
上,这样,紧随TD之后,可以破坏一个资源容器
。
除了最后一个TD(终止TD)之外,每个TD结构使用link
字段链接到下一个TD结构。对于前三个TD结构,MaxLen
子字段设置为0x40。前三个TD结构的分组ID子字段设置为USB_PID_SOF
,因此对于每个TD结构,光标将前进0x41。第四TD结构的分组ID也设置为USB_PID_SOF
,但是对于该TD,MaxLen
设置为从offset
参数计算的值。这使光标成为了一个可控量。在第五TD中,分组ID设置为USB_PID_OUT
,以便将data
缓冲器的内容写入光标位置。
现在,已经准备好了漏洞原语,接下来我们就要泄漏vmware-vmx.exe
的基地址。我们通过在TD之后破坏资源容器
中数据缓冲区的指针来完成的。该指针位于资源容器
内的偏移0x138处。该漏洞通过将其替换为0x00来破坏数据指针的最低有效字节。当引用损坏的指针时,它不再指向数据缓冲区。相反,它指向位于数据缓冲区附近的0x160字节的“相邻”资源容器
。这些“相邻”资源容器
中有一些函数指针,当数据被复制回guest虚拟机时,将得到vmware-vmx.exe
基地址:
为精确控制数据指针,我们需要计算移动的光标字节数:
资源容器
的堆头占用0x8个字节。资源容器
中数据指针的偏移量为0x138。因此,总和为0x140 + 0x8 + 0x138 = 0x280,这是光标必须移动的字节数,指向我们打算填写的字节。
为了将泄漏的函数指针写回到guest虚拟机,exp迭代喷射了2400次surface,并利用SVGA_3D_CMD_SURFACE_COPY
获取每个的数据。它继续迭代,直到找到能够泄露vmware-vmx.exe
基址的函数指针。
找kernel32.dll
基址,利用过程和偏移计算与vmware-vmx.exe
是一样的,除了一个小细节。它不是填充单个字节指针,而是利用vmware_vmx_base_address+0x7D42D8
覆盖整个数据指针,即Kernel32!MultiByteToWideCharStub
在导入地址表中的存储地址,这可以泄露出kernel32.dll
基地址。
为了实现代码执行,exp再次覆盖堆上的资源容器
。这次,漏洞会覆盖资源容器
的0x120字节。这完成了三件事:
1 - 将字符串calc.exe
写入资源容器
。
2 - 填写资源容器
的某些必要字段。
3 - 覆盖资源容器
中偏移量0x120的函数指针,使它指向kernel32!WinExec
。
这是资源容器
在被破坏后的样子:
最后当guest主机调用SVGA_3D_CMD_SURFACE_COPY
访问此资源容器
时,WinExec
函数将被调用,calc.exe
字符串的地址作为第一个参数传递。该漏洞必须遍历所有2400个surface,以确保使用到被破坏的资源容器
。
查看上述材料,漏洞利用可以总结如下:
- 堆布局:
---分配大小为0x158的2400个着色器
。
---释放大小为0x158的备用着色器
。
---对于每个被释放的着色器
,使用大小为0x150的资源容器
(例如,surface)填充hole。在此资源容器
中,将有一个指向大小为0x160的关联数据缓冲区的指针。另外还要创建两个着色器
,分配两个大小为0x160且与数据缓冲区相邻的资源容器
。
- 泄漏vmware-vmx.exe基地址(迭代64次,直到找到地址):
---调用WriteOOB
破坏大小为0x150的资源容器
并将指针的最低有效字节填写到其数据缓冲区,以便它指向相邻的0x160字节资源容器
。该内存包含一些函数指针。
---遍历2400个surface并使用SVGA_3D_CMD_SURFACE_COPY
将数据传回到guest主机,直到泄漏出地址。
- 泄漏kernel32.dll基地址(迭代64次,直到找到地址):
---调用WriteOOB
破坏大小为0x150的资源容器
,并将VMWare-vmx.exe中kernel32.dll
导入表中的函数地址填充到其数据缓冲区的指针。
---遍历2400个surface并使用SVGA_3D_CMD_SURFACE_COPY
将数据传回到guest主机,直到泄漏出地址。
- 逃离guest主机并在宿主机获得代码执行(迭代64次,直到代码执行):
---调用WriteOOB
以破坏大小为0x150的资源容器
。填充“calc.exe”字符串并使用kernel32!WinExec
地址填充到函数指针位置。
--- 通过SVGA_3D_CMD_SURFACE_COPY
迭代访问2400个surface,直到触发WinExec
的执行。
基于特定的内存损坏错误,可有效实现VMware虚拟机逃逸。该漏洞利用通过半暴力的方式实现代码执行。在VMware中发现可利用的漏洞仍然是一个挑战,但一旦发现漏洞,它也不会太过难以利用。VMware SVGA提供了各种操作和对象,例如资源容器
和着色器
。通过调整它们的大小以及存储的数据和函数指针,可以有效的辅助漏洞利用工作。
您可以在Twitter上找到我@ 0xAlsaheel,并关注ZDI 团队获取最新的漏洞利用技术和安全补丁。
</center>