被冰封的 Bug:Fishhook Crash 修復紀實

聲網Agora發表於2022-03-17

圖片

作者:郝連福,業界資深計算機技術專家,現任聲網Agora 首席前端架構師。先後擔任過 Principal Engineer/Engineering Director(UTStarcom)、Sr. architect(Intel)、T4 architect(YY)等職,曾設計開發電信核心網專用作業系統、高效能TCP/IP協議棧、以及聲網SDK架構重構等重大專案。

引言

本文是聲網Agora 與 RTC 開發者社群共同發起的 Dev for Dev(Developer for Developer)互動創新實踐活動的開篇,同時也是開源技術愛好者在一線工作中的真實記錄。文中遇到的情況頗具代表性,特整理分享出來以饗讀者。


通常在 iOS 中實現應用 Hook 的方式有以下三種:

1.Method Swizzling:利用 OC(Objective C)的 Runtime 特性,動態改變 SEL(方法編號)IMP(方法實現)的對應關係,達到 OC 方法呼叫流程改變的目的,只適用於動態的 OC 方法;

2.Fishhook:FaceBook(現更名為 Meta)提供的一個動態修改連結 Mach-O 檔案的工具,利用 Mach-O 檔案載入原理,通過修改懶載入和非懶載入兩個表的指標實現 C 函式 HOOK 的效果;適用於靜態的 C 方法;

3.Cydia Substrate:原名為 Mobile Substrate,是一個強大的框架,它的主要作用是針對 OC 方法、C 函式以及函式地址進行 HOOK 操作,適用於 OC 方法、C 函式以及函式地址,亦適用於 Android 平臺。

Fishhook 是一個由 Meta 公司開源的第三方框架,它能夠在模擬器和裝置上動態地重新繫結執行在 iOS/macOS 上的 Mach-O 二進位制檔案的符號,從而實現動態修改 C 語言函式,常用於應用的除錯/追蹤。這個框架只包含兩個核心檔案:fishhook.c 以及 fishhook.h 所以非常輕量,在許多企業級應用中頗受青睞。然而這個以精練著稱的開源專案中,卻埋藏著一個不易察覺的問題……

隨著 iOS 15 Beta 版的釋出,許多開發者發現了普遍的應用程式崩潰──這通常由系統相容性問題引發,而隨著排查過程的不斷深入,我們發現問題並沒有那麼簡單。起初,開發者把問題反饋到 Fishhook 之後,有不同的團體和個人貢獻了好幾個修復的PR,但都未能從根本上解決這個問題。在仔細分析了 iOS 和 macOS 的作業系統核心 XNU 原始碼後,我們最終定位到了問題的 RootCause。

對 Fishhook Crash 問題的溯源

為了定位問題,我們通常會根據現有的報錯日誌嘗試對問題進行復現,通過除錯追蹤我們發現,在 iOS 15 或者 macOS 12的環境下 Fishhook 程式碼在重繫結符號時會100%地發生崩潰現象,正是這個崩潰導致整合了 Fishhook 的應用變的不可用。鑑於這個問題的影響很大,一些使用了fishhook專案的應用在發現問題後緊急移除了該元件以緩解其影響。

造成 fishhook 崩潰的根本原因

Fishhook 的工作原理需要 Hook 修改符號動態繫結資料段,這些資料段的預設許可權一般是只讀的,所以需要加上“寫”許可權才能修改,而問題恰好就出在這裡──我們在排查過程中發現 Fishhook 裡增加“寫”許可權的程式碼存在 Bug,問題相關程式碼如下:

圖片

這段程式碼裡面有3個嚴重錯誤,為了便於閱讀,我們分別以紅綠藍3個顏色的框將相關程式碼標識出來,對這些錯誤的具體解釋如下:

1.首先,不能僅根據 __DATA_CONST 這個 segname 來判斷是否需要增加“寫”許可權,因為從 iOS 14.5 甚至更早的版本開始,都需要 Hook 一個叫 __AUTH_CONST 的 segment,因此只Hook 一個 __DATA_CONST 欄位是不夠的

2.其次,獲取當前的 vm prot 時,傳錯了地址,不應該是 rebindings,因為我們要寫入的地址是 indirect_symbol_bindings

