被冰封的 Bug:Fishhook Crash 修復(fù)紀(jì)實(shí)


作者:郝連福,業(yè)界資深計(jì)算機(jī)技術(shù)專家,現(xiàn)任聲網(wǎng)Agora 首席前端架構(gòu)師。先后擔(dān)任過 Principal Engineer/Engineering Director(UTStarcom)、Sr. architect(Intel)、T4 architect(YY)等職,曾設(shè)計(jì)開發(fā)電信核心網(wǎng)專用操作系統(tǒng)、高性能TCP/IP協(xié)議棧、以及聲網(wǎng)SDK架構(gòu)重構(gòu)等重大項(xiàng)目。
引言
本文是聲網(wǎng)Agora 與 RTC 開發(fā)者社區(qū)共同發(fā)起的 Dev for Dev(Developer for Developer)互動(dòng)創(chuàng)新實(shí)踐活動(dòng)的開篇,同時(shí)也是開源技術(shù)愛好者在一線工作中的真實(shí)記錄。文中遇到的情況頗具代表性,特整理分享出來以饗讀者。

通常在 iOS 中實(shí)現(xiàn)應(yīng)用 Hook 的方式有以下三種:
Method Swizzling:利用 OC(Objective C)的 Runtime 特性,動(dòng)態(tài)改變?SEL(方法編號(hào))和?IMP(方法實(shí)現(xiàn))的對(duì)應(yīng)關(guān)系,達(dá)到 OC 方法調(diào)用流程改變的目的,只適用于動(dòng)態(tài)的 OC 方法;
?Fishhook:FaceBook(現(xiàn)更名為 Meta)提供的一個(gè)動(dòng)態(tài)修改鏈接 Mach-O 文件的工具,利用 Mach-O 文件加載原理,通過修改懶加載和非懶加載兩個(gè)表的指針實(shí)現(xiàn) C 函數(shù) HOOK 的效果;適用于靜態(tài)的 C 方法;
Cydia Substrate:原名為?Mobile Substrate,是一個(gè)強(qiáng)大的框架,它的主要作用是針對(duì) OC 方法、C 函數(shù)以及函數(shù)地址進(jìn)行 HOOK 操作,適用于 OC 方法、C 函數(shù)以及函數(shù)地址,亦適用于 Android 平臺(tái)。
Fishhook?是一個(gè)由 Meta 公司開源的第三方框架,它能夠在模擬器和設(shè)備上動(dòng)態(tài)地重新綁定運(yùn)行在 iOS/macOS 上的 Mach-O 二進(jìn)制文件的符號(hào),從而實(shí)現(xiàn)動(dòng)態(tài)修改 C 語言函數(shù),常用于應(yīng)用的調(diào)試/追蹤。這個(gè)框架只包含兩個(gè)核心文件:fishhook.c?以及?fishhook.h?所以非常輕量,在許多企業(yè)級(jí)應(yīng)用中頗受青睞。然而這個(gè)以精練著稱的開源項(xiàng)目中,卻埋藏著一個(gè)不易察覺的問題……
隨著 iOS 15 Beta 版的發(fā)布,許多開發(fā)者發(fā)現(xiàn)了普遍的應(yīng)用程序崩潰──這通常由系統(tǒng)兼容性問題引發(fā),而隨著排查過程的不斷深入,我們發(fā)現(xiàn)問題并沒有那么簡單。起初,開發(fā)者把問題反饋到 Fishhook 之后,有不同的團(tuán)體和個(gè)人貢獻(xiàn)了好幾個(gè)修復(fù)的PR,但都未能從根本上解決這個(gè)問題。在仔細(xì)分析了 iOS 和 macOS 的操作系統(tǒng)內(nèi)核 XNU 源碼后,我們最終定位到了問題的 RootCause。
對(duì) Fishhook Crash 問題的溯源
為了定位問題,我們通常會(huì)根據(jù)現(xiàn)有的報(bào)錯(cuò)日志嘗試對(duì)問題進(jìn)行復(fù)現(xiàn),通過調(diào)試追蹤我們發(fā)現(xiàn),在 iOS 15 或者 macOS 12的環(huán)境下 Fishhook 代碼在重綁定符號(hào)時(shí)會(huì)100%地發(fā)生崩潰現(xiàn)象,正是這個(gè)崩潰導(dǎo)致集成了 Fishhook 的應(yīng)用變的不可用。鑒于這個(gè)問題的影響很大,一些使用了fishhook項(xiàng)目的應(yīng)用在發(fā)現(xiàn)問題后緊急移除了該組件以緩解其影響。
造成 fishhook 崩潰的根本原因
Fishhook 的工作原理需要 Hook 修改符號(hào)動(dòng)態(tài)綁定數(shù)據(jù)段,這些數(shù)據(jù)段的默認(rèn)權(quán)限一般是只讀的,所以需要加上“寫”權(quán)限才能修改,而問題恰好就出在這里──我們?cè)谂挪檫^程中發(fā)現(xiàn) Fishhook 里增加“寫”權(quán)限的代碼存在 Bug,問題相關(guān)代碼如下:

