前言
Xpatch是筆者開發的一款破解Android App工具,源碼地址:
https://github.com/WindySha/Xpatch
本文接着上一篇Xpatch源碼解析文章,繼續分析Xpatch的實現原理。
Xpatch加載Xposed插件流程
查找插件Apk
加載Xposed插件之前,首先需要遍歷所有安裝的應用,根據Xposed插件的特徵,找到其中的Xposed插件。
那什麼樣的應用纔是Xposed插件呢?
根據Xposed插件的書寫規範中要求,插件Apk的Manifest文件中需要包含android:name="xposedmodule"
這樣的meta-data
信息:
<application
<meta-data
android:name="xposedmodule"
android:value="true"/>
</application>
根據此特徵,我們獲取App PackageInfo中的meta data,從而過濾出插件Apk,具體實現源碼如下:
private static List<String> loadAllInstalledModule(Context context) {
PackageManager pm = context.getPackageManager();
List<String> modulePathList = new ArrayList<>();
// modulePathList.add("mnt/sdcard/app-debug.apk");
List<String> packageNameList = loadPackageNameListFromFile(true);
List<Pair<String, String>> installedModuleList = new ArrayList<>();
boolean configFileExist = configFileExist();
for (PackageInfo pkg : pm.getInstalledPackages(PackageManager.GET_META_DATA)) {
ApplicationInfo app = pkg.applicationInfo;
if (!app.enabled)
continue;
if (app.metaData != null && app.metaData.containsKey("xposedmodule")) {
String apkPath = pkg.applicationInfo.publicSourceDir;
String apkName = context.getPackageManager().getApplicationLabel(pkg.applicationInfo).toString();
if (TextUtils.isEmpty(apkPath)) {
apkPath = pkg.applicationInfo.sourceDir;
}
if (!TextUtils.isEmpty(apkPath) && (!configFileExist || packageNameList == null || packageNameList
.contains(app.packageName))) {
XLog.d(TAG, " query installed module path -> " + apkPath);
modulePathList.add(apkPath);
}
installedModuleList.add(Pair.create(pkg.applicationInfo.packageName, apkName));
}
}
final List<Pair<String, String>> installedModuleListFinal = installedModuleList;
// ...
// ...
return modulePathList;
}
加載插件Apk
找到了插件Apk之後,就可以得到此Apk的路徑(data/app/包名 目錄下面),然後就是根據此路徑加載插件。
加載插件的方法是:com.wind.xposed.entry.XposedModuleLoader.loadModule()
其主要流程參考了原版Xposed框架中的實現,過程如下:
- 根據插件Apk文件路徑構造DexClassLoader;
- 讀取Apk asset目錄下’'assets/xposed_init’文件中所有的類名;
- 根據類名和Classloader構造入口類,並執行類的入口方法
handleLoadPackage
。
流程源碼和註釋:
public static int loadModule(final String moduleApkPath, String moduleOdexDir, String moduleLibPath,
final ApplicationInfo currentApplicationInfo, ClassLoader appClassLoader) {
// ...
// 創建DexClassLoader
ClassLoader mcl = new DexClassLoader(moduleApkPath, moduleOdexDir, moduleLibPath, appClassLoader);
// 讀取asset目錄中文件裏寫入的所有類名
InputStream is = mcl.getResourceAsStream("assets/xposed_init");
// ...
BufferedReader moduleClassesReader = new BufferedReader(new InputStreamReader(is));
try {
String moduleClassName;
while ((moduleClassName = moduleClassesReader.readLine()) != null) {
moduleClassName = moduleClassName.trim();
if (moduleClassName.isEmpty() || moduleClassName.startsWith("#"))
continue;
try {
XLog.i(TAG, " Loading class " + moduleClassName);
// 構造對象
Class<?> moduleClass = mcl.loadClass(moduleClassName);
if (!XposedHelper.isIXposedMod(moduleClass)) {
Log.i(TAG, " This class doesn't implement any sub-interface of IXposedMod, skipping it");
continue;
} else if (IXposedHookInitPackageResources.class.isAssignableFrom(moduleClass)) {
Log.i(TAG, " This class requires resource-related hooks (which are disabled), skipping it.");
continue;
}
final Object moduleInstance = moduleClass.newInstance();
if (moduleInstance instanceof IXposedHookZygoteInit) {
XposedHelper.callInitZygote(moduleApkPath, moduleInstance);
}
// 執行對象中的`handleLoadPackage`入口方法,實現hook流程
if (moduleInstance instanceof IXposedHookLoadPackage) {
// hookLoadPackage(new IXposedHookLoadPackage.Wrapper((IXposedHookLoadPackage) moduleInstance));
IXposedHookLoadPackage.Wrapper wrapper = new IXposedHookLoadPackage.Wrapper((IXposedHookLoadPackage) moduleInstance);
XposedBridge.CopyOnWriteSortedSet<XC_LoadPackage> xc_loadPackageCopyOnWriteSortedSet = new XposedBridge.CopyOnWriteSortedSet<>();
xc_loadPackageCopyOnWriteSortedSet.add(wrapper);
XC_LoadPackage.LoadPackageParam lpparam = new XC_LoadPackage.LoadPackageParam(xc_loadPackageCopyOnWriteSortedSet);
lpparam.packageName = currentApplicationInfo.packageName;
lpparam.processName = currentApplicationInfo.processName;
lpparam.classLoader = appClassLoader;
lpparam.appInfo = currentApplicationInfo;
lpparam.isFirstApplication = true;
XC_LoadPackage.callAll(lpparam);
}
} catch (Throwable t) {
}
}
} catch (IOException e) {
} finally {
}
Apk中注入代碼的實現
往Apk中注入代碼,一般來說,有兩種主流方法:
- 最常用的方法,使用ApkTool將Apk反編譯爲smali代碼,修改smali文件,然後再將修改後的文件使用ApkTool打包,從而實現代碼的修改;
- 修改dex2jar工程源碼,使得在dex轉換爲jar過程中能夠插入java代碼,然後再使用jar2dex工具將修改後的jar轉換爲dex文件,從而實現代碼修改和回編。
這裏,我們選取了第二種方法。第二種方法的難點是如何修改dex2jar工程源碼實現代碼的插入。
爲此,需要先分析其實現原理。
Claud大神開源的dex2jar工具大致原理是,先根據dex文件格式規則解析dex文件中的所有類信息,然後再利用ASM工具根據這些信息生成Class文件。
對Java開發比較熟悉的人,應該很熟悉ASM。ASM是一個Java字節碼操作框架。它可以直接對class文件進行增刪改的操作,能被用來動態生成類或者增強既有類的功能。Java中許多的框架的實現是基於ASM,比如Java AOP的實現,JavaWeb開發中的Spring框架的實現等等。可以說ASM就是一把利劍,是深入Java必須學習的一個點。
這裏,我們就不講解ASM的原理和用法,只講解如何利用ASM修改dex2jar工程源碼,從而實現代碼的注入。
ASM代碼生成
在上一篇源碼解析文章中,我們說過,破解Apk,只需要在其Application類中注入這樣一段靜態代碼塊:
package com.test;
import android.app.Application;
import com.wind.xposed.entry.XposedModuleEntry;
public class MyApplication extends Application {
static {
XposedModuleEntry.init();
}
}
那這樣的一段代碼,如何用ASM工具生成呢。
假如對ASM的API熟悉的話,其實很容易就能實現這樣一小段代碼的生成。
假如不熟悉的話,也沒關係,我們可以利用Android Studio中的一個插件,查看這段代碼的ASM的實現。這個插件的名字是:ASM Bytecode Viewer
通過這個插件,我們可以清晰的看到生成這段代碼的ASM代碼的實現:
public class MyApplicationDump implements Opcodes {
public static byte[] dump() throws Exception {
ClassWriter cw = new ClassWriter(0);
FieldVisitor fv;
MethodVisitor mv;
AnnotationVisitor av0;
cw.visit(V1_7, ACC_PUBLIC + ACC_SUPER, "com/test/MyApplication", null, "android/app/Application", null);
cw.visitSource("MyApplication.java", null);
{
mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
mv.visitCode();
Label l0 = new Label();
mv.visitLabel(l0);
mv.visitLineNumber(7, l0);
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESPECIAL, "android/app/Application", "<init>", "()V", false);
mv.visitInsn(RETURN);
Label l1 = new Label();
mv.visitLabel(l1);
mv.visitLocalVariable("this", "Lcom/test/MyApplication;", null, l0, l1, 0);
mv.visitMaxs(1, 1);
mv.visitEnd();
}
{
mv = cw.visitMethod(ACC_STATIC, "<clinit>", "()V", null, null);
mv.visitCode();
Label l0 = new Label();
mv.visitLabel(l0);
mv.visitLineNumber(11, l0);
mv.visitMethodInsn(INVOKESTATIC, "com/wind/xposed/entry/XposedModuleEntry", "init", "()V", false);
Label l1 = new Label();
mv.visitLabel(l1);
mv.visitLineNumber(12, l1);
mv.visitInsn(RETURN);
mv.visitMaxs(0, 0);
mv.visitEnd();
}
cw.visitEnd();
return cw.toByteArray();
}
}
這段代碼中,第一個花括號中代碼用來生成這個類的默認構造方法,第二個花括號中是用來生成靜態代碼塊方法,去掉生成標籤行數等無關代碼後,最終需要的代碼僅僅是:
mv = cw.visitMethod(ACC_STATIC, "<clinit>", "()V", null, null);
mv.visitCode();
mv.visitMethodInsn(INVOKESTATIC, "com/wind/xposed/entry/XposedModuleEntry", "init", "()V", false);
mv.visitInsn(RETURN);
mv.visitMaxs(0, 0);
mv.visitEnd();
下面再分析如何將這段ASM代碼加到dex2jar工程中,從而實現代碼植入。
修改dex2jar源碼
通過不斷調試dex2jar源碼,我們可以找到使用ASM生成字節碼的代碼位置,在Dex2jar.java
文件的doTranslate ()
方法中:
// dex2jar項目源碼
// com.googlecode.d2j.dex.Dex2jar.java
private void doTranslate(final Path dist) throws IOException {
// ...
new ExDex2Asm(exceptionHandler) {
public void convertCode(DexMethodNode methodNode, MethodVisitor mv) {
if ((readerConfig & DexFileReader.SKIP_CODE) != 0 && methodNode.method.getName().equals("<clinit>")) {
// also skip clinit
return;
}
super.convertCode(methodNode, mv);
}
@Override
public void optimize(IrMethod irMethod) {
// ...
// ...
}
@Override
public void ir2j(IrMethod irMethod, MethodVisitor mv) {
new IR2JConverter(0 != (V3.OPTIMIZE_SYNCHRONIZED & v3Config)).convert(irMethod, mv);
}
}.convertDex(fileNode, cvf);
// ...
}
ExDex2Asm
方法convertCode
是其父類中對外暴露的方法,用於處理每個方法生成。
在這裏,我們可以判斷當前類是不是應用的Application類,以及方法是不是靜態代碼塊方法<clinit>
, 是的話,通過visitMethodInsn
加上XposedModuleEntry.init();
方法,代碼如下:
if (methodNode.method.getOwner().equals(applicationName) && methodNode.method.getName().equals("<clinit>")) {
isApplicationClassFounded = true;
mv.visitMethodInsn(Opcodes.INVOKESTATIC, XPOSED_ENTRY_CLASS_NAME, "init", "()V", false);
}
還有另外一種情形,也需要處理,就是當前應用自定義的Application類沒有方法靜態方法塊的情形。對於這種情形的處理,僅修改ExDex2Asm
類中的代碼,顯然無法實現。我們需要在其父類Dex2Asm
中增加一個非私有的空方法,暴露給子類ExDex2Asm
。這個方法需要包含類的節點信息DexClassNode
和ASM代碼生成對象ClassVisitor
。
通過分析Dex2Asm
類中代碼,最終選擇了在其convertClass
方法後面的位置調用此方法,代碼如下:
// com.googlecode.d2j.dex.Dex2jar.java
public void convertClass(int dexVersion, DexClassNode classNode,
ClassVisitorFactory cvf, Map<String, Clz> classes) {
accept(classNode.anns, cv);
// ...
if (classNode.fields != null) {
for (DexFieldNode fieldNode : classNode.fields) {
convertField(classNode, fieldNode, cv);
}
}
// 在這裏調用新增加的方法
addMethod(classNode, cv);
if (classNode.methods != null) {
for (DexMethodNode methodNode : classNode.methods) {
convertMethod(classNode, methodNode, cv);
}
}
cv.visitEnd();
}
// 這是新增加的方法,具體實現在子類中
public void addMethod(DexClassNode classNode, ClassVisitor cv) {
}
在addMethod
具體實現中,先判斷當前類是Application類,然後再遍歷類的所有方法,如果沒有靜態代碼塊方法,通過ASM加上靜態代碼塊方法,這段增加方法的ASM代碼,就是上面用Android Studio中的ASM插件生成的。
最終完整代碼如下:
// 修改後的dex2jar項目代碼
// com.googlecode.d2j.dex.Dex2jar.java
new ExDex2Asm(exceptionHandler) {
public void convertCode(DexMethodNode methodNode, MethodVisitor mv) {
// 增加的代碼,用於在Application靜態代碼塊中增加XposedModuleEntry.init();
if (methodNode.method.getOwner().equals(applicationName) && methodNode.method.getName().equals("<clinit>")) {
isApplicationClassFounded = true;
mv.visitMethodInsn(Opcodes.INVOKESTATIC, XPOSED_ENTRY_CLASS_NAME, "init", "()V", false);
}
if ((readerConfig & DexFileReader.SKIP_CODE) != 0 && methodNode.method.getName().equals("<clinit>")) {
// also skip clinit
return;
}
super.convertCode(methodNode, mv);
}
// 增加的代碼
@Override
public void addMethod(com.googlecode.d2j.node.DexClassNode classNode, ClassVisitor cv) {
// 找到應用的Application類
if (classNode.className.equals(applicationName)) {
isApplicationClassFounded = true;
boolean hasFoundClinitMethod = false;
if (classNode.methods != null) {
// 判斷是否存在靜態代碼塊
for (DexMethodNode methodNode : classNode.methods) {
if (methodNode.method.getName().equals("<clinit>")) {
hasFoundClinitMethod = true;
break;
}
}
}
// 通過ASM增加靜態代碼塊方法,並注入初始化方法XposedModuleEntry.init();
if (!hasFoundClinitMethod) {
MethodVisitor mv = cv.visitMethod(Opcodes.ACC_STATIC, "<clinit>", "()V", null, null);
mv.visitCode();
mv.visitMethodInsn(Opcodes.INVOKESTATIC, XPOSED_ENTRY_CLASS_NAME, "init", "()V", false);
mv.visitInsn(Opcodes.RETURN);
mv.visitMaxs(0, 0);
mv.visitEnd();
}
}
}
@Override
public void optimize(IrMethod irMethod) {
// ...
}
@Override
public void ir2j(IrMethod irMethod, MethodVisitor mv) {
new IR2JConverter(0 != (V3.OPTIMIZE_SYNCHRONIZED & v3Config)).convert(irMethod, mv);
}
}.convertDex(fileNode, cvf);
}
此外,Dex2Jar
類對象applicationName
是從外面傳入的應用定義的Application類全名,在Dex2jarCmd
類中傳入,Dex2jarCmd
類的修改點如下:
// com.googlecode.dex2jar.tools.Dex2jarCmd.java
public class Dex2jarCmd extends BaseCmd {
// ...
// ...
// 新增的命令行參數,用於傳應用的Application全類名
@Opt(opt = "app", longOpt = "applicationName", description = "application full name that method should be insert into",
argName = "application-name")
private String applicationName;
protected void doCommandLine() throws Exception {
// ...
// ...
dex2jar = Dex2jar.from(reader);
dex2jar.withExceptionHandler(handler).reUseReg(reuseReg).topoLogicalSort()
.skipDebug(!debugInfo).optimizeSynchronized(this.optmizeSynchronized).printIR(printIR)
.noCode(noCode).skipExceptions(skipExceptions)
.setApplicationName(applicationName).to(file); // 新增的代碼
// ...
// ...
}
...
// 新增的方法,用於暴露給外面,判斷當前Dex中是否存在應用的Application類
public boolean isApplicationClassFounded() {
if (dex2jar == null) {
return false;
}
return dex2jar.isApplicationClassFounded();
}
}
Dex2jar
類增加的兩個成員變量和相關方法如下:
// 修改後的dex2jar項目代碼
// com.googlecode.d2j.dex.Dex2jar.java
public class Dex2jar {
// ...
// ...
// 新增的兩個成員變量
private String applicationName;
private boolean isApplicationClassFounded = false;
// 增加應用application的名稱
public Dex2jar setApplicationName(String appName) {
this.applicationName = appName;
applicationName = applicationName.replace('.', '/');
if (!applicationName.endsWith(";")) {
applicationName += ";";
}
if (!applicationName.startsWith("L")) {
applicationName = "L" + applicationName;
}
return this;
}
public boolean isApplicationClassFounded() {
return isApplicationClassFounded;
}
// ...
// ...
}
至此,我們完成了dex2jar工程的改造,順利實現了給一個Apk注入代碼。
打包及簽名流程
有了上面的準備工作後,我們來分析Xpatch源碼中,調用dex2jar工具修改apk流程,以及對修改後的apk打包簽名的流程。
Xpatch源碼的入口類MainCommand
,其核心方法是doCommandLine()
。
在doCommandLine()
方法的主流程執行之前,先做了以下準備工作:`
- 解析命令行參數,主要是包括原Apk路徑和生成的Apk路徑;
- 解析Apk壓縮包,讀取dex文件的個數;
- 通過AxmlPrinter2工具解析Manifest文件中的Application全類名;
以上準備工具完成後,通過三個task處理Apk文件,源碼如下:
// 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));
// 4. excute these tasks
for (Runnable executor : mXpatchTasks) {
executor.run();
}
這三個task的作用分別是:
- 利用修改後的dex2jar工具和jar2dex工具修改Apk中應用Application類的代碼;
- 將用於加載Xposed插件的dex文件和so文件複製到Apk解壓後的文件目錄下;
- 將Apk解壓後的文件目錄重新壓縮爲zip壓縮包,並重新簽名。
第二個task和第三個task比較簡單,這裏就不一一分析。
主要分析一下第一個task,修改Apk源碼的task: ApkModifyTask
。
ApkModifyTask
的核心流程是遍歷Apk解壓出來的所有dex文件,對每個dex文件執行Dex2jarCmd
,這個cmd的作用就是找到dex中應用的Application類,並插入代碼,如果找到,就不繼續處理下一個dex文件,因爲每個App只有一個Application類,代碼細節如下:
private String dumpJarFile(int dexFileCount, String dexFilePath, String jarOutputPath, String applicationName) {
ArrayList<String> dexFileList = createClassesDotDexFileList(dexFileCount);
for (String dexFileName : dexFileList) {
String filePath = dexFilePath + dexFileName;
// 執行dex2jar命令,修改源代碼
boolean isApplicationClassFound = dex2JarCmd(filePath, jarOutputPath, applicationName);
// 找到了目標應用主application的包名,說明代碼注入成功,則返回當前dex文件
if (isApplicationClassFound) {
return dexFileName;
}
}
return "";
}
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);
// 執行完命令後,會返回查找Application Class的結果
boolean isApplicationClassFounded = cmd.isApplicationClassFounded();
if (showAllLogs) {
System.out.println("isApplicationClassFounded -> " + isApplicationClassFounded + "the dexPath is " +
dexPath);
}
return isApplicationClassFounded;
}
使用dex2jar修改完Apk的Application類之後,得到的是一個jar文件,再通過jar2dex工具轉爲dex文件:
private void jar2DexCmd(String jarFilePath, String dexOutPath) {
Jar2Dex cmd = new Jar2Dex();
String[] args = new String[]{
jarFilePath,
"-o",
dexOutPath
};
cmd.doMain(args);
}
最後刪除生成的jar文件,新的dex文件就是完成代碼注入後的dex。
最後,將這些dex文件和so文件壓縮爲Apk文件,並簽名。
至此,完成Apk的篡改,並實現App啓動時,加載設備上已安裝的所有Xposed插件模塊。
總結
最後,歸納一下Xpatch破解App的整體流程:
- 利用Android Art Hook框架(whale或者SandHook),開發能夠加載Xposed模塊的Apk,並導出其中的dex和so文件;
- 修改dex2jar工具,以實現在dex轉換爲jar的過程中,查找App的主Application類,並在此類中插入一段靜態代碼塊,實現加載Xposed模塊;
- 將修改後的dex和加載Xposed模塊的dex和so文件一起打包簽名,從而完成代碼注入,實現Xposed模塊的加載。
歡迎掃二維碼,關注我的技術公衆號Android葵花寶典 ,獲取高質量的Android乾貨分享: