Xposed的新打开方式--Xpatch工作流程分析

栏目: IOS · Android · 发布时间: 5年前

内容简介:项目地址:

Xpatch 概述

Xpatch 是一款利用重打包的方式,使得被处理的 Apk 启动时自动加载 Xposed 模块,来实现应用内 Hook 的工具。

项目地址:

https://github.com/WindySha/Xpatch

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 的外部库里,并调用了 Dex2jarCmddoMain 方法,给他传进去一些类似于命令行参数的东西,令我们比较提得起精神的是 -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;
 }
 //...
}

在这个阶段,并没有发现任何注入的代码,不急,我们继续跟踪,看到 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("<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 方法用于在函数内插入一条指令,看到两处调用 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 类,从名字可以看出它的任务是复制 sodex 的,具体是怎样的,我们看代码。 SoAndDexCopyTask 类,它也实现了 Runnable 接口, run方 法在任务被启动时调用:

@Override
public 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.jarassets 目录下的 libxpatch_wl.so 复制到apk解压目录的 lib/ <架构文件夹>下。这个 libxpatch_wl.so whale 框架提供 so 文件,为 Hook 提供可能。

除了复制 so ,如果我们在用 Xpatch 时使用 -xm 参数来将 Xposed 模块集成到 apk 中,那么模块会被就会被重命名成:以 libxpatch_xpmodule 为前缀,后面接着模块序号,最后再以 so 为后缀。最终这个模块被复制到 apklib 目录下。

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 ,并给压缩成的文件签名,这里就不细讲了。

被集成进apk中的dex分析

我们在上面提到过, Xpatchassets 目录下的 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
 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 方法代码比较多,上面标注释的地方是比较值得关注的,根据这些地方展开。

注释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 方法,并将当前 AppApplicationInfoClassLoader 传过去,从这里开始就开始碰到 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<? extends XCallback> 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 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;
}

这个函数是读取设备中已安装的 Apk ,根据 meta 信息判断它们是否属于 Xposed 模块,如果是并且外部存储不存在 xposed_config/modules.list 把它们的安装位置添加到列表中。并且开启一个线程,如果 xposed_config/modules.list 存在则读取, xposed_config/modules.list 文件记录着模块加载规则,具体可以去查看 Xpatch 项目的 README 。最后,将读取到的 Xposed 模块安装位置列表返回。

注释4:调用本类中的 a 方法,这个方法的参数是一个 ContextList。

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:调用 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。

总结

Xpatch 思路很好,不需要 ROOT ,不用担心 Xposed 在某些设备上的兼容性,不用每次调试 Xposed 模块都重启手机,很方便的就可以使用 Xposed 模块,实现应用内的 Hook 。但是在使用的过程中也发现了一个小问题,要处理的 Apk 如果没有手动继承 Application 类并在 AndroidManifest.xml 中指定,那么 Xpatch 就注入不了代码,也就无法正常使用。本文也只讲了 Xpatch 的基本流程,具体 whale 是怎么 Hook 的,能力有限,没能展开。

- End -

Xposed的新打开方式--Xpatch工作流程分析

看雪ID:落叶似秋    

https://bbs.pediy.com/user-815293.htm

本文由看雪论坛  落叶似秋    原创

转载请注明来自看雪社区

:warning: 注意

2019 看雪安全开发者峰会门票正在热售中!

长按识别下方 二维码 即可享受  2.5折  优惠!

Xposed的新打开方式--Xpatch工作流程分析

热门图书推荐

Xposed的新打开方式--Xpatch工作流程分析   立即购买!

Xposed的新打开方式--Xpatch工作流程分析

公众号ID:ikanxue

官方微博:看雪安全

商务合作:wsc@kanxue.com

点击下方“阅读原文”,查看更多干货


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

游戏编程模式

游戏编程模式

Robert Nystrom / GPP翻组 / 人民邮电出版社 / 2016-9-1 / 61.4

游戏开发一直是热门的领域,掌握良好的游戏编程模式是开发人员的应备技能。本书细致地讲解了游戏开发需要用到的各种编程模式,并提供了丰富的示例。 全书共分20章,通过三大部分内容全面介绍了与游戏编程模式相关的各类知识点。首部分介绍了基础知识和框架;第二部分深入探索设计模式,并介绍了模式与游戏开发之间的关联;第三部分介绍了13种有效的游戏设计模式。 本书提供了丰富的代码示例,通过理论和代码示例......一起来看看 《游戏编程模式》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具