search
尋找貓咪~QQ 地點 桃園市桃園區 Taoyuan , Taoyuan

漏洞挖掘之利用Broadcom的Wi-Fi棧

本文將繼續研究如何僅通過Wi-Fi通信就實現遠程內核代碼執行。我們在上文中開發了一個遠程代碼執行利用方法,使我們能控制Broadcom的Wi-Fi SoC(系統級晶元)。現在我們的任務是,利用該優勢將我們的許可權進一步提升到內核。

圖1

在本文中,我們將探討攻擊主機

操作系統

的兩種不同的途徑。在第1部分中,我們將發現並利用Wi-Fi固件和主機之間的通信協議中的漏洞,從而在內核中執行代碼。期間,我們也將研究一個一直持續到最近的奇特漏洞,攻擊者可利用該漏洞直接攻擊內部通信協議,而無需先利用Wi-Fi SoC!在第2部分,我們將探討使當前配置的Wi-Fi SoC無需漏洞即可完全控制主機的硬體設計選擇。

在上一篇文章中討論的漏洞已披露給Broadcom,並已得到修復,但硬體組件的利用依然如故,現在並無相應的緩解措施。我們希望通過發布這項研究來推動移動SoC製造商和驅動程序供應商打造更安全的設計,從而實現Wi-Fi SoC和應用處理器之間更高程度的分離。

第1部分——「較難」方式

通信通道

正如我們在上一篇博文中所確立的,Broadcom生產的Wi-Fi固件是一個FullMAC實現。因此其負責處理實施802.11標準(包括大多數MLME層)所需的大部分複雜性。

然而,雖然許多操作是封裝在Wi-Fi晶元的固件中,但在主機

操作系統

中需要對Wi-Fi狀態機進行一定程度的控制。某些事件不能單獨由Wi-Fi SoC處理,必須傳達給主機的

操作系統

。例如,必須向主機通知Wi-Fi掃描的結果,以便能將該信息呈現給用戶。

為了方便主機和Wi-Fi SoC希望彼此通信的情況,需要一個特殊的通信通道。

但是別忘了,Broadcom生產可通過多種不同的介面(包括USB、SDIO甚或PCIe)連接到主機的各種Wi-Fi SoC。這意味著依靠底層通信介面可能需要為每個受支持的通道重新實現共享通信協議——這是一個非常繁瑣的任務。

圖2

或許有一個更簡單的方法?我們一直可以確定的一件事是,無論使用哪個通信通道,晶元都必須能夠將接收到的幀傳送回主機。實際上,或許正是出於該原因,Broadcom選擇搭載在該通道之上,以便在SoC和主機之間建立通信通道。

當固件希望通知主機事件時,其只要編碼一個「特殊」幀並將其發送到主機即可。這些幀由「唯一的」EtherType值0x886C標記。其不包含實際接收到的數據,而是封裝了有關必須由主機驅動程序處理的固件事件的信息。

圖3

確保通道安全性

現在,讓我們切換到主機側。在主機上,可在邏輯上將驅動程序分為若干層。較低層處理通信介面本身(比如SDIO、PCIe,等等)和所綁定的任何傳輸協議。較高層然後處理幀的接收及其後續處理(如果需要)。

圖4

首先,上層對接收到的幀執行一些初始處理,例如去除可能已經添加到其上的封裝數據(比如由PHY模塊添加的傳輸功率指示符)。然後必須作出一個重要的區分——這是一個只需轉發到相關網路介面的常規幀,還是實際上是一個主機必須處理的編碼事件?

正如我們剛剛看到的,這一區分很容易作出。只需查看ethertype,並檢查其是否具有「特殊」值0x886C即可。如果有,則處理封裝事件並丟棄幀。

或許是?事實上,不能保證該ethertype不在其他網路和設備中使用。HPNA晶元中使用的LARQ協議碰巧也使用相同的ethertype。

這將我們的第一個問題擺在了面前——Wi-Fi SoC和主機驅動程序如何對外部接收到的具有0x886C ethertype的幀(應該轉發到網路介面)和內部生成的事件幀(不應該從外部來源收到)進行區分?

這是一個關鍵問題,內部事件通道極其強大,提供了一個巨大的、基本不受審查的攻擊面。如果攻擊者能通過無線方式注入隨後可被驅動程序作為事件幀處理的幀,那麼其很可能在主機的

操作系統

中實現代碼執行。

直到本研究發表的幾個月前(2016年中),固件並不過濾這些幀。作為數據RX路徑的一部分接收的任何幀,無論其是何種ethertype,均只是被盲目轉發到主機。因此,攻擊者能夠遠程發送包含特殊0x886C ethertype的幀——隨後被驅動程序當做固件本身創建的事件幀處理。

那麼,這個問題是如何解決的?我們已經明確,僅僅過濾ethertype本身是不夠的。觀察打補丁前和打補丁后的固件版本可以得到答案:Broadcom採用的是針對Wi-Fi SoC的固件和主機驅動程序的組合補丁。

