From 2b0a830c5542fa8315c1a8528b12a20f21286fc8 Mon Sep 17 00:00:00 2001 From: pengc Date: Fri, 18 Jun 2021 17:06:40 +0800 Subject: [PATCH] use apkzlib manipulate apk --- patch/build.gradle | 5 +- .../main/java/org/lsposed/patch/LSPatch.java | 165 +++++++++++------- .../patch/task/BuildAndSignApkTask.java | 37 ++-- .../patch/task/SaveApkSignatureTask.java | 45 ----- .../task/SaveOriginalApplicationNameTask.java | 37 ---- .../lsposed/patch/task/SoAndDexCopyTask.java | 132 -------------- 6 files changed, 121 insertions(+), 300 deletions(-) delete mode 100644 patch/src/main/java/org/lsposed/patch/task/SaveApkSignatureTask.java delete mode 100644 patch/src/main/java/org/lsposed/patch/task/SaveOriginalApplicationNameTask.java delete mode 100644 patch/src/main/java/org/lsposed/patch/task/SoAndDexCopyTask.java diff --git a/patch/build.gradle b/patch/build.gradle index bc7cf8f..9ffe55a 100644 --- a/patch/build.gradle +++ b/patch/build.gradle @@ -12,12 +12,11 @@ compileJava.options.encoding = "UTF-8" dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation project(':axmlprinter') - //implementation project(':apksigner') - implementation group: 'commons-io', name: 'commons-io', version: '2.8.0' implementation project(':share') + implementation 'commons-io:commons-io:2.10.0' + implementation 'com.android.tools.build:apkzlib:4.2.1' } -sourceSets.main.java.srcDirs += "$rootProject.projectDir/apksigner/src/main/java" sourceSets.main.java.srcDirs += "$rootProject.projectDir/apksigner/src/apksigner/java" jar { diff --git a/patch/src/main/java/org/lsposed/patch/LSPatch.java b/patch/src/main/java/org/lsposed/patch/LSPatch.java index a485b14..fcee9f1 100644 --- a/patch/src/main/java/org/lsposed/patch/LSPatch.java +++ b/patch/src/main/java/org/lsposed/patch/LSPatch.java @@ -3,26 +3,34 @@ package org.lsposed.patch; import static org.apache.commons.io.FileUtils.copyDirectory; import static org.apache.commons.io.FileUtils.copyFile; +import com.android.tools.build.apkzlib.zip.StoredEntry; +import com.android.tools.build.apkzlib.zip.ZFile; 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 org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; import org.lsposed.lspatch.share.Constants; import org.lsposed.patch.base.BaseCommand; import org.lsposed.patch.task.BuildAndSignApkTask; -import org.lsposed.patch.task.SaveApkSignatureTask; -import org.lsposed.patch.task.SaveOriginalApplicationNameTask; -import org.lsposed.patch.task.SoAndDexCopyTask; +import org.lsposed.patch.util.ApkSignatureHelper; import org.lsposed.patch.util.ZipUtils; import org.lsposed.patch.util.ManifestParser; import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FilenameFilter; import java.io.IOException; import java.nio.charset.Charset; import java.text.SimpleDateFormat; +import java.util.ArrayList; import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Set; import java.util.regex.Pattern; public class LSPatch extends BaseCommand { @@ -50,6 +58,15 @@ public class LSPatch extends BaseCommand { private int dexFileCount = 0; private static final String UNZIP_APK_FILE_NAME = "apk-unzip-files"; + private static final String APPLICATION_NAME_ASSET_PATH = "assets/original_application_name.ini"; + private final static String SIGNATURE_INFO_ASSET_PATH = "assets/original_signature_info.ini"; + private static final String[] APK_LIB_PATH_ARRAY = { + "lib/armeabi-v7a", + "lib/armeabi", + "lib/arm64-v8a", + "lib/x86", + "lib/x86_64" + }; public static void main(String... args) { new LSPatch().doMain(args); @@ -85,12 +102,18 @@ public class LSPatch extends BaseCommand { return; } + File finalApk = new File(String.format("%s-%s-unsigned.apk", getBaseName(srcApkFile.getAbsolutePath()), + Constants.CONFIG_NAME_SIGBYPASSLV + sigbypassLevel)); + FileUtils.copyFile(srcApkFile, finalApk); + + ZFile zFile = new ZFile(finalApk); + String currentDir = new File(".").getAbsolutePath(); System.out.println("currentDir: " + currentDir); System.out.println("apkPath: " + apkPath); if (outputPath == null || outputPath.length() == 0) { - String sig = "sigbypasslv" + sigbypassLevel; + String sig = Constants.CONFIG_NAME_SIGBYPASSLV + sigbypassLevel; outputPath = String.format("%s-%s-xposed-signed.apk", getBaseName(apkPath), sig); } @@ -108,8 +131,6 @@ public class LSPatch extends BaseCommand { outputApkFileParentPath = absPath.substring(0, index); } - System.out.println("output apk path: " + outputPath); - String apkFileName = getBaseName(srcApkFile); String tempFilePath = outputApkFileParentPath + File.separator + @@ -117,25 +138,26 @@ public class LSPatch extends BaseCommand { unzipApkFilePath = tempFilePath + apkFileName + "-" + UNZIP_APK_FILE_NAME + File.separator; - System.out.println("outputApkFileParentPath: " + outputApkFileParentPath); - System.out.println("unzipApkFilePath = " + unzipApkFilePath); - // save the apk original signature info, to support crach signature. - new SaveApkSignatureTask(apkPath, unzipApkFilePath).run(); + String originalSignature = ApkSignatureHelper.getApkSignInfo(apkPath); + if (originalSignature == null || originalSignature.isEmpty()) { + throw new IllegalStateException("get original signature failed"); + } + File osi = new File((unzipApkFilePath + SIGNATURE_INFO_ASSET_PATH).replace("/", File.separator)); + FileUtils.write(osi, originalSignature, Charset.defaultCharset()); + zFile.add(SIGNATURE_INFO_ASSET_PATH, new FileInputStream(osi)); - long currentTime = System.currentTimeMillis(); - ZipUtils.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); + // get the dex count in the apk zip file + dexFileCount = findDexFileCount(zFile); System.out.println("dexFileCount: " + dexFileCount); - String manifestFilePath = unzipApkFilePath + "AndroidManifest.xml"; - - currentTime = System.currentTimeMillis(); + // copy out manifest file from zlib + int copySize = IOUtils.copy(zFile.get("AndroidManifest.xml").open(), new FileOutputStream(unzipApkFilePath + "AndroidManifest.xml.bak")); + if (copySize <= 0) { + throw new IllegalStateException("wtf"); + } + String manifestFilePath = unzipApkFilePath + "AndroidManifest.xml.bak"; // parse the app main application full name from the manifest file ManifestParser.Pair pair = ManifestParser.parseManifestFile(manifestFilePath); @@ -147,31 +169,60 @@ public class LSPatch extends BaseCommand { System.out.println("original 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); - - // new manifest may not exist - if (manifestFile.exists() && manifestFile.length() > 0) { - fuckIfFail(manifestFileNew.delete()); - } - else { - fuckIfFail(manifestFileNew.renameTo(manifestFile)); - } + modifyManifestFile(manifestFilePath, new File(unzipApkFilePath, "AndroidManifest.xml").getPath()); // save original main application name to asset file even its empty - new SaveOriginalApplicationNameTask(applicationName, unzipApkFilePath).run(); + File oan = new File((unzipApkFilePath + APPLICATION_NAME_ASSET_PATH).replace("/", File.separator)); + FileUtils.write(oan, applicationName, Charset.defaultCharset()); + zFile.add(APPLICATION_NAME_ASSET_PATH, new FileInputStream(oan)); // copy so and dex files into the unzipped apk - new SoAndDexCopyTask(dexFileCount, unzipApkFilePath).run(); + Set apkArchs = new HashSet<>(); + + System.out.println("search target apk library arch.."); + for (StoredEntry storedEntry : zFile.entries()) { + for (String arch : APK_LIB_PATH_ARRAY) { + if (storedEntry.getCentralDirectoryHeader().getName().startsWith(arch)) { + apkArchs.add(arch); + } + } + } + + if (apkArchs.isEmpty()) { + apkArchs.add(APK_LIB_PATH_ARRAY[0]); + } + + for (String arch : apkArchs) { + // lib/armeabi-v7a -> armeabi-v7a + String justArch = arch.substring(arch.indexOf('/')); + File sod = new File("list-so", justArch); + File[] files = sod.listFiles(); + if (files == null) { + System.out.println("Warning: Nothing so file has been copied in " + sod.getPath()); + continue; + } + for (File file : files) { + zFile.add(arch + "/" + file.getName(), new FileInputStream(file)); + System.out.println("add " + file.getPath()); + } + } + + // copy all dex files in list-dex + File[] files = new File("list-dex").listFiles(); + if (files == null || files.length == 0) { + System.out.println("Warning: Nothing dex file has been copied"); + return; + } + for (File file : files) { + String copiedDexFileName = "classes" + (dexFileCount + 1) + ".dex"; + zFile.add(copiedDexFileName, new FileInputStream(file)); + dexFileCount++; + } // copy origin apk to assets // convenient to bypass some check like CRC - if (sigbypassLevel >= Constants.SIGBYPASS_LV_PM) { - copyFile(srcApkFile, new File(unzipApkFilePath, "assets/origin_apk.bin")); + if (sigbypassLevel >= Constants.SIGBYPASS_LV_PM_OPENAT) { + zFile.add("assets/origin_apk.bin", new FileInputStream(srcApkFile)); } File[] listAssets = new File("list-assets").listFiles(); @@ -179,16 +230,23 @@ public class LSPatch extends BaseCommand { System.out.println("Warning: No assets file copyied"); } else { - copyDirectory(new File("list-assets"), new File(unzipApkFilePath, "assets")); + for (File f : listAssets) { + if (f.isDirectory()) { + throw new IllegalStateException("unsupport directory in assets"); + } + zFile.add("assets/" + f.getName(), new FileInputStream(f)); + } } // save lspatch config to asset.. - FileUtils.write(new File(unzipApkFilePath, "assets" + File.separator + Constants.CONFIG_NAME_SIGBYPASSLV + sigbypassLevel), "lspatch", - Charset.defaultCharset()); + File sl = new File(unzipApkFilePath, "tmp"); + FileUtils.write(sl, "42", Charset.defaultCharset()); + zFile.add("assets/" + Constants.CONFIG_NAME_SIGBYPASSLV + sigbypassLevel, new FileInputStream(sl)); - // compress all files into an apk and then sign it. - new BuildAndSignApkTask(true, unzipApkFilePath, outputPath).run(); + zFile.update(); + zFile.close(); + new BuildAndSignApkTask(true, unzipApkFilePath, finalApk.getAbsolutePath(), outputPath).run(); System.out.println("Output APK: " + outputPath); } @@ -205,23 +263,12 @@ public class LSPatch extends BaseCommand { FileProcesser.processManifestFile(filePath, dstFilePath, property); } - private int findDexFileCount(String unzipApkFilePath) { - File zipfileRoot = new File(unzipApkFilePath); - if (!zipfileRoot.exists()) { - return 0; + private int findDexFileCount(ZFile zFile) { + for (int i = 2; i < 30; i++) { + if (zFile.get("classes" + i + ".dex") == null) + return i - 1; } - 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; + throw new IllegalStateException("wtf"); } // Use the current timestamp as the name of the build file diff --git a/patch/src/main/java/org/lsposed/patch/task/BuildAndSignApkTask.java b/patch/src/main/java/org/lsposed/patch/task/BuildAndSignApkTask.java index 3116a0f..e7592d3 100644 --- a/patch/src/main/java/org/lsposed/patch/task/BuildAndSignApkTask.java +++ b/patch/src/main/java/org/lsposed/patch/task/BuildAndSignApkTask.java @@ -2,6 +2,8 @@ package org.lsposed.patch.task; import com.android.apksigner.ApkSignerTool; +import com.android.tools.build.apkzlib.zip.ZFile; + import org.apache.commons.io.IOUtils; import org.lsposed.patch.LSPatch; import org.lsposed.patch.util.ZipUtils; @@ -17,14 +19,14 @@ import java.util.ArrayList; */ public class BuildAndSignApkTask implements Runnable { - private boolean keepUnsignedApkFile; + private final boolean keepUnsignedApkFile; + private final String signedApkPath; + private final String unzipApkFilePath; + private final String unsignedApkPath; - private String signedApkPath; - - private String unzipApkFilePath; - - public BuildAndSignApkTask(boolean keepUnsignedApkFile, String unzipApkFilePath, String signedApkPath) { + public BuildAndSignApkTask(boolean keepUnsignedApkFile, String unzipApkFilePath, String unsignedApkPath, String signedApkPath) { this.keepUnsignedApkFile = keepUnsignedApkFile; + this.unsignedApkPath = unsignedApkPath; this.unzipApkFilePath = unzipApkFilePath; this.signedApkPath = signedApkPath; } @@ -33,43 +35,30 @@ public class BuildAndSignApkTask implements Runnable { public void run() { try { - File unzipApkFile = new File(unzipApkFilePath); - - String unsignedApkPath = unzipApkFile.getParent() + File.separator + "unsigned.apk"; - ZipUtils.compressToZip(unzipApkFilePath, unsignedApkPath); - - String keyStoreFilePath = unzipApkFile.getParent() + File.separator + "keystore"; - - File keyStoreFile = new File(keyStoreFilePath); - // assets/keystore分隔符不能使用File.separator,否则在windows上抛出IOException !!! + File unzipApkPathFile = new File(unzipApkFilePath); + File keyStoreFile = new File(unzipApkPathFile, "keystore"); String keyStoreAssetPath; if (isAndroid()) { - // BKS-V1 类型 keyStoreAssetPath = "assets/android.keystore"; } else { - // BKS 类型 keyStoreAssetPath = "assets/keystore"; } try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(keyStoreAssetPath); - FileOutputStream out = new FileOutputStream(keyStoreFilePath)) { + FileOutputStream out = new FileOutputStream(keyStoreFile)) { IOUtils.copy(inputStream, out); } - boolean signResult = signApk(unsignedApkPath, keyStoreFilePath, signedApkPath); + boolean signResult = signApk(unsignedApkPath, keyStoreFile.getAbsolutePath(), signedApkPath); File unsignedApkFile = new File(unsignedApkPath); File signedApkFile = new File(signedApkPath); + // delete unsigned apk file if (!keepUnsignedApkFile && unsignedApkFile.exists() && signedApkFile.exists() && signResult) { LSPatch.fuckIfFail(unsignedApkFile.delete()); } - - // delete the keystore file - if (keyStoreFile.exists()) { - LSPatch.fuckIfFail(keyStoreFile.delete()); - } } catch (Exception err) { throw new IllegalStateException("wtf", err); diff --git a/patch/src/main/java/org/lsposed/patch/task/SaveApkSignatureTask.java b/patch/src/main/java/org/lsposed/patch/task/SaveApkSignatureTask.java deleted file mode 100644 index 0976ab7..0000000 --- a/patch/src/main/java/org/lsposed/patch/task/SaveApkSignatureTask.java +++ /dev/null @@ -1,45 +0,0 @@ -package org.lsposed.patch.task; - -import org.lsposed.patch.util.ApkSignatureHelper; - -import org.apache.commons.io.FileUtils; - -import java.io.File; -import java.nio.charset.StandardCharsets; - -/** - * Created by Wind - */ -public class SaveApkSignatureTask implements Runnable { - - private String apkPath; - private String dstFilePath; - - private final static String SIGNATURE_INFO_ASSET_PATH = "assets/original_signature_info.ini"; - - public SaveApkSignatureTask(String apkPath, String unzipApkFilePath) { - this.apkPath = apkPath; - this.dstFilePath = (unzipApkFilePath + SIGNATURE_INFO_ASSET_PATH).replace("/", File.separator); - } - - @Override - public void run() { - // First, get the original signature - String originalSignature = ApkSignatureHelper.getApkSignInfo(apkPath); - if (originalSignature == null || originalSignature.isEmpty()) { - System.out.println("Get original signature failed"); - return; - } - - // Then, save the signature chars to the asset file - File file = new File(dstFilePath); - try { - FileUtils.write(file, originalSignature, StandardCharsets.UTF_8); - } - catch (Exception err) { - // just crash now - // todo: pass result to caller - throw new IllegalStateException("wtf", err); - } - } -} diff --git a/patch/src/main/java/org/lsposed/patch/task/SaveOriginalApplicationNameTask.java b/patch/src/main/java/org/lsposed/patch/task/SaveOriginalApplicationNameTask.java deleted file mode 100644 index 0b72f06..0000000 --- a/patch/src/main/java/org/lsposed/patch/task/SaveOriginalApplicationNameTask.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.lsposed.patch.task; - -import org.apache.commons.io.FileUtils; - -import java.io.File; -import java.nio.charset.Charset; - -/** - * Created by xiawanli on 2019/4/6 - */ -public class SaveOriginalApplicationNameTask implements Runnable { - - private final String applcationName; - private final String unzipApkFilePath; - private String dstFilePath; - - private final String APPLICATION_NAME_ASSET_PATH = "assets/original_application_name.ini"; - - public SaveOriginalApplicationNameTask(String applicationName, String unzipApkFilePath) { - this.applcationName = applicationName; - this.unzipApkFilePath = unzipApkFilePath; - - this.dstFilePath = (unzipApkFilePath + APPLICATION_NAME_ASSET_PATH).replace("/", File.separator); - } - - @Override - public void run() { - try { - FileUtils.write(new File(dstFilePath), applcationName, Charset.defaultCharset()); - } - catch (Exception err) { - // just crash - // todo: pass result to caller - throw new IllegalStateException("wtf", err); - } - } -} \ No newline at end of file diff --git a/patch/src/main/java/org/lsposed/patch/task/SoAndDexCopyTask.java b/patch/src/main/java/org/lsposed/patch/task/SoAndDexCopyTask.java deleted file mode 100644 index 181b8a6..0000000 --- a/patch/src/main/java/org/lsposed/patch/task/SoAndDexCopyTask.java +++ /dev/null @@ -1,132 +0,0 @@ -package org.lsposed.patch.task; - -import org.apache.commons.io.FileUtils; - -import java.io.File; -import java.util.ArrayList; -import java.util.List; - -/** - * Created by Wind - */ -public class SoAndDexCopyTask implements Runnable { - - private final String[] APK_LIB_PATH_ARRAY = { - "lib/armeabi-v7a/", - "lib/armeabi/", - "lib/arm64-v8a/", - "lib/x86", - "lib/x86_64" - }; - - private int dexFileCount; - private String unzipApkFilePath; - - public SoAndDexCopyTask(int dexFileCount, String unzipApkFilePath) { - this.dexFileCount = dexFileCount; - this.unzipApkFilePath = unzipApkFilePath; - } - - @Override - public void run() { - // 复制xposed兼容层的dex文件以及so文件到当前目录下 - copySoFile(); - copyDexFile(dexFileCount); - - // 删除签名信息 - deleteMetaInfo(); - } - - private void copySoFile() { - List existLibPathArray = new ArrayList<>(); - for (String libPath : APK_LIB_PATH_ARRAY) { - String apkSoFullPath = fullLibPath(libPath); - File apkSoFullPathFile = new File(apkSoFullPath); - if (apkSoFullPathFile.exists()) { - existLibPathArray.add(libPath); - } else { - System.out.println("Target app dont have " + libPath + ", skip"); - } - } - - if (existLibPathArray.isEmpty()) { - System.out.println("Target app dont have any so in \"lib/\" dir, so create default \"armeabi-v7a\""); - String libPath = APK_LIB_PATH_ARRAY[0]; - String apkSoFullPath = fullLibPath(libPath); - File apkSoFullPathFile = new File(apkSoFullPath); - if (!apkSoFullPathFile.mkdirs()) { - throw new IllegalStateException("mkdir fail " + apkSoFullPathFile.getAbsolutePath()); - } - existLibPathArray.add(libPath); - } - - for (String libPath : existLibPathArray) { - if (libPath == null || libPath.isEmpty()) { - throw new IllegalStateException("fail eabi path"); - } - - String apkSoFullPath = fullLibPath(libPath); - String eabi = libPath.substring(libPath.indexOf("/")); - if (eabi.isEmpty()) { - throw new IllegalStateException("fail find eabi in " + libPath); - } - - File[] files = new File("list-so", eabi).listFiles(); - if (files == null) { - System.out.println("Warning: Nothing so file has been copied in " + libPath); - continue; - } - for (File mySoFile : files) { - File target = new File(apkSoFullPath, mySoFile.getName()); - try { - FileUtils.copyFile(mySoFile, target); - } catch (Exception err) { - throw new IllegalStateException("wtf", err); - } - System.out.println("Copy " + mySoFile.getAbsolutePath() + " to " + target.getAbsolutePath()); - } - } - } - - private void copyDexFile(int dexFileCount) { - try { - // copy all dex files in list-dex - File[] files = new File("list-dex").listFiles(); - if (files == null || files.length == 0) { - System.out.println("Warning: Nothing dex file has been copied"); - return; - } - for (File file : files) { - String copiedDexFileName = "classes" + (dexFileCount + 1) + ".dex"; - File target = new File(unzipApkFilePath, copiedDexFileName); - FileUtils.copyFile(file, target); - System.out.println("Copy " + file.getAbsolutePath() + " to " + target.getAbsolutePath()); - dexFileCount++; - } - } catch (Exception err) { - throw new IllegalStateException("wtf", err); - } - } - - private String fullLibPath(String libPath) { - return unzipApkFilePath + libPath.replace("/", File.separator); - } - - 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(); - } - } - } -}