前言
Xposed 使用方法
Xposed 原理概述
Android zygote 進程
基於 Dalvik 的方法 Hook
基於 ART 的方法 Hook
Xposed 工作流程
Xposed 項目結構
Xposed
XposedBridge
XposedInstaller
android_art
XposedTools
Xposed 源碼分析
Xposed 安裝
Xposed 啟動
Xposed 方法 Hook
總結
參考
前言Xposed 是 Android 平臺上著名的 Java 層 Hook 框架,通過在 Android 設備上安裝 Xposed 框架,編寫 Xposed 模塊,可實現對任意 Android 應用的 Java 方法的 Hook,以及應用資源的替換。
(Hook 是一種函數鉤子技術,能夠對函數進行接管,從而修改函數的返回值,改變函數的原始意圖)
本文將基於 Xposed 最新的開原始碼對 Xposed 的實現原理進行分析。Xposed 有兩種實現版本,一個是基於 Dalvik 虛擬機的實現,它是針對早期的 Android 4.4 之前的 Android 設備設計的;另一個是基於 ART 虛擬機的實現,自 Android 5.0 系統開始,Android 系統正式採用了 ART 虛擬機模式運行,Dalvik 就成了歷史,目前市面上幾乎所有的手機都是以 ART 模式運行的,下面將主要對於 ART 上的 Xposed 實現進行詳細分析,對於 Dalvik 上的 Xposed 的實現,進行必要性的分析。
通過了解 Xposed 的實現原理可以學到在 Android 平臺上對於 Java 層代碼的一種 Hook 機制的實現,同時複習 Android 系統的啟動原理以及增加對於 Android ART 虛擬機運行原理的了解。
Xposed 使用方法在對 Xposed 進行分析之前,先回顧一下 Xposed 基本 API 的使用。
Xposed 的核心用法就是對一個 Java 方法進行 Hook,它的典型調用如下:
XposedHelpers.findAndHookMethod(Application.class, "onCreate", Context.class,
new XC_MethodHook() {
@Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
Application app = (Application) param.thisObject;
Context context = (Context) param.args[0];
Log.d(TAG, "Application#onCreate(Context); this: " + app + " arg: " + context);
param.setResult(null);
}
@Override protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
}
});
以上代碼的意思是對 Application 這個類的 onCreate 方法進行 Hook,並使用 XC_MethodHook 對象提供一個 Hook 處理方法來接管原來方法的邏輯,當應用的 Application 類型的 onCreate 方法被調用時,beforeHookedMethod 將在被調用之前執行,同時 onCreate 的參數將會傳遞給 beforeHookedMethod 方法進行處理,上面的處理只是將參數列印了出來(一個 Context),同時還可以拿到被調用的 this 目標對象,也就是 Application 的對象,還可以使用 setResult 方法更改原始方法的返回值,不過這裡的 Application#onCreate 方法是 void 返回類型的,setResult 不起作用,如果是其他類型,那麼原方法的返回值將被更改。
這樣就達到了修改一個 Java 方法的目的,即改變了原始方法的邏輯和意圖。
public class App extends Application {
@Override void onCreate(Context context) {
// ...
}
}
可以看到,如果要使用 Xposed 對一個 Java 方法進行 Hook,需要提供要 Hook 方法的名字、參數列表類型和方法所在類,以及處理 Hook 的回調方法。
下面正式開始分析。
Xposed 原理概述首先概述 Xposed 原理,之後再對具體細節進行分析。
Xposed 是一個 Hook 框架,它提供了對任意 Android 應用的 Java 方法進行 Hook 的一種方法,通常它的使用方法如下:
首先按照 Xposed 官網提供的開發規範編寫一個 Xposed 模塊,它是一個普通的 Android 應用,包含一塊開發者自己定義的代碼,這塊代碼有能力通過 Xposed 框架提供的 Hook API 對任意應用的 Java 方法進行 Hook。
在要啟用 Xposed 的 Android 設備上安裝 Xposed 框架和這個 Xposed 模塊,然後在 Xposed 框架應用中啟用這個 Xposed 模塊,重新啟動設備後,Xposed 模塊將被激活,當任意的應用運行起來後,Xposed 模塊的 Hook 代碼將會在這個應用進程中被加載,然後執行,從而對這個應用的 Java 方法進行指定 Hook 操作。
那麼根據以上使用方法實現一個 Xposed 框架需要分成如下幾個部分:
提供用於 Hook 操作的 API,為了讓開發者進行模塊開發。它通常是一個 jar 包;
提供一個具有界面的管理器應用,用於安裝和管理 Xposed 本身和 Xposed 模塊;
提供將代碼加載到每一個應用進程中的能力,目的是支持 Xposed 模塊的代碼在進程中使用 Xposed API 進行 Hook 操作;
提供 Hook 任意 Java 方法的能力,為 Xposed 模塊的調用提供支持,當 Xposed 模塊在應用進程中執行時可對方法進行 Hook。
前兩點對於我們開發者來說都很熟悉,沒有什麼難點,後面兩點才是實現 Xposed 的核心。
首先是 Xposed 怎樣實現的將代碼加載到每一個應用進程中(Xposed 是基於 Root 權限實現的,所以有修改 Android 系統的能力)?
Xposed 是通過修改系統 zygote 進程的實現將代碼注入應用進程中的。
為了知道 Xposed 是如何修改 Zygote 進程的,下面首先介紹 Android 系統 Zygote 相關內容。
Android zygote 進程zygote 進程是 Android 系統中第一個擁有 Java 運行環境的進程,它是由用戶空間 1 號進程 init 進程通過解析 init.rc 文件創建出來的,從 init 進程 fork 而來。
zygote 進程是一個孵化器。Android 系統中所有運行在 Java 虛擬機中的系統服務以及應用均由 zygote 進程孵化而來。
zygote 通過克隆(fork)的方式創建子進程,fork 出來的子進程將繼承父進程的所有資源,基於這個特性,zygote 進程在啟動過程將創建 Java ART 虛擬機,預加載一個 Java 進程需要的所有系統資源,之後子進程被創建後,就可以直接使用這些資源運行了。
自 Android 5.0 系統開始,zygote 不再是一個進程,而是兩個進程,一個是 32 位 zygote,負責孵化 32 位進程(為了兼容使用了 armeabi 和 armeabi-v7a 等 32 位架構的本地動態庫的應用),另一個是 64 位 zygote 進程,負責孵化 64 位應用進程(可加載 arm64-v8a 等 64 位架構本地庫)。
init 進程是 Android 系統中的 pid 為 1 的進程,是用戶空間的第一個進程,它會在 Android 系統啟動時被內核創建出來,之後會對 init.rc 文件進行解析,init.rc 文件是一個按照特定規則編寫的腳本文件,init 進程通過解析它的規則來創建對應的服務進程。下面看一下 zygote 相關的 rc 文件的內容。
註:自 Android 5.0 開始,32 位 zygote 啟動內容在 init.zygote32.rc 文件中,64 位 zygote 啟動內容在 init.zygote64.rc 中。
註:自 Android 9.0 開始,兩個 zygote 啟動配置放在一個文件中 init.zygote64_32.rc。
這裡看一下 Android 8.1 系統的 32 位 zygote 的 rc 文件內容:
# init.zygote32.rc
service zygote /system/bin/app_process -Xzygote /system/bin --zygote --start-system-server
class main
socket zygote stream 660 root system
onrestart write /sys/android_power/request_state wake
onrestart write /sys/power/state on
onrestart restart media
onrestart restart netd
writepid /dev/cpuset/foreground/tasks
上面的含義是,創建一個名為 zygote 的服務進程,它的可執行文件在 /system/bin/app_process 中,後面的 -Xzygote、/system.bin 等是可執行文件的 main 函數將要接收的參數。
具體的 init 進程和 zygote 進程的啟動細節,可以參考之前的文章:
Android init 進程啟動分析
Android zygote 進程啟動分析
那麼現在回到 Xposed,Xposed 對 zygote 進程的實現源碼進行修改後,重新編譯出 app_process 可執行文件,替換了系統的 app_process 文件(包括 64 位 zygote),並在其中加載了 XposedBridge.jar 這個 Dex 代碼包,它包含 Xposed 的 Java 層實現代碼和提供給 Xposed 模塊的 API 代碼,那麼當 init 進程啟動 zygote 服務進程時,將執行修改過的 app_process 文件,此時 zygote 進程就具有了 Xposed 的代碼,Xposed 可以進行加載 Xposed 模塊代碼等任意操作了。
所有 Android 應用都是運行在 Java 虛擬機上的,所有的 Android 應用都是 zygote 的子進程,那麼當 Android 應用進程啟動後,將具備 zygote 進程加載的所有資源,從而將 Xposed 代碼繼承到了 Android 應用進程中,實現了將 Xposed 代碼加載到每一個進程中的目的。
接下來是如何實現對應用中 Java 方法的 Hook。Hook 的基本原理如下,將 Java 方法的原始邏輯,轉接到一個中間處理方法上,這個處理方法會對原始 Java 方法的參數進行轉發,轉發到一個用於處理 Hook 的方法上(即 XC_MethodHook 的實現),等處理 Hook 的方法執行自定義邏輯後(自定義邏輯可選擇調用原始邏輯先獲取原始返回值,再處理),再返回新的返回值。
下面分別是 Xposed 在 Dalvik 虛擬機和 ART 虛擬機下的 Hook 實現。
基於 Dalvik 的方法 Hook基於 Dalvik 的 Hook 方案是通過將被 Hook 方法修改為一個 JNI 方法,然後綁定一個 Xposed 自定義處理方法邏輯的函數上來實現的。
當需要 Hook 一個指定方法時,需要提供要 Hook 方法的名字、參數列表類型和方法所在類型,還要提供一個用於處理 Hook 的回調,回調方法用於修改原始方法的邏輯,它可以接收 Hook 方法的參數,然後返回一個新的返回值。
首先 Xposed 會取得這個方法的反射表示對象(例如通過 Class.getDeclaredMethod),它是一個 java.lang.reflect.Method 對象,然後取得這個對象的一個私有成員變量 slot 的值,將它和處理 Hook 的回調傳遞給 Xposed 的 Native 層代碼,這個 slot 變量實際上是一個 Java 方法在虛擬機中的索引,使用這個索引可以從 Dalvik 中用於表示 Java 類的 ClassObject 映射類型的 directMethod 和 virtualMethods 數組中取出一個 Method 對象,它在虛擬機中表示一個 Java 方法,Xposed 的 Native 層代碼接收到 Xposed Java 層傳遞過來的 slot 變量後,取出虛擬機中的 Method 對象,然後將這個 Method 對象的類型設置為 JNI 方法,即前面帶有 native 修飾符的方法,然後將它的 nativeFunc 賦值給一個處理 Hook 邏輯的函數上,這個函數中將對這個 Method 的參數進行處理,傳遞給一開始提供的 Java 層中用於處理 Hook 的回調方法,讓它來決定方法的新邏輯,從而返回新的返回值。此時便完成了 Hook。
那麼調用一個被 Hook 的方法的過程是:當一個 Android 應用內的代碼調用一個被 Hook 的方法時,Dalvik 將會進行代碼的解釋執行,Java 方法進入 Dalvik 虛擬機中會被轉化為一個 Method 對象,然後虛擬機判斷這個方法如果是一個 JNI 方法,就會直接調用它綁定的的 nativeFunc 函數,那麼就走到了 Xposed 處理 Hook 的函數中,這個函數將這個被 Hook 方法的參數進行轉發,讓 Xposed 模塊提供的處理 Hook 的回調方法來接管原來的邏輯,獲得新的返回值返回給被 Hook 方法,即可完成整個 Hook 操作。
基於 ART 的方法 Hook基於 ART 的 Hook 方案相比 Dalvik 要複雜一些,需要重新修改編譯 ART 虛擬機的源碼,重新編譯出 ART 虛擬機的可執行文件 libart.so,替換 Android 系統中的 ART 虛擬機實現。
它的核心原理就是直接修改一個方法對應的彙編代碼的地址,讓方法直接跳轉到指定地址執行,然後就可以執行自定義的邏輯進行 Hook 處理了。
ART 虛擬機為了提高執行效率,採用了 AOT(Ahead Of Time,預編譯) 模式運行,在應用運行之前先將整個 APK 包含的 Java 編譯為二進位代碼,然後應用運行時將執行每個方法對應的機器代碼,比採用 JIT(Just In Time Compiler,即時編譯) 的 Dalvik 虛擬機每次在運行時才編譯代碼執行的效率更高。
前面的過程和 Dalvik 一樣,都需要在 Hook 一個指定方法時,提供要 Hook 方法的名字、參數列表類型和方法所在類型,和一個用於處理 Hook 的回調,這個回調用於修改原始方法的邏輯。
接下來 Xposed 取得這個方法的反射表示對象,它是一個 java.lang.reflect.Method 對象,然後和用於處理 Hook 的回調一起傳遞給 Xposed 的 Native 層代碼,Native 層代碼使用 ArtMethod 的一個靜態轉換方法,將 Java 層的反射對象 Method 轉換為一個 ART 中用於表示一個 Java 方法的 ArtMethod 對象,獲取這個表示被 Hook 的 Java 方法的 ArtMethod 對象後,會創建它的副本對象用於備份,備份目的是可以在可是的時候再調用原始方法,然後給這個 ArtMethod 對象重新設置彙編代碼的地址,這個地址指向一段彙編代碼,這個彙編代碼是一段蹦床代碼(Trampoline),會跳入原本用於處理 Java 動態代理的方法的函數,Xposed 對其進行了修改,在其中加入了處理 Hook 的邏輯,也就是轉發被 Hook 方法的參數給處理 Hook 的回調方法,讓 Hook 回調方法處理被 Hook 方法的邏輯,從而完成 Hook。至此就完成了 ART 中的 Hook 處理。
那麼調用一個被 Hook 的方法的過程是:當一個 Android 應用內代碼調用一個被 Hook 的方法時,ART 將會對方法代碼進行執行,首先這個 Java 方法在 ART 虛擬機中將使用一個 ArtMethod 對象表示,然後進入 ART 的 Java 方法執行函數中,會跳入一段蹦床代碼中進行執行,這段蹦床代碼又會跳入這個 ArtMethod 對象設置的彙編代碼地址處,從而執行到 Xposed 用於處理 Hook 的代碼中,之後完成 Hook 邏輯。
上面使用書面語言分別概述了基於 Dalvik 和 ART 的方法 Hook 的實現,目的是對整個 Xposed 實現對方法的 Hook 原理進行概括,建立一個初步的印象。真正的細節還是在原始碼中,為了分析最終原始碼,下面進一步對 Xposed 進行分析。
Xposed 工作流程為了進一步分析 Xposed 的實現原理,先對 Xposed 的整體工作流程進行了解。
要使 Xposed 在 Android 設備上工作,首先需要安裝 Xposed 框架。
首先獲取 XposedInstaller 應用(去官方下載,或者通過 clone XposedInstaller 項目後自行編譯),安裝到已經 root 的設備上,然後打開 XposedInstaller。
XposedInstaller 主頁會有「INSTALL/UPDATE」 的按鈕,點擊將會出現 Install 和 Install via recovery 兩個選擇,一個是直接進行安裝;另一個是通過 recovery 進行刷入安裝。不管選擇哪個,都會首先從伺服器下載相同的 xposed 補丁包。
XposedInstaller 會根據系統版本和 CPU 支持架構下載對應的系統補丁包。
在 ARM64 架構 CPU 的 Android 8.1 系統上,補丁包內容如下:
xposed-v90-sdk27-arm64-beta3.zip
+-META-INF
│ +- CERT.RSA
│ +- CERT.SF
│ +- MANIFEST.MF
│ +- com/google/android
│ +- flash-script.sh
│ +- update-binary
│ +- updater-script
│
+- system
+- xposed.prop
+- bin
| +- app_process32_xposed
| +- app_process64_xposed
| +- dex2oat
| +- dexdiag
| +- dexlist
| +- dexoptanalyzer
| +- oatdump
| +- patchoat
| +- profman
|
+- framework
| +- XposedBridge.jar
|
+- lib
| +- libart-compiler.so
| +- libart-dexlayout.so
| +- libart.so
| +- libopenjdkjvm.so
| +- libsigchain.so
| +- libxposed_art.so
|
+- lib64
+- libart-compiler.so
+- libart-disassembler.so
+- libart.so
+- libopenjdkjvm.so
+- libsigchain.so
+- libxposed_art.so
壓縮包名為 xposed-v90-sdk27-arm64-beta3.zip,文件名包含系統版本、CPU 架構和 Xposed 版本信息。
META-INF 目錄存放文件籤名信息,和 Xposed 刷機腳本 flash-script.sh 文件,update-binary 為刷入文件時執行的文件,它的原始碼在 Android 原始碼 bootable/recovery/updater/ 目錄中。
system 目錄為 Xposed 所需的文件,刷入時將會複製到系統 system 目錄下,同名文件將進行覆蓋,其中 xposed.prop 為 Xposed 的屬性文件,裡面會存放 Xposed 版本相關信息,如下:
version=90-beta3
arch=arm64
minsdk=27
maxsdk=27
requires:fbe_aware=1
bin 目錄存放系統可執行文件;framwrok 目錄存放 Xposed 的 Java 層 Dex 代碼包,用於在 Zygote 進程中進行加載;lib、lib64 是 32 位和 64 位系統庫,包括 ART 虛擬機庫 libart.so 和依賴的庫,還有 Xposed Native 層代碼的實現 libxposed_art.so。
回到 XposedInstaller 中,如果選擇了 Install,那麼首先將壓縮包中的 system 目錄下的的可執行文件以及依賴庫、配置文件等複製入系統 system 中覆蓋相應系統文件,然後請求重啟 Android 系統,重啟後開機過程中,系統將會執行 app_process 可執行文件,從而啟動 Xposed 修改過的 zygote 進程,其中會把 XposedBridge.jar 代碼包加載起來,加載後其中的 Java 代碼會加載已經安裝的 Xposed 模塊,當手機中的應用進程啟動後,Xposed 模塊代碼將會被包含在應用進程中,開始工作;
如果是 Install via recovery,將創建文件 /cache/recovery/command 並寫入指定刷機包路徑的刷機命令,然後重啟手機進入 recovery 模式,recovery 模式會自動執行 command 文件中的命令將 Xposed 文件刷入,然後正常重啟至系統,啟動過程和上面一致。
了解了 Xposed 的整體工作流程,下面開始著手進行源碼分析。
Xposed 項目結構首先了解 Xposed 開源項目的結構,Xposed 包含如下幾個開源項目:
Xposed倉庫地址:https://github.com/rovo89/Xposed
Xposed Native 層代碼的實現,主要修改了系統 app_process 的實現(即 zygote 服務進程的實現),為將 Hook 代碼注入每個應用進程提供了入口。
XposedBridge倉庫地址:https://github.com/rovo89/XposedBridge
Xposed Java 層的代碼,它將單獨作為一個 jar 包的形式通過 zygote 的分裂(fork)注入到每一個應用進程中,內部會 Xposed 模塊,並為 Xposed 模塊中的 Hook 操作提供 API 支持。
XposedInstaller倉庫地址:https://github.com/rovo89/XposedInstaller
統一管理 Xposed 框架的 Android 應用,也是一個 Xposed 框架安裝器,用於安裝更新 Xposed 框架核心以及作為統一管理 Xposed 模塊安裝的模塊管理器。
android_art倉庫地址:https://github.com/rovo89/android_art
Xposed 修改後的 Android ART 虛擬機的實現,將編譯出 libart.so 和其依賴庫,替換系統的 ART 虛擬機實現。包含方法 Hook 的核心實現。
這個倉庫最新分支是基於 Android Nougat MR2 源碼修改的 ART 代碼,目前 Xposed 最新版本支持到了 Android 8.1 系統,說明作者沒有開源出最新代碼,不過都是基於 ART 實現的 Hook,核心 Hook 實現是一致的,不影響分析。
XposedTools倉庫地址:https://github.com/rovo89/XposedTools
用於編譯 Xposed 框架的腳本工具。
目前只分析 Xposed 的實現,不需要對 Xposed 進行定製,所以先不關注 XposedTools 這個項目。
Xposed 源碼分析可以對上面的項目進行 clone,然後用 Android Studio 和 VS Code 打開原始碼,方便閱讀。下面進入源碼中分析具體實現。
Xposed 安裝下載首先從 Xposed 的安裝開始分析,這部分代碼的實現在 XposedInstaller 中。
在一臺 Root 過的設備上安裝 XposedInstaller 後打開,點擊主頁的「INSTALL/UPDATE」,會彈出一個對話框,選擇「Install」或「Install via recovery」安裝 Xposed 框架,此時會首先進行框架核心文件的下載,進入 StatusInstallerFragment#download 方法中:
// StatusInstallerFragment.java
private void download(Context context, String title, FrameworkZips.Type type, final RunnableWithParam<File> callback) {
OnlineFrameworkZip zip = FrameworkZips.getOnline(title, type);
new DownloadsUtil.Builder(context)
.setTitle(zip.title)
// 設置下載 url
.setUrl(zip.url)
.setDestinationFromUrl(DownloadsUtil.DOWNLOAD_FRAMEWORK)
.setCallback(new DownloadFinishedCallback() {
@Override
public void onDownloadFinished(Context context, DownloadInfo info) {
// 下載完成,觸發回調
LOCAL_ZIP_LOADER.triggerReload(true);
callback.run(new File(info.localFilename));
}
})
.setMimeType(DownloadsUtil.MIME_TYPES.ZIP)
.setDialog(true)
.download();
}
其中 zip.url 為 Xposed 框架壓縮包的下載地址,我們重點關注安裝,所以這裡簡要描述 zip 對象,zip 是 OnlineFrameworkZip 類的對象,表示一個 Xposed 框架包,它包含 title、type 和 url 三個成員,type 有兩種,Installer 和 Uninstaller,即安裝包和卸載包,都是包含刷機腳本的 Xposed 補丁包(就是上面工作流程中的壓縮包),title 有三種,Xposed 測試版、Xposed 正式版、和 Uninstaller,用於界面顯示。上面的 zip.url 在 Android 8.1 的 Pixel 手機上運行出來是 http://dl-xda.xposed.info/framework/sdk27/arm64/xposed-v90-sdk27-arm64-beta3.zip,這個 url 是根據設備支持的 CPU 架構、系統版本和 Xposed 當前最新版本組合出來的,組合規則由一個 framework.json 提供,它的本地路徑是 /data/data/de.robv.android.xposed.installer/cache/framework.json,是從 http://dl-xda.xposed.info/framework.json 解析後得到的,內容如下:
{
"zips": [
{
"title": "Version 90-beta$(version)",
"url": "http://dl-xda.xposed.info/framework/sdk$(sdk)/$(arch)/xposed-v90-sdk$(sdk)-$(arch)-beta$(version).zip",
"versions": [
{ "version": "3", "current": true },
{ "version": "2" },
{ "version": "1" }
],
"archs": ["arm", "arm64", "x86"],
"sdks" : [26, 27]
},
{
"title": "Version $(version)",
"url": "http://dl-xda.xposed.info/framework/sdk$(sdk)/$(arch)/xposed-v$(version)-sdk$(sdk)-$(arch).zip",
"versions": [
{ "version": "89", "current": true },
{ "version": "88.2" },
{ "version": "88.1" },
{ "version": "88" },
...
],
"archs": ["arm", "arm64", "x86"],
"sdks" : [21, 22, 23, 24, 25],
"exclude": [
{
"versions": ["88.1"],
"sdks": [21, 22, 23]
},
{
"versions": ["78", "79", "80", "81", "82", "83", "84", "85", "86", "87"],
"sdks": [24, 25]
},
...
]
},
{
"title": "Uninstaller ($(version))",
"url": "http://dl-xda.xposed.info/framework/uninstaller/xposed-uninstaller-$(version)-$(arch).zip",
"type": "uninstaller",
"versions": [
{ "version": "20180117", "current": true },
{ "version": "20180108" },
...
],
"archs": ["arm", "arm64", "x86"],
"sdks" : [21, 22, 23, 24, 25, 26, 27]
}
]
}
能看到,其中包含了 Xposed 測試版、Xposed 正式版、和 Xposed 的 Uninstaller 三種 title 的下載信息,每個下載信息中的 url 為下載地址的模板,versions 為可用的版本,根據系統信息和 Xposed 版本對 url 模板進行填充後組成下載地址。
回到上面的下載,下載成功後,將進入回調根據用戶選擇的安裝類型進行安裝,看一下回調的實現:
// StatusInstallerFragment.java
if (action == ACTION_FLASH) {
runAfterDownload = new RunnableWithParam<File>() {
@Override
public void run(File file) {
// 直接刷入
flash(context, new FlashDirectly(file, type, title, false));
}
};
} else if (action == ACTION_FLASH_RECOVERY) {
runAfterDownload = new RunnableWithParam<File>() {
@Override
public void run(File file) {
// 依賴 recovery 模式進行刷入
flash(context, new FlashRecoveryAuto(file, type, title));
}
};
} else if (action == ACTION_SAVE) {
runAfterDownload = new RunnableWithParam<File>() {
@Override
public void run(File file) {
// 僅保存
saveTo(context, file);
}
};
}
上面兩個分支分別對應 Install 和 Install via recovery 兩種安裝方式的實現,flash 方法將會啟動一個新的負責展示安裝執行的界面,然後執行傳入的 Flashable 對象的 flash 方法,執行成功後展示一個對話框,詢問用戶是否重啟,重啟後將激活 Xposed。分別看一下兩種 Flashable 的實現。
直接刷入// FlashDirectly.java
public void flash(Context context, FlashCallback callback) {
ZipCheckResult zipCheck = openAndCheckZip(callback);
if (zipCheck == null) {
return;
}
// 獲取壓縮包文件
ZipFile zip = zipCheck.getZip();
if (!zipCheck.isFlashableInApp()) {
triggerError(callback, FlashCallback.ERROR_NOT_FLASHABLE_IN_APP);
closeSilently(zip);
return;
}
// 釋放 update-binary 文件至 cache 目錄中
ZipEntry entry = zip.getEntry("META-INF/com/google/android/update-binary");
File updateBinaryFile = new File(XposedApp.getInstance().getCacheDir(), "update-binary");
try {
AssetUtil.writeStreamToFile(zip.getInputStream(entry), updateBinaryFile, 0700);
} catch (IOException e) {
Log.e(XposedApp.TAG, "Could not extract update-binary", e);
triggerError(callback, FlashCallback.ERROR_INVALID_ZIP);
return;
} finally {
closeSilently(zip);
}
// 使用 Root 身份執行刷入命令
RootUtil rootUtil = new RootUtil();
if (!rootUtil.startShell(callback)) {
return;
}
callback.onStarted();
rootUtil.execute("export NO_UIPRINT=1", callback);
if (mSystemless) {
rootUtil.execute("export SYSTEMLESS=1", callback);
}
// 執行 update-binary 文件
int result = rootUtil.execute(getShellPath(updateBinaryFile) + " 2 1 " + getShellPath(mZipPath), callback);
if (result != FlashCallback.OK) {
triggerError(callback, result);
return;
}
callback.onDone();
}
直接刷入會直接使用 Root 身份執行 update-binary 可執行文件,其中會調用 flash-script.sh 文件,它將壓縮包中的目錄複製到對應的系統目錄中,同名文件進行覆蓋,在覆蓋前會對原始系統文件進行備份,例如 libart.so.orig.gz,為了在卸載時恢復。
刷入後正常重啟系統,系統在啟動時將會加載自定義的 app_process 可執行文件,啟動了帶有 Xposed 框架代碼的定製版 zygote 服務進程,為 Xposed 提供支持。
使用 recovery 刷入// FlashRecoveryAuto.java
@Override
public void flash(Context context, FlashCallback callback) {
ZipCheckResult zipCheck = openAndCheckZip(callback);
if (zipCheck == null) {
return;
} else {
closeSilently(zipCheck.getZip());
}
final String zipName = mZipPath.getName();
String cmd;
// 執行刷入命令
RootUtil rootUtil = new RootUtil();
if (!rootUtil.startShell(callback)) {
return;
}
callback.onStarted();
// 確認 /cache/recovery/ 目錄存在
if (rootUtil.execute("ls /cache/recovery", null) != 0) {
callback.onLine(context.getString(R.string.file_creating_directory, "/cache/recovery"));
if (rootUtil.executeWithBusybox("mkdir /cache/recovery", callback) != 0) {
callback.onError(FlashCallback.ERROR_GENERIC,
context.getString(R.string.file_create_directory_failed, "/cache/recovery"));
return;
}
}
// 複製 zip 到 /cache/recovery/ 目錄
callback.onLine(context.getString(R.string.file_copying, zipName));
cmd = "cp -a " + RootUtil.getShellPath(mZipPath) + " /cache/recovery/" + zipName;
if (rootUtil.executeWithBusybox(cmd, callback) != 0) {
callback.onError(FlashCallback.ERROR_GENERIC,
context.getString(R.string.file_copy_failed, zipName, "/cache/recovery"));
return;
}
// 將刷機命令寫入 /cache/recovery/command 文件中
callback.onLine(context.getString(R.string.file_writing_recovery_command));
cmd = "echo --update_package=/cache/recovery/" + zipName + " > /cache/recovery/command";
if (rootUtil.execute(cmd, callback) != 0) {
callback.onError(FlashCallback.ERROR_GENERIC,
context.getString(R.string.file_writing_recovery_command_failed));
return;
}
callback.onLine(context.getString(R.string.auto_flash_note, zipName));
callback.onDone();
}
通過 recovery 模式進行刷入就是首先複製壓縮包到 /cache/recovery/ 中,然後向 /cache/recovery/command 文件中寫入一條刷入壓縮包的命令,然後詢問用戶是否重啟至 recovery 模式,當系統處於 recovery 模式後將會自動檢測 command 文件是否存在,如果存在將執行其中的指令,然後執行刷機包提供的腳本,過程和上面直接刷入一致,首先執行 update-binary 可執行文件,然後其中會調用 flash-script.sh 文件,將刷機包中的文件進行複製。此時,系統退出 reocvery 正常重啟後將會加載成功 Xposed。
這裡就分析完了安裝,主要是通過刷入文件將系統關鍵組件替換為 Xposed 修改過的實現。
下面開始分析 Xposed 的啟動,當系統啟動後,init 進程將會通過解析 init.rc 文件後執行 app_process 創建 zygote 進程,此時就進入了 Xposed 重新編譯修改過的 app_process 文件中。
Xposed 啟動這部分的實現代碼在項目 Xposed 中,是使用 C++ 代碼編寫的,如果這些代碼出現崩潰,則會卡在開機界面,即 boot loop 情況。
Xposed 的 app_process 分為 Dalvik 和 ART 兩種實現,這裡只關注 ART 的實現,在 app_main2.cpp 中。
Native 層入口為 main 函數:
// app_main2.cpp
int main(int argc, char* const argv[])
{
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) < 0) {
if (errno != EINVAL) {
LOG_ALWAYS_FATAL("PR_SET_NO_NEW_PRIVS failed: %s", strerror(errno));
return 12;
}
}
// 1. 處理 xposed 測試選項
if (xposed::handleOptions(argc, argv)) {
return 0;
}
AppRuntime runtime(argv[0], computeArgBlockSize(argc, argv));
argc--;
argv++;
int i;
for (i = 0; i < argc; i++) {
if (argv[i][0] != '-') {
break;
}
if (argv[i][1] == '-' && argv[i][2] == 0) {
++i; // Skip --.
break;
}
runtime.addOption(strdup(argv[i]));
}
bool zygote = false;
bool startSystemServer = false;
bool application = false;
String8 niceName;
String8 className;
++i;
while (i < argc) {
const char* arg = argv[i++];
if (strcmp(arg, "--zygote") == 0) {
zygote = true;
niceName = ZYGOTE_NICE_NAME;
} else if (strcmp(arg, "--start-system-server") == 0) {
startSystemServer = true;
} else if (strcmp(arg, "--application") == 0) {
application = true;
} else if (strncmp(arg, "--nice-name=", 12) == 0) {
niceName.setTo(arg + 12);
} else if (strncmp(arg, "--", 2) != 0) {
className.setTo(arg);
break;
} else {
--i;
break;
}
}
Vector<String8> args;
if (!className.isEmpty()) {
args.add(application ? String8("application") : String8("tool"));
runtime.setClassNameAndArgs(className, argc - i, argv + i);
} else {
maybeCreateDalvikCache();
if (startSystemServer) {
args.add(String8("start-system-server"));
}
char prop[PROP_VALUE_MAX];
if (property_get(ABI_LIST_PROPERTY, prop, NULL) == 0) {
LOG_ALWAYS_FATAL("app_process: Unable to determine ABI list from property %s.",
ABI_LIST_PROPERTY);
return 11;
}
String8 abiFlag("--abi-list=");
abiFlag.append(prop);
args.add(abiFlag);
for (; i < argc; ++i) {
args.add(String8(argv[i]));
}
}
if (!niceName.isEmpty()) {
runtime.setArgv0(niceName.string());
set_process_name(niceName.string());
}
// 2.
if (zygote) {
// 初始化 xposed
isXposedLoaded = xposed::initialize(true, startSystemServer, NULL, argc, argv);
// #define XPOSED_CLASS_DOTS_ZYGOTE "de.robv.android.xposed.XposedBridge"
// 初始化成功則會 XposedBridge 流程,否則進入系統的 ZygoteInit 中
runtimeStart(runtime, isXposedLoaded ? XPOSED_CLASS_DOTS_ZYGOTE : "com.android.internal.os.ZygoteInit", args, zygote);
} else if (className) {
// 非 zygote 進程流程,用於支持使用命令行啟動自定義的類,這裡先不關心這個流程
isXposedLoaded = xposed::initialize(false, false, className, argc, argv);
runtimeStart(runtime, isXposedLoaded ? XPOSED_CLASS_DOTS_TOOLS : "com.android.internal.os.RuntimeInit", args, zygote);
} else {
fprintf(stderr, "Error: no class name or --zygote supplied.\n");
app_usage();
LOG_ALWAYS_FATAL("app_process: no class name or --zygote supplied.");
return 10;
}
}
通過對比原版的 ART 代碼,發現 main 函數中只有 1 和 2 兩處代碼進行了修改,當 init 進程解析 init.rc 文件時,會啟動 zygote 進程,此時就進入了 app_process 的 main 函數中,並將 init.rc 中附帶的選項使用 argv 參數傳遞進來。
先看第一處 handleOptions:
// xposed.cpp
bool handleOptions(int argc, char* const argv[]) {
parseXposedProp();
if (argc == 2 && strcmp(argv[1], "--xposedversion") == 0) {
printf("Xposed version: %s\n", xposedVersion);
return true;
}
if (argc == 2 && strcmp(argv[1], "--xposedtestsafemode") == 0) {
printf("Testing Xposed safemode trigger\n");
if (detectSafemodeTrigger(shouldSkipSafemodeDelay())) {
printf("Safemode triggered\n");
} else {
printf("Safemode not triggered\n");
}
return true;
}
argBlockStart = argv[0];
uintptr_t start = reinterpret_cast<uintptr_t>(argv[0]);
uintptr_t end = reinterpret_cast<uintptr_t>(argv[argc - 1]);
end += strlen(argv[argc - 1]) + 1;
argBlockLength = end - start;
return false;
}
處理了 --xposedversion 和 --xposedtestsafemode 兩個參數,不過查看 init.rc 文件中啟動 zygote 的選項中並沒有這兩項:
service zygote /system/bin/app_process -Xzygote /system/bin --zygote --start-system-server
...
所以應該是用於 Xposed 的測試代碼,那麼這裡就不再關心。
繼續下面第 2 部分,在 zygote 流程中,首先會初始化 Xposed,如果初始化成功就會傳入 XposedBridge 的完整類名,用於進入 Java 層的 XposedBridge 入口。
首先看 xposed::initialize 函數:
bool initialize(bool zygote, bool startSystemServer, const char* className, int argc, char* const argv[]) {
#if !defined(XPOSED_ENABLE_FOR_TOOLS)
if (!zygote)
return false;
#endif
// 判斷系統是否處於 minmal framework 模式,此時 /data 是 tmpfs 類型,無法加載 Xposed
if (isMinimalFramework()) {
ALOGI("Not loading Xposed for minimal framework (encrypted device)");
return false;
}
// 保存相關參數
// xposed 是一個用於共享信息的對象,XposedShared* xposed = new XposedShared;
xposed->zygote = zygote;
xposed->startSystemServer = startSystemServer;
xposed->startClassName = className;
xposed->xposedVersionInt = xposedVersionInt;
#if XPOSED_WITH_SELINUX
xposed->isSELinuxEnabled = is_selinux_enabled() == 1;
xposed->isSELinuxEnforcing = xposed->isSELinuxEnabled && security_getenforce() == 1;
#else
xposed->isSELinuxEnabled = false;
xposed->isSELinuxEnforcing = false;
#endif // XPOSED_WITH_SELINUX
if (startSystemServer) {
xposed::logcat::printStartupMarker();
} else if (zygote) {
// 給另一個架構的優先執行的 zygote 進程一些時間啟動,從而避免同時列印日誌,造成難以閱讀
sleep(10);
}
// 列印 Xposed 版本和 Device、ROM 等信息,開機時可以在 logcat 中看到
printRomInfo();
if (startSystemServer) {
// 確保 XposedInstaller uid 和 gid 存在,即表示安裝了 XposedInstaller
// 啟動 XposedService 服務
if (!determineXposedInstallerUidGid() || !xposed::service::startAll()) {
return false;
}
xposed::logcat::start();
#if XPOSED_WITH_SELINUX
} else if (xposed->isSELinuxEnabled) {
if (!xposed::service::startMembased()) {
return false;
}
#endif // XPOSED_WITH_SELINUX
}
#if XPOSED_WITH_SELINUX
if (xposed->isSELinuxEnabled) {
xposed::service::membased::restrictMemoryInheritance();
}
#endif // XPOSED_WITH_SELINUX
if (zygote && !isSafemodeDisabled() && detectSafemodeTrigger(shouldSkipSafemodeDelay()))
disableXposed();
if (isDisabled() || (!zygote && shouldIgnoreCommand(argc, argv)))
return false;
// 將 XposedBridge.jar 加入系統 CLASSPATH 變量,使其代碼中的類可被加載
return addJarToClasspath();
}
以上代碼主要是保存了 app_process 的啟動選項,設置一些 xposed 支持,最後使用 addJarToClasspath 將 XposedBridge.jar 加入系統路徑。
// xposed.cpp
bool addJarToClasspath() {
ALOGI("--");
// #define XPOSED_JAR "/system/framework/XposedBridge.jar"
if (access(XPOSED_JAR, R_OK) == 0) {
if (!addPathToEnv("CLASSPATH", XPOSED_JAR))
return false;
ALOGI("Added Xposed (%s) to CLASSPATH", XPOSED_JAR);
return true;
} else {
ALOGE("ERROR: Could not access Xposed jar '%s'", XPOSED_JAR);
return false;
}
}
這裡就初始化完成了,如果中間有一步執行失敗,返回 false,那麼 Xposed 就不能正常工作了,會通過傳遞 ZygoteInit 完整類名,進入系統正常的 zygote 流程。
現在回到 main 函數中,下面進入 runtimeStart:
// app_main2.cpp
static void runtimeStart(AppRuntime& runtime, const char *classname, const Vector<String8>& options, bool zygote)
{
#if PLATFORM_SDK_VERSION >= 23
runtime.start(classname, options, zygote);
#else
// try newer variant (5.1.1_r19 and later) first
void (*ptr1)(AppRuntime&, const char*, const Vector<String8>&, bool);
*(void **) (&ptr1) = dlsym(RTLD_DEFAULT, "_ZN7android14AndroidRuntime5startEPKcRKNS_6VectorINS_7String8EEEb");
if (ptr1 != NULL) {
ptr1(runtime, classname, options, zygote);
return;
}
// fall back to older variant
void (*ptr2)(AppRuntime&, const char*, const Vector<String8>&);
*(void **) (&ptr2) = dlsym(RTLD_DEFAULT, "_ZN7android14AndroidRuntime5startEPKcRKNS_6VectorINS_7String8EEE");
if (ptr2 != NULL) {
ptr2(runtime, classname, options);
return;
}
// should not happen
LOG_ALWAYS_FATAL("app_process: could not locate AndroidRuntime::start() method.");
#endif
}
其實就是直接調用系統 AppRuntime 的 start 函數,如果是 Android 5.1.1 之前需要通過通過獲取 AppRuntime::start 函數符號句柄的方式調用,後面一長串字符串是函數被編譯後的籤名字符串。
調用 AppRuntime::start 後,內部會創建 Java 虛擬機,然後執行傳入類的 main 函數:
// AndroidRuntime.cpp
void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote)
{
ALOGD(">>>>>> START %s uid %d <<<<<<\n",
className != NULL ? className : "(unknown)", getuid());
static const String8 startSystemServer("start-system-server");
// ...
JniInvocation jni_invocation;
jni_invocation.Init(NULL);
JNIEnv* env;
// 創建虛擬機
if (startVm(&mJavaVM, &env, zygote) != 0) {
return;
}
onVmCreated(env);
// 註冊系統類 JNI 方法
if (startReg(env) < 0) {
ALOGE("Unable to register all android natives\n");
return;
}
// ...
// 轉換為 JNI 格式類名:com/android/internal/os/XposedBridge
char* slashClassName = toSlashClassName(className);
jclass startClass = env->FindClass(slashClassName);
if (startClass == NULL) {
ALOGE("JavaVM unable to locate class '%s'\n", slashClassName);
} else {
jmethodID startMeth = env->GetStaticMethodID(startClass, "main",
"([Ljava/lang/String;)V");
if (startMeth == NULL) {
ALOGE("JavaVM unable to find main() in '%s'\n", className);
} else {
// 調用 XposedBridge.main();
env->CallStaticVoidMethod(startClass, startMeth, strArray);
}
}
// ...
}
下面就進入到了 Java 層的 XposedBridge#main 方法中:
// XposedBridge.java
protected static void main(String[] args) {
// Initialize the Xposed framework and modules
try {
// 判斷 native 加載成功
if (!hadInitErrors()) {
// Xposed 相關初始化
initXResources();
// SELinux 相關支持
SELinuxHelper.initOnce();
SELinuxHelper.initForProcess(null);
// runtime 表示 ART 還是 Dalivk
runtime = getRuntime();
XPOSED_BRIDGE_VERSION = getXposedVersion();
if (isZygote) {
// 為資源的 Hook 註冊回調
XposedInit.hookResources();
// 為代碼 Hook 註冊回調,將會調用每個 Xposed 模塊的入口
XposedInit.initForZygote();
}
// 加載設備上的 Xposed 模塊
XposedInit.loadModules();
} else {
Log.e(TAG, "Not initializing Xposed because of previous errors");
}
} catch (Throwable t) {
Log.e(TAG, "Errors during Xposed initialization", t);
disableHooks = true;
}
// 調用系統正常流程 Java 層
if (isZygote) {
ZygoteInit.main(args);
} else {
RuntimeInit.main(args);
}
}
重點關注 XposedInit.initForZygote(); 和 XposedInit.loadModules();:
/*package*/ static void initForZygote() throws Throwable {
// ...
// system_server 初始化
if (Build.VERSION.SDK_INT < 21) {
findAndHookMethod("com.android.server.ServerThread", null,
Build.VERSION.SDK_INT < 19 ? "run" : "initAndLoop", new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
SELinuxHelper.initForProcess("android");
loadedPackagesInProcess.add("android");
XC_LoadPackage.LoadPackageParam lpparam = new XC_LoadPackage.LoadPackageParam(XposedBridge.sLoadedPackageCallbacks);
lpparam.packageName = "android";
lpparam.processName = "android"; // it's actually system_server, but other functions return this as well
lpparam.classLoader = XposedBridge.BOOTCLASSLOADER;
lpparam.appInfo = null;
lpparam.isFirstApplication = true;
XC_LoadPackage.callAll(lpparam);
}
});
}
// ...
hookAllConstructors(LoadedApk.class, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
LoadedApk loadedApk = (LoadedApk) param.thisObject;
String packageName = loadedApk.getPackageName();
XResources.setPackageNameForResDir(packageName, loadedApk.getResDir());
if (packageName.equals("android") || !loadedPackagesInProcess.add(packageName))
return;
if (!getBooleanField(loadedApk, "mIncludeCode"))
return;
XC_LoadPackage.LoadPackageParam lpparam = new XC_LoadPackage.LoadPackageParam(XposedBridge.sLoadedPackageCallbacks);
lpparam.packageName = packageName;
lpparam.processName = AndroidAppHelper.currentProcessName();
lpparam.classLoader = loadedApk.getClassLoader();
lpparam.appInfo = loadedApk.getApplicationInfo();
lpparam.isFirstApplication = false;
XC_LoadPackage.callAll(lpparam);
}
});
// ...
}
上面省略了一部分代碼,上面的代碼主要是通過 Hook 系統關鍵類的流程,為 Xposed 模塊註冊加載代碼包的回調,當這些系統流程執行時,會通過 XC_LoadPackage.callAll(lpparm) 通知所有的 Xposed 模塊。
上面創建了 XC_LoadPackage.LoadPackageParam 的對象,就是為了給 Xposed 模塊的入口進行傳遞。
XposedBridge.sLoadedPackageCallbacks 是 Xposed 模塊回調的集合,是一個 CopyOnWriteSortedSet<XC_LoadPackage> 類型。
XC_LoadPackage 有一個 call 方法,用於回調自己的 handleLoadPackage 方法。
// XC_LoadPackage.java
@Override
protected void call(Param param) throws Throwable {
if (param instanceof LoadPackageParam)
handleLoadPackage((LoadPackageParam) param);
}
XC_LoadPackage.callAll 將會調用每一個 XC_LoadPackage 的 call 方法,從而向 Xposed 模塊傳遞 lpparm 參數。
// XC_LoadPackage.java
public static void callAll(Param param) {
if (param.callbacks == null)
throw new IllegalStateException("This object was not created for use with callAll");
for (int i = 0; i < param.callbacks.length; i++) {
try {
((XCallback) param.callbacks[i]).call(param);
} catch (Throwable t) { XposedBridge.log(t); }
}
}
再來看 XposedInit.loadModules();:
// XposedInit.java
/*package*/ static void loadModules() throws IOException {
// 從 modules.list 文件讀取 Xposde 模塊列表
final String filename = BASE_DIR + "conf/modules.list";
BaseService service = SELinuxHelper.getAppDataFileService();
if (!service.checkFileExists(filename)) {
Log.e(TAG, "Cannot load any modules because " + filename + " was not found");
return;
}
ClassLoader topClassLoader = XposedBridge.BOOTCLASSLOADER;
ClassLoader parent;
while ((parent = topClassLoader.getParent()) != null) {
topClassLoader = parent;
}
InputStream stream = service.getFileInputStream(filename);
BufferedReader apks = new BufferedReader(new InputStreamReader(stream));
String apk;
while ((apk = apks.readLine()) != null) {
// 加載每個 Xposed 模塊
loadModule(apk, topClassLoader);
}
apks.close();
}
// XposedInit.java
private static void loadModule(String apk, ClassLoader topClassLoader) {
Log.i(TAG, "Loading modules from " + apk);
// ...
DexFile dexFile;
try {
dexFile = new DexFile(apk);
} catch (IOException e) {
Log.e(TAG, " Cannot load module", e);
return;
}
// ...
ZipFile zipFile = null;
InputStream is;
try {
zipFile = new ZipFile(apk);
// 打開 Xposed 模塊 apk 文件中的 xposed_init 文件,
// 它的內容是 Xposed 模塊入口類的全類名
ZipEntry zipEntry = zipFile.getEntry("assets/xposed_init");
if (zipEntry == null) {
Log.e(TAG, " assets/xposed_init not found in the APK");
closeSilently(zipFile);
return;
}
is = zipFile.getInputStream(zipEntry);
} catch (IOException e) {
Log.e(TAG, " Cannot read assets/xposed_init in the APK", e);
closeSilently(zipFile);
return;
}
ClassLoader mcl = new PathClassLoader(apk, XposedBridge.BOOTCLASSLOADER);
BufferedReader moduleClassesReader = new BufferedReader(new InputStreamReader(is));
try {
String moduleClassName;
// 獲取 Xposed 模塊入口類名
while ((moduleClassName = moduleClassesReader.readLine()) != null) {
moduleClassName = moduleClassName.trim();
if (moduleClassName.isEmpty() || moduleClassName.startsWith("#"))
continue;
try {
Log.i(TAG, " Loading class " + moduleClassName);
// 加載入口類
Class<?> moduleClass = mcl.loadClass(moduleClassName);
// ...
final Object moduleInstance = moduleClass.newInstance();
if (XposedBridge.isZygote) {
if (moduleInstance instanceof IXposedHookZygoteInit) {
IXposedHookZygoteInit.StartupParam param = new IXposedHookZygoteInit.StartupParam();
param.modulePath = apk;
param.startsSystemServer = startsSystemServer;
((IXposedHookZygoteInit) moduleInstance).initZygote(param);
}
// 根據模塊關心代碼 Hook 還是資源 Hook 分別處理
if (moduleInstance instanceof IXposedHookLoadPackage)
// 註冊到 sLoadedPackageCallbacks 中
XposedBridge.hookLoadPackage(new IXposedHookLoadPackage.Wrapper((IXposedHookLoadPackage) moduleInstance));
if (moduleInstance instanceof IXposedHookInitPackageResources)
// 註冊資源的 Xposed 模塊回調
XposedBridge.hookInitPackageResources(new IXposedHookInitPackageResources.Wrapper((IXposedHookInitPackageResources) moduleInstance));
}
// ...
} catch (Throwable t) {
Log.e(TAG, " Failed to load class " + moduleClassName, t);
}
}
} catch (IOException e) {
Log.e(TAG, " Failed to load module from " + apk, e);
} finally {
closeSilently(is);
closeSilently(zipFile);
}
}
上面代碼首先從 conf/modules.list 文件加載所有 Xposed 模塊的 APK 路徑列表,然後通過讀取每一個 Xposed 模塊 APK 包中的 assets/xposed_init 文件獲得 Xposed 模塊的入口類名,最後將這個類通過 XposedBridge.hookLoadPackage 註冊到前面的 XposedBridge.sLoadedPackageCallbacks 中。
// XposedBridge.java
public static void hookLoadPackage(XC_LoadPackage callback) {
synchronized (sLoadedPackageCallbacks) {
sLoadedPackageCallbacks.add(callback);
}
}
那麼當前面 Hook 的系統關鍵類流程被觸發後,將會通過 sLoadedPackageCallbacks 回調每個 Xposed 模塊的入口。
到這裡 Xposed 模塊的啟動的核心邏輯就分析完了,主要是通過 Xposed 定製版的 zygote 加載 XposedBridge.jar,然後調用 XposedBridge#main 方法加載所有的 Xposed 模塊,當一個進程通過 zygote 進程 clone 出來時,就會攜帶 XposedBridge.jar 的代碼,同時在進程啟動時回調所有的 Xposed 模塊的入口,XposedBridge.jar 中還包含 Hook API,那麼 Xposed 模塊就可以通過這些 API 對應用程式進行 Hook 操作了。
接下來就是 Xposed 的方法 Hook 的實現代碼分析了。
Xposed 方法 Hook從 XposedHelpers.findAndHookMethod 方法開始,看 Xposed 是如何進行 Hook 的。
// XposedHelpers.java
public static XC_MethodHook.Unhook findAndHookMethod(Class<?> clazz, String methodName, Object... parameterTypesAndCallback) {
if (parameterTypesAndCallback.length == 0 || !(parameterTypesAndCallback[parameterTypesAndCallback.length-1] instanceof XC_MethodHook))
throw new IllegalArgumentException("no callback defined");
XC_MethodHook callback = (XC_MethodHook) parameterTypesAndCallback[parameterTypesAndCallback.length-1];
// 獲取方法的反射表示對象
Method m = findMethodExact(clazz, methodName, getParameterClasses(clazz.getClassLoader(), parameterTypesAndCallback));
// 下一步
return XposedBridge.hookMethod(m, callback);
}
首先使用 findMethodExact 獲取一個 Java 方法的反射表示對象 m:
// XposedHelpers.java
public static Method findMethodExact(Class<?> clazz, String methodName, Class<?>... parameterTypes) {
String fullMethodName = clazz.getName() + '#' + methodName + getParametersString(parameterTypes) + "#exact";
if (methodCache.containsKey(fullMethodName)) {
// 首先從緩存中取
Method method = methodCache.get(fullMethodName);
if (method == null)
throw new NoSuchMethodError(fullMethodName);
return method;
}
try {
// 通過反射 API 取得 Method 對象
Method method = clazz.getDeclaredMethod(methodName, parameterTypes);
method.setAccessible(true);
methodCache.put(fullMethodName, method);
return method;
} catch (NoSuchMethodException e) {
methodCache.put(fullMethodName, null);
throw new NoSuchMethodError(fullMethodName);
}
}
這裡也很簡單,使用了緩存保存方法的反射對象,然後繼續下一步,進入 XposedBridge#hookMethod 方法。
// XposedBridge.java
public static XC_MethodHook.Unhook hookMethod(Member hookMethod, XC_MethodHook callback) {
// 只允許 Method 和 Constructor 類型,Constructor 類型為了支持 findAndHookConstructor
if (!(hookMethod instanceof Method) && !(hookMethod instanceof Constructor<?>)) {
throw new IllegalArgumentException("Only methods and constructors can be hooked: " + hookMethod.toString());
} else if (hookMethod.getDeclaringClass().isInterface()) {
throw new IllegalArgumentException("Cannot hook interfaces: " + hookMethod.toString());
} else if (Modifier.isAbstract(hookMethod.getModifiers())) {
throw new IllegalArgumentException("Cannot hook abstract methods: " + hookMethod.toString());
}
boolean newMethod = false;
CopyOnWriteSortedSet<XC_MethodHook> callbacks;
synchronized (sHookedMethodCallbacks) {
callbacks = sHookedMethodCallbacks.get(hookMethod);
if (callbacks == null) {
// 創建 method 與 hook 回調列表關聯的映射表
callbacks = new CopyOnWriteSortedSet<>();
sHookedMethodCallbacks.put(hookMethod, callbacks);
newMethod = true;
}
}
// 添加 hook 回調到和這個 method 關聯的 hook 回調列表
callbacks.add(callback);
if (dnewMethod) {
Clss<?> declaringClass = hookMethod.getDeclaringClass();
int slot;
Class<?>[] parameterTypes;
Class<?> returnType;
if (runtime == RUNTIME_ART) {
slot = 0;
parameterTypes = null;
returnType = null;
} else if (hookMethod instanceof Method) {
// slot 在 Android 5.0 以下的系統,java.reflect.Method 類中的成員,
// 它是 Dralvik 虛擬機中這個 Method 在虛擬機中的地址。
// Android 5.0 開始正式使用了 ART 虛擬機,所以不存在這個成員
slot = getIntField(hookMethod, "slot");
parameterTypes = ((Method) hookMethod).getParameterTypes();
returnType = ((Method) hookMethod).getReturnType();
} else {
// 處理 Constructor
slot = getIntField(hookMethod, "slot");
parameterTypes = ((Constructor<?>) hookMethod).getParameterTypes();
returnType = null;
}
// 打包 Hook 回調相關信息,準備進入 Native 層
AdditionalHookInfo additionalInfo = new AdditionalHookInfo(callbacks, parameterTypes, returnType);
// 進入 Native 層代碼,傳入 method、class、slot、hook 回調等信息
hookMethodNative(hookMethod, declaringClass, slot, additionalInfo);
}
return callback.new Unhook(hookMethod);
}
上面主要是添加了 XC_MethodHook 類型的 Hook 回調,然後將相關信息全部傳入了 Xposed native 層代碼中。
最後返回一個 Unhook 對象,是為了取消 Hook,它的 unhook 方法如下:
// XC_MethodHook.java - class Unhook
public void unhook() {
XposedBridge.unhookMethod(hookMethod, XC_MethodHook.this);
}
// XposedBridge.java
public static void unhookMethod(Member hookMethod, XC_MethodHook callback) {
CopyOnWriteSortedSet<XC_MethodHook> callbacks;
synchronized (sHookedMethodCallbacks) {
callbacks = sHookedMethodCallbacks.get(hookMethod);
if (callbacks == null)
return;
}
callbacks.remove(callback);
}
就是直接移除與這個 Java 方法相關的 Hook 處理回調。
下面查看 hookMethodNative 函數的實現,發現它是一個 JNI 方法:
private native synchronized static void hookMethodNative(Member method, Class<?> declaringClass, int slot, Object additionalInfo);
它的實現在 libxposed_art.so 中,原始碼在 Xposed 項目中。
首先需要解決一個問題,這個動態庫是什麼時候加載的,它的 JNI 方法和 Java 層是什麼時候關聯的?
它是在 Java 虛擬機中創建時加載的,同時關聯的 JNI 方法。
在 app_main2.cpp 中,Xposed 除了改寫 app_process 的 main 函數,還改寫了 AppRuntime::onVmCreated 函數:
// app_main2.cpp
namespace android {
class AppRuntime : public AndroidRuntime
{
public:
// ...
virtual void onVmCreated(JNIEnv* env)
{
if (isXposedLoaded)
xposed::onVmCreated(env);
if (mClassName.isEmpty()) {
return;
}
char* slashClassName = toSlashClassName(mClassName.string());
mClass = env->FindClass(slashClassName);
if (mClass == NULL) {
ALOGE("ERROR: could not find class '%s'\n", mClassName.string());
env->ExceptionDescribe();
}
free(slashClassName);
mClass = reinterpret_cast<jclass>(env->NewGlobalRef(mClass));
}
// ...
};
}
回顧前面的內容,這個函數將在 Java 虛擬機創建後被回調:
// AndroidRuntime.cpp
void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote)
{
// ...
if (startVm(&mJavaVM, &env, zygote) != 0) {
return;
}
// 回調 Java 虛擬機創建
onVmCreated(env);
if (startReg(env) < 0) {
ALOGE("Unable to register all android natives\n");
return;
}
// ...
}
進入 xposed::onVmCreated 函數:
// xposed.cpp
void onVmCreated(JNIEnv* env) {
const char* xposedLibPath = NULL;
// 首先確認 Xposed 庫的路徑是 ART 還是 Dalvik
if (!determineRuntime(&xposedLibPath)) {
ALOGE("Could not determine runtime, not loading Xposed");
return;
}
// 打開 Xposed 動態庫
void* xposedLibHandle = dlopen(xposedLibPath, RTLD_NOW);
if (!xposedLibHandle) {
ALOGE("Could not load libxposed: %s", dlerror());
return;
}
dlerror();
// 調用初始化方法
bool (*xposedInitLib)(XposedShared* shared) = NULL;
*(void **) (&xposedInitLib) = dlsym(xposedLibHandle, "xposedInitLib");
if (!xposedInitLib) {
ALOGE("Could not find function xposedInitLib");
return;
}
#if XPOSED_WITH_SELINUX
xposed->zygoteservice_accessFile = &service::membased::accessFile;
xposed->zygoteservice_statFile = &service::membased::statFile;
xposed->zygoteservice_readFile = &service::membased::readFile;
#endif // XPOSED_WITH_SELINUX
if (xposedInitLib(xposed)) {
// 調用綁定的 onVmCreated 回調函數
xposed->onVmCreated(env);
}
}
首先是 determineRuntime 確認 Xposed 的庫路徑:
// xposed.cpp
static bool determineRuntime(const char** xposedLibPath) {
FILE *fp = fopen("/proc/self/maps", "r");
if (fp == NULL) {
ALOGE("Could not open /proc/self/maps: %s", strerror(errno));
return false;
}
bool success = false;
char line[256];
while (fgets(line, sizeof(line), fp) != NULL) {
char* libname = strrchr(line, '/');
if (!libname)
continue;
libname++;
if (strcmp("libdvm.so\n", libname) == 0) {
ALOGI("Detected Dalvik runtime");
// #define XPOSED_LIB_DALVIK POSED_LIB_DIR "libxposed_dalvik.so"
*xposedLibPath = XPOSED_LIB_DALVIK;
success = true;
break;
} else if (strcmp("libart.so\n", libname) == 0) {
ALOGI("Detected ART runtime");
// #define XPOSED_LIB_ART XPOSED_LIB_DIR "libxposed_art.so"
*xposedLibPath = XPOSED_LIB_ART;
success = true;
break;
}
}
fclose(fp);
return success;
}
根據系統中是否存在 libdvm.so 或 libart.so,確認加載支持 ART 還是 Dalvik 版本的 Xposed 庫,在 ART 上加載 libxposed_art.so。
然後使用 dlopen 加載連結了這個動態庫,那麼它的符號就可以被正常訪問了。
後面又調用了 xposedInitLib 函數:
// libxposed_art.cpp
bool xposedInitLib(XposedShared* shared) {
xposed = shared;
xposed->onVmCreated = &onVmCreatedCommon;
return true;
}
指定了一個 xposed->onVmCreated 為 onVmCreatedCommon,看一下它的實現。
// libxposed_common.cpp
void onVmCreatedCommon(JNIEnv* env) {
if (!initXposedBridge(env) || !initZygoteService(env)) {
return;
}
if (!onVmCreated(env)) {
return;
}
xposedLoadedSuccessfully = true;
return;
}
這裡主要關注 initXposedBridge,它會進行 JNI 方法的註冊。
// libxposed_common.cpp
bool initXposedBridge(JNIEnv* env) {
// #define CLASS_XPOSED_BRIDGE "de/robv/android/xposed/XposedBridge"
classXposedBridge = env->FindClass(CLASS_XPOSED_BRIDGE);
if (classXposedBridge == NULL) {
ALOGE("Error while loading Xposed class '%s':", CLASS_XPOSED_BRIDGE);
logExceptionStackTrace();
env->ExceptionClear();
return false;
}
classXposedBridge = reinterpret_cast<jclass>(env->NewGlobalRef(classXposedBridge));
ALOGI("Found Xposed class '%s', now initializing", CLASS_XPOSED_BRIDGE);
// 註冊 XposedBridge 關聯的 JNI 方法
if (register_natives_XposedBridge(env, classXposedBridge) != JNI_OK) {
ALOGE("Could not register natives for '%s'", CLASS_XPOSED_BRIDGE);
logExceptionStackTrace();
env->ExceptionClear();
return false;
}
// 緩存 XposedBridge 的 handleHookedMethod 方法的 jmethodID
methodXposedBridgeHandleHookedMethod = env->GetStaticMethodID(classXposedBridge, "handleHookedMethod",
"(Ljava/lang/reflect/Member;ILjava/lang/Object;Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;");
if (methodXposedBridgeHandleHookedMethod == NULL) {
ALOGE("ERROR: could not find method %s.handleHookedMethod(Member, int, Object, Object, Object[])", CLASS_XPOSED_BRIDGE);
logExceptionStackTrace();
env->ExceptionClear();
return false;
}
return true;
}
// libxposed_common.cpp
int register_natives_XposedBridge(JNIEnv* env, jclass clazz) {
const JNINativeMethod methods[] = {
NATIVE_METHOD(XposedBridge, hadInitErrors, "()Z"),
NATIVE_METHOD(XposedBridge, getStartClassName, "()Ljava/lang/String;"),
NATIVE_METHOD(XposedBridge, getRuntime, "()I"),
NATIVE_METHOD(XposedBridge, startsSystemServer, "()Z"),
NATIVE_METHOD(XposedBridge, getXposedVersion, "()I"),
NATIVE_METHOD(XposedBridge, initXResourcesNative, "()Z"),
// 註冊 hookMethodNative 方法
NATIVE_METHOD(XposedBridge, hookMethodNative, "(Ljava/lang/reflect/Member;Ljava/lang/Class;ILjava/lang/Object;)V"),
// ...
};
return env->RegisterNatives(clazz, methods, NELEM(methods));
}
其中 NATIVE_METHOD 是一個宏,方便註冊 JNI 方法:
// libxposed_common.h
#ifndef NATIVE_METHOD
#define NATIVE_METHOD(className, functionName, signature) \
{ #functionName, signature, reinterpret_cast<void*>(className ## _ ## functionName) }
#endif
現在回去,對 hookMethodNative 的具體實現進行分析,從這裡開始就是真正開始實現方法 Hook 了。
由於這裡是 Xposed 方法 Hook 的核心實現,所以同時分析一下基於 Dalvik 的實現。
Dalvik Hook 實現首先看一下 libxposed_dalvik.so 中的實現,驗證一下本文開頭基於 Dalvik 的方法 Hook 的描述。
// libxposed_dalvik.cpp
void XposedBridge_hookMethodNative(JNIEnv* env, jclass clazz, jobject reflectedMethodIndirect,
jobject declaredClassIndirect, jint slot, jobject additionalInfoIndirect) {
if (declaredClassIndirect == NULL || reflectedMethodIndirect == NULL) {
dvmThrowIllegalArgumentException("method and declaredClass must not be null");
return;
}
// 獲取 Dalvik 中表示 Java 類的 ClassObject 對象
ClassObject* declaredClass = (ClassObject*) dvmDecodeIndirectRef(dvmThreadSelf(), declaredClassIndirect);
// 利用 slot 變量從 ClassObject 中找到 Dalvik 中表示 Java 方法的 Method 對象
Method* method = dvmSlotToMethod(declaredClass, slot);
if (method == NULL) {
dvmThrowNoSuchMethodError("Could not get internal representation for method");
return;
}
// inline bool isMethodHooked(const Method* method) {
// return (method->nativeFunc == &hookedMethodCallback);
// }
if (isMethodHooked(method)) {
// 此方法已經被 Hook,直接返回
return;
}
// 保存原始方法的信息
XposedHookInfo* hookInfo = (XposedHookInfo*) calloc(1, sizeof(XposedHookInfo));
memcpy(hookInfo, method, sizeof(hookInfo->originalMethodStruct));
hookInfo->reflectedMethod = dvmDecodeIndirectRef(dvmThreadSelf(), env->NewGlobalRef(reflectedMethodIndirect));
hookInfo->additionalInfo = dvmDecodeIndirectRef(dvmThreadSelf(), env->NewGlobalRef(additionalInfoIndirect));
// 將此 Java 方法增加 native 描述符,即 JNI 方法
SET_METHOD_FLAG(method, ACC_NATIVE);
// 設置 native 函數的處理函數,那麼 Dalvik 解釋執行這個方法時,
// 首先判斷會它是 JNI 方法,然後會跳轉至 nativeFunc 進行執行
method->nativeFunc = &hookedMethodCallback;
method->insns = (const u2*) hookInfo;
method->registersSize = method->insSize;
method->outsSize = 0;
if (PTR_gDvmJit != NULL) {
char currentValue = *((char*)PTR_gDvmJit + MEMBER_OFFSET_VAR(DvmJitGlobals,codeCacheFull));
if (currentValue == 0 || currentValue == 1) {
MEMBER_VAL(PTR_gDvmJit, DvmJitGlobals, codeCacheFull) = true;
} else {
ALOGE("Unexpected current value for codeCacheFull: %d", currentValue);
}
}
}
這裡 Xposed 基於 Dalvik 實現的方法 Hook 處理比較簡單,就是先將這個 Java 方法修改為 native 方法,然後給它綁定一個 nativeFunc,當 Java 代碼調用這個方法時,由於它是 JNI 方法,虛擬機就會跳轉到 nativeFunc 進行執行。
Dalvik 虛擬機執行 Java 方法的實現如下:
// Stack.cpp
void dvmCallMethodV(Thread* self, const Method* method, Object* obj, bool fromJni, JValue* pResult, va_list args)
{
// ...
if (dvmIsNativeMethod(method)) {
TRACE_METHOD_ENTER(self, method);
// 如果是 native 方法,則跳轉 nativeFunc 進行執行
(*method->nativeFunc)((u4*)self->interpSave.curFrame, pResult, method, self);
TRACE_METHOD_EXIT(self, method);
} else {
dvmInterpret(self, method, pResult);
}
// ...
}
// Object.h
INLINE bool dvmIsNativeMethod(const Method* method) {
return (method->accessFlags & ACC_NATIVE) != 0;
}
可以看到,如果一個方法是 JNI 方法,那麼 Dalvik 虛擬機就會調用它綁定的 nativeFunc 函數。
前面設置的 hookedMethodCallback 函數將會把被調用的 Java 方法的參數進行轉發,最終會調用 Java 層 XposedBridge 的 handleHookedMethod 方法進行處理,就能夠達到 Hook 的目的了,至於 hookedMethodCallback 函數的實現,這裡不再詳細分析,可以自己看一下。Java 層 handleHookedMethod 方法的實現和 ART 沒有區別,都是在 XposedBridge.jar 中,在下面 ART 部分中會進行分析。
下面進入 libxposed_art.so 中的 hookMethodNative 函數實現:
ART Hook 實現接下來關注 libxposed_art.so 中的實現。
// libxposed_art.cpp
void XposedBridge_hookMethodNative(JNIEnv* env, jclass, jobject javaReflectedMethod,
jobject, jint, jobject javaAdditionalInfo) {
ScopedObjectAccess soa(env);
if (javaReflectedMethod == nullptr) {
#if PLATFORM_SDK_VERSION >= 23
ThrowIllegalArgumentException("method must not be null");
#else
ThrowIllegalArgumentException(nullptr, "method must not be null");
#endif
return;
}
// ART 虛擬機中表示 Java 方法的 artMethod 對象
ArtMethod* artMethod = ArtMethod::FromReflectedMethod(soa, javaReflectedMethod);
// fHook 這個方法
artMethod->EnableXposedHook(soa, javaAdditionalInfo);
}
上面使用 Java 層方法的反射表示對象 javaReflectedMethod,獲取了一個 ART 虛擬機中用來表示 Java 方法的 ArtMethod 對象,然後就直接進入 ArtMethod 的 EnableXposedHook 函數中了。
其中 FromReflectedMethod 是 ART 虛擬機本來就有的方法;ScopedObjectAccess 是一個工具類,需要藉助 env 進行操作。
下面進入 ArtMethod 的 EnableXposedHook 函數中,從這裡開始就進入 Xposed 修改過的 ART 虛擬機的項目 android_art 中了。
// art_method.cc
void ArtMethod::EnableXposedHook(ScopedObjectAccess& soa, jobject additional_info) {
if (UNLIKELY(IsXposedHookedMethod())) {
// 已被 Hook
return;
} else if (UNLIKELY(IsXposedOriginalMethod())) {
// 是用於備份的 ArtMethod 對象,通常不應該走到這
ThrowIllegalArgumentException(StringPrintf("Cannot hook the method backup: %s", PrettyMethod(this).c_str()).c_str());
return;
}
// 獲取 ClassLinker,它是連結器
auto* cl = Runtime::Current()->GetClassLinker();
// 獲取線性分配器,用於分配內存,類似於 malloc
auto* linear_alloc = cl->GetAllocatorForClassLoader(GetClassLoader());
// 創建一個新的 ArtMethod 對象,用於備份原始方法
ArtMethod* backup_method = cl->CreateRuntimeMethod(linear_alloc);
// 複製當前 ArtMethod 至 backup_method
backup_method->CopyFrom(this, cl->GetImagePointerSize());
// 添加 kAccXposedOriginalMethod 標記,說明是備份的方法
backup_method->SetAccessFlags(backup_method->GetAccessFlags() | kAccXposedOriginalMethod);
// 創建備份方法對應的反射對象
mirror::AbstractMethod* reflected_method;
if (IsConstructor()) {
reflected_method = mirror::Constructor::CreateFromArtMethod(soa.Self(), backup_method);
} else {
reflected_method = mirror::Method::CreateFromArtMethod(soa.Self(), backup_method);
}
reflected_method->SetAccessible<false>(true);
// 將備份的方法和一路從 Java 層傳過來的 additional_info(包含處理 Hook 的回調)裝到 XposedHookInfo 對象中
XposedHookInfo* hook_info = reinterpret_cast<XposedHookInfo*>(linear_alloc->Alloc(soa.Self(), sizeof(XposedHookInfo)));
hook_info->reflected_method = soa.Vm()->AddGlobalRef(soa.Self(), reflected_method);
hook_info->additional_info = soa.Env()->NewGlobalRef(additional_info);
hook_info->original_method = backup_method;
ScopedThreadSuspension sts(soa.Self(), kSuspended);
jit::ScopedJitSuspend sjs;
gc::ScopedGCCriticalSection gcs(soa.Self(),
gc::kGcCauseXposed,
gc::kCollectorTypeXposed);
ScopedSuspendAll ssa(__FUNCTION__);
// 清除本方法的調用者信息
cl->InvalidateCallersForMethod(soa.Self(), this);
jit::Jit* jit = art::Runtime::Current()->GetJit();
if (jit != nullptr) {
// 將本方法的 CodeCache 中的內容移動到備份方法對象中
// CodeCache 就是從 Dex 文件中解析到的類和方法的相關信息,
// 緩存起來,方便直接取用,而不是每次都解析 Dex 文件
jit->GetCodeCache()->MoveObsoleteMethod(this, backup_method);
}
// 將 hook_info 保存在用於原本用於存儲 JNI 方法的內存地址上
SetEntryPointFromJniPtrSize(reinterpret_cast<uint8_t*>(hook_info), sizeof(void*));
// 設置此方法對應的彙編代碼的地址,一個 Java 方法經過編譯器編譯後會對應一段彙編代碼,
// 當虛擬機執行這個 Java 方法時,如果處於 AOT 模式,就會直接跳轉到彙編代碼執行機器指令
SetEntryPointFromQuickCompiledCode(GetQuickProxyInvokeHandler());
// 設置 dex 中此方法的偏移為 0,表示它是 native 或 abstract 方法,沒有具體代碼
SetCodeItemOffset(0);
// 清除以下標誌
const uint32_t kRemoveFlags = kAccNative | kAccSynchronized | kAccAbstract | kAccDefault | kAccDefaultConflict;
// 添加 Xposed 自定義的 kAccXposedHookedMethod 標誌,用來標識它是被 Hook 的方法
// 添加後,IsXposedHookedMethod 函數就會返回 true
SetAccessFlags((GetAccessFlags() & ~kRemoveFlags) | kAccXposedHookedMethod);
MutexLock mu(soa.Self(), *Locks::thread_list_lock_);
Runtime::Current()->GetThreadList()->ForEach(StackReplaceMethodAndInstallInstrumentation, this);
}
上面代碼的主要工作是備份當前需要被 Hook 的方法,然後設置當前方法的彙編代碼地址為 GetQuickProxyInvokeHandler(),此時就完成了 Hook 目的。
當這個 Java 方法被調用時,會跳轉到上面設置的彙編代碼地址處,Xposed 將會對這個 Java 方法的參數進行轉發等處理,修改方法返回值,實現最終 Hook。
不過沒有看到最終的處理,並不知道是怎麼做的,下面繼續分析。
首先看 GetQuickProxyInvokeHandler() 的返回值:
extern "C" void art_quick_proxy_invoke_handler();
static inline const void* GetQuickProxyInvokeHandler() {
return reinterpret_cast<const void*>(art_quick_proxy_invoke_handler);
}
它是一個 art_quick_proxy_invoke_handler 函數的地址,這個函數是在其他地方實現的(有 extern 聲明),經過了解,發現它是由彙編代碼實現的,有 arm、arm64、mips、mips64、x86、x86_64 這幾個指令集的實現,這裡看一下 arm 上的實現:
// quick_entrypoints_arm.S
.extern artQuickProxyInvokeHandler
ENTRY art_quick_proxy_invoke_handler
SETUP_REFS_AND_ARGS_CALLEE_SAVE_FRAME_WITH_METHOD_IN_R0
// 傳遞相關參數
mov r2, r9 @ pass Thread::Current
mov r3, sp @ pass SP
// 跳轉至 artQuickProxyInvokeHandler 函數
blx artQuickProxyInvokeHandler @ (Method* proxy method, receiver, Thread*, SP)
ldr r2, [r9, #THREAD_EXCEPTION_OFFSET] @ load Thread::Current()->exception_
add sp, #(FRAME_SIZE_REFS_AND_ARGS_CALLEE_SAVE - FRAME_SIZE_REFS_ONLY_CALLEE_SAVE)
.cfi_adjust_cfa_offset -(FRAME_SIZE_REFS_AND_ARGS_CALLEE_SAVE - FRAME_SIZE_REFS_ONLY_CALLEE_SAVE)
RESTORE_REFS_ONLY_CALLEE_SAVE_FRAME
cbnz r2, 1f @ success if no exception is pending
vmov d0, r0, r1 @ store into fpr, for when it's a fpr return...
bx lr @ return on success
1:
DELIVER_PENDING_EXCEPTION
END art_quick_proxy_invoke_handler
art_quick_proxy_invoke_handler 跳轉至了 artQuickProxyInvokeHandler 函數中,那麼繼續跟進。
// qucik_trampoline_entrypoints.cc
extern "C" uint64_t artQuickProxyInvokeHandler(
ArtMethod* proxy_method, mirror::Object* receiver, Thread* self, ArtMethod** sp)
SHARED_REQUIRES(Locks::mutator_lock_) {
// bool IsXposedHookedMethod() {
// return (GetAccessFlags() & kAccXposedHookedMethod) != 0;
// }
const bool is_xposed = proxy_method->IsXposedHookedMethod();
if (!is_xposed) {
DCHECK(proxy_method->IsRealProxyMethod()) << PrettyMethod(proxy_method);
DCHECK(receiver->GetClass()->IsProxyClass()) << PrettyMethod(proxy_method);
}
const char* old_cause = self->StartAssertNoThreadSuspension("Adding to IRT proxy object arguments");
DCHECK_EQ((*sp), proxy_method) << PrettyMethod(proxy_method);
self->VerifyStack();
JNIEnvExt* env = self->GetJniEnv();
ScopedObjectAccessUnchecked soa(env);
ScopedJniEnvLocalRefState env_state(env);
const bool is_static = proxy_method->IsStatic();
jobject rcvr_jobj = is_static ? nullptr : soa.AddLocalReference<jobject>(receiver);
ArtMethod* non_proxy_method = proxy_method->GetInterfaceMethodIfProxy(sizeof(void*));
CHECK(is_xposed || !non_proxy_method->IsStatic()) << PrettyMethod(proxy_method) << " "
<< PrettyMethod(non_proxy_method);
std::vector<jvalue> args;
uint32_t shorty_len = 0;
const char* shorty = non_proxy_method->GetShorty(&shorty_len);
BuildQuickArgumentVisitor local_ref_visitor(sp, is_static, shorty, shorty_len, &soa, &args);
local_ref_visitor.VisitArguments();
if (!is_static) {
DCHECK_GT(args.size(), 0U) << PrettyMethod(proxy_method);
args.erase(args.begin());
}
if (is_xposed) {
jmethodID proxy_methodid = soa.EncodeMethod(proxy_method);
self->EndAssertNoThreadSuspension(old_cause);
// 處理 Hook 方法
JValue result = InvokeXposedHandleHookedMethod(soa, shorty, rcvr_jobj, proxy_methodid, args);
local_ref_visitor.FixupReferences();
// 返回 Java 方法的返回值
return result.GetJ();
}
// ...
}
可以大概看出來 artQuickProxyInvokeHandler 函數是用於處理動態代理方法的,不過 Xposed 對這個方法進行了修改,使其能夠處理被 Hook 的方法,重點關注下面判斷語句中的代碼,如果是被 Xposed Hook 的方法,那麼進入 InvokeXposedHandleHookedMethod 進行處理:
// entrypoint_utils.cc
JValue InvokeXposedHandleHookedMethod(ScopedObjectAccessAlreadyRunnable& soa, const char* shorty,
jobject rcvr_jobj, jmethodID method,
std::vector<jvalue>& args) {
soa.Self()->AssertThreadSuspensionIsAllowable();
jobjectArray args_jobj = nullptr;
const JValue zero;
int32_t target_sdk_version = Runtime::Current()->GetTargetSdkVersion();
// ...
// 取出 hook_info
const XposedHookInfo* hook_info = soa.DecodeMethod(method)->GetXposedHookInfo();
// 調用 Java 層的 XposedBridge.handleHookedMethod 方法
jvalue invocation_args[5];
invocation_args[0].l = hook_info->reflected_method;
invocation_args[1].i = 1;
invocation_args[2].l = hook_info->additional_info;
// 方法的目標作用對象 this
invocation_args[3].l = rcvr_jobj;
// 參數保存傳給方法的參數
invocation_args[4].l = args_jobj;
jobject result =
soa.Env()->CallStaticObjectMethodA(ArtMethod::xposed_callback_class,
ArtMethod::xposed_callback_method,
invocation_args);
if (UNLIKELY(soa.Self()->IsExceptionPending())) {
return zero;
} else {
if (shorty[0] == 'V' || (shorty[0] == 'L' && result == nullptr)) {
return zero;
}
size_t pointer_size = Runtime::Current()->GetClassLinker()->GetImagePointerSize();
mirror::Class* result_type = soa.DecodeMethod(method)->GetReturnType(true /* resolve */, pointer_size);
mirror::Object* result_ref = soa.Decode<mirror::Object*>(result);
JValue result_unboxed;
if (!UnboxPrimitiveForResult(result_ref, result_type, &result_unboxed)) {
DCHECK(soa.Self()->IsExceptionPending());
return zero;
}
return result_unboxed;
}
}
這裡就調用到了 Java 層 XposedBridge 的 handleHookedMethod 方法中。
// XposedBridge.java
private static Object handleHookedMethod(Member method, int originalMethodId, Object additionalInfoObj,
Object thisObject, Object[] args) throws Throwable {
// 取出 Hook 處理回調等信息
AdditionalHookInfo additionalInfo = (AdditionalHookInfo) additionalInfoObj;
if (disableHooks) {
try {
// 如果關閉 Hook,那麼調用原始方法
return invokeOriginalMethodNative(method, originalMethodId, additionalInfo.parameterTypes,
additionalInfo.returnType, thisObject, args);
} catch (InvocationTargetException e) {
throw e.getCause();
}
}
Object[] callbacksSnapshot = additionalInfo.callbacks.getSnapshot();
final int callbacksLength = callbacksSnapshot.length;
if (callbacksLength == 0) {
try {
// 沒有處理 Hook 的回調,則調用原始方法
return invokeOriginalMethodNative(method, originalMethodId, additionalInfo.parameterTypes,
additionalInfo.returnType, thisObject, args);
} catch (InvocationTargetException e) {
throw e.getCause();
}
}
MethodHookParam param = new MethodHookParam();
param.method = method;
param.thisObject = thisObject;
param.args = args;
int beforeIdx = 0;
do {
try {
// 回調 beforeHookedMethod 方法,表示在 Hook 之前
((XC_MethodHook) callbacksSnapshot[beforeIdx]).beforeHookedMethod(param);
} catch (Throwable t) {
XposedBridge.log(t);
param.setResult(null);
param.returnEarly = false;
continue;
}
if (param.returnEarly) {
beforeIdx++;
break;
}
} while (++beforeIdx < callbacksLength);
// Hook 回調沒有處理,則調用原始方法
if (!param.returnEarly) {
try {
param.setResult(invokeOriginalMethodNative(method, originalMethodId,
additionalInfo.parameterTypes, additionalInfo.returnType, param.thisObject, param.args));
} catch (InvocationTargetException e) {
param.setThrowable(e.getCause());
}
}
int afterIdx = beforeIdx - 1;
do {
Object lastResult = param.getResult();
Throwable lastThrowable = param.getThrowable();
try {
// 調用 afterHookedMethod 方法,表示 Hook 之後
((XC_MethodHook) callbacksSnapshot[afterIdx]).afterHookedMethod(param);
} catch (Throwable t) {
XposedBridge.log(t);
if (lastThrowable == null)
param.setResult(lastResult);
else
param.setThrowable(lastThrowable);
}
} while (--afterIdx >= 0);
// 如果有異常,則拋出異常,否則返回處理後的結果
if (param.hasThrowable())
throw param.getThrowable();
else
return param.getResult();
}
這裡就能清晰的看到 Hook 最終處理了,至此就完成了 Hook。其中 invokeOriginalMethodNative 的實現如下:
// libxposed_art.cpp
jobject XposedBridge_invokeOriginalMethodNative(JNIEnv* env, jclass, jobject javaMethod,
jint isResolved, jobjectArray, jclass, jobject javaReceiver, jobjectArray javaArgs) {
ScopedFastNativeObjectAccess soa(env);
if (UNLIKELY(!isResolved)) {
// 從備份的方法中取得原始方法
ArtMethod* artMethod = ArtMethod::FromReflectedMethod(soa, javaMethod);
if (LIKELY(artMethod->IsXposedHookedMethod())) {
javaMethod = artMethod->GetXposedHookInfo()->reflected_method;
}
}
#if PLATFORM_SDK_VERSION >= 23
// 調用虛擬機的執行方法調用原始方法邏輯
return InvokeMethod(soa, javaMethod, javaReceiver, javaArgs);
#else
return InvokeMethod(soa, javaMethod, javaReceiver, javaArgs, true);
#endif
}
還有最後一個問題,就是一個被 Hook 的方法的調用過程,上面只分析了處理過程,而沒有正向的調用,下面開始分析。
調用過程分析一個 Java 方法的調用,可以從 AndroidRuntime.start 中開始,Java 虛擬機執行的第一個類是 ZygoteInit 從此就進入了 Java 層,它使用的是 JNIEnv 提供的 CallStaticVoidMethod 方法,看一下它的實現。
// jni_internal.cc
static void CallStaticVoidMethod(JNIEnv* env, jclass, jmethodID mid, ...) {
va_list ap;
va_start(ap, mid);
CHECK_NON_NULL_ARGUMENT_RETURN_VOID(mid);
ScopedObjectAccess soa(env);
InvokeWithVarArgs(soa, nullptr, mid, ap);
va_end(ap);
}
調用了 InvokeWithVarArgs 函數:
// reflection.cc
JValue InvokeWithVarArgs(const ScopedObjectAccessAlreadyRunnable& soa, jobject obj, jmethodID mid,
va_list args)
SHARED_REQUIRES(Locks::mutator_lock_) {
// ...
ArtMethod* method = soa.DecodeMethod(mid);
bool is_string_init = method->GetDeclaringClass()->IsStringClass() && method->IsConstructor();
if (is_string_init) {
method = soa.DecodeMethod(WellKnownClasses::StringInitToStringFactoryMethodID(mid));
}
mirror::Object* receiver = method->IsStatic() ? nullptr : soa.Decode<mirror::Object*>(obj);
uint32_t shorty_len = 0;
const char* shorty = method->GetInterfaceMethodIfProxy(sizeof(void*))->GetShorty(&shorty_len);
JValue result;
ArgArray arg_array(shorty, shorty_len);
arg_array.BuildArgArrayFromVarArgs(soa, receiver, args);
// 調用 InvokeWithArgArray
InvokeWithArgArray(soa, method, &arg_array, &result, shorty);
if (is_string_init) {
UpdateReference(soa.Self(), obj, result.GetL());
}
return result;
}
繼續看 InvokeWithArgArray:
// reflection.cc
static void InvokeWithArgArray(const ScopedObjectAccessAlreadyRunnable& soa,
ArtMethod* method, ArgArray* arg_array, JValue* result,
const char* shorty)
SHARED_REQUIRES(Locks::mutator_lock_) {
uint32_t* args = arg_array->GetArray();
if (UNLIKELY(soa.Env()->check_jni)) {
CheckMethodArguments(soa.Vm(), method->GetInterfaceMethodIfProxy(sizeof(void*)), args);
}
method->Invoke(soa.Self(), args, arg_array->GetNumBytes(), result, shorty);
}
最終是調用到了 ArtMethod 的 Invoke 函數:
// reflection.cc
void ArtMethod::Invoke(Thread* self, uint32_t* args, uint32_t args_size, JValue* result,
const char* shorty) {
// ...
ManagedStack fragment;
self->PushManagedStackFragment(&fragment);
Runtime* runtime = Runtime::Current();
if (UNLIKELY(!runtime->IsStarted() || Dbg::IsForcedInterpreterNeededForCalling(self, this))) {
// ...
} else {
DCHECK_EQ(runtime->GetClassLinker()->GetImagePointerSize(), sizeof(void*));
constexpr bool kLogInvocationStartAndReturn = false;
bool have_quick_code = GetEntryPointFromQuickCompiledCode() != nullptr;
if (LIKELY(have_quick_code)) {
// ...
if (!IsStatic()) {
(*art_quick_invoke_stub)(this, args, args_size, self, result, shorty);
} else {
(*art_quick_invoke_static_stub)(this, args, args_size, self, result, shorty);
}
// ...
} else {
// ...
}
}
self->PopManagedStackFragment(fragment);
}
根據 Java 方法類型是非靜態還是靜態,跳入 art_quick_invoke_stub 或 art_quick_invoke_static_stub,看一下 art_quick_invoke_stub:
// reflection.cc
extern "C" void art_quick_invoke_stub(ArtMethod* method, uint32_t* args, uint32_t args_size,
Thread* self, JValue* result, const char* shorty) {
quick_invoke_reg_setup<false>(method, args, args_size, self, result, shorty);
}
// reflection.cc
template <bool kIsStatic>
static void quick_invoke_reg_setup(ArtMethod* method, uint32_t* args, uint32_t args_size,
Thread* self, JValue* result, const char* shorty) {
uint32_t core_reg_args[4];
uint32_t fp_reg_args[16];
uint32_t gpr_index = 1;
uint32_t fpr_index = 0;
uint32_t fpr_double_index = 0;
uint32_t arg_index = 0;
const uint32_t result_in_float = kArm32QuickCodeUseSoftFloat ? 0 :
(shorty[0] == 'F' || shorty[0] == 'D') ? 1 : 0;
if (!kIsStatic) {
core_reg_args[gpr_index++] = args[arg_index++];
}
for (uint32_t shorty_index = 1; shorty[shorty_index] != '\0'; ++shorty_index, ++arg_index) {
char arg_type = shorty[shorty_index];
if (kArm32QuickCodeUseSoftFloat) {
arg_type = (arg_type == 'D') ? 'J' : arg_type;
arg_type = (arg_type == 'F') ? 'I' : arg_type;
}
switch (arg_type) {
case 'D': {
fpr_double_index = std::max(fpr_double_index, RoundUp(fpr_index, 2));
if (fpr_double_index < arraysize(fp_reg_args)) {
fp_reg_args[fpr_double_index++] = args[arg_index];
fp_reg_args[fpr_double_index++] = args[arg_index + 1];
}
++arg_index;
break;
}
case 'F':
if (fpr_index % 2 == 0) {
fpr_index = std::max(fpr_double_index, fpr_index);
}
if (fpr_index < arraysize(fp_reg_args)) {
fp_reg_args[fpr_index++] = args[arg_index];
}
break;
// ...
}
}
// 進入下一步
art_quick_invoke_stub_internal(method, args, args_size, self, result, result_in_float,
core_reg_args, fp_reg_args);
}
最後是調用了 art_quick_invoke_stub_internal,它是彙編代碼實現的:
// quick_entrypoints_arm.S
ENTRY art_quick_invoke_stub_internal
SPILL_ALL_CALLEE_SAVE_GPRS @ spill regs (9)
mov r11, sp @ save the stack pointer
.cfi_def_cfa_register r11
mov r9, r3 @ move managed thread pointer into r9
add r4, r2, #4 @ create space for method pointer in frame
sub r4, sp, r4 @ reserve & align *stack* to 16 bytes: native calling
and r4, #0xFFFFFFF0 @ convention only aligns to 8B, so we have to ensure ART
mov sp, r4 @ 16B alignment ourselves.
mov r4, r0 @ save method*
add r0, sp, #4 @ pass stack pointer + method ptr as dest for memcpy
bl memcpy @ memcpy (dest, src, bytes)
mov ip, #0 @ set ip to 0
str ip, [sp] @ store null for method* at bottom of frame
ldr ip, [r11, #48] @ load fp register argument array pointer
vldm ip, {s0-s15} @ copy s0 - s15
ldr ip, [r11, #44] @ load core register argument array pointer
mov r0, r4 @ restore method*
add ip, ip, #4 @ skip r0
ldm ip, {r1-r3} @ copy r1 - r3
#ifdef ARM_R4_SUSPEND_FLAG
mov r4, #SUSPEND_CHECK_INTERVAL @ reset r4 to suspend check interval
#endif
ldr ip, [r0, #ART_METHOD_QUICK_CODE_OFFSET_32] @ get pointer to the code
blx ip @ call the method
mov sp, r11 @ restore the stack pointer
.cfi_def_cfa_register sp
ldr r4, [sp, #40] @ load result_is_float
ldr r9, [sp, #36] @ load the result pointer
cmp r4, #0
ite eq
strdeq r0, [r9] @ store r0/r1 into result pointer
vstrne d0, [r9] @ store s0-s1/d0 into result pointer
pop {r4, r5, r6, r7, r8, r9, r10, r11, pc} @ restore spill regs
END art_quick_invoke_stub_internal
其中中間部分一行代碼使用 ldr 指令設置 ip 寄存器的位置來指示指令地址,使用到了 ART_METHOD_QUICK_CODE_OFFSET_32 這個宏,它是 32,表示 EntryPointFromQuickCompiledCodeOffset 這個函數返回的成員的偏移,也就是 entry_point_from_quick_compiled_code_。
// asm_support.h
#define ART_METHOD_QUICK_CODE_OFFSET_32 32
ADD_TEST_EQ(ART_METHOD_QUICK_CODE_OFFSET_32,
art::ArtMethod::EntryPointFromQuickCompiledCodeOffset(4).Int32Value())
// art_method.h
static MemberOffset EntryPointFromQuickCompiledCodeOffset(size_t pointer_size) {
return MemberOffset(PtrSizedFieldsOffset(pointer_size) + OFFSETOF_MEMBER(
PtrSizedFields, entry_point_from_quick_compiled_code_) / sizeof(void*) * pointer_size);
}
回到前面 Hook 時,使用了 SetEntryPointFromQuickCompiledCode,其實就是設置這個變量。
// art_method.h
void SetEntryPointFromQuickCompiledCode(const void* entry_point_from_quick_compiled_code) {
SetEntryPointFromQuickCompiledCodePtrSize(entry_point_from_quick_compiled_code,
sizeof(void*));
}
ALWAYS_INLINE void SetEntryPointFromQuickCompiledCodePtrSize(
const void* entry_point_from_quick_compiled_code, size_t pointer_size) {
DCHECK(Runtime::Current()->IsAotCompiler() || !IsXposedHookedMethod());
SetNativePointer(EntryPointFromQuickCompiledCodeOffset(pointer_size),
entry_point_from_quick_compiled_code, pointer_size);
}
那麼下一步使用了 blx 跳轉指令,代碼就會跳轉到這個地址上執行,進入 GetQuickProxyInvokeHandler 返回的地址 art_quick_proxy_invoke_handler 中, 最後執行 artQuickProxyInvokeHandler 函數,Xposed 在這個函數裡面處理了 Hook,完成 Hook。
到這裡就分析完了 Xposed 的實現,其實還有很多細節沒有去分析,通過對比 ART 虛擬機的原始代碼和 Xposed 修改後的代碼,發現 Xposed 修改的地方還是很多的,大概有幾百處,雖然每個文件修改的代碼不多,但是足以說明 Xposed 作者對與於 Android 系統原理和 ART 虛擬機的了解的深入程度。
總結通過分析 Xposed 的實現原理,對於 Java 方法的 Hook 原理有了一些了解,同時回顧了 Android zygote 進程相關的內容,對於 ART 虛擬機執行方法的過程也有了一個大概的認識。
參考https://bbs.pediy.com/thread-257844.htm
https://blog.csdn.net/Innost/article/details/50461783
https://blog.csdn.net/Luoshengyang/article/details/39256813
https://blog.csdn.net/Luoshengyang/article/details/8914953