目录
-
[1.0 术语] -
[2.0 实用链接] -
[3.0 简介] -
[4.0 漏洞] -
4.1 修复 -
[4.2 可利用性] -
[4.3 影响]
[1.0] 术语
让我们从一些术语开始:
-
BLE = 蓝牙低功耗 -
HCI / 主机 / 控制器 – HCI代表主机控制器接口,这是主机(在这种情况下,Windows上的蓝牙组件)和控制器(带有固件的蓝牙设备)通信的方式。 -
适配器 = 带有其固件的物理蓝牙设备。 -
本地设备 / 控制器 / 适配器 = 易受攻击的设备,属于被攻击者攻击的目标。 -
远程设备 / 控制器 / 适配器 = 攻击者控制的设备,以触发漏洞。 -
PDU = 协议数据单元,这是作为数据包一部分传输的一些数据。你可以将PDU视为蓝牙数据包的“内容”部分,具有与数据包类型相对应的特定格式。
[2.0] 实用链接
-
蓝牙核心规范 v5.2 (PDF) – 你一辈子都读不完,但它是所有蓝牙事物的真理之源。 -
蓝牙低功耗 (Wikipedia) – 提供了这项技术的很好概述,尽管我们只需要知道其中的一小部分。 -
蓝牙低功耗广播 (Medium帖子) – 一篇博客文章,详细介绍了BLE广播。我在下面提供了摘要,但如果你感兴趣,这篇文章提供了一些额外的上下文。 -
微软快速配对 – 快速配对是一个相当不为人知Windows功能(至少我之前不知道),但在稍后的第一个漏洞的上下文中很重要。
[3.0] 简介
在我们深入漏洞之前,让我们先介绍BLE和广播。如果你熟悉它的工作原理,请随时跳到漏洞描述。
蓝牙低功耗 是蓝牙技术的一个子集,它针对的设备不需要在短时期内传输大量数据。BLE协议与经典蓝牙不同,不能互换使用,但设备可以同时支持两种风格。虽然经典蓝牙用于需要大量数据传输的事项,如流式传输音乐或传输文件,BLE则适用于不需要很多电力的更轻量级事项,如从智能手表实时传输心率数据,或通过信标发送信号。
兼容BLE的设备以多种不同的方式传输数据,但它们拥有的最基本功能之一是广播和扫描。设备可以使用广播来广播数据,出于多种不同的目的,但一般意图是让这种方式发送的数据包含有关设备的信息,主动扫描的设备可以使用这些信息来识别和分类广播设备。发送的信息通常包括设备名称、制造商ID、设备类型和功能,以及某些指示接收方设备是否可以连接等。简而言之,可以想象广播是设备向监听介绍的设备介绍自己。
广播信息以BLE数据包的形式发送,其中包含广播PDU。广播的生命周期由以下步骤定义:
-
想要发布一些信息的主机向控制器发送一组特定的HCI命令。这些命令旨在设置广播参数并启用或禁用广播。在外围设备的情况下,可能没有经典的主机控制器接口,广播参数可能在固件中硬编码。 -
想要监听广播的主机向控制器发送特定的HCI命令,表明应该启用扫描。控制器通常无论主机是否希望接收它们,都会被动地监听广播,此命令仅通知控制器将接收到的广播转发给主机。 -
当主机启用广播时,控制器开始定期以BLE数据包的形式广播广播,其中包含特定PDU,包含广播数据。 -
正在扫描广播的控制器接收数据包,并将广播数据以HCI事件的形式转发给主机。
在其生命周期中,广播数据以三个不同的步骤传输,每次以不同的格式。首先,当广播主机设置广播参数时,其中之一是广播数据。其次,当包含广播数据的BLE数据包在控制器之间传输时。第三,当接收控制器发送包含广播数据的HCI事件给主机时。我们实际上不关心前两个步骤中数据的格式,因为这与漏洞无关。重要的是要知道主机在扫描时接收广播数据的格式。
有两个HCI事件将广播数据传输给主机:LE Advertising Report 和 LE Extended Advertising Report。在这种情况下,报告代表从特定远程设备接收到的单个广播单元。扩展广播是“正常”广播的扩展,并在蓝牙5.0中引入,其主要目的是允许在单个广播报告中包含更多的广播数据。这些事件可以容纳来自不同设备的几个报告,因为控制器可以决定将它们批量在一起,避免在短时间内发送多个事件。这些事件传输的数据格式相似:
Num_Reports, -- 数据中的广播报告数量
Event_Type[i], -- 事件类型,用于指示设备是否可以连接,如果广播是定向的等
Address_Type[i], -- 广播设备地址的类型,可以是公共的、随机的等
Address[i], -- 广播设备的地址
Data_Length[i], -- 广播数据的长度
Data[i], -- 字节中的广播数据
RSSI[i] -- 接收数据包的信号强度
LE Advertising Report结构
Num_Reports, -- 同上
Event_Type[i], -- 同上
Address_Type[i], -- 同上
Address[i], -- 同上
Primary_PHY[i], -- 不重要
Secondary_PHY[i], -- 不重要
Advertising_SID[i], -- 不重要
TX_Power[i], -- 不重要
RSSI[i], -- 同上
Periodic_Advertising_Interval[i], -- 不重要
Direct_Address_Type[i], -- 如果广播是定向的,定向广播的地址类型
Direct_Address[i], -- 广播定向的地址
Data_Length[i], -- 同上
Data[i] -- 同上
LE Extended Advertising Report结构
扩展广播报告,虽然允许传输更多的数据,但也包括一些其他字段。这些主要与数据的物理传输有关,对我们来说并不重要,所以我没有解释。两个事件之间最大的区别在于它们可以包含的广播数据的最大长度:
-
传统LE广播报告最多可以包含31字节的广播数据,因为在使用传统广播时,单个PDU可以传输的广播数据限制为31字节。 -
扩展LE广播报告最多可以包含1650字节的广播数据。这是因为扩展广播PDU可以传输最多254字节的广播数据,并且可以将多个连续PDU链接成单个广播报告。然而,特定类型的广播有一些限制,我们将稍后看到。
广播数据本身的格式相当简单。它由一个或多个广播部分组成,每个部分都有特定的格式:
Length, -- 节中数据的长度,包括类型字段
Type, -- 节的类型,表示节包含哪种数据
Data -- 节中的数据
广播部分结构
例如,一个设备可以在广播数据部分中传输其名称。部分的类型将是 0x09(“完整本地名称”),数据的长度将是 ASCII 名称的长度(加上一个终止字符),数据本身将包含一个以空字符结尾的 ASCII 字符串。常见广播部分类型的列表可以在 这里 (PDF) 找到,但规范允许制造商特定的广播部分,其格式和内容可能保持不透明。
最后,了解存在哪些类型的广播事件以及广播报告可以携带的地址类型也是有用的。事件类型的目的是告诉广播接收方,广播设备是否可以连接或扫描,广播是否专门针对该接收方以及其他一些信息。事件类型可以是:
-
ADV_IND
– 广播设备可以连接和扫描,广播是非定向的。这种广播在外围设备中很常见,这些设备希望连接到任何中心设备,例如在与中心设备配对之前的蓝牙耳机。可以在这种类型的单个报告中发送的广播数据的最大长度为 251 字节。 -
ADV_DIRECT_IND
– 广播设备可以连接,广播是针对特定接收方的。重用前面的例子,这种广播将用于已与中心设备配对的蓝牙耳机 – 通过使用这种广播,耳机请求连接到那个特定设备。同样,单个报告中的数据最大量是 251 字节。 -
ADV_NONCONN_IND
– 广播设备既不可连接也不可扫描,广播是非定向的。这在信标中很常见,它们的唯一目的是向所有附近设备广播一些数据。单个报告中可以发送的广播数据的最大长度没有限制,即 1650 字节限制在这里适用。 -
ADV_SCAN_IND
– 与ADV_NONCONN_IND
相同,但设备允许被扫描。如果前面的例子中的信标只希望将信息传递给感兴趣的方,这会很有用 – 接收设备可以在接收到广播后请求扫描并接收实际信息。此类型报告不携带广播数据。 -
SCAN_RSP
– 表示此广播报告是对扫描请求的响应。在前面的例子中,如果一个可扫描设备被扫描,响应将以SCAN_RSP
广播报告的形式接收。同样,对单个报告中数据的最大长度没有限制。
至于地址,每个广播设备通常也会包含一个接收方应使用的地址,以便它们希望返回通信。地址由 6 个字节组成,可以是随机生成的或持久的(并假设是唯一的)。地址的类型可以是:
-
公共设备地址 – 这是设备的唯一 MAC 地址。 -
随机设备地址 – 这是一个临时随机地址。 -
公共身份地址 -
随机身份地址
目前我们需要知道的大致就是这些了。关于广播有很多要说的,但大部分与当前漏洞无关。如果你有兴趣了解本文未涉及的更多信息,可以在我上面链接的博客文章中找到更多信息。
[4.0] 漏洞
Windows 蓝牙堆栈非常复杂,跨越多个不同的驱动程序、服务和用户模式库。微软提供了架构的简要概述 这里。我将分享他们的表示:
![Windows 蓝牙设备攻击面分析 Windows 蓝牙设备攻击面分析](https://ctfiot.oss-cn-beijing.aliyuncs.com/uploads/2024/06/6-1719389305.png)
由于广播数据可以包含不同类型的信息,广播报告可能会在多个不同地方进行解析。为了避免在每个地方单独实现解析过程,微软使用一个静态库,该库链接到需要此功能的模块中。此库中涉及解析广播数据的两个函数是 BTHLELib_ADValidateEx
和 BthLeLib_ADValidateBasic
:
HRESULT BthLELib_ADValidateEx(const uint8_t* adv_data, uint16_t adv_data_size, uint8_t** out_sections, uint8_t* out_num_sections)
{
// 初步验证
uint8_t num_sections = 0;
HRESULT validation_res = BthLeLib_ADValidateBasic(adv_data, adv_data_size, &num_sections);
if (validation_res < 0) return validation_res;
if (num_sections == 0) return validation_res;
// 为广播部分数据分配数组
// 结构体 BTHLE_AD_SECTION { uint8_t size; uint8_t type; uint8_t data[0x151]; }
// sizeof(BTHLE_AD_SECTION) = 0x153
BTHLE_AD_SECTION* sections = BthLELibAllocatePoolEx(sizeof(BTHLE_AD_SECTION) * num_sections);
// 验证每个部分的数据并将数据复制到数组中
uint16_t section_offset = 0;
for (uint8_t i = 0; section_offset < adv_data_size; ++i)
{
BTHLE_AD_SECTION* section = sections[i];
uint8_t section_size = adv_data[section_offset];
uint8_t section_type = adv_data[section_offset + 1];
if (section_size == 0) break;
if (section_size + section_offset + 1 >= adv_data_size) return 0xC000000D;
// 将部分大小和部分类型写入输出部分
section->size = section_size;
section->type = section_type;
// 根据部分类型进行验证并根据输入部分数据写入输出部分数据
switch (section_type)
{
...
// 根据部分类型验证部分数据
// 这是通过调用 BthLELib_ADValidate??? 实现的,其中 ??? 表示部分类型
if (BthLELib_ADValidate???(...))
{
// 如果部分的验证失败,函数退出
ExFreePool(sections);
return 0xC000000D;
}
...
// 对于某些部分类型,更多的数据写入输出部分
// 例如厂商数据、签名数据部分...
case ... :
{
// 但通常情况下,数据是按原样复制到输出部分的
uint8_t section_data_offs = ...;
uint8_t section_data_size = ...;
memcpy(section->data, adv_data + section_offset + section_data_offs, section_data_size);
}
}
section_offset += section_size + 1;
}
*out_sections = sections;
*out_num_sections = num_sections;
return 0;
}
HRESULT BthLeLib_ADValidateBasic(const uint8_t* adv_data, uint16_t adv_data_size, uint8_t* out_num_sections)
{
uint16_t adv_data_offset = 0;
while (adv_data_offset < adv_data_size)
{
uint8_t section_size = adv_data[adv_data_offset];
if (section_size == 0) break;
if (section_size + adv_data_offset + 1 >= adv_data_size) return 0xC000000D;
/**** 这里存在溢出 ***/
++(*out_num_sections);
/**** 这里存在溢出 ***/
adv_data_offset += section_size + 1;
}
while (adv_data_offset < adv_data_size)
{
if (adv_data[adv_data_offset++] != 0) return 0xC0000000D;
}
return 0;
}
BTHLELib_ADValidateEx
是外部模块调用的函数,用于将 HCI 事件中接收的广播数据转换为更合适的格式。该函数接收一个指向原始广播数据的指针及其长度,以及指示输出广播部分数组和该数组长度的输出参数。在其开头,调用 BthLeLib_ADValidateBasic
,该函数确保每个广播部分具有正确的长度(即不超过数据末尾),同时计算数据中的总部分数。计算的计数然后被 BthLELib_ADValidateEx
用来为输出部分数组分配内存。函数的其余部分专注于解析特定广播部分并将数据从输入缓冲区复制到数组条目对应的部分中的适当格式。尽管一些输出数据是以更结构化的方式格式化的,但对于绝大多数部分类型,输入数据只是以原始形式复制到输出部分中。
漏洞位于广播部分计数的代码中。由于为此目的使用了 8 位无符号整数,如果数据中有超过 255 个部分,该变量的值将溢出。一旦发生这种情况,函数返回的计数值将低于数据中实际存在的部分数。为 sections
数组分配的内存量将低于预期,导致当个别部分的数据被复制到内存中时发生越界写入,这些内存应该属于部分数组。
触发漏洞的最简单的广播数据示例是具有 257 个“空”部分的广播数据,即每个部分是:
Length = 0x01
Type = 0x00, -- 0x00 is an invalid / reserved type
Data = []
在退出 BthLeLib_ADValidateBasic
后,num_sections
将等于 1
,为部分数组分配的内存量将为 0x153
字节。同时,BthLELib_ADValidateEx
中的循环将遍历所有 257 个部分,将每个部分的长度和类型复制到分配内存的末尾之外,即在循环的第二次迭代中,0x01 0x00
将被写入分配内存的末尾,在第三次迭代中相同的值将被写入偏移量 0x153
处的内存等。
[4.1] 修复
微软通过在 *out_num_sections
达到 255
时使 BthLeLib_ADValidateBasic
退出并返回错误来修复该漏洞。修复后的代码如下所示:
HRESULT BthLeLib_ADValidateBasic(const uint8_t* adv_data, uint16_t adv_data_size, uint8_t* out_num_sections)
{
uint16_t adv_data_offset = 0;
while (adv_data_offset < adv_data_size)
{
uint8_t section_size = adv_data[adv_data_offset];
if (section_size == 0) break;
/***** 修复 *****/
if (*out_num_sections == 255) return 0xC000000D;
/***** 修复 *****/
if (section_size + adv_data_offset + 1 >= adv_data_size) return 0xC000000D;
++(*out_num_sections);
adv_data_offset += section_size + 1;
}
while (adv_data_offset < adv_data_size)
{
if (adv_data[adv_data_offset++] != 0) return 0xC0000000D;
}
return 0;
}
虽然这防止了整数溢出,但不必要地增加了单个广播报告中可以包含的广播部分数量的限制。此类限制并未由标准强加,这只是使微软不符合标准。尽管在现实情况下,几乎不可能需要接近 255 个部分,因此这个限制可能不会产生多大影响。
[4.2] 可利用性
正如我们稍后会看到的,这个漏洞是非常可利用的。当然,我们假设攻击者完全控制广播数据,即它必须遵循预期格式,但其内容可以是任意的。
-
攻击者可以很好地控制分配的数据量,因为他们控制数据中的部分数量,从而控制 num_sections
的值。这意味着他们可以使分配落入几乎任何他们想要的堆桶中。 -
攻击者几乎可以完全控制将要越界写入的数据。限制包括写入必须从 0x153
的倍数开始,并且前两个字节必须表示部分的长度和类型。例如,某些部分(如“完整名称”部分)只是将输入缓冲区中的数据逐字复制到输出中的数组条目,这使得攻击者可以完全控制越界写入的数据。 -
在这种整数溢出的情况下,溢出导致输出缓冲区大小的计算错误,这种漏洞通常可能因为内存损坏“过于严重”而变得无用,即它必须遍历整个非溢出范围。然而,这里可以通过滥用个别部分验证来避免。一旦攻击者不希望在某些点之后损坏内存,他们可以提供一个特定类型部分验证失败的损坏部分。这将使代码提前退出,同时仍然保持内存损坏。
[4.3] 影响
这个漏洞函数存在于整个 Windows 蓝牙堆栈中的四个不同模块中:
-
(1) bthport.sys – 堆栈最底部的一个内核驱动程序 -
(2) Microsoft.Bluetooth.Service.dll – 蓝牙支持服务使用的模块 -
(3) Windows.Internal.Service.dll – 蓝牙支持服务使用的模块 -
(4) dafBth.dll – 设备关联服务使用的模块
该函数用于解析模块 (1)、(2) 和 (4) 中的远程广播数据。在模块 (3) 中,它用于解析从堆栈“上游”运行的本地程序发送的广播数据。因此,该漏洞可以用作 RCE 和 LPE 的载体。我们将在下面分别讨论这两种载体,因为它们的环境非常不同。
原文始发于微信公众号(3072):Windows 蓝牙设备攻击面分析