1. Xpatch概述
Xpatch是一款利用重打包的方式,使得被處理的Apk啟動時自動加載Xposed模塊,來實現(xiàn)應(yīng)用內(nèi)Hook的工具泳炉。
項目地址:https://github.com/WindySha/Xpatch
2. Xpatch處理apk分析
Xpatch修改apk,主要有三個步驟,代碼在MainCommand類的doCommandLine方法:
protected void doCommandLine() {
//...
if (!disableCrackSignature) {
// save the apk original signature info, to support crach signature.
new SaveApkSignatureTask(apkPath, unzipApkFilePath).run();
}
FileUtils.decompressZip(apkPath, unzipApkFilePath);
//...
// 1. modify the apk dex file to make xposed can run in it
mXpatchTasks.add(new ApkModifyTask(showAllLogs, keepBuildFiles, unzipApkFilePath, applicationName,
dexFileCount));
// 2. copy xposed so and dex files into the unzipped apk
mXpatchTasks.add(new SoAndDexCopyTask(dexFileCount, unzipApkFilePath, getXposedModules(xposedModules)));
// 3. compress all files into an apk and then sign it.
mXpatchTasks.add(new BuildAndSignApkTask(keepBuildFiles, unzipApkFilePath, output));
//...
for (Runnable executor : mXpatchTasks) {
executor.run();
}
//...
}
(1) 第一步
在Xpatch的源碼中叽躯,第一步對應(yīng)的是ApkModifyTask類,實現(xiàn)的是Runnable接口肌括,它的任務(wù)是修改Dex文件点骑,使得被處理的apk在啟動時能夠執(zhí)行指定的代碼。
如果反編譯被Xpatch處理過的Apk,查看App中Application的子類们童,會發(fā)現(xiàn)其中多了以下的代碼:
static {
XposedModuleEntry.init();
}
我們大膽的猜測畔况,這就是Xpatch給注入進去的入口代碼。我們回到Xpatch的源碼慧库,來看看它是如何注入的跷跪。查看ApkModifyTask類,一步步進行跟蹤:
ApkModifyTask類的run方法齐板,在任務(wù)被啟動時調(diào)用,它的代碼:
public void run() {
//...
String targetDexFileName = dumpJarFile(dexFileCount, unzipApkFilePath, jarOutputPath, applicationName);
//...
}
dumpJarFile方法:
private String dumpJarFile(int dexFileCount, String dexFilePath, String jarOutputPath, String applicationName) {
//...
boolean isApplicationClassFound = dex2JarCmd(filePath, jarOutputPath, applicationName);
//...
}
繼續(xù)跟蹤到dex2JarCmd方法:
private boolean dex2JarCmd(String dexPath, String jarOutputPath, String applicationName) {
Dex2jarCmd cmd = new Dex2jarCmd();
String[] args = new String[]{
dexPath,
"-o",
jarOutputPath,
"-app",
applicationName,
"--force"
};
cmd.doMain(args);
boolean isApplicationClassFounded = cmd.isApplicationClassFounded();
if (showAllLogs) {
System.out.println("isApplicationClassFounded -> " + isApplicationClassFounded + "the dexPath is " +
dexPath);
}
return isApplicationClassFounded;
}
看到了它創(chuàng)建了一個com.googlecode.dex2jar.tools.Dex2jarCmd
類實例吵瞻,這個類在名為dex-tools的外部庫里,并調(diào)用了Dex2jarCmd的doMain方法甘磨,給他傳進去一些類似于命令行參數(shù)的東西橡羞,令我們比較提得起精神的是-app
參數(shù),它傳進去一個applicationName济舆,這個applicationName的值來自MainCommand類的doCommandLine方法卿泽,邏輯是從解壓的apk中讀取AndroidManifest.xml,并讀取application
節(jié)點下的name屬性的值,最后將值賦予applicatioName
protected void doCommandLine() {
//...
ManifestParser.Pair pair = ManifestParser.parseManifestFile(manifestFilePath);
String applicationName;
if (pair != null && pair.applicationName != null) {
applicationName = pair.applicationName;
} else {
System.out.println(" Application name not found error !!!!!! ");
applicationName = DEFAULT_APPLICATION_NAME;
}
//...
}
exm?就這樣滋觉?并沒有發(fā)現(xiàn)任何注入的代碼啊签夭,不急,繼續(xù)跟蹤椎侠,看到applicationName傳進去了第租,一定能跟蹤到有用的信息。接下來就是進入dex-tools外部庫了我纪,代碼都是反編譯出來的
com.googlecode.dex2jar.tools.BaseCmd的doMain方法:
public void doMain(String... args) {
try {
this.initOptions();
this.parseSetArgs(args);
this.doCommandLine();
} catch (BaseCmd.HelpException var4) {
String msg = var4.getMessage();
if (msg != null && msg.length() > 0) {
System.err.println("ERROR: " + msg);
}
this.usage();
} catch (Exception var5) {
var5.printStackTrace(System.err);
}
}
主要看doCommandLine方法慎宾,doCommandLine是個抽象方法丐吓,它的真正實現(xiàn)是在Dex2jarCmd類里
protected void doCommandLine() throws Exception {
//...
for(var4 = 0; var4 < var3; ++var4) {
//...
BaseDexFileReader reader = MultiDexFileReader.open(Files.readAllBytes((new File(fileName)).toPath()));
BaksmaliBaseDexExceptionHandler handler = this.notHandleException ? null : new BaksmaliBaseDexExceptionHandler();
this.dex2jar = Dex2jar.from(reader);
this.dex2jar.withExceptionHandler(handler)
.reUseReg(this.reuseReg)
.topoLogicalSort()
.skipDebug(!this.debugInfo)
.optimizeSynchronized(this.optmizeSynchronized)
.printIR(this.printIR)
.noCode(this.noCode)
.skipExceptions(this.skipExceptions)
.setApplicationName(this.applicationName)
.to(file);
//...
}
}
跳轉(zhuǎn)到com.googlecode.d2j.dex.Dex2jar類的to方法
public void to(Path file) throws IOException {
if (Files.exists(file, new LinkOption[0]) && Files.isDirectory(file, new LinkOption[0])) {
this.doTranslate(file);
} else {
FileSystem fs = createZip(file);
Throwable var3 = null;
try {
this.doTranslate(fs.getPath("/"));
} catch (Throwable var12) {
var3 = var12;
throw var12;
} finally {
if (fs != null) {
if (var3 != null) {
try {
fs.close();
} catch (Throwable var11) {
var3.addSuppressed(var11);
}
} else {
fs.close();
}
}
}
}
}
to方法調(diào)用doTranslate方法
private void doTranslate(final Path dist) throws IOException {
//...
(new ExDex2Asm(this.exceptionHandler) {
public void convertCode(DexMethodNode methodNode, MethodVisitor mv) {
if (methodNode.method.getOwner().equals(Dex2jar.this.applicationName) && methodNode.method.getName().equals("<clinit>")) {
Dex2jar.this.isApplicationClassFounded = true;
mv.visitMethodInsn(184, "com/wind/xposed/entry/XposedModuleEntry", "init", "()V", false);
}
if ((Dex2jar.this.readerConfig & 4) == 0 || !methodNode.method.getName().equals("<clinit>")) {
super.convertCode(methodNode, mv);
}
}
public void addMethod(DexClassNode classNode, ClassVisitor cv) {
if (classNode.className.equals(Dex2jar.this.applicationName)) {
Dex2jar.this.isApplicationClassFounded = true;
boolean hasFoundClinitMethod = false;
if (classNode.methods != null) {
Iterator var4 = classNode.methods.iterator();
while(var4.hasNext()) {
DexMethodNode methodNode = (DexMethodNode)var4.next();
if (methodNode.method.getName().equals("<clinit>")) {
hasFoundClinitMethod = true;
break;
}
}
}
if (!hasFoundClinitMethod) {
MethodVisitor mv = cv.visitMethod(8, "<clinit>", "()V", (String)null, (String[])null);
mv.visitCode();
mv.visitMethodInsn(184, "com/wind/xposed/entry/XposedModuleEntry", "init", "()V", false);
mv.visitInsn(177);
mv.visitMaxs(0, 0);
mv.visitEnd();
}
}
}
//...
}).convertDex(fileNode, cvf);
}
doTranslate方法很長,但是我們很容易就能看到了很敏感的字符串:com/wind/xposed/entry/XposedModuleEntry
,這就是Xpatch插入自己初始化的代碼的地方趟据。visitMethodInsn方法用于在函數(shù)內(nèi)插入一條指令券犁,看到兩處調(diào)用visitMethodInsn來插入調(diào)用 com.wind.xposed.entry.XposedModuleEntry類的init方法的指令。
- convertCode函數(shù)中的visitMethodInsn之宿,邏輯是如果要處理的Application類中存在clinit方法族操,即存在靜態(tài)代碼段,就直接插入調(diào)用 com.wind.xposed.entry.XposedModuleEntry類的init方法的指令
- addMethod函數(shù)中的visitMethodInsn比被,如果要處理的Application類中不存在clinit方法色难,即不存在靜態(tài)代碼段,就創(chuàng)建一個靜態(tài)代碼段等缀,并在其中插入調(diào)用 com.wind.xposed.entry.XposedModuleEntry類的init方法的指令枷莉,最后返回void
注:上面的操作碼,184代表
invoke-static
尺迂,177代表return-void
笤妙。這些操作碼定義在org.objectweb.asm.Opcodes類中。
到這里噪裕,第一步我們已經(jīng)搞清楚了蹲盘。
(2) 第二步
對應(yīng)的是SoAndDexCopyTask類,從名字可以看出它的任務(wù)是復(fù)制so和dex的膳音,具體是怎樣的召衔,我們看代碼。
SoAndDexCopyTask類祭陷,它也實現(xiàn)了Runnable接口苍凛,run方法在任務(wù)被啟動時調(diào)用:
@Override
public void run() {
copySoFile();
copyDexFile(dexFileCount);
deleteMetaInfo();
}
這個類主要就做這三個動作:復(fù)制so文件,復(fù)制dex文件兵志,刪除Meta信息醇蝴。
我們先看copySoFile代碼:
private void copySoFile() {
for (String libPath : APK_LIB_PATH_ARRAY) {
String apkSoFullPath = fullLibPath(libPath);
if(new File(apkSoFullPath).exists()) {
copyLibFile(apkSoFullPath, SO_FILE_PATH_MAP.get(libPath));
}
}
// copy xposed modules into the lib path
if (xposedModuleArray != null && xposedModuleArray.length > 0) {
int index = 0;
for (String modulePath : xposedModuleArray) {
modulePath = modulePath.trim();
if (modulePath == null || modulePath.length() == 0) {
continue;
}
File moduleFile = new File(modulePath);
if (!moduleFile.exists()) {
continue;
}
for (String libPath : APK_LIB_PATH_ARRAY) {
String apkSoFullPath = fullLibPath(libPath);
String outputModuleName= XPOSED_MODULE_FILE_NAME_PREFIX + index + SO_FILE_SUFFIX;
if(new File(apkSoFullPath).exists()) {
File outputModuleSoFile = new File(apkSoFullPath, outputModuleName);
FileUtils.copyFile(moduleFile, outputModuleSoFile);
}
}
index++;
}
}
}
看代碼可以知道它的任務(wù)是把Xpatch.jar中assets目錄下的libxpatch_wl.so復(fù)制到apk解壓目錄的lib/<架構(gòu)文件夾>
下。這個libxpatch_wl.so是whale框架提供so文件想罕,為Hook提供可能悠栓。
除了復(fù)制so,如果我們在用Xpatch時使用-xm參數(shù)來將Xposed模塊集成到apk中按价,那么模塊會被就會被重命名成:以libxpatch_xp_module_為前綴惭适,后面接著模塊序號,最后再以so為后綴俘枫。最終這個模塊被復(fù)制到apk的lib目錄下腥沽。
copyDexFile方法:
private void copyDexFile(int dexFileCount) {
String copiedDexFileName = "classes" + (dexFileCount + 1) + ".dex";
FileUtils.copyFileFromJar("assets/classes.dex", unzipApkFilePath + copiedDexFileName);
}
邏輯也很明了逮走,把assets下的classes.dex復(fù)制到apk解壓目錄下鸠蚪,根據(jù)原來apk中的dex個數(shù)來給復(fù)制進去的dex重命名。
deleteMetaInfo方法:
private void deleteMetaInfo() {
String metaInfoFilePath = "META-INF";
File metaInfoFileRoot = new File(unzipApkFilePath + metaInfoFilePath);
if (!metaInfoFileRoot.exists()) {
return;
}
File[] childFileList = metaInfoFileRoot.listFiles();
if (childFileList == null || childFileList.length == 0) {
return;
}
for (File file : childFileList) {
String fileName = file.getName().toUpperCase();
if (fileName.endsWith(".MF") || fileName.endsWith(".RAS") || fileName.endsWith(".SF")) {
file.delete();
}
}
}
沒什么好說的,就是刪除<apk解壓目錄>/META-INF
下的指定文件茅信。
(3) 第三步
對應(yīng)的是BuildAndSignApkTask類盾舌,從名字可以看出它的任務(wù)是構(gòu)建和對apk簽名的。
這個BuildAndSignApkTask類也是實現(xiàn)Runnable接口蘸鲸,我們來看run方法:
public void run() {
//...
FileUtils.compressToZip(unzipApkFilePath, unsignedApkPath);
//...
signApk(unsignedApkPath, keyStoreFilePath, signedApkPath, false);
//...
}
這個方法做了兩件重要的事妖谴,把apk解壓目錄給壓縮成zip,并給壓縮成的文件簽名酌摇,這里就不細講了膝舅。
3. 被集成進apk中的dex分析
我們在上面提到過,Xpatch把assets目錄下的classes.dex文件復(fù)制進了目標apk里窑多,這個dex是不開源的仍稀,那么這個dex里面究竟有什么呢,我們把dex解壓出來埂息,拖進jadx中反編譯技潘。
既然Xpatch將初始化代碼注入到應(yīng)用的Application類,初始化代碼調(diào)用com.wind.xposed.entry.XposedModuleEntry
類的init
方法千康,那么我們從init方法開始看起享幽。
public static void init() {
if (b.compareAndSet(false, true)) {
Context createAppContext = XpatchUtils.createAppContext();//1
if (createAppContext == null) {
Log.e(a, "try to init XposedModuleEntry, but create app context failed !!!!");
return;
}
d = createAppContext;
if (VERSION.SDK_INT > 21 && !FileUtils.isFilePermissionGranted(createAppContext)) {
Log.e(a, "File permission is not granted, can not control xposed module by file ->xposed_config/modules.list");
}
XposedHelper.initSeLinux(createAppContext.getApplicationInfo().processName);
SharedPrefUtils.init(createAppContext);
ClassLoader classLoader = createAppContext.getClassLoader();
b.a(createAppContext.getApplicationInfo(), classLoader);//2
List<String> arrayList = new ArrayList();
List<String> a = a(createAppContext);//3
a(createAppContext, (List) arrayList);//4
if (a.size() > 0) {
String a2;
String a3;
List list = null;
for (String a32 : arrayList) {
if (list == null) {
list = new ArrayList();
}
a2 = a(createAppContext, a32);
String str = a;
StringBuilder stringBuilder = new StringBuilder("Current packed module path ----> ");
stringBuilder.append(a32);
stringBuilder.append(" packageName = ");
stringBuilder.append(a2);
XLog.d(str, stringBuilder.toString());
list.add(a2);
}
if (list == null || list.size() == 0) {
arrayList.addAll(a);
} else {
for (String str2 : a) {
a32 = a(createAppContext, str2);
a2 = a;
StringBuilder stringBuilder2 = new StringBuilder("Current installed module path ----> ");
stringBuilder2.append(str2);
stringBuilder2.append(" packageName = ");
stringBuilder2.append(a32);
XLog.d(a2, stringBuilder2.toString());
if (!list.contains(a32)) {
arrayList.add(str2);
}
}
}
}
for (String str3 : arrayList) {
String absolutePath = createAppContext.getDir("xposed_plugin_dex", 0).getAbsolutePath();
if (!TextUtils.isEmpty(str3)) {
Log.d(a, "Current truely loaded module path ----> ".concat(String.valueOf(str3)));
b.a(str3, absolutePath, createAppContext.getApplicationInfo(), classLoader);//5
}
}
}
}
init方法代碼比較多,上面標注釋的地方是比較值得關(guān)注的拾弃,根據(jù)這些地方展開
注釋1: 這里主要通過反射來創(chuàng)建Context,作為這么早執(zhí)行的代碼值桩,作者也通過很巧妙的方式創(chuàng)建了Context,有了Context后砸彬,很多事就好辦多了颠毙,XpatchUtils.createAppContext()的代碼如下:
public static Context createAppContext() {
try {
Class cls = Class.forName("android.app.ActivityThread");
Method declaredMethod = cls.getDeclaredMethod("currentActivityThread", new Class[0]);
declaredMethod.setAccessible(true);
Object invoke = declaredMethod.invoke(null, new Object[0]);
Field declaredField = cls.getDeclaredField("mBoundApplication");
declaredField.setAccessible(true);
Object obj = declaredField.get(invoke);
Field declaredField2 = obj.getClass().getDeclaredField("info");
declaredField2.setAccessible(true);
obj = declaredField2.get(obj);
Method declaredMethod2 = Class.forName("android.app.ContextImpl").getDeclaredMethod("createAppContext", new Class[]{cls, obj.getClass()});
declaredMethod2.setAccessible(true);
Object invoke2 = declaredMethod2.invoke(null, new Object[]{invoke, obj});
if (invoke2 instanceof Context) {
return (Context) invoke2;
}
} catch (ClassNotFoundException | IllegalAccessException | NoSuchFieldException | NoSuchMethodException | InvocationTargetException e) {
e.printStackTrace();
}
return null;
}
注釋2:調(diào)用com.wind.xposed.entry.b類的a方法,并將當(dāng)前App的ApplicationInfo和ClassLoader傳過去砂碉,從這里開始就開始碰到XposedBridge的代碼了
public static void a(ApplicationInfo applicationInfo, ClassLoader classLoader) {
Wrapper wrapper = new Wrapper(a.a());
CopyOnWriteSortedSet copyOnWriteSortedSet = new CopyOnWriteSortedSet();
copyOnWriteSortedSet.add(wrapper);
LoadPackageParam loadPackageParam = new LoadPackageParam(copyOnWriteSortedSet);
loadPackageParam.packageName = applicationInfo.packageName;
loadPackageParam.processName = applicationInfo.processName;
loadPackageParam.classLoader = classLoader;
loadPackageParam.appInfo = applicationInfo;
loadPackageParam.isFirstApplication = true;
XCallback.callAll(loadPackageParam);
}
方法第一行把a.a()傳給了Wrapper的構(gòu)造函數(shù)蛀蜜,a類完整類名是com.wind.xposed.entry.a,該類實現(xiàn)IXposedHookLoadPackage接口增蹭,a靜態(tài)方法返回a類實例滴某,那么Wrapper的構(gòu)造函數(shù)得到的就是IXposedHookLoadPackage接口的類實例。接著Wrapper類實例被添加到一個CopyOnWriteSortedSet中滋迈,這個CopyOnWriteSortedSet類是一個操作Object數(shù)組的類霎奢,CopyOnWriteSortedSet被傳到LoadPackageParam類的構(gòu)造函數(shù)中,調(diào)用這個構(gòu)造函數(shù)就是在給它父類(Param類)中的callbacks字段賦值饼灿。
public static abstract class Param {
public final Object[] callbacks;
//...
protected Param(CopyOnWriteSortedSet<? extends XCallback> copyOnWriteSortedSet) {
this.callbacks = copyOnWriteSortedSet.getSnapshot();
}
//...
}
接下來就是給LoadPackageParam的字段賦值幕侠,這些字段存儲著當(dāng)前應(yīng)用包名,進程名碍彭,ApplicationInfo,ClassLoader等等信息晤硕。
在com.wind.xposed.entry.b.a(ApplicationInfo applicationInfo, ClassLoader classLoader)
方法的最后悼潭,調(diào)用XCallback類的callAll方法
public static void callAll(Param param) {
if (param.callbacks != null) {
int i = 0;
while (true) {
Object[] objArr = param.callbacks;
if (i < objArr.length) {
try {
((XCallback) objArr[i]).call(param);
} catch (Throwable th) {
XposedBridge.log(th);
}
i++;
} else {
return;
}
}
}
//...
}
callAll方法遍歷Param類中的所有callback,調(diào)用它們的call方法
public void call(Param param) {
if (param instanceof LoadPackageParam) {
handleLoadPackage((LoadPackageParam) param);
}
}
饒了半天舞箍,就是調(diào)用傳進Wrapper類構(gòu)造函數(shù)的類的handleLoadPackage方法舰褪,那就是調(diào)用com.wind.xposed.entry.a
類的handleLoadPackage方法,而com.wind.xposed.entry.a
類的handleLoadPackage方法又去調(diào)用com.wind.xposed.entry.a.a
類的handleLoadPackage方法疏橄,那我們?nèi)タ?code>com.wind.xposed.entry.a.a類的handleLoadPackage的實現(xiàn)
public final void handleLoadPackage(LoadPackageParam loadPackageParam) {
Context a = XposedModuleEntry.a();
String readTextFromAssets = FileUtils.readTextFromAssets(a, "xpatch_asset/original_signature_info.ini");
Log.d("PackageSignatureHooker", "Get the original signature --> ".concat(String.valueOf(readTextFromAssets)));
if (!(readTextFromAssets == null || readTextFromAssets.isEmpty())) {
try {
WhaleRuntime.reserved2();
Class cls = Class.forName("android.app.ActivityThread");
Object invoke = cls.getDeclaredMethod("currentActivityThread", new Class[0]).invoke(null, new Object[0]);
Method declaredMethod = cls.getDeclaredMethod("getPackageManager", new Class[0]);
declaredMethod.setAccessible(true);
Object invoke2 = declaredMethod.invoke(invoke, new Object[0]);
Object newProxyInstance = Proxy.newProxyInstance(Class.forName("android.content.pm.IPackageManager").getClassLoader(), new Class[]{r7}, new a(invoke2, loadPackageParam.packageName, readTextFromAssets));
Field declaredField = cls.getDeclaredField("sPackageManager");
declaredField.setAccessible(true);
declaredField.set(invoke, newProxyInstance);
PackageManager packageManager = a.getPackageManager();
declaredField = packageManager.getClass().getDeclaredField("mPM");
declaredField.setAccessible(true);
declaredField.set(packageManager, newProxyInstance);
} catch (Exception e) {
Log.e("PackageSignatureHooker", " hookSignatureByProxy failed !!", e);
}
}
}
這個方法的作用是Hook相關(guān)的函數(shù)占拍,將被處理的apk的簽名替換成原來的,防止某些App檢測到自己的Apk被修改捎迫。apk在被Xpatch處理之前晃酒,簽名的信息的被保存了下來,對應(yīng)的任務(wù)類是SaveApkSignatureTask窄绒,上文沒有講到掖疮,感興趣可以去看一下。
注釋3:調(diào)用本類中的a方法颗祝,這個方法的參數(shù)只有一個參數(shù)Context
private static List<String> a(Context context) {
PackageManager packageManager = context.getPackageManager();
ArrayList arrayList = new ArrayList();
List a = a(true);
final ArrayList arrayList2 = new ArrayList();
boolean exists = new File(c, "xposed_config/modules.list").exists();
for (PackageInfo packageInfo : packageManager.getInstalledPackages(128)) {
ApplicationInfo applicationInfo = packageInfo.applicationInfo;
if (applicationInfo.enabled) {
Bundle bundle = applicationInfo.metaData;
if (bundle != null && bundle.containsKey("xposedmodule")) {
CharSequence charSequence = packageInfo.applicationInfo.publicSourceDir;
String charSequence2 = context.getPackageManager().getApplicationLabel(packageInfo.applicationInfo).toString();
if (TextUtils.isEmpty(charSequence)) {
charSequence = packageInfo.applicationInfo.sourceDir;
}
if (!TextUtils.isEmpty(charSequence) && (!exists || a == null || a.contains(applicationInfo.packageName))) {
XLog.d(a, " query installed module path -> ".concat(String.valueOf(charSequence)));
arrayList.add(charSequence);
}
arrayList2.add(Pair.create(packageInfo.applicationInfo.packageName, charSequence2));
}
}
}
new Thread(new Runnable() {
public final void run() {
List b = XposedModuleEntry.a(false);
if (b == null) {
b = new ArrayList();
}
List arrayList = new ArrayList();
for (Pair pair : arrayList2) {
if (!b.contains(pair.first)) {
XLog.d(XposedModuleEntry.a, " addPackageList packgagePair -> ".concat(String.valueOf(pair)));
arrayList.add(pair);
}
}
XposedModuleEntry.a(arrayList);
}
}).start();
return arrayList;
}
這個函數(shù)是讀取設(shè)備中已安裝的Apk,根據(jù)meta信息判斷它們是否屬于Xposed模塊浊闪,如果是并且外部存儲不存在xposed_config/modules.list
把它們的安裝位置添加到列表中。并且開啟一個線程螺戳,如果xposed_config/modules.list
存在則讀取搁宾,xposed_config/modules.list
文件記錄著模塊加載規(guī)則,具體可以去查看Xpatch項目的README倔幼。最后盖腿,將讀取到的Xposed模塊安裝位置列表返回
注釋4:調(diào)用本類中的a方法,這個方法的參數(shù)是一個Context和List
private static void a(Context context, List<String> list) {
String str = context.getApplicationInfo().nativeLibraryDir;
XLog.d(a, "Current loaded module libPath ----> ".concat(String.valueOf(str)));
File file = new File(str);
if (file.exists()) {
File[] listFiles = file.listFiles();
if (listFiles != null && listFiles.length > 0) {
for (File file2 : listFiles) {
if (file2.getName().startsWith("libxpatch_xp_module_")) {
XLog.d(a, "add xposed modules from libPath, this lib path is --> ".concat(String.valueOf(file2)));
list.add(file2.getAbsolutePath());
}
}
}
}
}
這個方法的目的是獲取所有打包進apk中的Xposed模塊的路徑添加到傳進來的List中
注釋5:調(diào)用com.wind.xposed.entry.b
類的a(String str, String str2, ApplicationInfo applicationInfo, ClassLoader classLoader)
方法
public static int a(String str, String str2, ApplicationInfo applicationInfo, ClassLoader classLoader) {
XLog.i("XposedModuleLoader", "Loading modules from ".concat(String.valueOf(str)));
if (new File(str).exists()) {
DexClassLoader dexClassLoader = new DexClassLoader(str, str2, null, classLoader);
InputStream resourceAsStream = dexClassLoader.getResourceAsStream("assets/xposed_init");
if (resourceAsStream == null) {
Log.i("XposedModuleLoader", "assets/xposed_init not found in the APK");
return 4;
}
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(resourceAsStream));
while (true) {
try {
String readLine = bufferedReader.readLine();
if (readLine != null) {
readLine = readLine.trim();
if (!(readLine.isEmpty() || readLine.startsWith("#"))) {
try {
String str3;
XLog.i("XposedModuleLoader", " Loading class ".concat(String.valueOf(readLine)));
Class loadClass = dexClassLoader.loadClass(readLine);
if (!XposedHelper.isIXposedMod(loadClass)) {
readLine = "XposedModuleLoader";
str3 = " This class doesn't implement any sub-interface of IXposedMod, skipping it";
} else if (IXposedHookInitPackageResources.class.isAssignableFrom(loadClass)) {
readLine = "XposedModuleLoader";
str3 = " This class requires resource-related hooks (which are disabled), skipping it.";
} else {
Object newInstance = loadClass.newInstance();
if (newInstance instanceof IXposedHookZygoteInit) {
XposedHelper.callInitZygote(str, newInstance);
}
if (newInstance instanceof IXposedHookLoadPackage) {
Wrapper wrapper = new Wrapper((IXposedHookLoadPackage) newInstance);
CopyOnWriteSortedSet copyOnWriteSortedSet = new CopyOnWriteSortedSet();
copyOnWriteSortedSet.add(wrapper);
LoadPackageParam loadPackageParam = new LoadPackageParam(copyOnWriteSortedSet);
loadPackageParam.packageName = applicationInfo.packageName;
loadPackageParam.processName = applicationInfo.processName;
loadPackageParam.classLoader = classLoader;
loadPackageParam.appInfo = applicationInfo;
loadPackageParam.isFirstApplication = true;
XCallback.callAll(loadPackageParam);
}
try {
resourceAsStream.close();
} catch (IOException unused) {
}
return 8;
}
Log.i(readLine, str3);
} catch (Throwable th) {
Log.e("XposedModuleLoader", " error ", th);
}
}
}
} catch (IOException e) {
Log.e("XposedModuleLoader", " error ", e);
} catch (Throwable th2) {
try {
resourceAsStream.close();
} catch (IOException unused2) {
}
}
try {
resourceAsStream.close();
} catch (IOException unused3) {
}
return 16;
}
}
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(str);
stringBuilder.append(" does not exist");
Log.e("XposedModuleLoader", stringBuilder.toString());
return 2;
}
這個函數(shù)讀取傳進來的Xposed模塊的信息损同,獲取DexClassLoader翩腐,讀取模塊assets下的xposed_init
文件,得到其中的類名并根據(jù)實例類型(IXposedHookZygoteInit或者IXposedHookLoadPackage)分別實例化它膏燃,是IXposedHookZygoteInit實例就callInitZygote茂卦,是IXposedHookLoadPackage實例就像上面的注釋2
所講的一樣調(diào)用模塊的handleLoadPackage方法。
講到這里好像并沒有涉及到whale框架组哩,我們編寫模塊的時候等龙,Hook的代碼都是寫在handleLoadPackage方法中,比如我們在handleLoadPackage方法內(nèi)伶贰,寫個findAndHookMethod蛛砰,最終就會調(diào)用WhaleRuntime.hookMethodNative
本地方法,來實現(xiàn)應(yīng)用內(nèi)的Hook
4. 總結(jié)
Xpatch思路很好黍衙,不需要ROOT泥畅,不用擔(dān)心Xposed在某些設(shè)備上的兼容性,不用每次調(diào)試Xposed模塊都重啟手機琅翻,很方便的就可以使用Xposed模塊位仁,實現(xiàn)應(yīng)用內(nèi)的Hook浅妆。但是在使用的過程中也發(fā)現(xiàn)了一個小問題,要處理的Apk如果沒有手動繼承Application類并在AndroidManifest.xml中指定障癌,那么Xpatch就注入不了代碼,也就無法正常使用辩尊。本文也只講了Xpatch的基本流程涛浙,具體whale是怎么Hook的,能力有限摄欲,沒能展開轿亮。