該補丁給固件的RX路徑和驅動程序添加了驗證方法(is_wlc_event_frame)。在晶元側,在將接收到的幀發送到主機之前立即調用該驗證方法。如果驗證方法將該幀視為事件幀,則其被丟棄。否則,該幀被轉發到驅動程序。然後,驅動程序對接收到的具有0x886C ethertype的幀調用完全相同的驗證方法,並只在其通過相同的驗證方法后才對其進行處理。以下是此流程的簡短示意圖:

圖5 只要驅動程序和固件中的驗證方法保持一致,外部接收的幀就不能被驅動程序作為事件處理。到目前為止這沒有問題。 然而,由於我們已經在Wi-Fi SoC上實現了代碼執行,所以我們可以簡單地「還原」補丁。我們要做的僅是撤掉」固件中的驗證方法,從而使任何接收到的幀再次被盲目轉發給主機。這反過來使我們能將任意消息注入到主機和Wi-Fi晶元之間的通信協議中。此外,由於驗證方法是存儲在RAM中,所有RAM均被標記為RWX,所以這與將「MOV R0, #0; BX LR」寫入函數的序言中一樣簡單。圖6 攻擊面 如前所述,內部通信通道暴露的攻擊面是巨大的。跟蹤來自處理事件幀(dhd_wl_host_event)的入口點的控制流,我們可以看到若干事件受到「特殊處理」,並被獨立處理(參見wl_host_event和wl_show_host_event)。初始處理完成後,幀隨即被插入到隊列中。事件然後被唯一目的是從隊列中讀取事件並將其分派到相應的處理程序函數的內核線程移出隊列。這種相關性是通過使用事件的內部「event-type」欄位作為名為evt_handler的處理函數數組的索引來完成的。圖7 雖然支持的事件代碼多達144種,但是Android的主機驅動程序bcmdhd只支持其中很小的一部分。儘管如此,驅動程序中支持大約35個事件,每個事件都包含自己精心設計的處理程序。 現在我們已確信攻擊面足夠大,所以我們可以開始尋找bug了!不巧的是,Wi-Fi晶元似乎被認為是「受信任的」,因此,主機驅動程序中的一些驗證是不夠的。事實上,通過審查上面列出的相關處理函數和輔助協議處理程序,我們發現了大量的漏洞。 漏洞 仔細研究我們發現的漏洞,我們可以看到這些漏洞彼此間均略有不同。一些允許較強的原語,一些允許較弱的原語。但是,最重要的是,其中很多有各種先決條件,滿足後方可成功觸發,一些僅限於某些物理介面,其他的僅在一定的驅動程序配置下有效。不過,有一個漏洞似乎在所有版本的bcmdhd和所有的配置中存在——如果能成功利用該漏洞,那就搞定了。 我們來仔細看看討論中的事件幀。"WLC_E_PFN_SWC"類型的事件用於指示固件中發生了「重要Wi-Fi改動」(SWC),且必須由主機處理。主機的驅動程序不是直接處理這些事件,而只是從固件中收集所有傳輸的數據,並通過Netlink向cfg80211層廣播「供應商事件」數據包。 更具體而言,由固件發送的每個SWC事件幀均包含一個事件數組(類型為wl_pfn_significant_net_t)、總計數(total_count)及數組中的事件數(pkt_count)。由於事件總數可能相當大,所以其可能無法容納在一個幀中(即其可能大於最大MSDU)。在這種情況下,可以連續發送多個SWC事件幀——其內部數據將由驅動程序累積,直到達到總計數,此時,驅動程序將處理整個事件列表。圖8 通讀驅動程序的代碼,我們可以看到,當接收到此事件代碼時,將觸發一個初始處理程序來處理該事件。然後處理程序內部調用「dhd_handle_swc_evt」函數來處理事件的數據。我們來仔細看看:

圖9 (其中「event_data」是封裝在從固件傳入的事件中的任意數據) 從上面可以看到,如上所述,函數首先分配一個數組來保存事件的總計數(如果之前未分配),然後繼續從緩衝區中相應的索引(results_rxed_so_far)開始連接封裝的數據。 但是,處理程序無法驗證total_count和pkt_count之間的關係!其只是「信任」「total_count足夠大,可以保存所有後續傳入的事件」之斷言。因此,能夠注入任意事件幀的攻擊者可以指定一個小的total_count和一個較大的pkt_count,從而可觸發一個簡單的內核堆溢出。 遠程內核堆整形 一切都沒有問題,但是我們如何從遠程有利位置來利用該原語?因為我們不在設備的本地位置,所以我們無法收集有關堆的當前狀態的任何數據,也沒有與地址空間相關的信息(除非我們能以某種方式泄漏此信息)。針對內核堆溢出的許多經典利用依賴於對內核堆進行整形的能力,即確保在觸發溢出之前處於某種狀態——我們目前也缺乏這一能力。 我們對分配算符本身有什麼了解?kmalloc分配算符(SLAB、SLUB、SLOB)有一些可能的底層實現,可在構建內核時配置。但是,在絕大多數設備上,kmalloc使用「SLUB」——一種未隊列化的per-CPU高速緩存「slab分配算符」。 每個「slab」只是一個小區域——從該區域雕刻相同大小的分配。每個slab中的第一個塊包含其元數據(例如slab的freelist),後續塊包含分配本身,沒有內聯元數據。有一些預定義的由kmalloc使用的slab大小類,大小通常小至64位元組,大至約8KB。不出所料,分配算符為每個分配使用最適合的slab(足夠大的最小slab)。最後,slab的freelist被線性消耗——連續的分配佔用連續的內存地址。但是,如果對象在slab中被釋放,則其可能變得碎片化——導致後續的分配填入slab中的「孔」中,而非線性進行。圖10 考慮到這一點,讓我們後退一步,分析一下手頭的原語。首先,由於我們能夠任意指定total_count中的任何值,所以我們可以選擇溢出緩衝區的大小作為sizeof(wl_pfn_significant_net)的任何倍數。這意味著我們可以使用我們選擇的任何slab緩存大小。因此,我們可以瞄準溢出的對象的大小沒有限制。但是,這還不夠。我們對slab的目前狀態仍然一無所知,也不能觸發我們選擇的slab中的遠程分配。 似乎我們首先需要找到一個方法來對slab進行遠程整形。但是回想一下,我們需要克服一些障礙。由於SLUB保持per-CPU高速緩存,所以執行分配的內核線程的親和性必須與分配溢出緩衝區的內核線程相同。在不同的CPU內核上獲取堆整形原語將導致從不同的slab進行分配。解決這個問題的最簡單的方法是將我們限制在可以從發生溢出的同一個內核線程中觸發的堆整形原語。這是一個相當大的限制,實質上,這強制我們忽略由於事件處理本身外部的進程所導致的分配。 無論如何,有了具體目標后,我們可以開始在每個事件幀的註冊處理程序中尋找堆整形原語了。幸運的是,審查過每個處理程序后,我們找到了非常適合的一個。 「WLC_E_PFN_BSSID_NET_FOUND」類型的事件幀由處理函數dhd_handle_hotlist_scan_evt處理。該函數累積掃描結果的鏈表。每次接收到一個事件時,其數據被附加到列表中。最後,當一個帶標記(表明事件是鏈中的最後一個事件)事件到達時,該函數傳遞收集的待處理事件列表。我們來仔細看看:

圖11 太棒了——看看上面的函數,似乎我們可以反覆導致大小分配{ sizeof(gscan_results_cache_t) + (N-1) * sizeof(wifi_gscan_result_t) | N > 0 } (其中N表示結果->計數)。此外,這些分配是在同一個內核線程中執行,其生命周期完全由我們控制!只要我們不發送具有PFN_COMPLETE狀態的事件,則不會釋放任何分配。 在我們繼續之前,我們需要選擇一個目slab大小。理想情況下,我們要尋找一個相對不活躍的slab。如果同一CPU上的其他線程選擇從同一個slab分配(或釋放)數據,這將增加該slab的狀態的不確定性,並可能使我們無法成功對其進行整形。在查看/proc/slabinfo並跟蹤具有與我們的目標內核線程相同的親和性的每個slab的kmalloc分配后,我們發現似乎kmalloc-1024 slab最不活躍。因此,我們將選擇在我們的利用方法中瞄準這一slab大小。 通過使用上面的堆整形原語,我們可以開始使用「gscan」對象填充任何給定大小的slab。每個「gscan」對象都有一個包含與掃描有關的元數據的短header,和一個指向鏈表中下一個元素的指針。對象的其餘部分然後由「掃描結果」的內聯數組填充,攜帶此節點的實際數據。圖12 回到手頭的問題——我們如何使用這個原語製作可預測的布局? 通過將堆整形原語與溢出原語相結合,我們應該能夠在觸發溢出之前對任何大小類的slab進行正確整形。回想一下,最初任何給定的slab均可能是碎片化的,如下所示:圖13 但是,在用我們的堆整形原語觸發足夠的分配(比如(SLAB_TOTAL_SIZE / SLAB_OBJECT_SIZE) - 1)后,當前slab中的所有孔(若有)應該被填充,導致後續相同大小類的分配連續進行。圖14 現在,我們可以發送一個特製的SWC事件幀,指示一個total_count——導致從同一個目標slab進行的分配。但是,我們還不想觸發溢出。在我們這樣做之前,我們還必須對當前的slab進行整形。為了防止溢出發生,我們將提供一個小的pkt_count,從而僅部分填充緩衝區。圖15 最後,再次使用堆整形原語,我們可以用更多的「gscan」對象填充slab的其餘部分,這使我們獲得以下堆狀態:圖16 我們快要到達目的地了!從上面可以看到,如果我們在這一點上選擇使用溢出原語,我們就可以用我們自己的任意數據覆蓋其中一個「gscan」對象的內容。但是,我們還沒有明確確定這會產生什麼樣的結果。 分析限制 為了確定覆蓋「gscan」對象的效果,我們來看看處理一連串「gscan」對象的流程(即接收到標記有「完成」的事件之後執行的操作)。該處理由wl_cfgvendor_send_hotlist_event處理。該函數檢查列表中的每個事件,將事件的數據打包到SKB中,然後通過Netlink將SKB廣播到任何潛在的監聽器。 但是,該函數確實有一定的障礙需要克服,任何給定的「gscan」節點均可能大於SKB的最大大小。因此,需要將節點分成若干個SKB。為了跟蹤該信息,使用了「gscan」結構中的「tot_count」和「tot_consumed」欄位。「tot_count」欄位表示在節點內聯數組中嵌入的掃描結果條目的總數,「tot_consumed」欄位表示到目前為止消耗(傳輸)的條目數。因此,該函數在處理列表時對其內容進行了略微修改。其實質上執行不變數,每個處理節點的「total_consumed」欄位將被修改,以匹配其「tot_count」欄位。至於正在傳輸的數據及其打包方法,為簡潔起見,我們將跳過這些細節。然而,重要的是要注意,除了上述副作用之外,該函數的危害似乎微乎其微(也就是說,無法從其「開採」進一步的原語)。在所有事件均被打包到SKB中並被傳送到任何監聽器后,就可以將其回收了。這可以通過審查列表並在每個條目上調用「kfree」來實現。 總而言之,這使我們在利用方面處於何種位置?假設我們選擇使用溢出原語覆蓋其中一個「gscan」條目,那我們可以修改其「next」欄位(或者說必須,因為其是結構中的第一個欄位),並將其指向任意地址。這將導致處理函數將該任意指針視作列表中的一個元素而予以使用。圖17 由於處理函數的不變數——在處理特製的條目之後,其第7個位元組(「tot_consumed」)將被修改,以匹配其第6個位元組(「tot_count」)。此外,處理鏈之後,指針將被kfreed。更重要的是,回想一下,處理函數迭代整個條目列表。這意味著,特製條目(其「next」欄位)中的前四個位元組必須指向包含「有效」列表節點的另一個內存位置(隨後必須滿足相同的約束),或必須保持值0( NULL)——表示這是列表中的最後一個元素。 這看起來不容易...有很多限制我們需要考慮。如果我們故意選擇忽略kfree一段時間,我們可以嘗試搜索前四個位元組為零、有利於修改第七個位元組(以匹配第六個位元組)的內存位置。當然,這只是冰山一角,我們可以反覆觸發相同的原語,從而將位元組反覆向左複製一位。或許,如果我們能找到一個有足夠的零位元組和足夠的我們選擇的位元組的內存地址,我們就可以通過連續使用這兩個原語來製作一個目標值。 為了衡量這種方法的可行性,我已經在一個小的SMT實例中對上述限制進行了編碼(使用Z3),並提供了來自內核的實際堆數據,以及各種目標值及其對應的位置。此外,由於內核的轉換表存儲在內核VAS中的一個不變地址,對其進行略微修改也可能導致可利用的條件,所以其內容(以及相應的目標值)也被添加到了SMT實例中。當且僅當任何目標值可在不超過十個「步驟」(每一步都是原語的調用)內佔用任何目標位置時,該實例滿足條件。不幸的是,結果相當嚴峻...似乎這種方法不夠強大。 此外,雖然這個想法在理論上可能很好,但實際上並不奏效。要知道,在任意地址調用kfree並不是沒有其副作用。包含內存地址的頁面必須標記為「slab」頁面或「compound」。這通常僅適用於slab分配算符實際使用的頁面。嘗試在沒有標記為此的頁面中的地址調用kfree會觸發內核恐慌(從而會導致設備崩潰)。 也許,相反,我們可以選擇忽略其他約束並專註於kfree?實際上,如果我們始終能找到一個其數據可用於利用目的分配,那麼我們就可以嘗試釋放該內存地址,然後使用我們的堆整形原語「重新捕獲」它。然而,這又引起了幾個其他問題。首先,我們始終能找到一個常駐slab地址嗎?其次,即使我們能找到這樣一個地址,其肯定與per-CPU緩存相關聯,意味著釋放它不一定能讓我們可以稍後回收。最後,無論我們選擇瞄準哪個分配,都必須滿足上面的約束——即前四個位元組必須為零,第7個位元組將被修改為與第6個位元組匹配。 然而,這正是我們可以巧妙利用之處。回想一下,kmalloc保持一些固定大小的緩存。然而,當請求更大的分配時會發生什麼?事實證明,在這種情況下,kmalloc只返回一連串的空閑頁面(使用__get_free_pages)並將其返回給調用者。這是在沒有任何per-CPU緩存的情況下完成的。因此,如果我們能夠釋放一個大的分配,那我們應該能夠在不必首先考慮哪個CPU進行的分配的情況下回收它。 這可能解決了親和性的問題,但它仍然無法幫助我們找到這些分配。不幸的是,slab緩存在內核引導過程中被分配得相當晚,而且其內容非常「嘈雜」。這意味著猜測slab中的一個地址也是非常困難的,對於遠程攻擊者而言更甚。但是,使用大分配流的早期分配(即使用__get_free_pages創建的)始終駐於相同的內存地址!也就是只要其在內核初始化期間發生得足夠早,因此沒有非確定性事件同時發生。 結合這兩個事實,我們可以搜索一個大的早期分配。在跟蹤大型分配路徑並重新引導內核后,似乎確實有很多這樣的分配。為了幫助導航此大的蹤跡,我們還可以使用一個特殊的GCC插件來編譯Linux內核,該插件可輸出內核中使用的每個結構的大小。使用這兩個蹤跡,我們可以快速導航早期大分配,並嘗試搜索潛在的匹配。 遍歷列表后,我們碰到一個看似有趣的條目:

圖18 匯總 在bcmdhd驅動程序初始化期間,其調用wiphy_new函數來分配一個wl_priv實例。該實例用於保存與驅動程序操作相關的大部分元數據。但是,還有一點詭異的數據隱藏在該結構中——用於處理傳入的事件幀的事件處理函數指針數組。事實上,我們之前討論的同一表格(evt_handler)存儲在該對象中。這將我們引向了利用的直接路徑——只需kfree這個對象,然後發送一個SWC事件幀來回收它,然後用我們自己的任意數據填充它。 然而,在我們這樣做之前,我們需要確保該對象滿足處理函數所要求的約束。也就是說,前四個位元組必須為零,我們必須能夠修改第7個位元組以匹配第6個位元組的值。第二個約束根本不構成任何問題,但第一個約束是個大問題。如前所述,前四個位元組不為零,但實際上指向與驅動程序相關的一個函數指針塊。這是否意味著我們完全不能使用這個對象? 不是的——碰巧,我們還有一個訣竅!事實證明,當kfree一個大的分配時,kfree的代碼路徑不需要傳入的指針指向分配的起始。相反,其只是提取與分配相對應的頁面,然後釋放它們。這意味著通過指定位於匹配約束的結構中的地址,我們將既能滿足處理函數提出的要求,又可以釋放基礎對象。太棒了。圖19 綜合起來,我們現在可以發送一個SWC事件幀,以回收evt_handler函數指針數組,並用我們自己的內容填充它。由於沒有KASLR,我們可以在內核映像中搜索一個堆棧樞紐小工具,其可以使我們實現代碼執行。出於利用目的,我已選擇用堆棧樞紐將WLC_E_SET_SSID的事件處理程序替換為事件幀本身(當執行事件處理程序時存儲在R2中)。最後,通過替換專門設計的WLC_E_SET_SSID類型的事件幀中的 ROP棧,我們現在可以控制內核線程的執行,從而可完成我們的利用。圖20 你可以在此處找到該漏洞的一個利用示例。其包括一個只調用printk的短ROP鏈。該利用方法針對使用自定義內核版本的Nexus 5構建。要修改該方法以適用於不同的內核版本,你需要填入適當地符合(symbols.py下)。此外,雖然原語仍然存在於64位設備中,但為了針對那些平台調整利用方法,可能還需要額外的工作。 接下來,讓我們轉到本文的第二部分。 第2部分——「較易」方式 能有多簡單? 雖然我們已經看到Wi-Fi固件和主機之間的高級別通信協議可能會受到影響,但我們也看到,要編寫一個完全有效的利用方法委實不易。實際上,上述利用方法需要有關目標設備的足夠信息(比如符號)。此外,利用期間的任何錯誤都可能導致內核崩潰,這會導致設備重新啟動,這要求我們從頭再來。這一事實,再加上我們對Wi-Fi SoC的瞬態控制,使這些類型的利用鏈很難可靠地利用。 也就是說,到目前為止,我們只考慮了固件暴露的高級別攻擊面。實際上,我們是將Wi-Fi SoC和應用處理器作為兩個彼此完全獨立的不同實體。實際上,沒有什麼可以遠離真相。Wi-Fi SoC和主機不僅物理上彼此接近,還共享物理通信介面。 如前所述,Broadcom生產支持各種介面的SoC,包括SDIO、USB及PCIe。雖然SDIO介面過去很受歡迎,但近年來已不再受移動設備青睞。SDIO「消失」的主要原因是其傳輸速度有限。Broadcom的BCM4339 SoC支持SDIO 3.0,這是一個相當高級的SDIO版本。儘管如此,其理論最大匯流排速度僅為104 MB/s。另一方面,802.11ac的理論最大速度為166 MB/s——遠超SDIO。

