use apkzlib manipulate apk

This commit is contained in:
pengc 2021-06-18 17:06:40 +08:00
parent e3c998b36c
commit 2b0a830c55
6 changed files with 121 additions and 300 deletions

View File

@ -12,12 +12,11 @@ compileJava.options.encoding = "UTF-8"
dependencies { dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(':axmlprinter') implementation project(':axmlprinter')
//implementation project(':apksigner')
implementation group: 'commons-io', name: 'commons-io', version: '2.8.0'
implementation project(':share') 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" sourceSets.main.java.srcDirs += "$rootProject.projectDir/apksigner/src/apksigner/java"
jar { jar {

View File

@ -3,26 +3,34 @@ package org.lsposed.patch;
import static org.apache.commons.io.FileUtils.copyDirectory; import static org.apache.commons.io.FileUtils.copyDirectory;
import static org.apache.commons.io.FileUtils.copyFile; 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.core.FileProcesser;
import com.wind.meditor.property.AttributeItem; import com.wind.meditor.property.AttributeItem;
import com.wind.meditor.property.ModificationProperty; import com.wind.meditor.property.ModificationProperty;
import com.wind.meditor.utils.NodeValue; import com.wind.meditor.utils.NodeValue;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.lsposed.lspatch.share.Constants; import org.lsposed.lspatch.share.Constants;
import org.lsposed.patch.base.BaseCommand; import org.lsposed.patch.base.BaseCommand;
import org.lsposed.patch.task.BuildAndSignApkTask; import org.lsposed.patch.task.BuildAndSignApkTask;
import org.lsposed.patch.task.SaveApkSignatureTask; import org.lsposed.patch.util.ApkSignatureHelper;
import org.lsposed.patch.task.SaveOriginalApplicationNameTask;
import org.lsposed.patch.task.SoAndDexCopyTask;
import org.lsposed.patch.util.ZipUtils; import org.lsposed.patch.util.ZipUtils;
import org.lsposed.patch.util.ManifestParser; import org.lsposed.patch.util.ManifestParser;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern; import java.util.regex.Pattern;
public class LSPatch extends BaseCommand { public class LSPatch extends BaseCommand {
@ -50,6 +58,15 @@ public class LSPatch extends BaseCommand {
private int dexFileCount = 0; private int dexFileCount = 0;
private static final String UNZIP_APK_FILE_NAME = "apk-unzip-files"; 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) { public static void main(String... args) {
new LSPatch().doMain(args); new LSPatch().doMain(args);
@ -85,12 +102,18 @@ public class LSPatch extends BaseCommand {
return; 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(); String currentDir = new File(".").getAbsolutePath();
System.out.println("currentDir: " + currentDir); System.out.println("currentDir: " + currentDir);
System.out.println("apkPath: " + apkPath); System.out.println("apkPath: " + apkPath);
if (outputPath == null || outputPath.length() == 0) { 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); 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); outputApkFileParentPath = absPath.substring(0, index);
} }
System.out.println("output apk path: " + outputPath);
String apkFileName = getBaseName(srcApkFile); String apkFileName = getBaseName(srcApkFile);
String tempFilePath = outputApkFileParentPath + File.separator + String tempFilePath = outputApkFileParentPath + File.separator +
@ -117,25 +138,26 @@ public class LSPatch extends BaseCommand {
unzipApkFilePath = tempFilePath + apkFileName + "-" + UNZIP_APK_FILE_NAME + File.separator; 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. // 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(); // get the dex count in the apk zip file
ZipUtils.decompressZip(apkPath, unzipApkFilePath); dexFileCount = findDexFileCount(zFile);
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); System.out.println("dexFileCount: " + dexFileCount);
String manifestFilePath = unzipApkFilePath + "AndroidManifest.xml"; // copy out manifest file from zlib
int copySize = IOUtils.copy(zFile.get("AndroidManifest.xml").open(), new FileOutputStream(unzipApkFilePath + "AndroidManifest.xml.bak"));
currentTime = System.currentTimeMillis(); if (copySize <= 0) {
throw new IllegalStateException("wtf");
}
String manifestFilePath = unzipApkFilePath + "AndroidManifest.xml.bak";
// parse the app main application full name from the manifest file // parse the app main application full name from the manifest file
ManifestParser.Pair pair = ManifestParser.parseManifestFile(manifestFilePath); ManifestParser.Pair pair = ManifestParser.parseManifestFile(manifestFilePath);
@ -147,31 +169,60 @@ public class LSPatch extends BaseCommand {
System.out.println("original application name: " + applicationName); System.out.println("original application name: " + applicationName);
// modify manifest // modify manifest
File manifestFile = new File(manifestFilePath); modifyManifestFile(manifestFilePath, new File(unzipApkFilePath, "AndroidManifest.xml").getPath());
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));
}
// save original main application name to asset file even its empty // 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 // copy so and dex files into the unzipped apk
new SoAndDexCopyTask(dexFileCount, unzipApkFilePath).run(); Set<String> 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 // copy origin apk to assets
// convenient to bypass some check like CRC // convenient to bypass some check like CRC
if (sigbypassLevel >= Constants.SIGBYPASS_LV_PM) { if (sigbypassLevel >= Constants.SIGBYPASS_LV_PM_OPENAT) {
copyFile(srcApkFile, new File(unzipApkFilePath, "assets/origin_apk.bin")); zFile.add("assets/origin_apk.bin", new FileInputStream(srcApkFile));
} }
File[] listAssets = new File("list-assets").listFiles(); File[] listAssets = new File("list-assets").listFiles();
@ -179,16 +230,23 @@ public class LSPatch extends BaseCommand {
System.out.println("Warning: No assets file copyied"); System.out.println("Warning: No assets file copyied");
} }
else { 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.. // save lspatch config to asset..
FileUtils.write(new File(unzipApkFilePath, "assets" + File.separator + Constants.CONFIG_NAME_SIGBYPASSLV + sigbypassLevel), "lspatch", File sl = new File(unzipApkFilePath, "tmp");
Charset.defaultCharset()); 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. zFile.update();
new BuildAndSignApkTask(true, unzipApkFilePath, outputPath).run(); zFile.close();
new BuildAndSignApkTask(true, unzipApkFilePath, finalApk.getAbsolutePath(), outputPath).run();
System.out.println("Output APK: " + outputPath); System.out.println("Output APK: " + outputPath);
} }
@ -205,23 +263,12 @@ public class LSPatch extends BaseCommand {
FileProcesser.processManifestFile(filePath, dstFilePath, property); FileProcesser.processManifestFile(filePath, dstFilePath, property);
} }
private int findDexFileCount(String unzipApkFilePath) { private int findDexFileCount(ZFile zFile) {
File zipfileRoot = new File(unzipApkFilePath); for (int i = 2; i < 30; i++) {
if (!zipfileRoot.exists()) { if (zFile.get("classes" + i + ".dex") == null)
return 0; return i - 1;
} }
File[] childFiles = zipfileRoot.listFiles(); throw new IllegalStateException("wtf");
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 // Use the current timestamp as the name of the build file

View File

@ -2,6 +2,8 @@ package org.lsposed.patch.task;
import com.android.apksigner.ApkSignerTool; import com.android.apksigner.ApkSignerTool;
import com.android.tools.build.apkzlib.zip.ZFile;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.lsposed.patch.LSPatch; import org.lsposed.patch.LSPatch;
import org.lsposed.patch.util.ZipUtils; import org.lsposed.patch.util.ZipUtils;
@ -17,14 +19,14 @@ import java.util.ArrayList;
*/ */
public class BuildAndSignApkTask implements Runnable { 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; public BuildAndSignApkTask(boolean keepUnsignedApkFile, String unzipApkFilePath, String unsignedApkPath, String signedApkPath) {
private String unzipApkFilePath;
public BuildAndSignApkTask(boolean keepUnsignedApkFile, String unzipApkFilePath, String signedApkPath) {
this.keepUnsignedApkFile = keepUnsignedApkFile; this.keepUnsignedApkFile = keepUnsignedApkFile;
this.unsignedApkPath = unsignedApkPath;
this.unzipApkFilePath = unzipApkFilePath; this.unzipApkFilePath = unzipApkFilePath;
this.signedApkPath = signedApkPath; this.signedApkPath = signedApkPath;
} }
@ -33,43 +35,30 @@ public class BuildAndSignApkTask implements Runnable {
public void run() { public void run() {
try { try {
File unzipApkFile = new File(unzipApkFilePath); File unzipApkPathFile = new File(unzipApkFilePath);
File keyStoreFile = new File(unzipApkPathFile, "keystore");
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 !!!
String keyStoreAssetPath; String keyStoreAssetPath;
if (isAndroid()) { if (isAndroid()) {
// BKS-V1 类型
keyStoreAssetPath = "assets/android.keystore"; keyStoreAssetPath = "assets/android.keystore";
} }
else { else {
// BKS 类型
keyStoreAssetPath = "assets/keystore"; keyStoreAssetPath = "assets/keystore";
} }
try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(keyStoreAssetPath); try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(keyStoreAssetPath);
FileOutputStream out = new FileOutputStream(keyStoreFilePath)) { FileOutputStream out = new FileOutputStream(keyStoreFile)) {
IOUtils.copy(inputStream, out); IOUtils.copy(inputStream, out);
} }
boolean signResult = signApk(unsignedApkPath, keyStoreFilePath, signedApkPath); boolean signResult = signApk(unsignedApkPath, keyStoreFile.getAbsolutePath(), signedApkPath);
File unsignedApkFile = new File(unsignedApkPath); File unsignedApkFile = new File(unsignedApkPath);
File signedApkFile = new File(signedApkPath); File signedApkFile = new File(signedApkPath);
// delete unsigned apk file // delete unsigned apk file
if (!keepUnsignedApkFile && unsignedApkFile.exists() && signedApkFile.exists() && signResult) { if (!keepUnsignedApkFile && unsignedApkFile.exists() && signedApkFile.exists() && signResult) {
LSPatch.fuckIfFail(unsignedApkFile.delete()); LSPatch.fuckIfFail(unsignedApkFile.delete());
} }
// delete the keystore file
if (keyStoreFile.exists()) {
LSPatch.fuckIfFail(keyStoreFile.delete());
}
} }
catch (Exception err) { catch (Exception err) {
throw new IllegalStateException("wtf", err); throw new IllegalStateException("wtf", err);

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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<String> 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();
}
}
}
}