前言
Xpatch是一款免Root實(shí)現(xiàn)App加載Xposed插件的工具木张,可以非常方便地實(shí)現(xiàn)App的逆向破解(再也不用改smali代碼了)术吗,源碼也已經(jīng)上傳到Github上,歡迎各位Fork and Star渊抽。
本文主要介紹Xpatch的實(shí)現(xiàn)原理达传。由于其原理比較復(fù)雜,所以分三篇文章來詳細(xì)講解劳吠。
由于Xpatch處理Xposed module的方法參考了Xposed框架部分源碼引润,所以本文先介紹Xposed框架加載Xposed模塊原理,再詳細(xì)講解Xpatch如何兼容Xposed模塊痒玩。
Xposed框架加載Xposed Module的原理
Xposed是github上rovo89大神設(shè)計(jì)的一個(gè)針對Android平臺的動(dòng)態(tài)劫持項(xiàng)目淳附,其主要原理是通過替換/system/bin/app_process程序控制zygote進(jìn)程,使得app_process在啟動(dòng)過程中會(huì)加載XposedBridge.jar這個(gè)jar包蠢古,從而完成對Zygote進(jìn)程及其創(chuàng)建的app進(jìn)程的劫持奴曙。
XposedBridge.jar的入口方法是main(),其主要邏輯如下:
//de.robv.android.xposed.XposedBridge.java
protected static void main(String[] args) {
// Initialize the Xposed framework and modules
try {
if (!hadInitErrors()) {
initXResources();
SELinuxHelper.initOnce();
SELinuxHelper.initForProcess(null);
runtime = getRuntime();
XPOSED_BRIDGE_VERSION = getXposedVersion();
if (isZygote) {
XposedInit.hookResources();
XposedInit.initForZygote();
}
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;
}
// Call the original startup code
if (isZygote) {
ZygoteInit.main(args);
} else {
RuntimeInit.main(args);
}
}
這里最核心的一行代碼是:
XposedInit.loadModules();
在這個(gè)方法里草讶,通過讀取/data/data/de.robv.android.xposed.installer/conf/modules.list
這個(gè)文件洽糟,找到需要加載的Xposed插件(APK)路徑。而這些路徑都是通過Xposed Installer這個(gè)App里的開關(guān)控制的堕战。在Xposed Installer App里坤溃,有一個(gè)已安裝的Xposed插件列表,用戶選定某個(gè)插件后嘱丢,就會(huì)將該插件APK路徑寫到modules.list文件里薪介,從而實(shí)現(xiàn)插件開關(guān)的控制。
在modules.list文件里查找到所有插件APK路徑后越驻,根據(jù)Apk的絕對路徑構(gòu)造一個(gè)PathClassLoader()汁政,然后用此Classloader加載全類名寫在資源文件assets/xposed_init
里的入口類道偷,其核心邏輯代碼如下:
//de.robv.android.xposed.XposedInit.java
...
...
ClassLoader mcl = new PathClassLoader(apk, XposedBridge.BOOTCLASSLOADER);
BufferedReader moduleClassesReader = new BufferedReader(new InputStreamReader(is));
try {
String moduleClassName;
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);
if (!IXposedMod.class.isAssignableFrom(moduleClass)) {
Log.e(TAG, " This class doesn't implement any sub-interface of IXposedMod, skipping it");
continue;
} else if (disableResources && IXposedHookInitPackageResources.class.isAssignableFrom(moduleClass)) {
Log.e(TAG, " This class requires resource-related hooks (which are disabled), skipping it.");
continue;
}
...
...
加載到這些類之后,將這些類使用全局變量保存起來:
if (moduleInstance instanceof IXposedHookLoadPackage)
XposedBridge.hookLoadPackage(new IXposedHookLoadPackage.Wrapper((IXposedHookLoadPackage) moduleInstance));
...
...
//保存在全局變量sLoadedPackageCallbacks里
public static void hookLoadPackage(XC_LoadPackage callback) {
synchronized (sLoadedPackageCallbacks) {
sLoadedPackageCallbacks.add(callback);
}
}
保存起來后烂完,何時(shí)執(zhí)行這些類里的入口方法呢试疙?
下面一段代碼,給出了答案:
// normal process initialization (for new Activity, Service, BroadcastReceiver etc.)
findAndHookMethod(ActivityThread.class, "handleBindApplication", "android.app.ActivityThread.AppBindData", new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
...
XC_LoadPackage.LoadPackageParam lpparam = new XC_LoadPackage.LoadPackageParam(XposedBridge.sLoadedPackageCallbacks);
lpparam.packageName = reportedPackageName;
lpparam.processName = (String) getObjectField(param.args[0], "processName");
lpparam.classLoader = loadedApk.getClassLoader();
lpparam.appInfo = appInfo;
lpparam.isFirstApplication = true;
XC_LoadPackage.callAll(lpparam);
...
});
通過上面代碼可知抠蚣,是在main入口處攔截了ActivityThread
的handleBindApplication
方法祝旷,在這個(gè)方法執(zhí)行之前,加載了Xposed插件里的Hook代碼(入口代碼)嘶窄。而ActivityThread
的handleBindApplication
方法的主要功能就是創(chuàng)建Application
怀跛,并調(diào)用其attachBaseContext
,onCreate
等方法柄冲。因此吻谋,在App的Application創(chuàng)建之前就實(shí)現(xiàn)了Xposed Hook。
至此现横,Xposed框架加載Xposed module的流程就非常清晰了漓拾。
免Root實(shí)現(xiàn)Xposed的探索
由于Xposed框架是修改了/system/bin/app_process程序,控制的zygote進(jìn)程的啟動(dòng)戒祠,從而在app進(jìn)程啟動(dòng)之前執(zhí)行了加載xposed模塊骇两,實(shí)現(xiàn)了App代碼的Hook。因此姜盈,只有Root的手機(jī)才能使用Xposed低千。
那有沒有辦法實(shí)現(xiàn)免Root下也能讓App加載Xposed模塊呢?
其中一種已經(jīng)被探索過的方法是使用App雙開工具馏颂,讓其他App運(yùn)行在自己的App里面示血。比如,利用大名鼎鼎的開源雙開工具VirtualApp救拉,讓其他App運(yùn)行其中难审,這樣VirtualApp就可以控制其他App的進(jìn)程啟動(dòng)了,當(dāng)然也就可以實(shí)現(xiàn)免Root加載Xposed模塊了亿絮。
不過剔宪,這樣做也有一些問題,其兼容性和穩(wěn)定性比較差壹无。而且葱绒,由于VirtualApp的開源版本已經(jīng)很少維護(hù),bug會(huì)比較多斗锭,有些App在里面啟動(dòng)非车氐恚卡頓,甚至無法啟動(dòng)
那有沒有更好的方案呢岖是?
有帮毁,那就是基于Apk二次打包的Xpatch方案实苞。
為了實(shí)現(xiàn)免Root Xposed,我們可以修改App入口代碼烈疚,在App的Application初始化時(shí)黔牵,插入我們加載Xposed模塊的代碼,并對App進(jìn)行重新打包簽名即可爷肝。
Xpatch加載Xposed模塊的方法
通過以上分析猾浦,Xposed框架執(zhí)行Xposed模塊的入口位置是通過Hook ActivityThread
的handleBindApplication
方法,從而使在創(chuàng)建應(yīng)用的Application之前就執(zhí)行Xposed模塊里的入口方法(執(zhí)行Hook流程)灯抛。
由于我們是修改應(yīng)用代碼金赦,因此入口只能在應(yīng)用的Application
里,可以是Application
的靜態(tài)方法塊对嚼,也可以是attachBaseContext
方法或onCreate
方法夹抗。
那到底應(yīng)該選擇哪個(gè)作為加載Xposed模塊的入口呢?
首先纵竖,肯定是越早Hook越好漠烧,否則可能會(huì)出現(xiàn)有些方法調(diào)用之后才執(zhí)行Hook方法,導(dǎo)致方法沒有被Hook到靡砌。而且加載Xposed插件需要用到Applicatin的context參數(shù)已脓,所以筆者選了在attachBaseContext
方法的第一行代碼執(zhí)行加載Xposed模塊:
// MyApplication.java
@Override
protected void attachBaseContext(Context base) {
XposedModuleEntry.init(base);
...
//App其他業(yè)務(wù)代碼
...
super.attachBaseContext(base);
}
通過對一些app進(jìn)行測試,發(fā)現(xiàn)大多數(shù)應(yīng)用走這個(gè)流程都沒問題乏奥,唯一有問題的是微信,修改后的微信一啟動(dòng)就奔潰亥曹,具體原因暫時(shí)不清楚邓了。
因此,我嘗試將加載Xposed模塊的入口代碼放在Application的靜態(tài)代碼塊里媳瞪,靜態(tài)代碼塊在類創(chuàng)建的時(shí)候就會(huì)執(zhí)行骗炉,比attachBaseContext
方法執(zhí)行的時(shí)機(jī)更早。
通過測試蛇受,修改后的微信的確能夠成功啟動(dòng)>淇!
// MyApplication.java中的靜態(tài)代碼塊
static {
XposedModuleEntry.init();
}
但是兢仰,在Application的靜態(tài)代碼塊中乍丈,并沒有Application的context參數(shù),而加載Xposed模塊時(shí)把将,需要傳遞參數(shù)轻专,參數(shù)中的applicationInfo和應(yīng)用的classLoader都是需要從Context中取得,沒有Context怎么辦察蹲?總不能傳個(gè)空過去吧请垛。
既然沒有Context催训,我們就自己構(gòu)造一個(gè)Context。
創(chuàng)建App Context流程
Android sdk并沒有提供應(yīng)用自己創(chuàng)建context的方法宗收,為了找到構(gòu)建一個(gè)Application Context的方法漫拭,我們先來了解android Framework里是如何創(chuàng)建Application的Context。
Application里最早出現(xiàn)Context的地方是attachBaseContext
方法:
// Application的父類android.content.ContextWrapper.java
protected void attachBaseContext(Context base) {
if (mBase != null) {
throw new IllegalStateException("Base context already set");
}
mBase = base;
}
這個(gè)方法唯一調(diào)用的地方是:
// android.app.Application.java
/**
* @hide
*/
/* package */ final void attach(Context context) {
attachBaseContext(context);
mLoadedApk = ContextImpl.getImpl(context).mPackageInfo;
}
Application里的attach
方法是在new Application之后立即調(diào)用的混稽,具體是在android.app.Instrumentation.java
里:
// android.app.Instrumentation.java
public Application newApplication(ClassLoader cl, String className, Context context)
throws InstantiationException, IllegalAccessException,
ClassNotFoundException {
return newApplication(cl.loadClass(className), context);
}
static public Application newApplication(Class<?> clazz, Context context)
throws InstantiationException, IllegalAccessException,
ClassNotFoundException {
Application app = (Application)clazz.newInstance();
app.attach(context);
return app;
}
Instrumentation類的newApplication
方法最終又是在android.app.LoadedApk.java
類里的makeApplication
方法調(diào)用的:
// android.app.LoadedApk.java
public Application makeApplication(boolean forceDefaultAppClass,
Instrumentation instrumentation) {
...
//代碼省略
...
String appClass = mApplicationInfo.className;
if (forceDefaultAppClass || (appClass == null)) {
appClass = "android.app.Application";
}
...
//代碼省略
...
ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
app = mActivityThread.mInstrumentation.newApplication(
cl, appClass, appContext);
appContext.setOuterContext(app);
...
//代碼省略
...
}
終于看到構(gòu)造context的方法了
ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
這個(gè)方法的實(shí)現(xiàn)是:
// android.app.ContextImpl.java
static ContextImpl createAppContext(ActivityThread mainThread, LoadedApk packageInfo) {
if (packageInfo == null) throw new IllegalArgumentException("packageInfo");
ContextImpl context = new ContextImpl(null, mainThread, packageInfo, null, null, null, 0,
null);
context.setResources(packageInfo.getResources());
return context;
}
createAppContext
方法需要傳兩個(gè)參數(shù)采驻,一個(gè)是mActivityThread,另一個(gè)是this荚坞,也就是LoadedApk對象挑宠。mActivityThread這個(gè)對象比較容易找到,因?yàn)橐粋€(gè)進(jìn)程只有一個(gè)ActivityThread對象颓影,只用通過反射調(diào)用ActivityThread的靜態(tài)方法currentActivityThread
即可:
// android.app.ActivityThread.java
public static ActivityThread currentActivityThread() {
return sCurrentActivityThread;
}
反射:
//反射調(diào)用ActivityThread.java的靜態(tài)方法currentActivityThread()
Class activityThreadClass = Class.forName("android.app.ActivityThread");
Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
currentActivityThreadMethod.setAccessible(true);
Object activityThreadObj = currentActivityThreadMethod.invoke(null);
另外一個(gè)對象LoadedApk該如何獲取呢各淀?
LoadedApk對象的獲取
上面代碼分析過,App啟動(dòng)時(shí)最先調(diào)用 ActivityThead
的handleBindApplication(AppBindData data)
方法诡挂,并在其這個(gè)方法里創(chuàng)建Application碎浇,而創(chuàng)建Application的唯一方法是
// android.app.LoadedApk.java
public Application makeApplication(boolean forceDefaultAppClass,
Instrumentation instrumentation){}
查看handleBindApplication
方法具體實(shí)現(xiàn)過程,發(fā)現(xiàn)makeApplication
確實(shí)被調(diào)用到:
// android.app.ActivityThread.java
private void handleBindApplication(AppBindData data) {
...
//其他代碼省略
...
mBoundApplication = data;
mConfiguration = new Configuration(data.config);
mCompatConfiguration = new Configuration(data.config);
...
//其他代碼省略
...
data.info = getPackageInfoNoCheck(data.appInfo, data.compatInfo);
...
//其他代碼省略
...
// 創(chuàng)建Application
Application app = data.info.makeApplication(data.restrictedBackupMode, null);
mInitialApplication = app;
...
//其他代碼省略
...
}
根據(jù)以上代碼可知璃俗,LoadedApk
的實(shí)例就是data.info
奴璃,而data.info
是通過方法getPackageInfoNoCheck
來獲取的,而且data.info
對象存到了全局變量mBoundApplication
里城豁,因此苟穆,mBoundApplication
對象里的info
變量就是我們要找的LoadedApk實(shí)例。
我們可以通過反射來獲取它:
// 獲取ActivityThread的mBoundApplication變量
Field boundApplicationField = activityThreadClass.getDeclaredField("mBoundApplication");
boundApplicationField.setAccessible(true);
Object mBoundApplicationObj = boundApplicationField.get(activityThreadObj); // mBoundApplication: AppBindData
// 獲取mBoundApplication的info變量(LoadedApk)
Field infoField = mBoundApplicationObj.getClass().getDeclaredField("info"); // info: LoadedApk
infoField.setAccessible(true);
Object loadedApkObj = infoField.get(mBoundApplicationObj); // LoadedApk
獲取到ActivityThread
和LoadedApk
后唱星,通過反射調(diào)用ContextImpl
的靜態(tài)方法createAppContext
就可以構(gòu)造一個(gè)context對象:
Class contextImplClass = Class.forName("android.app.ContextImpl");
//Get createAppContext method
Method createAppContextMethod = contextImplClass.getDeclaredMethod("createAppContext", activityThreadClass, loadedApkObj.getClass());
createAppContextMethod.setAccessible(true);
// call method: ContextImpl.createAppContext()
Object context = createAppContextMethod.invoke(null, activityThreadObj, loadedApkObj);
至此雳旅,我們在Application的靜態(tài)代碼塊中成功得到Application context。
Comming soon...
下一篇Xpatch源碼解析中间聊,我們將接著這部分內(nèi)容介紹XposedModuleEntry.init();
這個(gè)方法的具體實(shí)現(xiàn)邏輯攒盈,然后再介紹如何利用dex2jar工具修改Apk中dex文件。
關(guān)注我的技術(shù)公眾號獲取最新高質(zhì)量Android技術(shù)文章:Android葵花寶典
掃一掃關(guān)注