作者:Gh0u1L5
本文為作者投稿,Seebug Paper 期待你的分享,凡經采用即有禮品相送!
投稿郵箱:paper@seebug.org

9月27號,黑客 ani0mX 在推特上公布了蘋果公司的“史詩級安全漏洞”。該漏洞的影響范圍極其廣泛,囊括了絕大部分型號的蘋果手機、平板、手表及智能電視等。而且由于它是一個“半硬件層”的漏洞,所以蘋果永遠無法通過軟件更新修補這個漏洞。

漏洞發布當天,我在推特上看到另一名黑客 littlelailo 公布了一段30多行的草稿,簡略聊了聊 checkm8 的攻擊原理。我以為接下來很快國內安全社區也會有人放出更多細節,然而等了很久也沒等到,索性自己開篇文章聊聊吧。我希望這篇文章:

  1. 能夠讓讀者對 iPhone 啟動機制有簡單的了解。
  2. 能夠讓讀者初步掌握如何逆向 iPhone 的 Secure ROM 。
  3. 能夠講明白 checkm8 漏洞攻擊的基本思路和相關技術。

只要不以盈利為目的,任何個人或組織均可在注明出處的情況下自由轉載,轉載前通過評論區/私信簡單告知本人即可。


0x00 iOS 安全啟動機制簡介

已熟知 iOS 安全啟動鏈與 Secure ROM 防護機制的讀者可直接跳過本節。

為了保證 iOS 系統的代碼不被惡意篡改,蘋果公司使用了一套名為 安全啟動鏈(Secure Boot Chain) 的技術。他們將開機過程分為四到五個階段,每個階段負責檢查下個階段的代碼,如果檢查出任何問題,比如簽名錯誤、安全模式不符,就立馬中止開機。

在一些過時的資料里, iPhone 的開機過程分為以下五個階段:

iOS Secure Boot Chain Outdated

雖然這五個階段被人引用過很多次,但其實它已經錯了三年多了。從 A10 處理器以來,蘋果就已經放棄了雙階段加載,也就是說上圖的那個LLB已經被刪掉了,更新后的啟動流程如下:

iOS Secure Boot Chain Latest

這四個階段從左到右分別是:

  • ROM / Secure ROM:開機啟動時執行的第一段程序,負責檢查并加載接下來的 iBoot 。
  • iBoot:蘋果開發的引導程序,負責檢查并加載系統內核。
  • Kernel:iOS 系統內核。
  • OS:iOS 系統的用戶界面、后臺服務等非核心組件。

Secure ROM 作為系統啟動時執行的第一段程序,扮演著整個安全啟動鏈技術的信任基石。 一旦攻破了它,接下來所有階段的代碼都能隨意篡改,因此蘋果公司下了很大功夫來保護這段 ROM 程序:

封殺寫權限 :這段程序燒寫在 CPU 的硅片內部,無法拆解,無法替換。在工廠里一次性燒錄完之后,就連蘋果自己都沒辦法改動它。

封殺讀權限 :這段程序完成工作后,會直接把自己所在的儲存器鎖掉,再沒有任何辦法能讀取它。也就是說,啟動之后哪怕你攻陷了整個系統,也讀不到這段程序的內容。

蘋果的想法很單純——如果一段程序黑客讀都讀不到,改也改不了,那么這段程序應該就會很安全。等到文章結尾的時候,我會再花點筆墨聊聊這個想法為什么不現實。但現在,蘋果的這些安全措施確實給我們造成了一點麻煩:我們連程序內容都看不到,怎么分析程序漏洞?


0x01 抓取 Secure ROM

剛剛我們提到, Secure ROM 完成工作后,才會把儲存器鎖住。換句話說,只要 Secure ROM 還沒完成工作,我們就有機會從內存里讀到它的內容。

如何抓住這個機會呢?這就輪到 checkm8 出場了。