這段代碼里面有3個(gè)嚴(yán)重錯(cuò)誤,為了便于閱讀,我們分別以紅綠藍(lán)3個(gè)顏色的框?qū)⑾嚓P(guān)代碼標(biāo)識(shí)出來,對(duì)這些錯(cuò)誤的具體解釋如下:
?首先,不能僅根據(jù)?__DATA_CONST?這個(gè) segname 來判斷是否需要增加“寫”權(quán)限,因?yàn)閺?iOS 14.5 甚至更早的版本開始,都需要 Hook 一個(gè)叫?__AUTH_CONST?的 segment,因此只Hook 一個(gè)?__DATA_CONST?字段是不夠的;
其次,獲取當(dāng)前的 vm prot 時(shí),傳錯(cuò)了地址,不應(yīng)該是?rebindings,因?yàn)槲覀円獙懭氲牡刂肥?strong>?indirect_symbol_bindings;
最后,XNU 內(nèi)核的 C-O-W 機(jī)制與 Linux Kernel 不同,對(duì)于 RO 的 vm ?segment?mapping 需要顯式指定?VM_PROT_COPY?才能增加“寫”權(quán)限,但是?XNU BSD?的mprotect 系統(tǒng)調(diào)用根本就做不到這一點(diǎn),故而這句 mprotect 系統(tǒng)調(diào)用形同虛設(shè),相當(dāng)于什么也沒做!XNU MACH 關(guān)鍵代碼邏輯如下:

Fishhook 代碼存在的上述 3 個(gè)錯(cuò)誤疊加在一起,最終導(dǎo)致在修改?indirect_symbol_bindings?所指向的數(shù)據(jù)時(shí)發(fā)生了“寫”保護(hù)錯(cuò)誤,進(jìn)而發(fā)生的 Crash 影響了整個(gè)應(yīng)用系統(tǒng)。
修復(fù) Fishhook 崩潰的最佳方法
既然我們已經(jīng)找到了 Bug 位置所在,修復(fù)的思路便只需對(duì)癥下藥即可:
將原來寫錯(cuò)的地址 rebindings 修改成?indirect_symbol_bindings;
將 mprotect 系統(tǒng)調(diào)用改成使用vm_protect系統(tǒng)調(diào)用,并增加?VM_PROT_COPY?選項(xiàng);
代碼邏輯上修改為只有 vm_protect 系統(tǒng)調(diào)用執(zhí)行成功時(shí),才能去做“寫”動(dòng)作。
因此 Bug 修復(fù)的核心代碼如下:

