漏洞位于:hci/src/packet_fragmenter.cc
HCI 层
HCI 层位于蓝牙协议栈高层协议和低层协议之间,提供了对基带控制器和链路管理器的命令以及访问蓝牙硬件的统一接口方法,其接口适用于BR/EDR控制器、BR/EDR/LE控制器、LE控制器、AMP控制器,与底层的结构关系如下图:
HCI 包格式
HCI通过包的方式来传送数据、命令和事件的,所有在主机和主机控制器之间的通信都以包的形式进行。包括每个命令的返回参数都通过特定的事件包来传输。HCI有数据、命令和事件三种类型的包。命令包COMMAND(0x01)只能从主机发往主机控制器,其中数据包是双向的,分为两类:ACL(0x02)、SCO(0x03),而事件包EVENT(0x04)始终是主机控制器发向主机的。主机发出的大多数命令包都会触发主机控制器产生相应的事件包作为响应,在传输过程中会有一个句柄,用于识别主机之间的逻辑通道和控制器,共有三种类型的句柄:连接句柄、逻辑链路句柄和物理链路句柄。
根据需要,这里只介绍ACL数据包格式,ACL 数据用于主机和控制器之间的非同步数据交换,如播放音乐数据的数据包,格式如下图:
字段说明:
字段 | 说明 |
---|---|
Handle | Connection_Handle用于在主控制器上传输数据包或段。 |
PB Flag | 包边界和适应范围。 |
BC Flag | 广播标志。 |
Data Total Length | 以八位位组为单位的数据长度,包含高层协议data。 |
其中 PB FLAGS 是我们要注意的 ,设置为 00’b 的时候,代表 Host -> Contoller 的 L2CAP 的首包。设置为 01’b 的时候,代表 Host -> Contoller 或者 Contoller -> Host 的 L2CAP 的续包(中间的)。设置为 10’b 的时候,代表 Contoller -> Host 的 L2CAP 的首包。
字段 | 表示 |
---|---|
00’b -> 0 | Host -> Contoller 的 L2CAP 的首包 |
01’b -> 1 | Host -> Contoller 或者 Contoller -> Host 的 L2CAP 的续包 |
10’b -> 2 | 设置为 10’b 的时候,代表 Contoller -> Host 的 L2CAP 的首包。 |
L2CAP 数据包格式
上面提到了 L2CAP ,那么什么是L2CAP呢?
分段(Fragmentation)和重组(Reassembly )
这里需要提及一下 分段与重组
分段是将PDU分解成较小的部分,以便从L2CAP传递到较低层。重组是根据从下层传递来的片段重组PDU的过程。分段和重组可以应用于任何L2CAP PDU。
我们可以简单把 L2CAP 当成 HCI data payload
数据包格式
L2CAP是基于分组的,但也遵循信道传输的通信模型。L2CAP支持的信道有两种:面向连接的信道和面向无连接的信道。在面向连接的信道中,L2CAP数据包的格式如下图所示
数据包中每个字段的说明如下所示:
字段 | 说明 |
---|---|
Length | 2字节,表示信息有效负载的大小,不包括长度L2CAP头。 |
Channel ID**(**CID**)** | 2字节,用于标识目的信道的终端。通道ID的范围与正在发送数据包的设备相关。 |
Information**(**Payload**)** | 信息负载。长度为0到65535字节。 |
漏洞分析
CVE-2020-0022漏洞位于HCI层,漏洞补丁代码位于hci/src/packet_fragmenter.cc(以8.1.0_r33为例)中的reassemble_and_dispatch()函数中,该函数是用于数据包分片的重组。对于过长的ACL数据包需要进行包的重组,主要是根据ACL包中的PB Flag标志位进行重组,如果当前是起始部分并且是不完整的,则生成一个部分包(partial_packet)放到map里,等下次收到它的后续部分进行拼装,拼装完毕后就分发出去。详细分析reassemble_and_dispatch()函数如下:
Patch
clone 一份代码:
1 | git clone https://android.googlesource.com/platform/system/bt |
咱们先看 一眼 patch:
1 | diff --git a/hci/src/packet_fragmenter.cc b/hci/src/packet_fragmenter.cc |
将 packet->len 的值在原本的基础上加上了 packet->offset
code review
代码处理逻辑在 reassemble_and_dispatch
函数里:
首先读取相关变量, handle 和 acl_length :
handle 相当于链接 seesion
acl_length 则为 Data Total Length 这是包括 header + data
131 行校验 长度是否合法
133 行 通过 handle 读取 boundary_flag ,就是前文提及的 PB flag
136 行 判断 PB flag 是否为 2 ,即 是否为 Contoller -> Host 的 L2CAP 的首包,如是则进入 if 条件分支
145 的分支则判断 packet 是否已经被处理过
156 行则判断 acl_length 是否正常
然后紧接着计算 full_length ,
1 | uint16_t full_length = |
L2cap_length 也是从 stream 读取的
上面提及了 acl_length为Data Total Length,该data数据域中存放着L2CAP数据包分片(也可能是一个完整的L2CAP数据包) 则 l2cap_length 是一个完整的L2CAP数据包中payload的长度。
所以 full_length = 完整的L2CAP数据包中的payload的长度 + 一个L2CAP头部长度和一个HCI头部长度
168 行 又是一次数据大小检查
177 行开始,通过检查大小 判断当前 packet 是否还有后续的包,如果没有就直接 callbacks->reassembled(packet);
并返回
如果有后续包,则开始进一步处理
分配一块内存,用来 packet 数据重组
设置一些变量,partial_packet->event 、 partial_packet->len、partial_packet->offset
其中:
partial_packet->len = full_length;
以及partial_packet->offset = packet->len;
将partial_packet->len设置为full_length,将partial_packet->offset设置为packet->len即当前头包packet->data的长度
调用memcpy,将头包packet中HCI数据包整体拷贝到partial_packet中
更新acl_length为一个完整的L2CAP数据包长度
200 行 将partial_packet存放到容器中
最后 释放当前头包packet,表示已经处理完第一个packet
当下一个包过来,且不是首包的时候,就会进入到
204 行这个分支
206 行,判断通过 handle 判断当前包是否是一个链路里的,否则 drop
215 行, 设置 packet->offset 等于 HCI_ACL_PRESMBLE_SIXE 即等于4 ,此时 packet->offset 就指向了 data 域
216 行, 开始计算 projected_offset ,等于 partial_packet->offset 与 (packet->len - HCI_ACL_PREAMBLE_SIZE) 之和:
此时
- projected_offset为partial_packet->offset
- (packet->len - HCI_ACL_PREAMBLE_SIZE) 为 L2CAP数据包分片
则,projected_offset 为 partial_packet->offset + L2CAP数据包分片
接着就是重要的地方了,判断 projected_offset 是否大于 partial_packet->len
projected_offset 是刚才求和的结果 , partial_packet->len 则 full_lenght
如果 projected_offset 大于 partial_packet->len 则进行一次减法运算:
packet->len = partial_packet->len - partial_packet->offset;
projected_offset = partial_packet->len;
重新设置 packet->len 与 projected_offset,packet->len为partial_packet剩余空间的长度。然后,将projected_offset设置为partial_packet->len
如果此时 packet->len
的结果是一个小的数,那么在
228 行进行拷贝的时候,packet->len - packet->offset 就可能为一个负数,当size 为负数,由于memcpy 的参数是无符号的,所以会被强制转换为一个大数,然后造成堆溢出。
PoC 构建
PoC 链接可以看着: https://gist.github.com/leommxj/c9ba42e54faa9629cff5db3b4daeccef
- 我们要构建 packet->len = partial_packet->len - partial_packet->offset; 为一个小数,或者干脆就是一个负数
通过前文分析,我们知道 packet->len 等于 partial_packet->len - partial_packet->offset 相减,partial_packet->len 为 full_length ->
1 | full_length = |
partial_packet->offset 为:
1 | partial_packet->offset = packet->len; |
1 | printf("\nCreating a HCI BLE connection...\n"); |
首先,整个l2cap 长度为 : datalen + header 即 30 + 4
1 | full_length = |
那么此时 full_length 就等于 34 + 4 + 4 等于 42
1 | packet->offset = HCI_ACL_PREAMBLE_SIZE; |
此时 projected_offset = 36 + (11 - 4)
36 是由 12 + 12 + 12 当最后一个包的时候 partial_packet->offset 就为 36
那么此时 projected_offset = 43
当 42-43 的时候为负数 则 packet->len 等于 -1
参考链接
https://mp.weixin.qq.com/s/MgttHkorVd5UrW1Cnlc5Xw
https://stackoverflow.com/questions/60116790/sending-gap-acl-l2cap-data-packets
https://insinuator.net/2020/02/critical-bluetooth-vulnerability-in-android-cve-2020-0022/