cover

水一篇分析文章,站在巨人的肩膀上。

漏洞分析

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”选项,这个选项是可以随便写的。

参考

https://mp.weixin.qq.com/s/sl9-2GZaJfqGwoHAHG8onQ