這里需注意,首先,為符號(hào)動(dòng)態(tài)綁定的數(shù)據(jù)段增加“寫”權(quán)限時(shí)一定要添加?VM_PROT_COPY?選項(xiàng),否則寫入操作會(huì)失??;其次,要在代碼邏輯中添加“只有?vm_protect?系統(tǒng)調(diào)用返回成功”才能真正去執(zhí)行“寫”這些數(shù)據(jù)段的操作,否則就什么都不要做。
經(jīng)過嚴(yán)格的測試和反復(fù)驗(yàn)證,我們徹底修復(fù)了這個(gè) Bug,并在2021年的6月12日向 Fishhook 官方提交了 PR(https://github.com/facebook/fishhook/pull/87),F(xiàn)ishhook的維護(hù)團(tuán)隊(duì)在對(duì)比了多個(gè)修復(fù)方案后,最終選擇 Merge 了我們的修復(fù)補(bǔ)丁并將其合并進(jìn)主分支,至此該問題最終得以解決。
系統(tǒng)升溫(級(jí))使“冰封”的 Bug 得見天日
讀者大概率會(huì)好奇,為什么在 iOS 15 或者 macOS 12 之前的版本沒有這個(gè)問題呢?
事實(shí)上,在 iOS 15 或者 macOS 12 之前的操作系統(tǒng)自身也存在這個(gè)缺陷,對(duì)這些數(shù)據(jù)段的保護(hù)并不嚴(yán)謹(jǐn),對(duì)應(yīng)該“只讀”的數(shù)據(jù)段并沒有去掉“寫”權(quán)限,我們調(diào)查到相關(guān)的證據(jù)如下:

在上述證據(jù)片段中,protection?數(shù)值 3 表示權(quán)限為“可讀可寫”,因此 Fishhook 代碼里面做Hook動(dòng)作的“寫”操作在老版本的 iOS/macOS 中并沒有任何問題。但是 iOS 15/macOS 12 新版本操作系統(tǒng)中對(duì)這些數(shù)據(jù)段的保護(hù)更加嚴(yán)格,對(duì)相應(yīng)的權(quán)限做了一些調(diào)整──將本應(yīng)賦予“只讀”數(shù)據(jù)段的“可讀可寫”權(quán)限修正為“只讀”,也就是說上述證據(jù)片段中 protection 的數(shù)值發(fā)生了變化,相關(guān)的證據(jù)如下:

上述代碼片段中的 protection?數(shù)值1代表“只讀”──也理應(yīng)如此。但正是這種“修正”與原來“不當(dāng)”的配置產(chǎn)生了邏輯上的沖突,最終 Fishhook 的這個(gè) Bug 在較新的 iOS 15/macOS 12 系統(tǒng)中暴露出來,導(dǎo)致了嚴(yán)重的崩潰問題。從代碼的角度來看 Fishhook 的這個(gè) Bug 顯然是一直存在的,只是在早期的 iOS 和 macOS 版本中沒有構(gòu)成觸發(fā)的條件,故而隱患一直被雪藏,直到相關(guān)的條件被改變。
總結(jié)
通常在應(yīng)用開發(fā)過程中,本著不重復(fù)造輪子和快速上線、不斷迭代的原則,我們經(jīng)常會(huì)引入第三方模塊,尤其是有著廣泛應(yīng)用的底層開源組件。但隨著 IT 基礎(chǔ)設(shè)施的變遷,系統(tǒng)環(huán)境會(huì)隨著時(shí)間的推移不斷增加新特性、拋棄舊實(shí)現(xiàn),在這個(gè)過程中由于依賴問題我們的應(yīng)用不可避免地會(huì)不斷遭遇不可用的挑戰(zhàn)。作為業(yè)務(wù)應(yīng)用的開發(fā)者,我們必須不斷提高向上游組件進(jìn)行問題溯源的能力,秉持開發(fā)者的初心,取自開源、回饋開源。

Dev for Dev專欄介紹
Dev for Dev(Developer for Developer)是聲網(wǎng)Agora 與 RTC 開發(fā)者社區(qū)共同發(fā)起的開發(fā)者互動(dòng)創(chuàng)新實(shí)踐活動(dòng)。透過工程師視角的技術(shù)分享、交流碰撞、項(xiàng)目共建等多種形式,匯聚開發(fā)者的力量,挖掘和傳遞最具價(jià)值的技術(shù)內(nèi)容和項(xiàng)目,全面釋放技術(shù)的創(chuàng)造力。