1. Xpatch概述
Xpatch是一款利用重打包的方式,使得被处理的Apk启动时自动加载Xposed模块,来实现应用内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的源码中,第一步对应的是ApkModifyTask类,实现的是Runnable接口,它的任务是修改Dex文件,使得被处理的apk在启动时能够执行指定的代码。
如果反编译被Xpatch处理过的Apk,查看App中Application的子类,会发现其中多了以下的代码:
static { XposedModuleEntry.init();}
我们大胆的猜测,这就是Xpatch给注入进去的入口代码。我们回到Xpatch的源码,来看看它是如何注入的。查看ApkModifyTask类,一步步进行跟踪:
ApkModifyTask类的run方法,在任务被启动时调用,它的代码:
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); //...}
继续跟踪到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;}
看到了它创建了一个com.googlecode.dex2jar.tools.Dex2jarCmd
类实例,这个类在名为dex-tools的外部库里,并调用了Dex2jarCmd的doMain方法,给他传进去一些类似于命令行参数的东西,令我们比较提得起精神的是-app
参数,它传进去一个applicationName,这个applicationName的值来自MainCommand类的doCommandLine方法,逻辑是从解压的apk中读取AndroidManifest.xml,并读取application
节点下的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?就这样?并没有发现任何注入的代码啊,不急,继续跟踪,看到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是个抽象方法,它的真正实现是在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); //... }}
跳转到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方法调用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("")) { 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(" ")) { 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(" ")) { hasFoundClinitMethod = true; break; } } } if (!hasFoundClinitMethod) { MethodVisitor mv = cv.visitMethod(8, " ", "()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方法用于在函数内插入一条指令,看到两处调用visitMethodInsn来插入调用 com.wind.xposed.entry.XposedModuleEntry类的init方法的指令。
- convertCode函数中的visitMethodInsn,逻辑是如果要处理的Application类中存在clinit方法,即存在静态代码段,就直接插入调用 com.wind.xposed.entry.XposedModuleEntry类的init方法的指令
- addMethod函数中的visitMethodInsn,如果要处理的Application类中不存在clinit方法,即不存在静态代码段,就创建一个静态代码段,并在其中插入调用 com.wind.xposed.entry.XposedModuleEntry类的init方法的指令,最后返回void
注:上面的操作码,184代表
invoke-static
,177代表return-void
。这些操作码定义在org.objectweb.asm.Opcodes类中。
到这里,第一步我们已经搞清楚了。
(2) 第二步
对应的是SoAndDexCopyTask类,从名字可以看出它的任务是复制so和dex的,具体是怎样的,我们看代码。
SoAndDexCopyTask类,它也实现了Runnable接口,run方法在任务被启动时调用:
@Overridepublic void run() { copySoFile(); copyDexFile(dexFileCount); deleteMetaInfo();}
这个类主要就做这三个动作:复制so文件,复制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++; } }}
看代码可以知道它的任务是把Xpatch.jar中assets目录下的libxpatch_wl.so复制到apk解压目录的lib/<架构文件夹>
下。这个libxpatch_wl.so是框架提供so文件,为Hook提供可能。
除了复制so,如果我们在用Xpatch时使用-xm参数来将Xposed模块集成到apk中,那么模块会被就会被重命名成:以libxpatch_xp_module_为前缀,后面接着模块序号,最后再以so为后缀。最终这个模块被复制到apk的lib目录下。
copyDexFile方法:
private void copyDexFile(int dexFileCount) { String copiedDexFileName = "classes" + (dexFileCount + 1) + ".dex"; FileUtils.copyFileFromJar("assets/classes.dex", unzipApkFilePath + copiedDexFileName);}
逻辑也很明了,把assets下的classes.dex复制到apk解压目录下,根据原来apk中的dex个数来给复制进去的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) 第三步
对应的是BuildAndSignApkTask类,从名字可以看出它的任务是构建和对apk签名的。
这个BuildAndSignApkTask类也是实现Runnable接口,我们来看run方法:
public void run() { //... FileUtils.compressToZip(unzipApkFilePath, unsignedApkPath); //... signApk(unsignedApkPath, keyStoreFilePath, signedApkPath, false); //...}
这个方法做了两件重要的事,把apk解压目录给压缩成zip,并给压缩成的文件签名,这里就不细讲了。
3. 被集成进apk中的dex分析
我们在上面提到过,Xpatch把assets目录下的classes.dex文件复制进了目标apk里,这个dex是不开源的,那么这个dex里面究竟有什么呢,我们把dex解压出来,拖进jadx中反编译。
既然Xpatch将初始化代码注入到应用的Application类,初始化代码调用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 ListarrayList = new ArrayList(); List 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方法代码比较多,上面标注释的地方是比较值得关注的,根据这些地方展开
注释1: 这里主要通过反射来创建Context,作为这么早执行的代码,作者也通过很巧妙的方式创建了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:调用com.wind.xposed.entry.b类的a方法,并将当前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的构造函数,a类完整类名是com.wind.xposed.entry.a,该类实现IXposedHookLoadPackage接口,a静态方法返回a类实例,那么Wrapper的构造函数得到的就是IXposedHookLoadPackage接口的类实例。接着Wrapper类实例被添加到一个CopyOnWriteSortedSet中,这个CopyOnWriteSortedSet类是一个操作Object数组的类,CopyOnWriteSortedSet被传到LoadPackageParam类的构造函数中,调用这个构造函数就是在给它父类(Param类)中的callbacks字段赋值。
public static abstract class Param { public final Object[] callbacks; //... protected Param(CopyOnWriteSortedSet copyOnWriteSortedSet) { this.callbacks = copyOnWriteSortedSet.getSnapshot(); } //...}
接下来就是给LoadPackageParam的字段赋值,这些字段存储着当前应用包名,进程名,ApplicationInfo,ClassLoader等等信息。
在com.wind.xposed.entry.b.a(ApplicationInfo applicationInfo, ClassLoader classLoader)
方法的最后,调用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,调用它们的call方法
public void call(Param param) { if (param instanceof LoadPackageParam) { handleLoadPackage((LoadPackageParam) param); }}
饶了半天,就是调用传进Wrapper类构造函数的类的handleLoadPackage方法,那就是调用com.wind.xposed.entry.a
类的handleLoadPackage方法,而com.wind.xposed.entry.a
类的handleLoadPackage方法又去调用com.wind.xposed.entry.a.a
类的handleLoadPackage方法,那我们去看com.wind.xposed.entry.a.a
类的handleLoadPackage的实现
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相关的函数,将被处理的apk的签名替换成原来的,防止某些App检测到自己的Apk被修改。apk在被Xpatch处理之前,签名的信息的被保存了下来,对应的任务类是SaveApkSignatureTask,上文没有讲到,感兴趣可以去看一下。
注释3:调用本类中的a方法,这个方法的参数只有一个参数Context
private static Lista(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;}
这个函数是读取设备中已安装的Apk,根据meta信息判断它们是否属于Xposed模块,如果是并且外部存储不存在xposed_config/modules.list
把它们的安装位置添加到列表中。并且开启一个线程,如果xposed_config/modules.list
存在则读取,xposed_config/modules.list
文件记录着模块加载规则,具体可以去查看Xpatch项目的README。最后,将读取到的Xposed模块安装位置列表返回
注释4:调用本类中的a方法,这个方法的参数是一个Context和List
private static void a(Context context, Listlist) { 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:调用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; }
这个函数读取传进来的Xposed模块的信息,获取DexClassLoader,读取模块assets下的xposed_init
文件,得到其中的类名并根据实例类型(IXposedHookZygoteInit或者IXposedHookLoadPackage)分别实例化它,是IXposedHookZygoteInit实例就callInitZygote,是IXposedHookLoadPackage实例就像上面的注释2
所讲的一样调用模块的handleLoadPackage方法。
讲到这里好像并没有涉及到whale框架,我们编写模块的时候,Hook的代码都是写在handleLoadPackage方法中,比如我们在handleLoadPackage方法内,写个findAndHookMethod,最终就会调用WhaleRuntime.hookMethodNative
本地方法,来实现应用内的Hook
4. 总结
Xpatch思路很好,不需要ROOT,不用担心Xposed在某些设备上的兼容性,不用每次调试Xposed模块都重启手机,很方便的就可以使用Xposed模块,实现应用内的Hook。但是在使用的过程中也发现了一个小问题,要处理的Apk如果没有手动继承Application类并在AndroidManifest.xml中指定,那么Xpatch就注入不了代码,也就无法正常使用。本文也只讲了Xpatch的基本流程,具体whale是怎么Hook的,能力有限,没能展开。