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

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的,能力有限摄欲,沒能展開轿亮。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市轰豆,隨后出現(xiàn)的幾起案子龟再,更是在濱河造成了極大的恐慌握恳,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件但骨,死亡現(xiàn)場離奇詭異,居然都是意外死亡智袭,警方通過查閱死者的電腦和手機奔缠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來吼野,“玉大人校哎,你說我怎么就攤上這事⊥剑” “怎么了闷哆?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長单起。 經(jīng)常有香客問我抱怔,道長,這世上最難降的妖魔是什么嘀倒? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任野蝇,我火速辦了婚禮,結(jié)果婚禮上括儒,老公的妹妹穿的比我還像新娘绕沈。我一直安慰自己,他們只是感情好帮寻,可當(dāng)我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布乍狐。 她就那樣靜靜地躺著,像睡著了一般固逗。 火紅的嫁衣襯著肌膚如雪浅蚪。 梳的紋絲不亂的頭發(fā)上藕帜,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天,我揣著相機與錄音惜傲,去河邊找鬼洽故。 笑死,一個胖子當(dāng)著我的面吹牛盗誊,可吹牛的內(nèi)容都是我干的时甚。 我是一名探鬼主播,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼哈踱,長吁一口氣:“原來是場噩夢啊……” “哼荒适!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起开镣,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤刀诬,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后邪财,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體陕壹,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年树埠,在試婚紗的時候發(fā)現(xiàn)自己被綠了帐要。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡弥奸,死狀恐怖榨惠,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情盛霎,我是刑警寧澤赠橙,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站愤炸,受9級特大地震影響期揪,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜规个,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一凤薛、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧诞仓,春花似錦缤苫、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春舒憾,著一層夾襖步出監(jiān)牢的瞬間镀钓,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工镀迂, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留丁溅,地道東北人。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓探遵,卻偏偏與公主長得像窟赏,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子别凤,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內(nèi)容