圖21 BCM4339框圖 傳輸速率的提高使得PCIe成為用於在現代移動設備中連接Wi-Fi SoC的最流行的介面。與PCI不同,PCIe是基於點對點拓撲。每個設備都有將自身連接到主機的自己的串列鏈路。由於這種設計,PCIe的每通道速率遠高於PCI上的同等速率(因為匯流排訪問不需要仲裁),PCIe 1.0在單個通道上的吞吐量為250 MB / s(與通道數呈線性關係)。 我們來具體看看現代移動設備中PCIe的採用率。以Nexus手機為例,從Nexus 6開始,所有設備都使用PCIe介面(不再是SDIO)。同樣,所有iPhone也從iPhone 6開始使用PCIe。三星旗艦設備Galaxy從 S6開始使用PCIe。 介面隔離 那麼,為什麼該信息與我們的追求有關?PCIe在隔離方面與SDIO和USB顯著不同。SDIO在不進入每個介面的內部的情況下就允許串列傳輸小命令「數據包」(在CMD引腳上),可能伴隨數據(在DATA引腳上)。SDIO控制器然後解碼命令並相應響應。雖然SDIO可以支持DMA,但該功能不在移動設備上使用,並不是SDIO的固有部分。此外,BCM SoC上的低級SDIO通信由「SDIOD」內核處理。為了製作特殊的SDIO命令,我們很可能需要先獲得對該控制器的訪問權。 同樣,USB(最高3.1版)不包括對DMA的支持。USB協議由主機的USB控制器進行處理,該控制器執行所需的內存訪問。當然,可能可以破壞USB控制器本身,然後將其介面用於內存系統,以獲得內存訪問權。比如,在Intel Hub Architecture上,USB控制器通過能夠進行DMA的PCI連接到PCH。但這種攻擊也相當複雜,僅限於特定的架構和USB控制器。 與這兩個介面相比,PCIe允許通過設計進行DMA。這允許PCIe以極高的速度運行,而不會導致主機的性能下降。一旦數據傳輸到主機的內存,就會觸發一個中斷來指示該工作需要完成。 在事務層上,PCIe通過發送小批量的數據(適當命名為「事務層包」(TLP))進行操作。每個TLP可以由交換機網路路由,直到其到達預定外圍設備為止。然後外圍設備解碼數據包並執行請求的內存操作。TLP的header編碼這是否是請求的讀取或寫入操作,其body包含與請求相關的任何伴隨數據。

圖22 事務層包(TLP)的結構 IOU一個MMU 雖然PCIe支持通過設計實現DMA,但這並不意味著連接到外圍設備的任何PCIe都應該能夠自由訪問主機上的任何內存地址。事實上,現代架構在將外設連接到主存儲器的IO匯流排上具有額外的內存映射單元(IOMMU),因為具有針對支持DMA的外設的防禦能力。 ARM指定其自己的IOMMU版本,稱為「系統內存映射單元」(SMMU)。使用SMMU的其中一個目的是管理暴露於不同SoC組件的內存視圖。簡而言之,每個內存事務流都與「流ID」相關聯。然後,SMMU執行稱為「上下文確定」的一個步驟,以便將流ID轉換為相應的內存上下文。 使用內存上下文,SMMU便能夠將內存操作與包含請求設備的映射的轉換表相關聯。很像常規的ARM MMU,查詢轉換表是為了將輸入地址(虛擬地址或中間物理地址)轉換為相應的物理地址。當然,期間SMMU也確保請求的內存操作實際上被允許。如果這些步驟中的任何一個失敗,就會產生故障。

圖23 雖然這在理論上很好,但並不意味著SMMU實際上在實踐中被使用。不幸的是,移動SoC是專有的,因此很難確定SMMU實際上如何和在哪裡就位。話雖如此,我們仍然可以從公開的信息中獲取一些洞察力。通過查看Linux內核中的IOMMU綁定,我們可以看到,顯然,Qualcomm和三星都有自己的SMMU專有實現,有其自己獨特的設備樹綁定。但是,可疑的是,Broadcom Wi-Fi晶元的設備樹條目似乎缺少這些IOMMU綁定... 相反,Broadcom的主機驅動程序(bcmdhd)也許在每個外圍存儲器訪問之前手動配置SMMU?為了回答這個問題,我們需要仔細看看通過PCIe使用的通信協議的驅動程序實現。Broadcom實現其自己的稱為「MSGBUF」的專有協議,以便通過PCIe與Wi-Fi晶元進行通信。主機的協議實現和處理PCIe的代碼分別可以在dhd_msgbuf.c和dhd_pcie.c下找到。 查看代碼后,我們獲得了對通信協議的內部工作機制的一些關鍵了解。首先,與預期一致,驅動程序掃描PCIe介面,訪問PCI配置空間,並將所有共享資源映射到主機的內存中。接下來,主機分配一組「環」。每個環均由DMA相干內存區域支持。MSGBUF協議將四個環用於數據流,一個環用於控制。每個數據路徑(RX或TX)都有兩個相應的環——一個用於指示請求的提交,另一個用於指示其完成。然而,到目前為止,仍然沒有提到驅動程序中的SMMU。也許我們要更深入的挖掘... 那麼Wi-Fi晶元如何了解這些環的位置?畢竟,到目前為止,其只是在驅動程序中分配的一堆物理連續的緩衝區。查看驅動程序的代碼后可知,主機和晶元似乎擁有共享的結構,pciedev_shared_t,包含所有PCIe相關元數據,包括每個環形緩衝區的位置。主機保持其自己的該結構的副本,但Wi-Fi SoC在何處保持其副本?根據dhdpcie_readshared函數,似乎Wi-Fi晶元在其RAM的最後四個位元組中存儲了一個指向此結構的指針。圖24 我們來繼續看看結構的內容。為了略微簡化這個過程,我寫了一個使用固件RAM快照(使用dhdutil生成)小腳本,從RAM的末端讀取指向PCIe共享結構的指針,並轉出相關的信息:

圖25 在rings_info_ptr欄位之後,我們還可以轉儲有關每個環的信息,包括其大小、當前索引及物理內存地址:

圖26 我們可以看到,這些緩衝區中指定的內存地址實際上似乎是主機內存中的物理內存地址。這有點可疑...在SMMU存在的情況下,晶元應該使用完全不同的地址範圍(應該由SMMU轉換為物理地址)。但是,僅僅是懷疑是不夠的,為了檢查SMMU是否存在(或活躍),我們需要設置一個小實驗! 回想一下,對於RX和TX路徑,MSGBUF協議使用上述環形緩衝區來指示事件的提交和完成。實質上,在幀傳輸期間,主機寫入TX提交環。一旦晶元傳輸幀,其便寫入TX完成環,以指示此情況。同樣,當接收到幀時,固件寫入RX提交環,隨後主機在接收到幀時寫入RX完成環。 如果是這樣,如果我們修改對應於固件的PCIe元數據結構中的TX完成環的環地址,並將其指向任意的內存地址,結果會如何?如果SMMU就位,並且所選的內存地址未映射到Wi-Fi晶元,則SMMU將生成故障,並且不會進行任何修改。但是,如果沒有SMMU,我們就應該能夠通過從主機轉儲相應的物理內存範圍(例如,通過使用/dev/mem)來觀察此修改。這個小型實驗還讓我們可以暫時不用對Wi-Fi固件的MSGBUF協議的實現進行逆向工程,該逆向工程毫無疑問是相當繁瑣的。 為了使事情更有趣,讓我們修改TX完成環的地址,以指向Linux內核代碼段的起始(Nexus 6P上的0x80000:見/proc/iomem)。在產生一些Wi-Fi流量並檢查物理內存的內容之後,我們得到以下結果:

圖27 哈哈!Wi-Fi晶元成功DMA到包含主機內核的物理地址範圍,沒有任何干擾!這最終證實了我們的懷疑,要麼不存在SMMU,要麼其沒有被配置為可防止晶元訪問主機的RAM。 這種訪問不僅不需要漏洞,還可以更可靠地利用。不需要確切的內核符號或任何其他初步信息。Wi-Fi SoC可以使用其DMA訪問來掃描物理地址範圍,以定位內核。然後,其可以識別RAM中內核的符號表,分析它來定位其所需的任何內核函數,並通過覆蓋其代碼來劫持該函數(在類似的類DMA攻擊中可以看到一個這樣的示例)。總而言之,這種攻擊風格完全可移植且100%可靠,相比我們看到的以前的利用方法是一個重大的升級。 我們可以到此為止,不過讓我們再稍作努力,以便稍微更好地控制這個原語。雖然我們能DMA進主機的內存,但此時我們是相當「盲目地」實現的。我們不控制正在寫入的數據,而是依靠Wi-Fi固件的MSGBUF協議的實現來破壞主機的內存。通過進一步研究,我們應該能夠弄清Wi-Fi晶元上的DMA引擎是如何工作的,並手動利用它來訪問主機的內存(而不是依賴如上所示的副作用)。 那麼我們從哪裡開始?搜索「MSGBUF」字元串,我們可以看到與協議相關的一些初始化常式,這是特殊「回收」區域的一部分(因此僅在晶元初始化期間使用)。然而,對這些函數進行逆向工程后表明,其引用Wi-Fi晶元RAM中的一組函數。幸運的是,這些函數的一些名稱存在於ROM中!其名稱似乎很相關:「dma64_txfast」、「dma64_txreset」——看起來我們在正確的軌道上。我們再一次避免了一些逆向工程的努力。Broadcom的SoftMAC驅動程序brcmsmac包含這些確切函數的實現。雖然我們可以預期有一些差異,但總體思路應保持不變。 梳理代碼后發現,似乎對於每個具有DMA能力的源或接收器,都存在一個相應的DMA元數據結構,稱為「dma_info」。該結構包含指向DMA RX和TX寄存器的指針,以及插入DMA源或目標地址的DMA描述符環。另外,每個結構都被分配一個用於標識自身的8位元組的名稱。更重要的是,每個dma_info結構都以指向包含DMA函數的RAM函數塊的指針開始——與我們之前確定的塊相同。因此,我們可以通過在Wi-Fi SoC的RAM中搜索這個指針來定位這些DMA元數據結構的所有實例。圖28 現在我們知道了這些元數據結構的格式,並且有辦法找到它們,所以我們可以嘗試搜索對應於從Wi-Fi晶元到主機的DMA TX路徑的實例。 不過,這說易行難。畢竟,我們可以預期找到這些結構的多個實例,因為Wi-Fi晶元可對多個源和接收器進行正向和反向DMA。比如,固件可能使用SoC內部DMA引擎來訪問內部RX和TX FIFO。那麼我們如何識別正確的DMA描述符? 回想一下,每個描述符都有一個關聯的「名稱」欄位。我們來搜索RAM中的所有DMA描述符(通過搜索DMA函數塊指針),並輸出每個實例的相應名稱:

圖29 太好了!雖然有一些可能在內部使用的難以歸類的dma_info實例(和懷疑的一樣),但也有兩個實例似乎對應於主機到設備(H2D)和設備到主機(D2H)DMA訪問。由於我們對DMA進主機的內存感興趣,所以我們來仔細看看D2H的結構:

圖30 注意,RX和TX寄存器指向Wi-Fi固件的ROM和RAM之外的區域。實際上,其指向對應於DMA引擎寄存器的背板地址。相比之下,RX和TX描述符環指針確實指向SoC的RAM中的內存位置。 通過審查brcmsmac中的DMA代碼和主機驅動程序中的MSGBUF協議實現,我們最終得以將細節拼湊起來。首先,主機使用MSGBUF協議將物理地址(對應於SKB)發送到晶元。然後由固件的MSGBUF實現將這些地址插入DMA描述符環中。一旦環被填充,Wi-Fi晶元就會寫入背板寄存器,以便「啟動」DMA引擎。然後,DMA引擎將審查描述符列表,並在DMA訪問的當前環索引處消耗描述符。一旦DMA描述符被消耗,其值便被設置為一個特殊的「魔術」值(0xDEADBEEF)。 因此,為了操縱DMA引擎寫入我們自己的任意物理地址,我們需要做的就是修改DMA描述符環。由於MSGBUF協議在幀來回發送時始終運行,所以描述符環快速變化。如果我們可以「鉤住」DMA TX流程中調用的其中一個函數,那我們就可以用我們自己設計的值快速替換當前的描述符。 幸運的是,dmx64_txfast函數位於ROM中,其序言從指向RAM的分支開始。這使我們可以使用上一篇博文中的補丁程序來掛接這個函數,然後執行我們自己的shellcode存根。我們來寫一個小存根,以審查D2H DMA描述符,並將每個非消耗的描述符更改為我們自己的指針。通過這樣做,對DMA引擎的後續調用應將接收到的幀的內容寫入上述地址。在應用補丁並生成Wi-Fi流量后,我們收穫了以下結果:

圖31 我們成功將任意數據DMA到了我們選擇的地址。使用該原語,我們終於可以用我們自己製作的數據來劫持任何內核函數。 最後一點,上述實驗是在Nexus 6P(基於Qualcomm的Snapdragon 810 SoC)上進行的。這引起了一個問題:也許不同的SoC會展現不同的行為?為了測試這個理論,讓我們在Galaxy S7 Edge(基於三星的Exynos 8890 SoC)上重複相同的實驗。 使用先前披露的許可權提升將代碼注入到system_server中,我們可以直接發出與bcmdhd驅動程序交互所需的ioctl,從而取代了上述實驗中由dhdutil提供的晶元內存訪問功能。同樣,利用先前披露的內核利用方法,我們能夠在內核中執行代碼,使我們能夠觀察內核代碼段的更改。 綜合起來,我們可以提取Wi-Fi晶元(BCM43596)的ROM,對其進行檢查,並按照上述方法定位DMA函數。然後我們可以插入相同的掛鉤,將任何未消耗的DMA RX描述符指向內核代碼的物理地址。安裝掛鉤併產生一些Wi-Fi流量后,我們觀察到以下結果:圖32 我們又一次可以自由DMA進內核(期間繞過了RKP保護)。似乎三星的Exynos 8890 SoC和Qualcomm的Snapdragon 810要麼缺乏SMMU,要麼未能使用SMMU。 結束語 總而言之,我們已經看到,可以而且應該改進主機和Wi-Fi SoC之間的隔離。雖然主機和晶元之間的通信協議存在缺陷,但是經過一定時間最終可以予以解決。然而,目前缺乏對流氓Wi-Fi晶元的保護令人擔憂。 由於移動SoC是專有的,因此當前這一代的SoC是否能夠促進這種隔離仍然是未知數。我們希望確實有能力實現內存保護(比如通過SMMU方式)的SoC儘快選擇這樣做。對於不能這樣做的SoC,也許這項研究將成為設計下一代硬體的促進因素。 目前的缺乏隔離也可能會產生一些令人驚訝的副作用。例如,能夠與Wi-Fi固件交互的Android上下文可以利用Wi-Fi SoC的DMA能力來直接劫持內核。因此,這些上下文應該被認為是「具有內核許可權」,我認為,目前安卓的安全架構還沒有作出這樣的假設。 固件日益複雜,Wi-Fi在不斷向前邁進,這兩者表明固件bug可能還要徘徊很長一段時間。該假設有事實的支持——即使對固件進行相對淺層的檢查也可以發現很多bug,且都可以被遠程攻擊者利用。 雖然內存隔離本身有助於防禦流氓Wi-Fi SoC,但固件的防禦也可以支持攻擊。目前,固件缺乏利用緩解措施(如堆棧cookie),並沒有充分利用現有的安全機制(如MPU)。希望未來的版本能通過實施現代利用緩解措施和採用SoC安全機制來更好地防範這種攻擊。



熱門推薦

本文由 yidianzixun 提供 原文連結

寵物協尋 相信 終究能找到回家的路
寫了7763篇文章,獲得2次喜歡
留言回覆
回覆
精彩推薦