如何利用Frida實現原生Android函數的插樁

一、前言

在上一篇文章中,Rohit向我們介紹瞭如何使用Frida完成基本的運行時測試任務。簡而言之,Frida可以動態改變Android應用的行為,比如可以繞過檢測Android設備是否處於root狀態的函數。對於在ART(Android Runtime,Android運行時)環境中運行的應用來說,我們可以使用Java.perform來hook函數。

然而,在某些情況下,開發者會使用Android NDK來執行各種操作,比如檢測root狀態等,這種情況下,開發者就可以使用C++或C語言來開發代碼,也可以訪問APK中的函數。

在本文中,我們介紹瞭如何實現使用Android NDK開發的代碼的動態插樁,具體而言,我們會介紹如何利用Frida來hook使用C++或C開發的函數。

二、動機

像Xposed之類的框架默認情況下沒有提供hook原生函數(native function)的功能,而其他工具,如android eagle eye對初學者來說並不友好,學習曲線非常陡峭。然而,我們可以使用Frida來hook基於Android NDK框架構建的那些函數。接下來我們可以看看具體的操作流程。

三、目標:Rootinspector

在本文中,我們的測試對象為Rootinspector應用,這個應用可以檢查設備的root狀態,應用由純C++語言編寫的原生代碼構建而成。我們的目標是hook這些函數,繞過root檢測邏輯。

在Rootinspector中,與root狀態檢測邏輯有關的代碼分為兩個部分。APK中的一個封裝函數會調用由C++編寫的checkifstream()底層函數,這一過程所對應的java函數為checkRootMethodNative12(),如下圖所示。

checkRootMethodNative12()是Android APK中使用Java編寫的函數,會調用底層的checkifstream()函數,後者使用C++編寫。

這個Android APK中聲明的所有原生函數如下所示。

檢查原生函數的源代碼後,我們發現這個函數的具體實現為JavacomdevadvancerootinspectorRootcheckifstream,這個字符串由包名及函數名構成,由“”符隔開。

我們首先嘗試hook checkRootMethodNative12()這個Java函數,所使用的代碼如下所示:

然而,上述代碼沒法實現hook任務,出現的錯誤如下所示。Frida無法獲得Root類對應的“localRoot”對象的引用。

在這種情況下,我們無法hook使用C++編寫的那些函數,因為這些函數沒有運行在Java VM上下文環境中。因此,我們必須做些改變,才能hook到原生的C++代碼。

四、Hook原生代碼

我們可以使用Frida中的Interceptor函數,深入到設備的底層內存中,hook特定的庫或者內存地址。

當APK被封裝打包時,編譯器會編譯C++代碼,將其存放在APK文件lib目錄中的“libnative.so”,如下所示。

使用Interceptor時,我們需要hook libnative2.so這個.so以及Javacomdevadvance_rootinspectorRootcheckfopen函數。我們需要使用十六進制編輯器或者調試器來讀取.so文件,通過逆向工程獲取函數名。這裡我們耍瞭點小聰明,因為我們對應用的源代碼已經非常熟悉。

現在,我們可以運行如下代碼,看看我們是否可以成功攔截到checkfopen這個原生函數。

執行上述代碼後,我們又遇到一個錯誤,錯誤提示某個指針不存在,這意味著libnative.so文件沒有被正確加載,或者應用沒有找到這個文件。

然而,再次運行代碼,保持應用處於啟動狀態,我們的代碼就能正常執行。具體操作為,先結束第一次運行的腳本,保持應用處於打開狀態,再次運行腳本,點擊“inspect using native code”按鈕後,程序的運行狀態如下圖所示。

我們有必要瞭解發生這種情況的具體原因。在Android 1.5中,Android NDK提供瞭動態鏈接庫(與Windows環境中的DLL類似),以支持NDK中的動態加載特性。當我們第一次啟動應用時,dll文件(libnative2.so)沒有被加載,因此我們會得到一個“expected a pointer”的錯誤信息。現在,當我們終止腳本、保持應用處於打開狀態時,再次運行腳本,程序發現dll文件已經被加載,因此此時我們就可以hook目標函數。

現在換個思路,不必等待程序加載dll文件,我們可以在“dlopen”函數上設置一個陷阱,這個函數是一個原生系統調用,可以用來加載與應用有關的所有動態鏈接庫。一旦dlopen函數hook成功,我們就可以檢查我們的目標dll有沒有被加載。如果dll是第一次被加載,我們可以繼續運行,hook原生函數。我們使用didHookApi佈爾值檢查hook過程,避免dlopen被多次hook。

我們使用如下代碼來直接hook原生函數。代碼可以分為兩部分。

在代碼第17-30行中,我們首先嘗試使用Frida的Module.findExportByName API來hook dlopen函數,然後搜索內存中的dlopen函數(這裡隻能祈禱該函數沒有被覆蓋)。

在onLeave事件中,我們首先檢查我們的目標DLL有沒有被加載,隻有DLL已經被加載的情況下,我們才會hook原生函數。

執行最終的腳本後,我們就可以通過原生函數,繞過Rootinspector的root檢測機制,過程如下所示。

腳本運行之前如下所示:

現在,關掉應用,在不啟動應用的情況下運行腳本。腳本會自己打開這個應用。我們隻需要點擊“Inspect Using Native”這個按鈕即可,如下所示。

You May Also Like