Xpatch是一款利用重打包的方式,使得被處理的Apk啓動時自動加載Xposed模塊,來實現應用內Hook的工具。android
項目地址:https://github.com/WindySha/Xpatchgit
Xpatch修改apk,主要有三個步驟,代碼在MainCommand類的doCommandLine方法:github
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) 第一步web
在Xpatch的源碼中,第一步對應的是ApkModifyTask類,實現的是Runnable接口,它的任務是修改Dex文件,使得被處理的apk在啓動時可以執行指定的代碼。數組
若是反編譯被Xpatch處理過的Apk,查看App中Application的子類,會發現其中多瞭如下的代碼:架構
static { XposedModuleEntry.init(); }
咱們大膽的猜想,這就是Xpatch給注入進去的入口代碼。咱們回到Xpatch的源碼,來看看它是如何注入的。查看ApkModifyTask類,一步步進行跟蹤:app
ApkModifyTask類的run方法,在任務被啓動時調用,它的代碼:框架
public void run() { //... String targetDexFileName = dumpJarFile(dexFileCount, unzipApkFilePath, jarOutputPath, applicationName); //... }
dumpJarFile方法:ide
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("<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方法的指令。
注:上面的操做碼,184表明
invoke-static
,177表明return-void
。這些操做碼定義在org.objectweb.asm.Opcodes類中。
到這裏,第一步咱們已經搞清楚了。
(2) 第二步
對應的是SoAndDexCopyTask類,從名字能夠看出它的任務是複製so和dex的,具體是怎樣的,咱們看代碼。
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.jar中assets目錄下的libxpatch_wl.so複製到apk解壓目錄的lib/<架構文件夾>
下。這個libxpatch_wl.so是whale框架提供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,並給壓縮成的文件簽名,這裏就不細講了。
咱們在上面提到過,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 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方法,並將當前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<? 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方法,這個方法的參數是一個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:調用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的,能力有限,沒能展開。