diff --git a/loader/patch/.gitignore b/loader/patch/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/loader/patch/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/loader/patch/build.gradle b/loader/patch/build.gradle new file mode 100644 index 0000000..e493c42 --- /dev/null +++ b/loader/patch/build.gradle @@ -0,0 +1,8 @@ +plugins { + id 'java-library' +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_7 + targetCompatibility = JavaVersion.VERSION_1_7 +} \ No newline at end of file diff --git a/loader/patch/libs/ManifestEditor-1.0.1.jar b/loader/patch/libs/ManifestEditor-1.0.1.jar new file mode 100644 index 0000000..e1664cc Binary files /dev/null and b/loader/patch/libs/ManifestEditor-1.0.1.jar differ diff --git a/loader/patch/libs/dex-tools-2.1-SNAPSHOT.jar b/loader/patch/libs/dex-tools-2.1-SNAPSHOT.jar new file mode 100644 index 0000000..0b780ac Binary files /dev/null and b/loader/patch/libs/dex-tools-2.1-SNAPSHOT.jar differ diff --git a/loader/patch/src/main/assets/android.keystore b/loader/patch/src/main/assets/android.keystore new file mode 100644 index 0000000..879c1dc Binary files /dev/null and b/loader/patch/src/main/assets/android.keystore differ diff --git a/loader/patch/src/main/assets/keystore b/loader/patch/src/main/assets/keystore new file mode 100644 index 0000000..67f1939 Binary files /dev/null and b/loader/patch/src/main/assets/keystore differ diff --git a/loader/patch/src/main/java/com/storm/wind/xpatch/MainCommand.java b/loader/patch/src/main/java/com/storm/wind/xpatch/MainCommand.java new file mode 100644 index 0000000..1653bd0 --- /dev/null +++ b/loader/patch/src/main/java/com/storm/wind/xpatch/MainCommand.java @@ -0,0 +1,287 @@ +package com.storm.wind.xpatch; + +import com.storm.wind.xpatch.base.BaseCommand; +import com.storm.wind.xpatch.task.ApkModifyTask; +import com.storm.wind.xpatch.task.BuildAndSignApkTask; +import com.storm.wind.xpatch.task.SaveApkSignatureTask; +import com.storm.wind.xpatch.task.SaveOriginalApplicationNameTask; +import com.storm.wind.xpatch.task.SoAndDexCopyTask; +import com.storm.wind.xpatch.util.FileUtils; +import com.storm.wind.xpatch.util.ManifestParser; +import com.wind.meditor.core.FileProcesser; +import com.wind.meditor.property.AttributeItem; +import com.wind.meditor.property.ModificationProperty; +import com.wind.meditor.utils.NodeValue; + +import java.io.File; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.regex.Pattern; + +import static org.apache.commons.io.FileUtils.copyFile; + +public class MainCommand extends BaseCommand { + + private String apkPath; + + private String unzipApkFilePath; + + @Opt(opt = "o", longOpt = "output", description = "output .apk file, default is " + + "$source_apk_dir/[file-name]-xposed-signed.apk", argName = "out-apk-file") + private String output; // 输出的apk文件的目录以及名称 + + @Opt(opt = "f", longOpt = "force", hasArg = false, description = "force overwrite") + private boolean forceOverwrite = false; + + @Opt(opt = "pn", longOpt = "proxyname", description = "special proxy app name with full dot path", argName = "proxy app name") + private String proxyname = "com.wind.xposed.entry.MMPApplication"; + + @Opt(opt = "c", longOpt = "crach", hasArg = false, + description = "disable craching the apk's signature.") + private boolean disableCrackSignature = false; + + // 使用dex文件中插入代码的方式修改apk,而不是默认的修改Manifest中Application name的方式 + @Opt(opt = "dex", longOpt = "dex", hasArg = false, description = "insert code into the dex file, not modify manifest application name attribute") + private boolean dexModificationMode = false; + + @Opt(opt = "pkg", longOpt = "packageName", description = "modify the apk package name", argName = "new package name") + private String newPackageName; + + @Opt(opt = "d", longOpt = "debuggable", description = "set 1 to make the app debuggable = true, " + + "set 0 to make the app debuggable = false", argName = "0 or 1") + private int debuggable = -1; // 0: debuggable = false 1: debuggable = true + + @Opt(opt = "vc", longOpt = "versionCode", description = "set the app version code", + argName = "new-version-code") + private int versionCode; + + @Opt(opt = "vn", longOpt = "versionName", description = "set the app version name", + argName = "new-version-name") + private String versionName; + + // 原来apk中dex文件的数量 + private int dexFileCount = 0; + + private static final String UNZIP_APK_FILE_NAME = "apk-unzip-files"; + + private List mXpatchTasks = new ArrayList<>(); + + public static void main(String... args) { + new MainCommand().doMain(args); + } + + private void fuckIfFail(boolean b) { + if (!b) { + throw new IllegalStateException("wtf", new Throwable("DUMPBT")); + } + } + + @Override + protected void doCommandLine() throws IOException { + if (remainingArgs.length != 1) { + if (remainingArgs.length == 0) { + System.out.println("Please choose one apk file you want to process. "); + } + if (remainingArgs.length > 1) { + System.out.println("This tool can only used with one apk file."); + } + usage(); + return; + } + + apkPath = remainingArgs[0]; + + File srcApkFile = new File(apkPath); + + if (!srcApkFile.exists()) { + System.out.println("The source apk file not exsit, please choose another one. " + + "current apk file is = " + apkPath); + return; + } + + String currentDir = new File(".").getAbsolutePath(); + System.out.println("currentDir: " + currentDir); + System.out.println("apkPath: " + apkPath); + + if (output == null || output.length() == 0) { + output = getBaseName(apkPath) + "-xposed-signed.apk"; + } + + File outputFile = new File(output); + if (outputFile.exists() && !forceOverwrite) { + System.err.println(output + " exists, use --force to overwrite"); + usage(); + return; + } + + String outputApkFileParentPath = outputFile.getParent(); + if (outputApkFileParentPath == null) { + String absPath = outputFile.getAbsolutePath(); + int index = absPath.lastIndexOf(File.separatorChar); + outputApkFileParentPath = absPath.substring(0, index); + } + + System.out.println("output apk path: " + output); + System.out.println("disableCrackSignature: " + disableCrackSignature); + + String apkFileName = getBaseName(srcApkFile); + + String tempFilePath = outputApkFileParentPath + File.separator + + currentTimeStr() + "-tmp" + File.separator; + + unzipApkFilePath = tempFilePath + apkFileName + "-" + UNZIP_APK_FILE_NAME + File.separator; + + System.out.println("outputApkFileParentPath: " + outputApkFileParentPath); + System.out.println("unzipApkFilePath = " + unzipApkFilePath); + + if (!disableCrackSignature) { + // save the apk original signature info, to support crach signature. + new SaveApkSignatureTask(apkPath, unzipApkFilePath).run(); + } + + long currentTime = System.currentTimeMillis(); + FileUtils.decompressZip(apkPath, unzipApkFilePath); + + System.out.println("decompress apk cost time: " + (System.currentTimeMillis() - currentTime) + "ms"); + + // Get the dex count in the apk zip file + dexFileCount = findDexFileCount(unzipApkFilePath); + + System.out.println("dexFileCount: " + dexFileCount); + + String manifestFilePath = unzipApkFilePath + "AndroidManifest.xml"; + + currentTime = System.currentTimeMillis(); + + // parse the app main application full name from the manifest file + ManifestParser.Pair pair = ManifestParser.parseManifestFile(manifestFilePath); + String applicationName = null; + if (pair != null && pair.applicationName != null) { + applicationName = pair.applicationName; + } + + System.out.println("Get application name cost time: " + (System.currentTimeMillis() - currentTime) + "ms"); + System.out.println("Get the application name: " + applicationName); + + // modify manifest + File manifestFile = new File(manifestFilePath); + String manifestFilePathNew = unzipApkFilePath + "AndroidManifest" + "-" + currentTimeStr() + ".xml"; + File manifestFileNew = new File(manifestFilePathNew); + fuckIfFail(manifestFile.renameTo(manifestFileNew)); + + modifyManifestFile(manifestFilePathNew, manifestFilePath, applicationName); + + // new manifest may not exist + if (manifestFile.exists() && manifestFile.length() > 0) { + fuckIfFail(manifestFileNew.delete()); + } else { + fuckIfFail(manifestFileNew.renameTo(manifestFile)); + } + + // save original main application name to asset file + if (isNotEmpty(applicationName)) { + mXpatchTasks.add(new SaveOriginalApplicationNameTask(applicationName, unzipApkFilePath)); + } + + // modify the apk dex file to make xposed can run in it + if (dexModificationMode && isNotEmpty(applicationName)) { + mXpatchTasks.add(new ApkModifyTask(true, true, unzipApkFilePath, applicationName, + dexFileCount)); + } + + // copy xposed so and dex files into the unzipped apk + mXpatchTasks.add(new SoAndDexCopyTask(dexFileCount, unzipApkFilePath)); + + // compress all files into an apk and then sign it. + mXpatchTasks.add(new BuildAndSignApkTask(true, unzipApkFilePath, output)); + + // copy origin apk to assets + // convenient to bypass some check like CRC + copyFile(srcApkFile, new File(unzipApkFilePath, "assets/origin_apk.bin")); + + // excute these tasks + for (Runnable executor : mXpatchTasks) { + currentTime = System.currentTimeMillis(); + executor.run(); + + System.out.println(executor.getClass().getSimpleName() + " cost time: " + + (System.currentTimeMillis() - currentTime) + "ms"); + } + + System.out.println("Output APK: " + output); + } + + private void modifyManifestFile(String filePath, String dstFilePath, String originalApplicationName) { + ModificationProperty property = new ModificationProperty(); + boolean modifyEnabled = false; + if (isNotEmpty(newPackageName)) { + modifyEnabled = true; + property.addManifestAttribute(new AttributeItem(NodeValue.Manifest.PACKAGE, newPackageName).setNamespace(null)); + } + + if (versionCode > 0) { + modifyEnabled = true; + property.addManifestAttribute(new AttributeItem(NodeValue.Manifest.VERSION_CODE, versionCode)); + } + + if (isNotEmpty(versionName)) { + modifyEnabled = true; + property.addManifestAttribute(new AttributeItem(NodeValue.Manifest.VERSION_NAME, versionName)); + } + + if (debuggable >= 0) { + modifyEnabled = true; + property.addApplicationAttribute(new AttributeItem(NodeValue.Application.DEBUGGABLE, debuggable != 0)); + } + + property.addApplicationAttribute(new AttributeItem("extractNativeLibs", true)); + + if (!dexModificationMode || !isNotEmpty(originalApplicationName)) { + modifyEnabled = true; + property.addApplicationAttribute(new AttributeItem(NodeValue.Application.NAME, proxyname)); + } + + if (modifyEnabled) { + FileProcesser.processManifestFile(filePath, dstFilePath, property); + } + } + + private int findDexFileCount(String unzipApkFilePath) { + File zipfileRoot = new File(unzipApkFilePath); + if (!zipfileRoot.exists()) { + return 0; + } + File[] childFiles = zipfileRoot.listFiles(); + if (childFiles == null || childFiles.length == 0) { + return 0; + } + int count = 0; + for (File file : childFiles) { + String fileName = file.getName(); + if (Pattern.matches("classes.*\\.dex", fileName)) { + count++; + } + } + return count; + } + + // Use the current timestamp as the name of the build file + private String currentTimeStr() { + SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss"); + return df.format(new Date()); + } + + private String[] getXposedModules(String modules) { + if (modules == null || modules.isEmpty()) { + return null; + } + return modules.split(File.pathSeparator); + } + + private static boolean isNotEmpty(String str) { + return str != null && !str.isEmpty(); + } +} diff --git a/loader/patch/src/main/java/com/storm/wind/xpatch/base/BaseCommand.java b/loader/patch/src/main/java/com/storm/wind/xpatch/base/BaseCommand.java new file mode 100644 index 0000000..2ead0d2 --- /dev/null +++ b/loader/patch/src/main/java/com/storm/wind/xpatch/base/BaseCommand.java @@ -0,0 +1,478 @@ +package com.storm.wind.xpatch.base; + +import java.io.File; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Field; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +/** + * Created by Wind + */ +public abstract class BaseCommand { + + private String onlineHelp; + + protected Map optMap = new HashMap(); + + @Opt(opt = "h", longOpt = "help", hasArg = false, description = "Print this help message") + private boolean printHelp = false; + + protected String[] remainingArgs; + protected String[] orginalArgs; + + @Retention(value = RetentionPolicy.RUNTIME) + @Target(value = { ElementType.FIELD }) + static public @interface Opt { + String argName() default ""; + + String description() default ""; + + boolean hasArg() default true; + + String longOpt() default ""; + + String opt() default ""; + + boolean required() default false; + } + + static protected class Option implements Comparable