CVE-2017-1000251 是 Linux BlueZ 蓝牙协议栈中 L2CAP (Logical Link Control and Adaption Protocol) 模块曝出的 stack overflow 漏洞。由于 BlueZ 把 L2CAP 实现在了 Linux 内核中,所以触发该漏洞极易导致 Linux 系统崩溃。当然该漏洞也可以实现内核级的 RCE。
本文的分析环境
存在漏洞的操作系统使用镜像 ubuntu-16.04.1-desktop-amd64.iso 安装。该系统使用的内核版本如下:
$ uname -rv
4.4.0-31-generic #50-Ubuntu SMP Wed Jul 13 00:07:12 UTC 2016
执行如下命令可以得到该内核的源码:
wget https://launchpad.net/ubuntu/+archive/primary/+sourcefiles/linux/4.4.0-31.50/linux_4.4.0.orig.tar.gz https://launchpad.net/ubuntu/+archive/primary/+sourcefiles/linux/4.4.0-31.50/linux_4.4.0-31.50.diff.gz https://launchpad.net/ubuntu/+archive/primary/+sourcefiles/linux/4.4.0-31.50/linux_4.4.0-31.50.dsc
tar -xvf linux_4.4.0.orig.tar.gz
gzip -dk linux_4.4.0-31.50.diff.gz
cd linux-4.4
patch -p1 < ../linux_4.4.0-31.50.diff
背景知识
L2CAP 简介
L2CAP 在蓝牙世界中的地位相当于因特网中的 TCP/UDP 协议。它可以把逻辑链路复用为多个逻辑信道,同时在这些信道上为高层提供面向连接或无连接的数据传输以及数据分段重组服务。
L2CAP 连接的建立过程
L2CAP 连接的建立过程分 3 个阶段:
-
信息交换
该阶段涉及的数据包为
L2CAP_INFORMATION_REQ
与L2CAP_INFORMATION_RSP
。两个蓝牙设备在建立 L2CAP 连接时会先交换各自的信息。这些信息的类型可能如下: -
创建连接
该阶段涉及的数据包为
L2CAP_CONNECTION_REQ
与L2CAP_CONNECTION_RSP
。其中前者使用 PSM (Protocol/Service Multiplexer) 向远端说明了当前连接服务的上层协议,并说明本地为连接分配的 CID,即 SCID 字段;若远端设备同意连接,则后者也会说明远端设备为当前连接分配的 CID,即 DCID 字段。SCID-DCID pair 标识了一个 L2CAP 连接。 -
配置连接
- BLUETOOTH CORE SPECIFICATION Version 5.2 | Vol 3, Part A page 1122, 7.1 CONFIGURATION PROCESS
- BLUETOOTH CORE SPECIFICATION Version 5.2 | Vol 3, Part A page 1138, 7.10 SUPPORTING EXTENDED FLOW SPECIFICATION FOR BR/EDR AND BR/EDR/LE CONTROLLERS
- BLUETOOTH CORE SPECIFICATION Version 5.2 | Vol 3, Part A page 1095, 5.6 EXTENDED FLOW SPECIFICATION OPTION
连接创建后,蓝牙设备会立即对它做进一步的配置。该阶段涉及的数据包为
L2CAP_CONFIGURATION_REQ
与L2CAP_CONFIGURATION_RSP
。本文分析的漏洞就存在于这个阶段。蓝牙设备可能会多次协商配置连接的参数。响应
L2CAP_CONFIGURATION_REQ
的L2CAP_CONFIGURATION_RSP
也可能携带配置选项(Config 字段)。此时表示原先请求配置的数据可能不被设备接受。同时响应中携带的配置选项就是设备给出的可接受配置,即“你原先提供的配置我不支持,你看我提出的配置是否可以?”另外配置连接的流程有两种可能,一种是 Standard process 另一种是 Lockstep process。当两个蓝牙设备都支持 EFS (Extended Flow Specification) 时,将使用 Lockstep process 而非 Standard process。
漏洞分析
当本地设备接收到响应配置请求的 L2CAP_CONFIGURATION_RSP
时,内核将调用 l2cap_config_rsp()
处理它:
// net/bluetooth/l2cap_core.c#4129
static inline int l2cap_config_rsp(
struct l2cap_conn *conn,
struct l2cap_cmd_hdr *cmd, u16 cmd_len,
u8 *data) {
... ...
}
该函数的 conn
和 cmd
参数很好理解。前者指向的结构体描述了当前处理的 L2CAP_CONFIGURATION_RSP
packet 所属的 L2CAP 连接;后者指向的结构体存储了 L2CAP_CONFIGURATION_RSP
command header:
然后先说明 data
参数再说明 cmd_len
参数。data
参数指向的 buffer 存储了 L2CAP_CONFIGURATION_RSP
packet 的 data 字段,即下图的红框部分:
cmd_len
参数则表示上面红框中字段的总长度,等同于 command header 中的 Length 字段。l2cap_config_rsp()
仅使用该参数确定红框中可变长字段 Config 的长度 len
:
// net/bluetooth/l2cap_core.c#4133
struct l2cap_conf_rsp *rsp = (struct l2cap_conf_rsp *)data;
... ...
// net/bluetooth/l2cap_core.c#4136
int len = cmd_len - sizeof(*rsp);
... ...
// net/bluetooth/l2cap_core.c#4139
if (cmd_len < sizeof(*rsp))
return -EPROTO;
之后 l2cap_config_rsp()
会根据 L2CAP_CONFIGURATION_RSP
packet 包含的 Result 字段,分情况对它做进一步处理。Result 字段有如下几种情况:
本文所分析的 Linux 内核 (4.4.0-31.50) 仅对 0x0000 L2CAP_CONF_SUCCESS
、0x0004 L2CAP_CONF_PENDING
以及 0x0001 L2CAP_CONF_UNACCEPT
做了特殊的处理。对于其他 Result,内核将直接发送 L2CAP_DISCONNECTION_REQ
断开 L2CAP 连接:
// net/bluetooth/l2cap_core.c#4210
default:
l2cap_chan_set_err(chan, ECONNRESET);
__set_chan_timer(chan, L2CAP_DISC_REJ_TIMEOUT);
l2cap_send_disconn_req(chan, ECONNRESET);
goto done;
在处理 Result 为 L2CAP_CONF_PENDING
和 L2CAP_CONF_UNACCEPT
的包时,l2cap_config_rsp()
都可能创建一个 64 字节 buffer,并把它传入存在漏洞的函数 l2cap_parse_conf_rsp()
:
// net/bluetooth/l2cap_core.c#4159
case L2CAP_CONF_PENDING:
set_bit(CONF_REM_CONF_PEND, &chan->conf_state);
if (test_bit(CONF_LOC_CONF_PEND, &chan->conf_state)) {
char buf[64];
len = l2cap_parse_conf_rsp(chan, rsp->data, len,
buf, &result);
... ...
}
goto done;
case L2CAP_CONF_UNACCEPT:
if (chan->num_conf_rsp <= L2CAP_CONF_MAX_CONF_RSP) {
char req[64];
if (len > sizeof(req) - sizeof(struct l2cap_conf_req)) {
l2cap_send_disconn_req(chan, ECONNRESET);
goto done;
}
/* throw out any old stored conf requests */
result = L2CAP_CONF_SUCCESS;
len = l2cap_parse_conf_rsp(chan, rsp->data, len,
req, &result);
... ...
}
漏洞函数 l2cap_parse_conf_rsp()
的原型如下:
// net/bluetooth/l2cap_core.c#3508
static int l2cap_parse_conf_rsp(
struct l2cap_chan *chan, void *rsp, int len,
void *data, u16 *result) {
... ...
}
上面的 chan
参数比较好理解,它用于描述当前 L2CAP 连接使用的 channel。
rsp
参数指向的 buffer 存储了 L2CAP_CONFIGURATION_RSP
packet 的 Config 字段。len
参数则与 rsp
参数对应,说明了可变长 Config 字段的大小。
根据 l2cap_config_rsp()
两处调用 l2cap_parse_conf_rsp()
传入的参数可知,data
参数指向了大小为 64 字节的 buffer (char buf[64]
或 char req[64]
)。但是 l2cap_parse_conf_rsp()
本身并不知道 data
指向的 buffer 有 64 字节的限制,因为该 buffer 的长度没有作为参数传入 l2cap_parse_conf_rsp()
,这也是导致漏洞的地方。
由于收到的 L2CAP_CONFIGURATION_RSP
中有新的配置数据,这说明对端设备可能不同意本地设备先前提供的配置,同时新的配置数据就是对端设备期望继续协商的内容。于是本地设备需要继续发送新的 request 来协商配置。这也是在 l2cap_parse_conf_rsp()
的开头,data
buffer 直接被转换为了 l2cap_conf_req
结构体指针的原因:
// net/bluetooth/l2cap_core.c#3511
struct l2cap_conf_req *req = data;
void *ptr = req->data;
然后漏洞函数会逐个解析 L2CAP_CONFIGURATION_RSP
携带的配置选项,并根据此构造自己一方新的协商配置,然后存储到 data
buffer 中。这些新的协商配置将放在后续的配置请求中发给对端设备。
比如当 L2CAP_CONFIGURATION_RSP
携带 MTU (Maximum Transmission Unit) 配置选项时,如果其值不满足本地 incoming MTU 的要求,l2cap_parse_conf_rsp()
会首先重设 Result 字段(result
参数)为 unacceptable,并给一个协议默认的最小值作为新的 incoming MTU。同时不论值是否满足本地要求,漏洞函数都会构造一个新的 MTU 配置选项存储在 data
buffer (下面的 ptr
) 中,供后续的配置请求使用:
BLUETOOTH CORE SPECIFICATION Version 5.2 | Vol 3, Part A page 1081, 5.1 MAXIMUM TRANSMISSION UNIT (MTU)
// net/bluetooth/l2cap_core.c#3524
case L2CAP_CONF_MTU:
if (val < L2CAP_DEFAULT_MIN_MTU) {
*result = L2CAP_CONF_UNACCEPT;
chan->imtu = L2CAP_DEFAULT_MIN_MTU;
} else
chan->imtu = val;
l2cap_add_conf_opt(&ptr, L2CAP_CONF_MTU, 2, chan->imtu);
break;
如果接收的 L2CAP_CONFIGURATION_RSP
packet 恶意包含了过多的 MTU 配置选项,那么漏洞函数将新构造同样多的 MTU 选项并把它们存储 data
buffer 中。于是 64 字节的 buffer 就会溢出,最终导致栈溢出漏洞。
构造 DoS Exp
BLUETOOTH CORE SPECIFICATION Version 5.2 | Vol 3, Part A page 1054, Flags
-
构造
L2CAP_CONNECTION_REQ
首先发送连接请求与目标设备建立连接。这里 PSM 选用 0x0001,因为它对应的上层应用为 SDP (Service Discovery Protocol),几乎所有 BR/EDR 设备都支持 SDP。这能保证连接建立成功;SCID 选一个动态分配的 CID 即可,比如 0x0050。
对于远端响应的
L2CAP_CONNECTION_RSP
,我们需要保存其中的 DCID,供后续的包使用。 -
构造
L2CAP_CONFIGURATION_REQ
连接成功后,我们发起配置连接的请求,目的是让目标系统进入 pending state。因为根据内核中的如下代码,要调用到漏洞函数
l2cap_parse_conf_rsp()
需信道的配置状态为CONF_LOC_CONF_PEND
:// // net/bluetooth/l2cap_core.c#4162 if (test_bit(CONF_LOC_CONF_PEND, &chan->conf_state)) { char buf[64]; len = l2cap_parse_conf_rsp(chan, rsp->data, len, buf, &result); ... ... }
其中信道的配置状态
chan->conf_state
只可能在内核解析L2CAP_CONFIGURATION_REQ
时被置CONF_LOC_CONF_PEND
。具体位置在l2cap_parse_conf_req()
中:// net/bluetooth/l2cap_core.c#3414 if (remote_efs) { if (chan->local_stype != L2CAP_SERV_NOTRAFIC && efs.stype != L2CAP_SERV_NOTRAFIC && efs.stype != chan->local_stype) { result = L2CAP_CONF_UNACCEPT; if (chan->num_conf_req >= 1) return -ECONNREFUSED; l2cap_add_conf_opt(&ptr, L2CAP_CONF_EFS, sizeof(efs), (unsigned long) &efs); } else { /* Send PENDING Conf Rsp */ result = L2CAP_CONF_PENDING; set_bit(CONF_LOC_CONF_PEND, &chan->conf_state); } }
根据上面的代码可知,执行到
set_bit()
需满足两个条件,即对端设备的L2CAP_CONFIGURATION_REQ
要携带 EFS option,且该选项的 Service Type (efs.stype
) 为 No Traffic (L2CAP_SERV_NOTRAFIC
)。DCID 字段设置为上一步我们从
L2CAP_CONNECTION_RSP
中提取的 DCID 即可。根据蓝牙协议,flags 字段的合法值目前只有 0x0000 和 0x0001。这里需要把它设置为 0x0000,表示我们走 Lockstep 配置流程。如果设置成 0x0001,会导致配置流程走 Standard process 而不是 Lockstep process。如果不走 Lockstep process,
l2cap_config_req()
将直接跳过l2cap_parse_conf_req()
,于是就算我们携带了精心构造的 EFS option,目标内核也不会处理它,CONF_LOC_CONF_PEND
也不会被设置:// net/bluetooth/l2cap_core.c#4063 if (flags & L2CAP_CONF_FLAG_CONTINUATION) { /* Incomplete config. Send empty response. */ l2cap_send_cmd(conn, cmd->ident, L2CAP_CONF_RSP, l2cap_build_conf_rsp(chan, rsp, L2CAP_CONF_SUCCESS, flags), rsp); goto unlock; } /* Complete config. */ len = l2cap_parse_conf_req(chan, rsp);
-
构造
L2CAP_CONFIGURATION_RSP
该包用于响应目标系统发送的
L2CAP_CONFIGURATION_REQ
,同时将恶意 payload 注入其内核。SCID 字段设置为远端设备为当前信道分配的 CID 即可,即先前
L2CAP_CONNECTION_RSP
中的 DCID。根据协议 Flags 字段目前只有两个合法值 0x0000 或 0x0001。这里将其设置为 0x0000,表示我们支持 Extended Flow Specification,走 Lockstep process。其实设置成 0x0001 也可以,因为在处理
L2CAP_CONNECTION_RSP
时,内核处理完 result 字段(漏洞函数所在的位置)后才会处理 flags 字段,因此这里 flags 不影响漏洞的触发:// net/bluetooth/l2cap_core.c#4153 switch (result) { ... ... } if (flags & L2CAP_CONF_FLAG_CONTINUATION) goto done;
Result 字段设为 pending (0x0004)。因为这是让处理响应包的
l2cap_config_rsp()
调用漏洞函数l2cap_parse_conf_rsp()
的前提条件:// net/bluetooth/l2cap_core.c#4159 case L2CAP_CONF_PENDING: set_bit(CONF_REM_CONF_PEND, &chan->conf_state); if (test_bit(CONF_LOC_CONF_PEND, &chan->conf_state)) { char buf[64]; len = l2cap_parse_conf_rsp(chan, rsp->data, len, buf, &result); ... ... } goto done;
Configuration options 字段就用于填写恶意的 payload。比如我们填写 100 个 MTU option。每个 MTU option 占用 4 bytes,总共 400 bytes 远超存在溢出点 buffer 的 64 bytes 大小:
漏洞系统对 DoS Exp 的反应
漏洞系统的蓝牙地址为 12:43:24:14:23:34
,处于 page scan 状态,即可连接状态:
此时攻击者执行脚本,打出 DoS Exp:
之后目标虚拟机将直接崩溃: