CVE-2020-0022
参考连接:
CVE-2020-0022 蓝牙漏洞初探(上)一个bug引发的血案-安全客 - 安全资讯平台 (anquanke.com)
CVE-2020-0022 “BlueFrag”漏洞分析 (bestwing.me)
Diff - 3cb7149d8fed2d7d77ceaa95bf845224c4db3baf^! - platform/system/bt - Git at Google (googlesource.com)
个人学习记录,参考很多前辈的文章。如果有错误,请指出,我将不胜感激。
一、 环境搭建
受害机器:Nexus 5X + Android 7.1.2
攻击机器:Ubuntu + 外接蓝牙适配器
二、 漏洞分析
HCI与L2CAP介绍
在手机、笔记本电脑等设备中,蓝牙的结构一般是分为host和controller,controller也就是蓝牙芯片。两个设备之间通过hci层来进行数据的传递。如下图:
CVE-2020-0022漏洞位于HCI层,漏洞补丁代码位于hci/src/packet_fragmenter.cc中的reassemble_and_dispatch()函数中,该函数是用于数据包分片的重组。
L2CAP层是HCI层的上层,可以将L2CAP协议类比为网络中的TCP/UDP协议,实现了更为完善的数据传输功能。
- 在发送路径上,上不同的上层协议层传输给L2CAP的数据统称为SDU(Service Data Unit)。SDU的最大长度可达65536字节,当较大的SDU其进入L2CAP层后,则会触发分段(Segmentation),得到多个较小的数据片段,L2CAP层对每个片段添加L2CAP协议头,封装为PDU数据包并传递至HCI层。当单个PDU进入HCI层,若其大小超过HCI层的限定,则触发分解(Fragmentation),再对得到的每块碎片数据分别添加HCI头,并依次发送至蓝牙控制器。
- 在接收路径上,相应地,对于蓝牙控制器收到的多个HCI数据包,HCI层需要对其进行重组(与分解对应),得到PDU传递给L2CAP层;类似地,L2CAP层需要对多个PDU进行重组(与分段对应),得到SDU传递给上层协议层。
- 在蓝牙核心规范中,HCI ACL数据包的格式如下
其中,PB Flag是Packet Boundary Flag的缩写,该字段占用两个bit:
当其数值为00或10时,表征当前HCI包是上层L2CAP PDU分解所得碎片中的首包;
当其数值为01时,表征当前HCI包是上层L2CAP PDU分解所得碎片中的续包;
当其值为11时,表征其封装了一个完整的上层L2CAP PDU。
在蓝牙核心规范中,普通有连接的L2CAP 包的格式如下 (参考L2CAP数据包)
注:Length字段不包括Basic L2CAP header的长度
漏洞代码分析
- CVE-2020-0022漏洞位于HCI层,漏洞补丁代码位于hci/src/packet_fragmenter.cc(以7.1.2_r1为例)packet_fragmenter.c - Android Code Search 中的reassemble_and_dispatch()函数中,该函数是用于数据包分片的重组。对于过长的ACL数据包需要进行包的重组。
- 首先该函数从stream中读取一些信息并且设置了一些变量:
- l2cap_length:整个l2cap数据包的数据部分的长度
- acl_length:整个数据包的长度
- HCI_ACL_PREAMBLE_SIZE:是固定值4字节
- boundary_flag:是从数据包中读取了分段标记,PB的值
- PB FLAGS == 00(bit) 时,代表 Host -> Contoller 的 L2CAP 的首包
- PB FLAGS == 10(bit) 时,代表 Contoller -> Host 的 L2CAP 的首包
- PB FLAGS == 01(bit) 时,代表 Host -> Contoller 或者 Contoller -> Host 的 L2CAP 的续包
- 当读取道boundary_flag是10(10是二进制,也就是十进制的2)的时候,进入首包的处理逻辑:
- 需要注意full_length代表的是组装完全的数据包的长度,可能传入的时候是分包的,第一个包的长度不等于full_length
- 需要注意full_length代表的是组装完全的数据包的长度,可能传入的时候是分包的,第一个包的长度不等于full_length
- 接下来有一些错误处理掠过,如果条件全部满足,则会创建一个partial_packet来处理数据包:
- 数据包拼接的结果就会放在这个partial_packet中,所以他的长度已经被提前设置为full_length了,拼接全部完成之后这个partial_packet的长度就会等于full_length
- 其中还要关注offset,offset就是偏移的意思,就是标记数据包处理到了哪个位置
- 如果是分包中的首包的话,就直接拼接道partial_packet就可以了,后续的包的处理就是把data部分截出来,拼接道partial_packet中。这里这个memcpy就是首包的拼接。
帮助理解的图
- 后续包的处理逻辑 (漏洞位置):
- 首先关注projected_offset的计算是 partial_packet已经处理到的位置 + 目前这个包的数据部分。实际上projected_offset就等于拼接上现在这个包的数据部分后,这个partial_packet的长度。
- 根据partial_packet的长度的定义可以知道,正常来说,全部拼接完成之后,长度也不会超过一开始设置的full_length,也就是partial_packet->len。
- 所以下方有一个逻辑判断,projected_offset有没有大于partial_packet->len,如果大于了,就意味着拼接完的包,比最开始说好的长度要长,那么肯定是出了问题。
- 下面的处理逻辑就是将目前packet的长度修正,将多余的部分减去。将projected_offset也修正,让他等于全部拼接完成后的数据包的长度。
- 后面再开始本次数据包的拼接,然而问题在于packet->len - packet->offset这里。packet->offset是常量4,那么如果packet->len的值小于4就会出现负数的情况,memcpy函数中没有负数的概念,-1-2这种负数由于补码就会变成极大值,最终导致溢出。漏洞就出现在这里。
- 再去查看上面packet->len的修复逻辑是,partial_packet->len - partial_packet->offset,实际上等于下图的橙色部分。只要让这一部分小于4就会产生漏洞。也就是让发送的数据包正好需要分包。比如说1024个字节之后需要分包,那么就发送1026个字节,分包的同时也可以让橙色部分等于2
三、 漏洞复现
leommxj/cve-2020-0022: poc for cve-2020-0022 (github.com)