3.最後,XNU 核心的 C-O-W 機制與 Linux Kernel 不同,對於 RO 的 vm segment mapping 需要顯式指定 VM_PROT_COPY 才能增加“寫”許可權,但是 XNU BSD 的 mprotect 系統呼叫根本就做不到這一點,故而這句 mprotect 系統呼叫形同虛設,相當於什麼也沒做!XNU MACH 關鍵程式碼邏輯如下:

圖片

Fishhook 程式碼存在的上述 3 個錯誤疊加在一起,最終導致在修改 indirect_symbol_bindings 所指向的資料時發生了“寫”保護錯誤,進而發生的 Crash 影響了整個應用系統。

修復 Fishhook 崩潰的最佳方法

既然我們已經找到了 Bug 位置所在,修復的思路便只需對症下藥即可:

  • 將原來寫錯的地址 rebindings 修改成 indirect_symbol_bindings
  • 將 mprotect 系統呼叫改成使用 vm_protect 系統呼叫,並增加 VM_PROT_COPY 選項;
  • 程式碼邏輯上修改為只有 vm_protect 系統呼叫執行成功時,才能去做“寫”動作。

因此 Bug 修復的核心程式碼如下:

圖片

這裡需注意,首先,為符號動態繫結的資料段增加“寫”許可權時一定要新增 VM_PROT_COPY 選項,否則寫入操作會失敗;其次,要在程式碼邏輯中新增“只有 vm_protect 系統呼叫返回成功”才能真正去執行“寫”這些資料段的操作,否則就什麼都不要做。

經過嚴格的測試和反覆驗證,我們徹底修復了這個 Bug,並在2021年的6月12日向 Fishhook 官方提交了 PR(https://github.com/facebook/f...),Fishhook的維護團隊在對比了多個修復方案後,最終選擇 Merge 了我們的修復補丁並將其合併進主分支,至此該問題最終得以解決。

系統升溫(級)使“冰封”的 Bug 得見天日

讀者大概率會好奇,為什麼在 iOS 15 或者 macOS 12 之前的版本沒有這個問題呢?

事實上,在 iOS 15 或者 macOS 12 之前的作業系統自身也存在這個缺陷,對這些資料段的保護並不嚴謹,對應該“只讀”的資料段並沒有去掉“寫”許可權,我們調查到相關的證據如下:

圖片

在上述證據片段中,protection 數值 3 表示許可權為“可讀可寫”,因此 Fishhook 程式碼裡面做Hook動作的“寫”操作在老版本的 iOS/macOS 中並沒有任何問題。但是 iOS 15/macOS 12 新版本作業系統中對這些資料段的保護更加嚴格,對相應的許可權做了一些調整──將本應賦予“只讀”資料段的“可讀可寫”許可權修正為“只讀”,也就是說上述證據片段中 protection 的數值發生了變化,相關的證據如下:

圖片

上述程式碼片段中的 protection 數值1代表“只讀” ──也理應如此。但正是這種“修正”與原來“不當”的配置產生了邏輯上的衝突,最終 Fishhook 的這個 Bug 在較新的 iOS 15/macOS 12 系統中暴露出來,導致了嚴重的崩潰問題。從程式碼的角度來看 Fishhook 的這個 Bug 顯然是一直存在的,只是在早期的 iOS 和 macOS 版本中沒有構成觸發的條件,故而隱患一直被雪藏,直到相關的條件被改變。

總結

通常在應用開發過程中,本著不重複造輪子和快速上線、不斷迭代的原則,我們經常會引入第三方模組,尤其是有著廣泛應用的底層開源元件。但隨著 IT 基礎設施的變遷,系統環境會隨著時間的推移不斷增加新特性、拋棄舊實現,在這個過程中由於依賴問題我們的應用不可避免地會不斷遭遇不可用的挑戰。作為業務應用的開發者,我們必須不斷提高向上遊元件進行問題溯源的能力,秉持開發者的初心,取自開源、回饋開源。

Dev for Dev專欄介紹

Dev for Dev(Developer for Developer)是聲網Agora 與 RTC 開發者社群共同發起的開發者互動創新實踐活動。透過工程師視角的技術分享、交流碰撞、專案共建等多種形式,匯聚開發者的力量,挖掘和傳遞最具價值的技術內容和專案,全面釋放技術的創造力。

相關文章