checkm8 是一個任意代碼執行漏洞,允許我們在 ROM 運行期間植入 payload。更貼心的是, ani0mX 還在自己發布的 exploit 里附上了一段高質量的 payload 。允許我們通過 USB 給 payload 發送指令,執行各種高權限的操作,比如:

  • ./ipwndfu --dump-rom將 iPhone 的 Secure ROM 直接從內存里抓取出來,保存為文件。
  • ./ipwndfu --demote:啟用 JTAG 模式。配合一條5800多元的 Bonobo 線,你就可以用 gdb 隨意調試 iPhone 內核了。如果公司或者實驗室給報銷的話,我真的強烈建議買一條(笑)。

另外, axi0mX 的 payload 里還有一個 execute 命令非常好用,但是沒有放出命令行接口,只能自己寫 Python 代碼來調用。這個命令允許你調用內存里存在的任意函數,能傳遞參數,還能拿到返回值。但他的代碼有個問題,傳第8個參數的時候會傳成第7個,用之前需要自己動手改一下。

好了,書歸正傳,在 checkm8 的幫助下,竊取蘋果公司層層保護的代碼僅需三步:

  1. 使用網上搜到的按鍵組合,把 iPhone 手機重啟到 DFU 模式(固件升級模式)。
  2. 執行./ipwndfu -p命令植入 payload,如果顯示漏洞利用失敗的話就多試幾次。
  3. 執行./ipwndfu --dump-rom命令讀取 ROM 并保存到當前文件夾下,完工。

這套操作,真的,猴子訓練一下都能做。checkm8 光靠這一個功能,我覺得就無愧于“史詩級”這個評價了。

成功拿到 ROM 的二進制機器碼之后,接下來扔給反編譯器就可以了。

蘋果的 CPU 從 A7 開始都是 AArch64 架構, little-endian 字序, ROM 的起始地址都是0x100000000,設定好這三項之后,反編譯器就能直出正確的匯編代碼了。

iOS Secure ROM Decompile Result

除了反編譯得到的這些代碼外,網上還有一些開源的 iBoot 項目,以及蘋果某實習生泄露出來的一份四五年前的舊版 Secure ROM 代碼,這些材料對我們的逆向分析都非常有幫助。

但是,由于發布這些泄露代碼鐵定會吃一張蘋果的律師函,所以我不會在這篇文章里引用或發布那份泄露代碼,有需要的讀者還請自己動手搜索一下。

最后要說的是,剛才那套輕松的招數最多只能用到 iPhone X 上,從 Xs / Xr 開始 checkm8 漏洞就沒法用了。對于這些手機,目前我們也沒有什么好辦法,只能用黑盒測試、舊 ROM 代碼和 iBoot 代碼這三樣湊活著挖漏洞。

iBoot 的代碼能用來挖 ROM 的漏洞,是因為 iBoot 和 ROM 有一部分功能重疊,所以代碼也有重疊。比如這次的 checkm8 漏洞,就是 ani0mX 在分析一個 iBoot 補丁的時候發現的。

至于解密 iBoot 的具體方法,因為好像有點偏題了,所以將來有機會的話再開篇文章講講吧。有興趣的讀者可以先自行了解一下 iOS 的 GID Key、IMG3/IMG4、KBAG 這幾個概念。


0x02 漏洞原理解析

我前文中提到過,利用 checkm8 前需要先把手機重啟到 DFU 模式,因為這次的漏洞正是出在這個 DFU 模式上。

蘋果的 DFU 模式大致相當于一個“應急啟動模式”,重啟到這個模式后,用戶可以用 USB 傳入一個臨時系統,用臨時系統開機啟動。(當然,這個臨時系統必須是蘋果官方系統。)

