CVE-2023-20869/20870: EXPLOITING VMWARE WORKSTATION AT PWN2OWN VANCOUVER
This post covers an exploit chain demonstrated by Nguyễn Hoàng Thạch (@hi_im_d4rkn3ss) of STAR Labs SG Pte. Ltd. during the Pwn2Own Vancouver event in 2023. During the contest, he used an uninitialized variable bug and a stack-based buffer overflow in VMware to escalate from a guest OS to execute code on the underlying hypervisor. His successful demonstration earned him $80,000 and 8 points towards Master of Pwn. All Pwn2Own entries are accompanied by a full whitepaper describing the vulnerabilities being used and how they were exploited. The following blog is an excerpt from that whitepaper detailing CVE-2023-20869 and CVE-2023-20870 with minimal modifications.
Prior to being patched by VMware, a pair of vulnerabilities existed within the implementation of the virtual Bluetooth USB device inside VMware Workstation. During the event, the VMware version used was 17.0.1 build-21139696. An attacker could leverage these two bugs together to execute arbitrary code in the context of the hypervisor. To exploit this vulnerability, the attacker must have the ability to execute high-privileged code on the guest OS. Bluetooth functionality is also required, but this is enabled by default. These bugs were patched in late April with VMSA-2023-0008.
CVE-2023-20870 – The Uninitialized Variable Info Leak
In VMware Workstation, in the USB Controller setting, there is the “Share Bluetooth devices with the virtual machine” option. This is enabled by default. It allows guest OSes to use Bluetooth devices. This functionality is handled by the Vbluetooth component, which is implemented in the vmware-vmx.exe binary. The VBluetooth device information can be read by lsusb
command (in Linux) as follows:
$ lsusb -t | |
/: Bus 02.Port 1: Dev 1, Class=root_hub, Driver=uhci_hcd/2p, 12M | |
|__ Port 1: Dev 2, If 0, Class=Human Interface Device, Driver=usbhid, 12M | |
|__ Port 2: Dev 3, If 0, Class=Hub, Driver=hub/7p, 12M | |
|__ Port 1: Dev 4, If 0, Class=Wireless, Driver=, 12M | |
|__ Port 1: Dev 4, If 1, Class=Wireless, Driver=, 12M | |
/: Bus 01.Port 1: Dev 1, Class=root_hub, Driver=ehci-pci/6p, 480M |
Each time a guest OS sends a USB Request Block (URB) request to the VBluetooth device, the function VUsbBluetooth_OpNewUrb()
is invoked to allocate memory to read or write data. The following code snippet is the sub_140740EB0
function in vmware-vmx.exe:
VUsbURB *__fastcall VUsbBluetooth_OpNewUrb(VUsbDevice_Bluetooth *dev, unsigned int num_pkts, unsigned int num_bytes) | |
{ | |
vbt_urb_wrapper = UtilSafeMalloc1(12i64 * num_pkts + 160); | |
vbt_urb_wrapper->urb.global_buffer = &unk_14132C238; | |
v6 = VBluetoothHCI_RBufNew(dev->add.hci, num_bytes); | |
vbt_urb_wrapper->rbuf = v6; | |
vbt_urb_wrapper->urb.urb_data = RBuf_MutableData(v6); // [1] | |
return &vbt_urb_wrapper->urb; | |
} |
This function returns a VUsbURB
object. Data is stored in this object in the urb_data
buffer. This buffer is allocated by the RBuf_New()
function and assigned to urb_data
at [1]. Note that the RBuf_New()
function calls the malloc
function to allocate memory, so the memory is uninitialized. Then, the URB data is handled by VUsbBluetooth_OpSubmitUrb()
function. This code snippet is from the sub_140740F50
function in vmware-vmx.exe:
__int64 __fastcall VUsbBluetooth_OpSubmitUrb(VUsbURB *urb) | |
{ | |
VUsbPipe = urb->VUsbPipe; | |
total_urb_len = urb->total_urb_len; | |
rbuf = *(urb – 1); | |
urb_data = urb->urb_data; | |
VUsbDevice = VUsbPipe->VUsbDevice; | |
VUSBVirtualIF = VUsbDevice[1].VUSBVirtualIF; | |
urb->status = 0; | |
urb->urb_actualsize = total_urb_len; // [2] | |
endpt = pipe->endpt; | |
if ( endpt ) | |
{ | |
/* … */ | |
} | |
/* process CTRL endpoint */ | |
if ( (urb_data->bmRequestType & VUSB_REQ_MASK) == VUSB_REQ_CLASS ) | |
{ | |
/* … */ | |
} | |
if ( VUsbDevice_OpSubmitNonReqCtl(urb) ) | |
return (gUsblibClientCb->VUsb_CompleteUrbAndContinue)(urb); | |
if ( (urb_data->bmRequestType & VUSB_REQ_MASK) != VUSB_REQ_STANDARD ) | |
goto LABEL_24; | |
bRequest = urb_data->bRequest; | |
if ( bRequest == VUSB_REQ_SET_CONFIGURATION ) | |
{ | |
/* … */ | |
} | |
if ( bRequest != VUSB_REQ_SET_INTERFACE ) // [3] | |
{ | |
LABEL_24: | |
urb->status = 4; | |
return (gUsblibClientCb->VUsb_CompleteUrbAndContinue)(urb); | |
} | |
v16 = urb_data->wIndex; | |
if ( !v16 ) | |
{ | |
if ( urb_data->wValue ) | |
urb->status = 3; | |
return (gUsblibClientCb->VUsb_CompleteUrbAndContinue)(urb); | |
} | |
if ( v16 != 1 || urb_data->wValue >= 6u ) | |
LABEL_23: | |
urb->status = 3; | |
return (gUsblibClientCb->VUsb_CompleteUrbAndContinue)(urb); | |
} |
total_urb_len
is the length of data the guest OS wants to read from or write to the URB. This value is controllable by the attacker. At [2], total_urb_len
is assigned to urb->urb_actualsize
without a check. Then, based on the endpoint type and URB packet, the corresponding function is invoked. Afterwards, urb->urb_actualsize
is set again in the handler function, but only if the packet is valid. We can see in the VUsbBluetooth_OpSubmitUrb()
function that if the urb_data->bRequest
is an invalid opcode (checked at [3]), urb->urb_actualsize
will not be set, and it will remain set to an attacker-controllable value.
Finally, the UHCI_UrbResponse()
function is invoked to send data back to the guest. The following snippet is in the sub_1401F7C50
function in vmware-vmx.exe, corresponding to assembly code from address 0x1401F7CBF
:
char __fastcall UHCI_UrbResponse(UHCI_Controller *ctl, VUsbURB *urb) | |
{ | |
/* … */ | |
td_token = td->td.token; | |
td_len = ((td_token >> 21) + 1) & 0x7FF; | |
urb_actualsize = td_len; | |
if ( td_len > urb->urb_actualsize ) | |
urb_actualsize = urb->urb_actualsize; | |
if ( urb_actualsize ) | |
{ | |
if ( td_token == USB_TOKEN_PID_IN ) | |
{ | |
phys_buffer = td->td.phys_buffer; | |
if ( !td->td.phys_buffer | |
|| !PhysMem_ValidateAndCopyToMemory(phys_buffer, urb->urb_data_cursor, urb_actualsize, 0, 6u) ) | |
{ | |
Warning(“UHCI: Bad %s pointer %#I64x\n“, “TDBuf“, phys_buffer); | |
hc->guestSts = ERROR_BAD_PTR; | |
} | |
} | |
} | |
/* … */ | |
} |
A maximum of urb->urb_actualsize
bytes of data from the urb->urb_data
buffer will be returned to the guest. Since an attacker could control the value of urb->urb_actualsize
and the urb->urb_data
buffer is uninitialized, the guest OS could read uninitialized data from the heap.
CVE-2023-20869 – The Stack-based Overflow
The VBluetooth device also implements an Service Discovery Protocol (SDP) feature. When a guest OS wants to send an SDP packet to a specific Bluetooth peer, it must initialize a L2CAP connection to this peer. This is done by sending an L2CAP_CMD_CONN_REQ
packet to the L2CAP_SIGNALLING_CID
channel with the Protocol/Service Multiplexer (PSM) field set to 0x1. The result is a newly created SDP socket. This socket is used when processing subsequent SDP operations.
The SDP protocol data unit (PDU) format is well explained here. When the host OS processes an SDP PDU from the guest, it invokes SDPData_ReadElement()
to parse the PDU. Here’s a look at the SDPData_ReadElement()
function from the sub_14083C1D0
function in vmware-vmx.exe:
char __fastcall SDPData_ReadElement(RBuf **in_rbuf, int type, SDPData_Element *ele) | |
{ | |
v3 = *in_rbuf; | |
addition_size_desc = 1; | |
if ( !RBuf_CopyOutHeader(*in_rbuf, &v20, 1ui64) ) | |
return 0; | |
switch ( v20 & 7 ) // [1] | |
{ | |
case 0: | |
ele_size = (v20 & 0xF8) != 0; | |
break; | |
case 1: | |
ele_size = 2; | |
break; | |
case 2: | |
ele_size = 4; | |
break; | |
case 3: | |
ele_size = 8; | |
break; | |
case 4: | |
ele_size = 16; | |
break; | |
case 5: | |
addition_size_desc = 2; | |
if ( !RBuf_CopyOutHeader(v3, &v20, 2ui64) ) | |
return 0; | |
ele_size = v21; | |
break; | |
case 6: | |
addition_size_desc = 3; | |
if ( !RBuf_CopyOutHeader(v3, &v20, 3ui64) ) | |
return 0; | |
ele_size = __ROL2__(v21, 8); | |
break; | |
case 7: | |
addition_size_desc = 5; | |
if ( !RBuf_CopyOutHeader(v3, &v20, 5ui64) ) | |
return 0; | |
ele_size = ((v21 & 0xFF00 | (v21 << 16)) << 8) | ((HIWORD(v21) | v21 & 0xFF0000) >> 8); | |
break; | |
} | |
ele_type = v20 >> 3; | |
if ( !SDPData_Slice(in_rbuf, addition_size_desc) || type != –1 && ele_type != type ) | |
return 0; | |
if ( ele_size > RBuf_Length(*in_rbuf) ) | |
return 0; | |
ele->ele_type = ele_type; | |
ele->ele_size = ele_size; | |
switch ( ele_type ) | |
{ | |
case SDP_DE_NULL: | |
_mm_lfence(); | |
return ele_size == 0; | |
case SDP_DE_UINT: | |
_mm_lfence(); | |
return SDPData_ReadRawInt(in_rbuf, ele_size, &ele->ele_data, &ele->field_10); // [2] | |
/* … */ | |
} |
The switch-case [1] is used to parse the data element size descriptor to determine the size of the raw data. Then this size is passed to SDPData_ReadRawInt()
to parse the unsigned int at [2].
Here’s another code snippet. This one is from the sub_14083C570
function in vmware-vmx.exe. Since the PDU is submitted by the guest OS, an attacker can control the size
argument, which leads to a possible stack buffer overflow at [3]/[4].
char __fastcall SDPData_ReadRawInt(RBuf **in_rbuf, unsigned int size, _QWORD *a3, _QWORD *a4) | |
{ | |
v4 = size; | |
result = RBuf_CopyOutHeader(*in_rbuf, Src, size); // [3] | |
if ( result ) | |
{ | |
memcpy(&Src[-v4], Src, v4); // [4] | |
*a3 = 0i64; | |
if ( a4 ) | |
*a4 = 0i64; | |
return SDPData_Slice(in_rbuf, v4); | |
} | |
return result; | |
} |
These bugs were combined at Pwn2Own Vancouver to pop calc on the target system. The exploit itself started in the guest OS while the calculator spawned on the host OS.
Thanks again to Nguyễn Hoàng Thạch for providing this write-up and for his participation in Pwn2Own Vancouver. He has participated in multiple Pwn2Own contests, and we certainly hope to see more submissions from him in the future. Until then, follow the team on Twitter, Mastodon, LinkedIn, or Instagram for the latest in exploit techniques and security patches.
原文始发于ZDI Research Team:CVE-2023-20869/20870: EXPLOITING VMWARE WORKSTATION AT PWN2OWN VANCOUVER
转载请注明:CVE-2023-20869/20870: EXPLOITING VMWARE WORKSTATION AT PWN2OWN VANCOUVER | CTF导航