分享一個基本不可能被檢測到的hook方案
背景
最近項目里需要通過hook對APP的行為進行監控,需要滿足如下幾個需求:
(1)兼容性好,能夠兼容99.9%的APP。
(2)不能被APP的安全機制檢測到導致APP邏輯不正常或者hook失敗。
(3)能夠實時地與PC端進行通信,上報監控信息。
(4)hook時機要非常早,不能出現漏檢的情況。
(5)因為APP量非常大,需要一種通用化方案,不能出現對特定APP進行定制的情況。
因此,考慮了下述幾種方案:
Frida Server hook方案
這也是之前在使用的方案。基于frida的hook,是一種常見的hook方案,該方案實現簡單,可擴展性強,易于調試和修改,hook代碼可以隨時修改,還能通過RPC方便地在PC上進行交互,能夠繞過大部分hook檢測,自然也就成了逆向人員最常見的一種分析手段。
缺點:
該方案雖然有很多優勢,但也存在著一些缺陷:
(1)該方案過于出名,導致很多加固廠商對其進行了針對性的檢測,在部分保護強度較高的APP上無法正常運行。
(2)frida本身雖然兼容性雖然較好,但在注入過程中,仍會出現在部分APP上無法正常運行的情況。
因此,該方案雖然在90%的情況下能夠滿足需求,但是需求1和需求2仍有小概率不滿足。
Frida Gadget hook方案
這是Frida Server在部分場景下被檢測到,而且在部分APP中無法正常注入,從而萌生的思路。
為了實現持久化,frida給出了Gadget方案,該方案通過對APK進行重打包,向lib目錄中添加Gadget的動態鏈接庫,然后反編譯dex,在dex中添加加載該動態鏈接庫的代碼,以此實現持久化。
缺點:
(1)重打包方案是Gadget的最原始使用方式。該方案雖然避免了運行時的注入過程,提高了兼容性,但是對APK的重打包,需要繞過APK本身的簽名校驗和文件校驗,雖然可以用自動化腳本實現簽名校驗和文件校驗的繞過,但是如果被檢測的APP是使用匯編編寫的open()函數,也是繞不過去的。需求2不滿足。
(2)除了重打包方案,因為之前自己做過SO的靜態注入,所以也考慮過對APP安裝后解壓出來的so文件進行靜態注入,這樣就可以避過簽名校驗和文件校驗。但是這種方案比較依賴于so文件的加載時機,雖然加固后的APP,殼so往往都會在最早的時候進行加載,但是也難以保證適用于所有APP。需求4不滿足。
XPOSED方案
XPOSED方案比較容易被檢測,而且在與PC的交互上也不是太好。因此需求3和需求2都不滿足。
定制ROM方案
定制ROM方案類似于脫殼機,基本上不可能被檢測,而且兼容性也非常好。但是該方案基本上只能用于開放源代碼的系統機型,而且系統的編譯時間非常長,每一點微小的改動都需要重新編譯并刷機并重啟系統,開發調試費時費力,后續更新極其不方便。而且與PC端的通信也比較麻煩。
該方案屬于一種可行方案,但是耗時過長,成本太高,屬于最后實在沒有其他辦法了之后的辦法。
基于系統組件替換的定制ROM方案
總結上述幾種方案,可以發現,目前的幾種方案主要有以下幾個問題需要解決:
(1)對APP或進程的侵入式修改,會在侵入過程中留下痕跡,這些痕跡容易被hook檢測機制檢測到。
(2)與PC端進行實時通信。
(3)降低開發與維護成本。
系統so替換
這是早期的思路,通過對系統so進行靜態注入,使得APP在啟動時加載系統so,順帶的把我們自定義的so加載起來,因為這個自定義的so名稱可以自定義,以及Android的碎片化,APP基本上不可能根據so的名稱來對這種hook方式進行檢測。
然而,想法很豐滿,現實很骨感,實際測試下來發現,Android系統啟動時,會先生成Zygote進程,在Zygote進程中加載所有必需的so,后續所有的APP進程都是Zygote fork出來的,因此,替換后的系統so的加載時機并不是在APP啟動時。
那么,能不能啟動一個線程,不斷循環檢測要hook的APP是否被fork了呢?然而事實又給了我打擊,Zygote在fork進程之前,會先等待進程中的所有子線程結束,以免某些子線程中的任務還沒完成就進行了fork,導致功能異常。因此,如果啟動一個線程,就會導致整個系統直接就起不來了(因為還有一些包括Launcher在內的系統APP需要通過Zygote fork出來)。
有沒有一個so,是在APP啟動時進行加載的呢?通過對比Zygote進程和一個簡單demo的maps列表,發現/system/lib/hw/gralloc.default.so這個so是在APP啟動時加載的,經過一番百度,這個so應該是與APP圖形界面渲染有關的so,而根據so的命名可以看出,這個so是有可能根據硬件的不同而變化的。而且經過測試,這個so是在Activity被啟動時才加載的,因此,假如APP在Application中進行了一些操作,這種方案是監控不到的。
Riru插件定制
其實Riru的整體思路和系統so替換的思路是很相似的,不過個人感覺系統so替換的思路要比Riru的思路實現起來更簡單一些。
Riru相比于系統so替換的強大之處在于他對APP啟動時會調用的函數進行了hook,并以插件的形式對外提供了接口,免去了自己再去研究源碼,尋找hook點。
接下來編寫了一個插件使用Frida Gadget進行嘗試,然后再次發現悲催了,Frida Gadget的Listen模式中,需要PC端找到APP進程然后恢復程序運行,但是Riru提供的接口中,進程的相關環境還沒有完全初始化好,使用PS命令查找不到進程。
最后的最后,在Frida的JS API中找到了網絡相關的API,因此采用了Frida Gadget的script的模式,然后在hook接口中實現了一個TCP服務器,將hook信息通過網絡通信及adb轉發發送到PC端。
整個流程為:
(1)編寫Riru插件gadget_loader:功能為讀取配置文件(放置在/data/local/tmp目錄下),根據配置文件,若當前APP是需要檢測的APP,則加載libgadget.so。
(2)在插件中添加libgadget.so及其配置文件。
(3)安裝gadget_loader插件。
(4)將libloader.so的配置文件和libgadget.so需要執行的腳本放置到/data/local/tmp目錄下。
(5)libgadget.so腳本通過hook對應API監控APP接口調用行為,并通過網絡連接將監控信息發送至PC。
總結
最后總結一下這個方案的幾個特點:
(1)不需要對APK進行修改,也不需要對進程進行注入,因此可以避開APP的檢測機制。
(2)方案為了方便,最后采用了Riru插件進行實現,這使得APP有可能檢測到存在Riru插件,如果碰到這種情況,完全可以采用系統so替換的方案,然后模仿Riru的思路hook幾個API,實現Gadget so的加載,這樣就基本上不可能被檢測到了。
(3)因為整個方案只是替換一個系統so,并添加了幾個文件到系統中,實現了APK的零侵入,對系統的侵入也非常的小,兼容性會非常好(主要取決于Frida Gadget中是否有bug)。
(4)與PC端的交互采用的是socket通信,這個通信的接口是可自定義的,在保證了與PC端交互的同時又能避免固定端口被檢測。
(5)整個方案的實現過程非常簡單,低版本的系統上可以直接替換系統so,高版本的系統因為一些Android的安全機制,只能采用Magisk插件的形式進行so的替換,不過高版本系統上的root本來就幾乎都是使用Magisk,這點小小的前置要求就也就幾近于無了。
保密原因,代碼就不放了。