水一篇分析文章,站在巨人的肩膀上。
漏洞分析
CVE-2020-12351,该漏洞CVSS 评分为8.3 分,是一个基于堆的类型混淆(type confusion) 漏洞。在受害者蓝牙范围内的远程攻击者在指导目标设备的bd地址的情况下就可以利用该漏洞。攻击者可以通过发送恶意l2cap包的方式来触发该漏洞,引发DoS 或kernel 权限的任意代码执行。谷歌安全研究人员称该漏洞是一个零点击漏洞,也就是说利用的过程中无需任何的用户交互。
A heap-based 类型混淆 affecting Linux kernel 4.8 and higher was discovered in net/bluetooth/l2cap_core.c.
当 CID 不是 L2CAP_CID_SIGNALING, L2CAP_CID_CONN_LESS 或 L2CAP_CID_LE_SIGNALING时, l2cap_recv_frame 会调用 l2cap_data_channel() 。在l2cap_data_channel()中需要关注的是第8行的第27行的这两个分支。
///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);
if (!chan) {
if (cid == L2CAP_CID_A2MP) {
chan = a2mp_channel_create(conn, skb); //here
if (!chan) {
kfree_skb(skb);
return;
}
l2cap_chan_lock(chan);
} else {
BT_DBG("unknown cid 0x%4.4x", cid);
/* Drop packet and return */
kfree_skb(skb);
return;
}
}
...
switch (chan->mode) {
...
case L2CAP_MODE_ERTM:
case L2CAP_MODE_STREAMING:
l2cap_data_rcv(chan, skb); //here
goto done;
...
}
drop:
kfree_skb(skb);
done:
l2cap_chan_unlock(chan);
}
第27行:在 l2cap_data_channel 函数里如果 channel 的 mode 是 L2CAP_MODE_ERTM 或 L2CAP_MODE_STREAMING, 就会调用 l2cap_data_rcv()。
///net/bluetooth/l2cap_core.c
static int l2cap_data_rcv(struct l2cap_chan *chan, struct sk_buff *skb)
{
struct l2cap_ctrl *control = &bt_cb(skb)->l2cap;
u16 len;
u8 event;
__unpack_control(chan, skb);
len = skb->len;
/*
* We can just drop the corrupted I-frame here.
* Receiver will miss it and start proper recovery
* procedures and ask for retransmission.
*/
if (l2cap_check_fcs(chan, skb))
goto drop;
if (!control->sframe && control->sar == L2CAP_SAR_START)
len -= L2CAP_SDULEN_SIZE;
if (chan->fcs == L2CAP_FCS_CRC16)
len -= L2CAP_FCS_SIZE;
if (len > chan->mps) {
l2cap_send_disconn_req(chan, ECONNRESET);
goto drop;
}
if ((chan->mode == L2CAP_MODE_ERTM ||
chan->mode == L2CAP_MODE_STREAMING) && sk_filter(chan->data, skb)) //here
goto drop;
...
}
当packet的 checksum 被验证通过 , 继续调用 sk_filter()//sk_filter是对sk_filter_trim_cap的简单封装。
第8行: l2cap_data_channel 函数里 当使用的 CID 是 L2CAP_CID_A2MP 并且还没建立一个channel时 , a2mp_channel_create() 将被调用。
///net/bluetooth/a2mp.c
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;
BT_DBG("conn %p mgr %p", conn, mgr);
mgr->l2cap_conn = conn;
chan = a2mp_chan_open(conn, locked); //here
if (!chan) {
kfree(mgr);
return NULL;
}
mgr->a2mp_chan = chan;
chan->data = mgr;
...
return mgr;
}
a2mp_chan_open 创建了一个 channel 并且把 mode 初试化为 L2CAP_MODE_ERTM
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;
BT_DBG("chan %p", chan);
chan->chan_type = L2CAP_CHAN_FIXED;
chan->scid = L2CAP_CID_A2MP;
chan->dcid = L2CAP_CID_A2MP;
...
chan->mode = L2CAP_MODE_ERTM;
...
return chan;
}
!!!问题在这里:
amp_mgr_create()里 chan->data 的类型是struct amp_mgr*
static struct amp_mgr *amp_mgr_create(struct l2cap_conn *conn, bool locked)
{
struct amp_mgr *mgr;
...
chan->data = mgr;
...
}
在l2cap_data_rcv()调用了sk_filter(chan->data, skb),定义是这样的 sk_filter(struct sock *sk, struct sk_buff skb); chan->data被转换成了struct sock\类型,类型混淆在此产生。
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;
...
}
int sk_filter(struct sock *sk, struct sk_buff *skb);
{}
POC
https://github.com/google/security-research/security/advisories/GHSA-h637-c88j-47wq
mode:a2mp_chan_open 创建 channel的时候已把 mode 初试化为 L2CAP_MODE_ERTM。
cid:不应是 L2CAP_CID_SIGNALING, L2CAP_CID_CONN_LESS 或 L2CAP_CID_LE_SIGNALING,这里选择L2CAP_CID_A2MP。
#define L2CAP_CID_SIGNALING 0x0001
#define L2CAP_CID_CONN_LESS 0x0002
#define L2CAP_CID_A2MP 0x0003
#define L2CAP_CID_ATT 0x0004
#define L2CAP_CID_LE_SIGNALING 0x0005
#define L2CAP_CID_SMP 0x0006
#define L2CAP_CID_SMP_BREDR 0x0007
#define L2CAP_CID_DYN_START 0x0040
#define L2CAP_CID_DYN_END 0xffff
#define L2CAP_CID_LE_DYN_END 0x007f
crash了
真机调试环境搭建
Debugger配置
1、下载符号文件和内核源码文件
为了更好地调试内核,gdb需要Debuggee的内核符号文件和源代码文件来实现源码级别的调试。之前发现了一个launchpad.net(https://launchpad.net/ubuntu/+source/linux/5.4.0-42.46)的站点,该站点提供了当前发行版Linux系统的内核符号文件和源码文件供开发者下载。
找到Debuggee的ubuntu版本、 找带dbgsym、unsigned的、系统架构amd64
2、安装符号文件
下载得到linux-image-unsigned-5.4.0-42-generic-dbgsym_5.4.0-42.46_amd64.ddeb文件,在Debugger中执行“dpkg -i”命令安装符号文件。 vmlinux-5.4.0-42-generic是Linux内核公共部分的可执行文件的符号版本
file /usr/lib/debug/boot/vmlinux-5.11.0-38-generic
/usr/lib/debug/boot/vmlinux-5.11.0-38-generic: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=3943780ac8a93e50813a3906c205bba1515216d3, with debug_info, not stripped
file /usr/lib/debug/lib/modules/5.11.0-38-generic/kernel/drivers/net/ethernet/realtek/r8169.ko
/usr/lib/debug/lib/modules/5.11.0-38-generic/kernel/drivers/net/ethernet/realtek/r8169.ko: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), BuildID[sha1]=ffa1e397cf00e133476ed9d9bcf168dac70a70c7, with debug_info, not stripped
Debugee配置
1、开启kgdb
双机调试需要Debuggee开启Kgdb功能,当前Ubuntu发行版内核已经默认开启了 Kgdb支持,通过命令“cat /boot/config-$(uname -r)| grep -i GDB”查看可知当前内核支持Kgdb以及串口调试。
q1iq@q1iq:~$ cat /boot/config-$(uname -r)| grep -i GDB
CONFIG_CFG80211_REQUIRE_SIGNED_REGDB=y
CONFIG_CFG80211_USE_KERNEL_REGDB_KEYS=y
CONFIG_SERIAL_KGDB_NMI=y
CONFIG_GDB_SCRIPTS=y
CONFIG_HAVE_ARCH_KGDB=y
CONFIG_KGDB=y
CONFIG_KGDB_HONOUR_BLOCKLIST=y
CONFIG_KGDB_SERIAL_CONSOLE=y
# CONFIG_KGDB_TESTS is not set
CONFIG_KGDB_LOW_LEVEL_TRAP=y
CONFIG_KGDB_KDB=y
如果内核不支持Kgdb,则可以通过下载、编译、安装对应版本的内核源码来打开Kgdb。以Linux5.4.0内核为例,需要设置的编译选项有:
CONFIG_KGDB=y //开启kgdb服务
CONFIG_KGDB_SERIAL_CONSOLE=y //kgdb默认连接到主板串口
CONFIG_DEBUG_INFO=y //内核中加入调试符号
2、配置grub文件
内核开启Kgdb功能后需要手动配置grub启动文件才能在开机的时候进入Kgdb调试选项,因为默认情况下Kgdb是不工作的,需要向内核传递相关启动参数才能启用。编辑/etc/grub.d/40_custom文件,添加如下menuentry。
kgdboc(串口调试)应加的选项
kgdbwait kgdboc=ttyS0,115200 nokaslr
kgdbwait:进入该启动选项后等待远程主机连接Kgdb
kgdboc:“kgdb over console”的缩写,表示远程主机通过串口连接到Kgdb
ttyS0:在本地默认串口监听连接事件,通常这也是主板上唯一的串口
115200:本地串口的波特率 nokaslr:关闭内核地址随机化。kaslr选项会干扰内核的调试因此要关闭
除了串口还可以使用以太网连接Kgdb,除了传递启动参数还可以在运行时通过sysfs文件系统开启Kgdb 更多详细的Kgdb操作请参考“linux5.4/Documentation/dev-tools/kgdb.rst”。Linux的documentation是一个非常有用的东⻄
kgbboe(网络调试)应该加这些选项(https://mirrors.edge.kernel.org/pub/linux/kernel/people/jwessel/kgdb/ch03s04.html)
kgdbwait kgdbcon kgdboe=@192.168.43.82/,@192.168.43.206/ nokaslr
kgdboe=[src-port]@<src-ip>/[dev],[tgt-port]@<tgt-ip>/[tgt-macaddr]
src-port默认6443
tgt-port默认6442
系统运行时修改kgdboe
echo "@/,@10.0.2.2/" > /sys/module/kgdboe/paramters/kgdboe
编辑完成后在控制台执行“sudo update-grub”命令更新启动项,重启Debuggee,可以在grub页面看到多出了“Ubuntu,KGDB with nokaslr”选项,这个选项是可以随便写的。