作者:f0rm2l1n@浙江大學AAA戰隊,team BlockSec
本文為作者投稿,Seebug Paper 期待你的分享,凡經采用即有禮品相送! 投稿郵箱:paper@seebug.org

最近一段時間,我們團隊針對Linux內核的藍牙棧代碼進行了漏洞挖掘。如安全維護者Greg所感慨的,Linux的藍牙實現是buggy的

seems to be a busy time with people hammering on the bluetooth stack these days...

非常幸運,我們找到了一些品相不錯的漏洞,其中有些可以穩定的造成任意代碼執行以提升攻擊者權限,在本文中,我將介紹其中特別的一位: 藍色華容道 (CVE-2021-3573)

對于臨界區的代碼,雖然使用了鎖從而看起來很安全,但是錯誤的鎖搭配,就像派關羽去守華容道那樣,最終只得不達所期

概述

CVE-2021-3573是一個在藍牙控制器卸載過程中,由條件競爭 (race condition) 帶來的釋放后使用漏洞 (use-after-free)。具有 CAP_NET_ADMIN 權限的本地攻擊者可以在用戶態偽造一個假的控制器,并主動地卸載該控制器以觸發這個條件競爭。基于這個UAF,攻擊者可以巧妙利用堆噴去覆蓋惡意數據,以進一步劫持控制流,完成權限提升。

漏洞細節

既然是 race 造成的 UAF,那我們肯定要研究一條 USING 的線程以及一條 FREEING 的線程。不過在此之前,我們首先看一個系統調用實現,即藍牙 HCI 套接字的綁定過程,函數 hci_sock_bind()

注: 所有代碼片段以內核 v5.12.0 作為參考