基于 littlelailo 的草稿、 iPhone 8 的逆向結果,以及一些“開源”的 iBoot 項目,我整理出了 DFU 應急啟動的八個步驟:

  1. 手機以 DFU 模式開機后,負責處理 USB 的主模塊會先調用usb_dfu_init()函數,初始化 DFU 子模塊。初始化過程主要做兩件事:
    1. 分配一塊 2048 字節的內存作為緩沖區,我們叫它io_buffer
    2. 把 DFU 事件處理函數提交給 USB 驅動 ,等待用戶發來的 DFU 請求。
  2. 當用戶想要加載臨時系統時,會先發送一個DFU_DNLOAD請求。主模塊將它轉發給 DFU 事件處理函數。
  3. DFU 檢查這個請求,如果用戶想要發來一段長度為wLength的數據,那么 DFU 將會檢查wLength是否超過 2048 字節。
    • 超過的話,發送一個 STALL 包掐斷 USB 會話,向主模塊返回-1。
    • 不超過的話,用指針將 io_buffer傳遞給一個全局變量 ,向主模塊返回wLength
  4. 主模塊把wLength等信息記錄到另一個全局變量中,為接下來接收數據做好準備。
  5. 用戶接下來將數據陸續發送給主模塊,主模塊將這些數據復制到io_buffer中。等到所有的數據都接收完畢后,主模塊通知 DFU 模塊處理這些數據。
  6. DFU 模塊拿到io_buffer,確認里面數據的長度確實是用戶剛開始允諾的wLength,然后將這些數據復制到臨時系統的加載地址,比如0x18001C000(iPhone 8/X)。
  7. 緩沖區數據處理完畢之后,主模塊 清空之前的所有全局變量 ,準備接受下一個 USB 請求。
  8. 當用戶分批發送完臨時系統的所有內容后,會發送一個DFU_DONE請求。主模塊將它轉發給 DFU ,通知 DFU 開機,于是 DFU 模塊 釋放掉io_buffer ,嘗試開機。如果開機失敗,再次執行usb_dfu_init(),開始第二輪 DFU 啟動。

有了我加黑標粗的幾個關鍵點,有人也許已經能看出來這次漏洞的原理了。

第3步 DFU 將io_buffer地址記錄到了一個全局變量里,如果用戶接著發送一個DFU_DONE請求的話,5~7步就會被直接跳過。第8步 DFU 釋放掉io_buffer這塊內存,開機失敗跳回到第1步,開始第二輪 DFU 啟動。這時之前那個全局變量記錄的,還是已經釋放掉的io_buffer,這就構成了一個 Use-After-Free 漏洞。

在這個 UAF 漏洞的基礎上,只要找到一個合適的攻擊目標,用堆風水引導 malloc 把攻擊目標分配到io_buffer上,就能通過寫io_buffer修改這個攻擊目標的內容了。

說到這里,我忍不住想說句八卦。 littlelailo 在推特上抱怨說,自己早在今年3月就發現了 checkm8 漏洞,但由于他只攻破了 A8 和 A9 處理器,所以就沒掀起什么波瀾。我沒看過他的攻擊代碼,不知道跟 ani0mX 的代碼比起來到底差了哪里。但既然大家原理一模一樣,那搞不好就是堆風水的時候出了差別。由此可見,玩風水的造詣確實是能決定一個黑客的運勢,古人誠不欺我啊。

最后,給想要自己逆向的讀者指個路吧。在 iPhone 8 / iPhone X 的 ROM 中,幾個關鍵函數的位置分別位于:

  • USB 主模塊代碼:0x10000B24C
  • DFU 請求處理代碼:0x10000BCCC
  • DFU 數據處理代碼:0x10000BEF4

0x03 構建ROP

這一節我其實本來想順著聊聊 checkm8 里面堆風水的處理的,然而由于我這篇文章寫得三天打魚兩天曬網,所以寫到這里的時候外網已經有人發文章詳細討論過 checkm8 堆風水的處理了,還配了好看又細致的插圖。那我覺得就沒必要再寫一遍了,反正也寫不過人家,干脆直接貼個鏈接(Technical analysis of the checkm8 exploit),然后往下跳到構建 ROP 的部分。

為了構建 ROP 調用鏈, ani0mX 盯上了一個名叫usb_device_io_request的數據結構。這個數據結構里面保存著發給 USB 驅動的 IO 請求,正常情況下,USB 驅動會挨個處理這些請求,完成數據收發。但是如果用戶要求重置 USB 會話的話,驅動就會 一口氣清空所有請求 ,并且 調用每個請求的回調函數

通過逆向 iPhone 8 的 Secure ROM ,我整理出了這個請求的具體數據結構:

struct usb_device_io_request

這個結構里面,我們主要看兩個成員:

  • next指針,用來指向下一個要處理的請求對象,構成一串請求鏈表。
  • callback回調函數,雖然圖里我把它標成一個void *,但它實際的類型是一個函數指針,void (*callback) (struct usb_device_io_request *io_request)

整個攻擊思路是這樣的:

  1. 構建一串假的 IO 請求,讓它們的callback依次指向我們想執行的 gadget 。
  2. 布置一套堆風水布局,操縱 malloc 把一個真請求放到我們掌控的io_buffer上。
  3. io_buffer寫數據,把那串假請求寫進內存,接到真請求的后面。
  4. 發送 USB reset 請求,重置會話,讓 USB 驅動執行 ROP 鏈。

有了這套思路之后,剩下的就是選 gadget 之類的細節了,我們暫不贅述。至此,checkm8 的攻擊原理已經算是基本揭露完了。

想要自己動手逆向本節內容的讀者,我再給你們指個路吧。在iPhone 8 / X 的 Secure ROM 中,幾個關鍵的函數分別位于:

  • USB 主模塊 reset 請求處理函數:0x10000B84C
  • USB 驅動 reset 請求處理函數:0x100004A44

0x04 后記

從這次的 checkm8 漏洞里我們能學到什么?

首先,我覺得最重要的一點就是再次強調了那個業界共識:“保密不等于安全”。

當然啦,一定會有人反問我:蘋果的這套保密體系不是效果很好嗎?這么顯眼的漏洞,將近十年都沒被黑客發現啊?這還不夠安全嗎?

然而我們要注意一點, ROM 漏洞并不是將近十年 沒人發現 ,而是將近十年 沒人公布 ,這兩字之差就是天壤之別。

在漏洞挖掘這個領域,大家所求的東西各不相同,但頂尖玩家一般就三種:有求名的,比如騰訊、360、知道創宇這些公司的實驗室,需要 Apple、Google 時不時發感謝信來維護實驗室的招牌。有求財的,比如 Zerodium 這些網絡軍火商,同樣的漏洞蘋果頂多懸賞 20~100 萬美金,而這幫軍火商開口就是150萬美金,因為這些漏洞落到他們手里能變現出更大的利益。剩下一批頂尖玩家是各國的國家隊,揣著明確的軍事目標在挖掘漏洞。

當某個產品漏洞挖掘的門檻抬得過高時(比如 Secure ROM),各家實驗室會迫于經營壓力/指標壓力,轉去尋找更好拿下的山頭。整個賽場上就只剩下軍火商和國家隊,這兩種人目標明確,蘋果懸賞區區50萬、100萬根本打動不了他們,挖出的漏洞也就全被他們悄悄吞下來了。

所以對于大公司來說,最好的安全策略其實是擁抱透明,把求名的伙計們更多地拉下場,把愿意公布漏洞賺干凈錢的白帽黑客拉下場。如果 Apple 采用這個戰略的話, checkm8 可能根本沒機會發展成一個橫跨7、8代蘋果產品的史詩級漏洞,而是會在 iPhone 5、iPhone 6 發布的時候就被騰訊玄武實驗室之類的白帽組織報了出來。然后蘋果只要發發錦旗、奉上20萬50萬美元的賞金,事情就解決了,哪有今天這個尷尬局面?

其次,我覺得這個漏洞還說明了一點:對所有出現數據吞吐的地方,都應該進行細致的 fuzz 測試。

這次 checkm8 的成因,主要是對 USB 請求處理不當造成的 UAF 漏洞。個人感覺這個完全可以用 fuzz 檢測出來啊?發完 setup 包之后跳過 data phase ,這個 ROM 程序應該就直接炸了啊?Secure ROM 作為安全啟動鏈的起點,就算不做徹底的形式化驗證, fuzz 也應該會做到位吧?感覺有點搞不懂蘋果為什么在這里會漏下一個大坑漏了這么多年,感覺有點不可思議。

嘛,這次的文章就寫到這里吧,正文有什么錯誤歡迎在評論區指正,就這樣了。


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