static int hci_sock_bind(struct socket *sock, struct sockaddr *addr,
             int addr_len)
{
...
    switch (haddr.hci_channel) {
    case HCI_CHANNEL_RAW:
        if (hci_pi(sk)->hdev) {
            err = -EALREADY;
            goto done;
        }

        if (haddr.hci_dev != HCI_DEV_NONE) {
            hdev = hci_dev_get(haddr.hci_dev);
            if (!hdev) {
                err = -ENODEV;
                goto done;
            }

            atomic_inc(&hdev->promisc);
        }
...
        hci_pi(sk)->hdev = hdev;
...
}

簡單來說,函數 hci_sock_bind() 將通過用戶傳遞的參數 haddr 中關鍵的 hci_dev 索引去尋找特定標號的控制器設備,并通過代碼 hci_pi(sk)->hdev = hdev; 在該設備(即對象hdev)與當前套接字之間建立聯系。當這個 bind 系統調用完成之后,這個套接字就可以被稱為一個綁定過的套接字了(bound socket)。

可以看到,這里取得 hdev 是通過 hci_dev_get 函數,換言之,hdev 通過引用計數進行維護。

USING 線程

一個完成綁定的套接字是允許調用 hci_sock_bound_ioctl() 函數中的命令的,見如下代碼

/* Ioctls that require bound socket */
static int hci_sock_bound_ioctl(struct sock *sk, unsigned int cmd,
                unsigned long arg)
{
    struct hci_dev *hdev = hci_pi(sk)->hdev;

    if (!hdev)
        return -EBADFD;
...
    switch (cmd) {
...

    case HCIGETCONNINFO:
        return hci_get_conn_info(hdev, (void __user *)arg);

    case HCIGETAUTHINFO:
        return hci_get_auth_info(hdev, (void __user *)arg);

    case HCIBLOCKADDR:
        if (!capable(CAP_NET_ADMIN))
            return -EPERM;
        return hci_sock_blacklist_add(hdev, (void __user *)arg);

    case HCIUNBLOCKADDR:
        if (!capable(CAP_NET_ADMIN))
            return -EPERM;
        return hci_sock_blacklist_del(hdev, (void __user *)arg);
    return -ENOIOCTLCMD;
}

可以看到函數里提供了四個有效的額外命令,分別和訪問當前連接的信息、當前連接的認證,以及設備的黑名單相關。這四個命令分別由四個額外的函數來響應 - hci_get_conn_info() - hci_get_auth_info() - hci_sock_blacklist_add() - hci_sock_blacklist_del()

響應函數實際上都最終會去操作 hdev 對象中維護的鏈表,舉個例子,我們可以看黑名單添加函數 hci_sock_blacklist_add()

static int hci_sock_blacklist_add(struct hci_dev *hdev, void __user *arg)
{
    bdaddr_t bdaddr;
    int err;

    if (copy_from_user(&bdaddr, arg, sizeof(bdaddr)))
        return -EFAULT;

    hci_dev_lock(hdev);

    err = hci_bdaddr_list_add(&hdev->blacklist, &bdaddr, BDADDR_BREDR);

    hci_dev_unlock(hdev);

    return err;
}

代碼邏輯很簡單,其通過 copy_from_usr 去獲取用戶態提供的一個藍牙地址,隨后會遍歷 hdev->blacklist 來決定是否要將該地址插入鏈表。其他三個函數類似,他們都使用到了 hdev 上相關的數據成員。

FREEING 線程

正常情況下,一個完成綁定的套接字應該通過如下的代碼片段來解除其和下層設備 hdev 之間的聯系。

static int hci_sock_release(struct socket *sock)
{
    hdev = hci_pi(sk)->hdev;
    if (hdev) {
...
        atomic_dec(&hdev->promisc);
        hci_dev_put(hdev);
    }
...
}

可以看到,這里的操作和 bind 中的操作是非常對稱的,看起來也相當的安全。

可是,這里并非唯一一個能解除聯系的代碼片段。試想現在電腦上運行的藍牙控制器(就比如市面上買的USB的那種)突然被拔掉,這個時候這些綁定到該設備的套接字怎么辦?理論上,下層的代碼應該要通知套接字去主動放棄該聯系。

負責傳達的代碼就是 hci_sock_dev_event(),當控制器被移除時,內核會調用到 hci_unregister_dev() 函數,該函數會以 HCI_DEV_UNREG 的形式去調用 hci_sock_dev_event(),見如下代碼。

void hci_sock_dev_event(struct hci_dev *hdev, int event)
{
...
    if (event == HCI_DEV_UNREG) {
        struct sock *sk;

        /* Detach sockets from device */
        read_lock(&hci_sk_list.lock);
        sk_for_each(sk, &hci_sk_list.head) {
            bh_lock_sock_nested(sk);
            if (hci_pi(sk)->hdev == hdev) {
                hci_pi(sk)->hdev = NULL; // {1}
                sk->sk_err = EPIPE;
                sk->sk_state = BT_OPEN;
                sk->sk_state_change(sk);

                hci_dev_put(hdev); // {2}
            }
            bh_unlock_sock(sk);
        }
        read_unlock(&hci_sk_list.lock);
    }
}

可以見到,當事件是 HCI_DEV_UNREG 時,該函數會遍歷全局的套接字鏈表 hci_sk_list 并尋找綁定到了正要移除設備的那些套接字(hci_pi(sk)->hdev == hdev)。隨后,標記為{1}的代碼行會更新套接字結構體并通過{2}代碼放棄 hdev 的引用。

hdev 對象的最后引用會在驅動代碼調用 hci_free_dev() 時候減少到0,并由 bt_host_release 完成對其內存的回收。

這條不那么常規的 FREEING 線程是很不安全的,事實上,它可以與 USING 線程形成如下的條件競爭。

hci_sock_bound_ioctl thread    |    hci_unregister_dev thread
                               |
                               |
if (!hdev)                     |
    return -EBADFD;            |
                               |
                               |    hci_pi(sk)->hdev = NULL;
                               |    ...
                               |    hci_dev_put(hdev);
                               |    ...
                               |    hci_free_dev(hdev);
// UAF, for example            |
hci_dev_lock(hdev);            |
                               |
                               |

讀者可以訪問當時OSS上的漏洞描述 (https://www.openwall.com/lists/oss-security/2021/06/08/2) 去查看我準備的POC樣例以及UAF KASan捕獲時候的棧報告。

漏洞利用

可能已經有讀者開始發牢騷了:條件競爭,哼,什么玩意兒。條件競爭漏洞可以說是漏洞里面品相最差的了,即使這一個能構成UAF,但不能穩定觸發便是絕對軟肋。

好吧,很顯然有牢騷的讀者并沒有去OSS上閱讀漏洞描述,實際上,這個條件競爭可以被100%穩定的觸發。

如果讀者有過CTF經驗,那么再仔細讀一下代碼的話一定可以發現個鐘奧妙

static int hci_sock_blacklist_add(struct hci_dev *hdev, void __user *arg)
{
    bdaddr_t bdaddr;
    int err;

    if (copy_from_user(&bdaddr, arg, sizeof(bdaddr))) // {3}
        return -EFAULT;

可以看到{3}標記的代碼是使用了copy_from_user()的,那么,只要依靠 userfaultfd 黑魔法,我們可以隨心所欲的掌控 USING 的線程掛起時間。

hci_sock_bound_ioctl thread    |    hci_unregister_dev thread
                               |
                               |
if (!hdev)                     |
    return -EBADFD;            |
                               |
copy_from_user()               |
____________________________   |
                               |
                               |    hci_pi(sk)->hdev = NULL;
                               |    ...
    userfaultfd 掛起            |    hci_dev_put(hdev);
                               |    ...
                               |    hci_free_dev(hdev);
____________________________   |
// UAF, for example            |
hci_dev_lock(hdev);            |
                               |
                               |

OK,在可以穩定觸發漏洞的基礎上,讓我們來試著做更多事情吧

實話實說,這是我的首個 0 day 利用,寫的過程可以說是感慨萬千,不過整體而言,跟做一個CTF內核題的區別不大 另外,如下的利用中使用的USING thread并非是上文討論的hci_sock_bound_ioctl而是hci_sock_sendmsg,其同樣也可以用userfaultfd輔助,就不贅述了

Leaking

想要打穿內核,放到最前面的一個任務便是繞過隨機化KASLR,在這一關卡上我是摔了跟斗的,因為當時的我斗氣一定想要用另外一個自己發現的OOB read漏洞來泄露指針。

在錯過一次過后(主要是泄露的成功率比較低)便還是撥亂反正,就用這一個洞來同時完成泄露以及內存破壞。原理也很簡單:我只要讓 USING 線程觸發到一個 WARNING 或者碰到內核不會掛掉的頁錯誤即可。

如下是一個可用的NPD造成的泄露。

[   17.793908] BUG: kernel NULL pointer dereference, address: 0000000000000000
[   17.794222] #PF: supervisor read access in kernel mode
[   17.794405] #PF: error_code(0x0000) - not-present page
[   17.794637] PGD 0 P4D 0
[   17.794816] Oops: 0000 [#1] SMP NOPTI
[   17.795043] CPU: 0 PID: 119 Comm: exploit Not tainted 5.12.1 #18
[   17.795217] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS rel-1.14.0-0-g155821a1990b-prebuilt.qemu.org 04/01/2014
[   17.795543] RIP: 0010:__queue_work+0xb2/0x3b0
[   17.795728] Code: 8b 03 eb 2f 83 7c 24 04 40 0f 84 ab 01 00 00 49 63 c4 49 8b 9d 08 01 00 00 49 03 1c c6 4c 89 ff e8 73 fb ff ff 48 85 c0 74 d5 <48> 39 030
[   17.796191] RSP: 0018:ffffac4d8021fc20 EFLAGS: 00000086
[   17.796329] RAX: ffff9db3013af400 RBX: 0000000000000000 RCX: 0000000000000000
[   17.796545] RDX: 0000000000000000 RSI: 0000000000000003 RDI: ffffffffbdc4cf10
[   17.796769] RBP: 000000000000000d R08: ffff9db301400040 R09: ffff9db301400000
[   17.796926] R10: 0000000000000000 R11: ffffffffbdc4cf18 R12: 0000000000000000
[   17.797109] R13: ffff9db3021b4c00 R14: ffffffffbdb106a0 R15: ffff9db302260860
[   17.797328] FS:  00007fa9edf9d740(0000) GS:ffff9db33ec00000(0000) knlGS:0000000000000000
[   17.797541] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[   17.797699] CR2: 0000000000000000 CR3: 000000000225c000 CR4: 00000000001006f0
[   17.797939] Call Trace:
[   17.798694]  queue_work_on+0x1b/0x30
[   17.798865]  hci_sock_sendmsg+0x3bc/0x960
[   17.798973]  sock_sendmsg+0x56/0x60
[   17.799081]  sock_write_iter+0x92/0xf0
[   17.799170]  do_iter_readv_writev+0x145/0x1c0
[   17.799303]  do_iter_write+0x7b/0x1a0
[   17.799386]  vfs_writev+0x93/0x160
[   17.799527]  ? hci_sock_bind+0xbe/0x650
[   17.799638]  ? __sys_bind+0x8f/0xe0
[   17.799725]  ? do_writev+0x53/0x120
[   17.799804]  do_writev+0x53/0x120
[   17.799882]  do_syscall_64+0x33/0x40
[   17.799969]  entry_SYSCALL_64_after_hwframe+0x44/0xae
[   17.800186] RIP: 0033:0x7fa9ee08d35d
[   17.800405] Code: 28 89 54 24 1c 48 89 74 24 10 89 7c 24 08 e8 ca 26 f9 ff 8b 54 24 1c 48 8b 74 24 10 41 89 c0 8b 7c 24 08 b8 14 00 00 00 0f 05 <48> 3d 008
[   17.800798] RSP: 002b:00007ffe3c870e00 EFLAGS: 00000293 ORIG_RAX: 0000000000000014
[   17.800969] RAX: ffffffffffffffda RBX: 0000556f50a02f30 RCX: 00007fa9ee08d35d
[   17.801118] RDX: 0000000000000003 RSI: 00007ffe3c870ea0 RDI: 0000000000000005
[   17.801267] RBP: 00007ffe3c870ee0 R08: 0000000000000000 R09: 00007fa9edf87700
[   17.801413] R10: 00007fa9edf879d0 R11: 0000000000000293 R12: 0000556f50a00fe0
[   17.801560] R13: 00007ffe3c870ff0 R14: 0000000000000000 R15: 0000000000000000
[   17.801769] Modules linked in:
[   17.801928] CR2: 0000000000000000
[   17.802233] ---[ end trace 2bbc14e693eb3d8f ]---
[   17.802373] RIP: 0010:__queue_work+0xb2/0x3b0
[   17.802492] Code: 8b 03 eb 2f 83 7c 24 04 40 0f 84 ab 01 00 00 49 63 c4 49 8b 9d 08 01 00 00 49 03 1c c6 4c 89 ff e8 73 fb ff ff 48 85 c0 74 d5 <48> 39 030
[   17.802874] RSP: 0018:ffffac4d8021fc20 EFLAGS: 00000086
[   17.803019] RAX: ffff9db3013af400 RBX: 0000000000000000 RCX: 0000000000000000
[   17.803166] RDX: 0000000000000000 RSI: 0000000000000003 RDI: ffffffffbdc4cf10
[   17.803313] RBP: 000000000000000d R08: ffff9db301400040 R09: ffff9db301400000
[   17.803458] R10: 0000000000000000 R11: ffffffffbdc4cf18 R12: 0000000000000000
[   17.803605] R13: ffff9db3021b4c00 R14: ffffffffbdb106a0 R15: ffff9db302260860
[   17.803753] FS:  00007fa9edf9d740(0000) GS:ffff9db33ec00000(0000) knlGS:0000000000000000
[   17.803921] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[   17.804042] CR2: 0000000000000000 CR3: 000000000225c000 CR4: 00000000001006f0

Wow,可以看到寄存器 RDI, R11 以及 R14 都放著非常可疑的內核地址。通過查看 System.map 我們發現寄存器 R14 正好存放著全局數據對象 __per_cpu_offset 的地址 (調試環境下還沒有開啟KASLR),那么我們可以通過它來計算 KASLR 的偏移,以繞過隨機化保護。

$ cat System.map | grep bdb106a0
ffffffffbdb106a0 R __per_cpu_offset

Exploitation

RIP hijacking

在KASLR繞過之后,下一個目標便是怎樣去劫持控制流。為達此目標,一個UAF漏洞最簡單的方式就是基于堆噴去覆蓋目標對象上的函數指針,這樣子,只要這些被覆寫的函數指針被用到的時候,便可以完成控制流劫持了。嗯,思路簡單直接,而且這件事情看起來相當容易:因為 hdev 對象是 hci_dev 結構體,并由 kmalloc-8k 的緩存進行維護。由于對象的大小已經如此之大,這使得其所在的緩存相當的穩定,我們很簡單的就可以通過像 setxattr 這樣的方法完成對該目標的占位。

此外,這個結構體的尾巴上實在是有很多可口的函數指針啊

struct hci_dev {
...
    int (*open)(struct hci_dev *hdev);
    int (*close)(struct hci_dev *hdev);
    int (*flush)(struct hci_dev *hdev);
    int (*setup)(struct hci_dev *hdev);
    int (*shutdown)(struct hci_dev *hdev);
    int (*send)(struct hci_dev *hdev, struct sk_buff *skb);
    void (*notify)(struct hci_dev *hdev, unsigned int evt);
    void (*hw_error)(struct hci_dev *hdev, u8 code);
    int (*post_init)(struct hci_dev *hdev);
    int (*set_diag)(struct hci_dev *hdev, bool enable);
    int (*set_bdaddr)(struct hci_dev *hdev, const bdaddr_t *bdaddr);
    void (*cmd_timeout)(struct hci_dev *hdev);
    bool (*prevent_wake)(struct hci_dev *hdev);
};

好的,在假設我們能堆噴并完全覆蓋整個 hdev 對象的前提下,我們能完成控制流劫持么?Emmm,事情好像沒那么容易,因為 USING 線程的第一現場并沒有調用到任何函數指針。

static int hci_sock_sendmsg(struct socket *sock, struct msghdr *msg,
                size_t len)
{
...
    hdev = hci_pi(sk)->hdev;
    if (!hdev) {
        err = -EBADFD;
        goto done;
    }
...
    if (memcpy_from_msg(skb_put(skb, len), msg, len)) {
        err = -EFAULT;
        goto drop;
    }

    hci_skb_pkt_type(skb) = skb->data[0];
    skb_pull(skb, 1);

    if (hci_pi(sk)->channel == HCI_CHANNEL_USER) {
...
    } else if (hci_skb_pkt_type(skb) == HCI_COMMAND_PKT) {
...
        if (ogf == 0x3f) {
            skb_queue_tail(&hdev->raw_q, skb);
            queue_work(hdev->workqueue, &hdev->tx_work); // {4}
        } else {
            /* Stand-alone HCI commands must be flagged as
             * single-command requests.
             */
            bt_cb(skb)->hci.req_flags |= HCI_REQ_START;

            skb_queue_tail(&hdev->cmd_q, skb);
            queue_work(hdev->workqueue, &hdev->cmd_work); // {5}
        }
    } else {
        if (!capable(CAP_NET_RAW)) {
            err = -EPERM;
            goto drop;
        }

        skb_queue_tail(&hdev->raw_q, skb);
        queue_work(hdev->workqueue, &hdev->tx_work); // {4}
    }
...
}

整個 hci_sock_sendmsg() 函數做的事情就是去用戶態拿到要發送的數據包,并根據數據包的類型去決定要將 cmd_work 還是 tx_work 放入工作隊列。

誒?工作隊列?雖然不是直接的函數調用,這也是和控制流相關的邏輯啊。可能老師傅們已經悟到,可以通過覆蓋 hdev->cmd_work 或者 hdev->tx_work 來完成控制流劫持了。實際上,相關的 work_struct 中確實存在可口的函數指針。

typedef void (*work_func_t)(struct work_struct *work);

struct work_struct {
    atomic_long_t data;
    struct list_head entry;
    work_func_t func;
#ifdef CONFIG_LOCKDEP
    struct lockdep_map lockdep_map;
#endif
};

即成員 work_func_t func。由于我們可以覆蓋完整的整個 hdev 對象,去把這幾個 work_struct 改掉看起來也只是小菜一碟哈。

只不過,我又錯了。

僅僅去覆蓋掉 work_struct 是沒有用的,因為 queue_work() 必須要求一個合法的工作隊列來承載這個要被調度的工作。即我們需要一個合法的 hdev->workqueue 才能完成上述的攻擊。

這有可能可以做到么?workqueuehdev下的一個指針對象,如果我們能將其改寫成一個已知的而且指向合法的工作隊列的指針的話,事情就可以順利進行。

雖然聽起來合理,但這個方案難度是很大的。因為 workqueue_struct 并非是全局的數據結構,而是在 hdev 對象注冊時候被動態創建的,位于內核的SLUB堆上。即使在前文我們已經完成了對于KASLR的泄露繞過,但我們并沒有任意讀能力,因此想泄露出一個合法工作隊列所在的堆地址這一方案實在是黃粱美夢。

當然,安全研究者永不言敗,如果沒法覆蓋一個新的工作隊列指針上去,那我們就想辦法用舊的吧!hdev對象在堆噴覆蓋之前,其workqueue成員指向的是已經在hci_unregister_dev()中被釋放掉的一個工作隊列,換言之,其指向的是一個被釋放了的,kmalloc-512的堆對象。我們可以再次使用堆噴的方式,想辦法在該位置上噴射上去一個合法的工作隊列。

針對workqueue_struct的噴灑已經是利用中的第二次堆噴了,有趣的是,這一次堆噴并不是要噴我們自己的數據,而是想噴上去一個合法的工作隊列結構體。所以堆噴的路徑并非大家知道的msg, setxattr。我的做法就是想辦法再多創建一些虛擬的藍牙設備,畢竟每個設備初始化的時候都會創建hdev中的工作隊列的。

值得一提,對于workqueue_struct的噴射比之前對于hdev的噴射要困難了許多,這是因為kmalloc-512的對象好像非常“熱門”,總是有地方冒出來。在我的利用中,我通過調整設備初始化的順序來增加噴射的成功率,細節可以見代碼。

當這一次噴射成功時,workqueue指針就指向了一個合法的工作隊列,而 queue_work 就可以成功將需要被調度的 work_struct 壓入工作隊列。不過呢,因為在 hdevworkqueuecmd_worktx_work 的前面,所以我們沒法在這一步就去覆蓋掉 work_structfn 成員。

不過這其實還好,因為將要被調度的 hci_cmd_work 或者 hci_tx_work 都會跑到一個會使用到 hdev 內函數指針的 hci_send_frame 代碼內,我們可以在那個時候再搞定控制流。

只不過呢,我又又錯了。

因為,這個利用思路非常不穩定:我們沒法很好地預測工作隊列調度目標 work_struct 的時間,這個時延可能非常短,以至于我根本沒有機會讓 setxattr 噴上我想要的數據而函數指針就已經被用過了。這些該死的函數指針偏偏放在結構體的末尾,我又偏偏需要hdev中保留的workqueue的值,如下邏輯。

====> overwrite the hdev
+--+-----------+-----+---------+----------+---------+-----+---------------+
   | workqueue | ... | rx_work | cmd_work | tx_work | ... | code pointers |
+--+-----------+-----+---------+----------+---------+-----+---------------+

真是傷腦筋,難道就沒有一個比較好的,可以預測的訪問函數指針的位置么?

當然,或者說碰巧,是有的。正如老話所言,當上帝把門關上時,他一定會打開一扇窗。我在已有的調用路徑上發現了又一個財寶,那就是延時工作: delayed_work

static void hci_cmd_work(struct work_struct *work)
{
    struct hci_dev *hdev = container_of(work, struct hci_dev, cmd_work);
...
        hdev->sent_cmd = skb_clone(skb, GFP_KERNEL);
        if (hdev->sent_cmd) {
            ...
            if (test_bit(HCI_RESET, &hdev->flags))
                cancel_delayed_work(&hdev->cmd_timer);
            else
                schedule_delayed_work(&hdev->cmd_timer,
                              HCI_CMD_TIMEOUT); // {6}
        } else {
            skb_queue_head(&hdev->cmd_q, skb);
            queue_work(hdev->workqueue, &hdev->cmd_work);
        }

{6}標記的代碼會為發出去的命令注冊一個延遲工作,以處理該命令的回復超時的情況。delayed_work的邏輯其實很work_struct非常想,只不過呢,我們有一段非常可預測的時間窗了

#define HCI_CMD_TIMEOUT     msecs_to_jiffies(2000)  /* 2 seconds */

2秒,看起來非常合適。現在,我們可以讓堆噴的setxattr先一直卡著,直到接近2秒的時候再覆蓋上我們的惡意數據,這樣子就可以保證前文所計劃的攻擊都能完成,并且我們獲得了一個劫持RIP的原語。

ROP

ROP的故事并沒有控RIP的故事那樣精彩,不過在做棧遷移的時候還是有一些小技巧的。

/* HCI command timer function */
static void hci_cmd_timeout(struct work_struct *work)
{
    struct hci_dev *hdev = container_of(work, struct hci_dev,
                        cmd_timer.work);
...
    if (hdev->cmd_timeout)
        hdev->cmd_timeout(hdev);

在正常的情況下,cmd_timer會喚醒函數hci_cmd_timeout去完成超時處理,我們看到函數內有基于hdev->cmd_timeout的函數指針使用。在該位置劫持控制流后,第一個跳向的gadget一定得想辦法將棧遷移到可控的堆上去(最好就是我們覆蓋的 hdev 成員)。在內核中找了幾遍后,我們卻沒有找到非常合適的gadget。

比如說,我們經常使用的一個類型的gadget便是直接通過mov去寫rsp

For example, the popular one

0xffffffff81e0103f: mov rsp, rax; push r12; ret;

但是,此時我們劫持控制流的代碼hdev->cmd_timeout(hdev)其底層實現是__x86_indirect_thunk_rax,也就是說,此時的rax寄存器是剛好指向要跳往的gadget的,一心不可二用,rax此時又怎么能指向堆地址呢?

還有一些經典的通過 xchg 去遷移棧的,只不過那往往是用于 SMAP 保護關閉的情況。我們的目標環境是保護全開的,這類 gadget 也不好用。

遷棧的問題確實困擾了我許久,感謝隊內大佬Nop幫助,我們最好找到了一個非常合適的遷棧方法。

首先,我們使用的gadget是

   0xffffffff81060a41 <__efi64_thunk+81>:   mov    rsp,QWORD PTR [rsp+0x18]
   0xffffffff81060a46 <__efi64_thunk+86>:   pop    rbx
   0xffffffff81060a47 <__efi64_thunk+87>:   pop    rbp
   0xffffffff81060a48 <__efi64_thunk+88>:   ret

其會將棧上 rsp + 0x18 位置的值給 RSP 寄存器,那么,接著就是一個關鍵點,它既能滿足控制流劫持,又可以剛好讓 [rsp + 0x18] 指向合適的堆地址。

我最后選定的目標是 hci_error_reset,其內部又一個 hdev->hw_error 的調用。而且通過調試,我們發現調用點的棧滿足所需,[rsp + 0x18]剛好是指向 hdev 內部的,Perfect!

static void hci_error_reset(struct work_struct *work)
{
    struct hci_dev *hdev = container_of(work, struct hci_dev, error_reset);

    BT_DBG("%s", hdev->name);

    if (hdev->hw_error)
        hdev->hw_error(hdev, hdev->hw_error_code);
    else
        bt_dev_err(hdev, "hardware error 0x%2.2x", hdev->hw_error_code);
...
}

剩下的工作就是大家都熟悉的ROP了,出于只是展示的需要,我實現的ROP其僅僅完成的是對于modprobe_path的修改。代碼以及demo開源在github上 https://github.com/f0rm2l1n/ExP1oiT5/tree/main/CVE-2021-3573

感興趣的讀者可以試著寫一下更完善的ROP

修復的故事

如果是你,你會怎樣修復在這樣一個條件競爭的漏洞呢?

當我提交該漏洞時候,我向內核社區提供了一份如下的補丁作為參考。

---
 net/bluetooth/hci_sock.c | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/net/bluetooth/hci_sock.c b/net/bluetooth/hci_sock.c
index 251b9128f530..eed0dd066e12 100644
--- a/net/bluetooth/hci_sock.c
+++ b/net/bluetooth/hci_sock.c
@@ -762,7 +762,7 @@ void hci_sock_dev_event(struct hci_dev *hdev, int event)
        /* Detach sockets from device */
        read_lock(&hci_sk_list.lock);
        sk_for_each(sk, &hci_sk_list.head) {
-           bh_lock_sock_nested(sk);
+           lock_sock(sk);
            if (hci_pi(sk)->hdev == hdev) {
                hci_pi(sk)->hdev = NULL;
                sk->sk_err = EPIPE;
@@ -771,7 +771,7 @@ void hci_sock_dev_event(struct hci_dev *hdev, int event)

                hci_dev_put(hdev);
            }
-           bh_unlock_sock(sk);
+           release_sock(sk);
        }
        read_unlock(&hci_sk_list.lock);
    }
--
2.30.2

從漏洞發現者的角度來看,這個漏洞的根本成因在于有一個特殊的 FREEING 線程,其可能可以在別的 USING 線程(如hci_sock_bound_ioctl and hci_sock_sendmsg)還在使用 hdev 對象時候便將該目標給釋放掉。

所以呢,我的補丁通過替換鎖來完成對于該 FREEING 線程的堵塞。在打上這個補丁之后,KASAN并不會再有任何的報告,感覺是沒啥問題的。

悲哀的是,我又又又錯了。

因為我本人并不是非常清楚內核中的同步機制,這里對于鎖的替換僅僅是參考相關的 USING 線程,以直覺方式完成的。我并沒有去仔細進行鎖的分析,以至于我提供的補丁是有可能造成死鎖的(我真的不是故意的嗚嗚嗚)

更糟糕的是,內核并沒有經過任何猶豫便打上了我提供的補丁。

災難大概在補丁進入內核主線的一周之后開始初見端倪:我開始收到各種各樣的郵件來控訴這個荒唐的補丁。其中最早來的是谷歌的 Anand K. Mistry,他向我展示了在開啟 CONFIG_LOCK_DEBUG 后生成的錯誤報告以及死鎖的可能性分析。在他之后,也有越來越多的內核開發者注意到了這條有問題的補丁。其中很大的促進因素是谷歌的模糊測試機器人 syzbot

Also, this regression is currently 7th top crashers for syzbot

這個機器人將觸發這個鎖錯誤的測試報告不斷發送給藍牙的維護者(因為實在太好觸發了,設備一旦卸載這個錯誤就會被捕獲)。

我實在是羞愧的想挖個洞把自己給埋了。可能你會很好奇,再提交一份正確的修復不就好了么?但悲哀的事實是,這個條件競爭并不好修復,社區中也開展了充足的討論,讀者可以閱讀下面的鏈接去了解該情況。

https://lore.kernel.org/linux-bluetooth/nycvar.YFH.7.76.2107131924280.8253@cbobk.fhfr.pm/ https://www.spinics.net/lists/linux-bluetooth/msg92649.html https://marc.info/?l=linux-bluetooth&m=162441276805113&w=2 https://marc.info/?l=linux-bluetooth&m=162653675414330&w=2

讓人欣慰的是,內核大佬 Tetsuo Handa 與藍牙維護者 Luiz 對這個問題是十分上心的,我相信一份正確的補丁會很快成埃落定的。

== 7月28日更新 ==

Yeah,一份看起來不錯的補丁已經發布在了bluetooth-next分支上:

這次連著幾天的討論甚至讓我接觸到了Linus,心情實在難以平復 :)

結論

這是我的首個Linux內核0 day利用,說真的,這個過程中我真的學到了很多:寫漏洞利用真的就是一門藝術。

當然,需要承認的是這個漏洞雖然可以穩定觸發,但品相也還是有缺點的:其要求 CAP_NET_ADMIN 權限,所以在野場景下的 fullchain 利用要求攻擊者先攻破具有該權限的 daemon 才行。

這是挖掘本地藍牙棧漏洞的一個固有缺點,因為我們需要在用戶態模擬一個假的藍牙控制器,而這件事情顯然不會是零權限的。更好品質的漏洞自然應該像 BleedingTooth 那樣在不要求任何點擊的情況下,在遠程完成代碼執行。

我相信這種理想類型的漏洞會是我們的終極目標。


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1653/