Use tmpdir

This commit is contained in:
LoveSy 2021-06-19 11:57:09 +08:00
parent 2fb8750c75
commit 6a1303f4d6
4 changed files with 228 additions and 274 deletions

View File

@ -1,47 +1,49 @@
package org.lsposed.patch;
import static org.apache.commons.io.FileUtils.copyDirectory;
import static org.apache.commons.io.FileUtils.copyFile;
import com.android.apksigner.ApkSignerTool;
import com.android.tools.build.apkzlib.zip.StoredEntry;
import com.android.tools.build.apkzlib.zip.ZFile;
import com.beust.jcommander.JCommander;
import com.beust.jcommander.Parameter;
import com.wind.meditor.base.BaseCommand;
import com.wind.meditor.core.FileProcesser;
import com.wind.meditor.core.ManifestEditor;
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.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.lsposed.lspatch.share.Constants;
import org.lsposed.patch.task.BuildAndSignApkTask;
import org.lsposed.patch.util.ApkSignatureHelper;
import org.lsposed.patch.util.ManifestParser;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
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.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
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 {
@Parameter(description = "apk path")
private String apkPath;
static class PatchError extends Error {
PatchError(String message) {
super(message);
}
}
private String unzipApkFilePath;
@Parameter(description = "apk")
private String apkPath = null;
@Parameter(names = "--help", help = true, order = 0)
@Parameter(names = {"-h", "--help"}, help = true, order = 0, description = "Print this message")
private boolean help = false;
@Parameter(names = {"-o", "--output"}, description = "Output apk file")
@ -53,17 +55,30 @@ public class LSPatch {
@Parameter(names = {"-p", "--proxyname"}, description = "Special proxy app name with full dot path")
private String proxyName = "org.lsposed.lspatch.appstub.LSPApplicationStub";
@Parameter(names = {"-d", "--debuggable"}, description = "Set true to make the app debuggable, otherwise set 0 (default) to make the app non-debuggable")
@Parameter(names = {"-d", "--debuggable"}, description = "Set app to be debuggable")
private boolean debuggableFlag = false;
@Parameter(names = {"-l", "--sigbypasslv"}, description = "Signature bypass level. 0 (disable), 1 (pm), 2 (pm+openat). default 0")
private int sigbypassLevel = 0;
@Parameter(names = {"--v1"}, description = "Sign with v1 signature")
private boolean v1 = true;
@Parameter(names = {"--v2"}, description = "Sign with v2 signature")
private boolean v2 = true;
@Parameter(names = {"--v3"}, description = "Sign with v3 signature")
private boolean v3 = true;
@Parameter(names = {"-v", "--verbose"}, description = "Verbose output")
private boolean verbose = false;
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 SIGNATURE_INFO_ASSET_PATH = "assets/original_signature_info.ini";
private static final String ORIGIN_APK_ASSET_PATH = "assets/origin_apk.bin";
private static final String ANDROID_MANIFEST_XML = "AndroidManifest.xml";
private static final String[] APK_LIB_PATH_ARRAY = {
"lib/armeabi-v7a",
"lib/armeabi",
@ -80,191 +95,238 @@ public class LSPatch {
.addObject(lsPatch)
.build();
jCommander.parse(args);
lsPatch.doCommandLine();
try {
lsPatch.doCommandLine();
} catch (PatchError e) {
System.err.println(e.getMessage());
}
}
public void doCommandLine() throws IOException {
public void doCommandLine() throws PatchError, IOException {
if (help) {
jCommander.usage();
return;
}
if (apkPath == null || apkPath.isEmpty()) {
jCommander.usage();
return;
}
File srcApkFile = new File(apkPath);
File srcApkFile = new File(apkPath).getAbsoluteFile();
if (!srcApkFile.exists()) {
System.out.println("The source apk file not exsit, please choose another one.");
return;
}
if (!srcApkFile.exists())
throw new PatchError("The source apk file does not exit. Please provide a correct path.");
File finalApk = new File(String.format("%s-%s-unsigned.apk", srcApkFile.getName(),
Constants.CONFIG_NAME_SIGBYPASSLV + sigbypassLevel));
FileUtils.copyFile(srcApkFile, finalApk);
var workingDir = Files.createTempDirectory("LSPatch").toFile();
try {
File tmpApk = new File(workingDir, String.format("%s-%s-unsigned.apk", srcApkFile.getName(),
Constants.CONFIG_NAME_SIGBYPASSLV + sigbypassLevel));
if (verbose) {
System.out.println("work dir: " + workingDir);
System.out.println("apk path: " + srcApkFile);
}
ZFile zFile = ZFile.openReadWrite(finalApk);
String apkFileName = srcApkFile.getName();
String currentDir = new File(".").getAbsolutePath();
System.out.println("work dir: " + currentDir);
System.out.println("apk path: " + apkPath);
if (outputPath == null || outputPath.length() == 0) {
outputPath = String.format("%s-lv%s-xposed-signed.apk", FilenameUtils.getBaseName(apkFileName), sigbypassLevel);
}
if (outputPath == null || outputPath.length() == 0) {
String sig = Constants.CONFIG_NAME_SIGBYPASSLV + sigbypassLevel;
outputPath = String.format("%s-%s-xposed-signed.apk", new File(apkPath).getName(), sig);
}
File outputFile = new File(outputPath);
if (outputFile.exists() && !forceOverwrite)
throw new PatchError(outputPath + " exists. Use --force to overwrite");
File outputFile = new File(outputPath);
if (outputFile.exists() && !forceOverwrite) {
System.err.println(outputPath + " exists, use --force to overwrite");
return;
}
System.out.println("Copying to tmp apk...");
String outputApkFileParentPath = outputFile.getParent();
if (outputApkFileParentPath == null) {
String absPath = outputFile.getAbsolutePath();
int index = absPath.lastIndexOf(File.separatorChar);
outputApkFileParentPath = absPath.substring(0, index);
}
FileUtils.copyFile(srcApkFile, tmpApk);
String apkFileName = srcApkFile.getName();
System.out.println("Parsing original apk...");
ZFile zFile = ZFile.openReadWrite(tmpApk);
String tempFilePath = outputApkFileParentPath + File.separator +
currentTimeStr() + "-tmp" + File.separator;
// save the apk original signature info, to support crach signature.
String originalSignature = ApkSignatureHelper.getApkSignInfo(apkPath);
if (originalSignature == null || originalSignature.isEmpty()) {
throw new PatchError("get original signature failed");
}
if (verbose)
System.out.println("Original signature\n" + originalSignature);
try (var is = new ByteArrayInputStream(originalSignature.getBytes(StandardCharsets.UTF_8))) {
zFile.add(SIGNATURE_INFO_ASSET_PATH, is);
} catch (IOException e) {
throw new PatchError("Error when saving signature: " + e);
}
unzipApkFilePath = tempFilePath + apkFileName + "-" + UNZIP_APK_FILE_NAME + File.separator;
// get the dex count in the apk zip file
dexFileCount = findDexFileCount(zFile);
// save the apk original signature info, to support crach signature.
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));
if (verbose)
System.out.println("dexFileCount: " + dexFileCount);
// get the dex count in the apk zip file
dexFileCount = findDexFileCount(zFile);
// copy out manifest file from zlib
var manifestEntry = zFile.get(ANDROID_MANIFEST_XML);
if (manifestEntry == null)
throw new PatchError("Provided file is not a valid apk");
System.out.println("dexFileCount: " + dexFileCount);
// parse the app main application full name from the manifest file
ManifestParser.Pair pair = ManifestParser.parseManifestFile(manifestEntry.open());
if (pair == null)
throw new PatchError("Failed to parse AndroidManifest.xml");
String applicationName = pair.applicationName == null ? "" : pair.applicationName;
// 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";
if (verbose)
System.out.println("original application name: " + applicationName);
// 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("Patching apk...");
// modify manifest
try (var is = new ByteArrayInputStream(modifyManifestFile(manifestEntry.open()))) {
zFile.add(APPLICATION_NAME_ASSET_PATH, is);
} catch (IOException e) {
throw new PatchError("Error when modifying manifest: " + e);
}
System.out.println("original application name: " + applicationName);
// save original main application name to asset file even its empty
try (var is = new ByteArrayInputStream(applicationName.getBytes(StandardCharsets.UTF_8))) {
zFile.add(APPLICATION_NAME_ASSET_PATH, is);
} catch (IOException e) {
throw new PatchError("Error when saving signature: " + e);
}
// modify manifest
modifyManifestFile(manifestFilePath, new File(unzipApkFilePath, "AndroidManifest.xml").getPath());
// copy so and dex files into the unzipped apk
Set<String> apkArchs = new HashSet<>();
// save original main application name to asset file even its empty
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
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 (verbose)
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]);
}
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 (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.err.println("Warning: No so file has been copied in " + sod.getPath());
continue;
}
for (File file : files) {
zFile.add(arch + "/" + file.getName(), new FileInputStream(file));
if (verbose)
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.err.println("Warning: No dex file has been copied");
return;
}
for (File file : files) {
zFile.add(arch + "/" + file.getName(), new FileInputStream(file));
System.out.println("add " + file.getPath());
String copiedDexFileName = "classes" + (dexFileCount + 1) + ".dex";
zFile.add(copiedDexFileName, new FileInputStream(file));
dexFileCount++;
}
}
// 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_OPENAT) {
zFile.add(ORIGIN_APK_ASSET_PATH, new FileInputStream(srcApkFile));
}
// copy origin apk to assets
// convenient to bypass some check like CRC
if (sigbypassLevel >= Constants.SIGBYPASS_LV_PM_OPENAT) {
zFile.add("assets/origin_apk.bin", new FileInputStream(srcApkFile));
}
File[] listAssets = new File("list-assets").listFiles();
if (listAssets == null || listAssets.length == 0) {
System.out.println("Warning: No assets file copyied");
}
else {
for (File f : listAssets) {
if (f.isDirectory()) {
throw new IllegalStateException("unsupport directory in assets");
File[] listAssets = new File("list-assets").listFiles();
if (listAssets == null || listAssets.length == 0) {
System.err.println("Warning: No assets file copyied");
} else {
for (File f : listAssets) {
if (f.isDirectory()) {
throw new PatchError("unsupported directory in assets");
}
zFile.add("assets/" + f.getName(), new FileInputStream(f));
}
zFile.add("assets/" + f.getName(), new FileInputStream(f));
}
// save lspatch config to asset..
try (var is = new ByteArrayInputStream("42".getBytes(StandardCharsets.UTF_8))) {
zFile.add("assets/" + Constants.CONFIG_NAME_SIGBYPASSLV + sigbypassLevel, is);
} catch (IOException e) {
throw new PatchError("Error when saving signature: " + e);
}
zFile.update();
zFile.close();
System.out.println("Signing apk...");
signApkUsingAndroidApksigner(workingDir, tmpApk, outputFile);
System.out.println("Done. Output APK: " + outputFile.getAbsolutePath());
} finally {
FileUtils.deleteDirectory(workingDir);
}
// save lspatch config to asset..
File sl = new File(unzipApkFilePath, "tmp");
FileUtils.write(sl, "42", Charset.defaultCharset());
zFile.add("assets/" + Constants.CONFIG_NAME_SIGBYPASSLV + sigbypassLevel, new FileInputStream(sl));
zFile.update();
zFile.close();
new BuildAndSignApkTask(true, unzipApkFilePath, finalApk.getAbsolutePath(), outputPath).run();
System.out.println("Output APK: " + outputPath);
}
private void modifyManifestFile(String filePath, String dstFilePath) {
private byte[] modifyManifestFile(InputStream is) {
ModificationProperty property = new ModificationProperty();
property.addApplicationAttribute(new AttributeItem(NodeValue.Application.DEBUGGABLE, debuggableFlag));
property.addApplicationAttribute(new AttributeItem("extractNativeLibs", true));
property.addApplicationAttribute(new AttributeItem(NodeValue.Application.NAME, proxyName));
FileProcesser.processManifestFile(filePath, dstFilePath, property);
var os = new ByteArrayOutputStream();
(new ManifestEditor(is, os, property)).processManifest();
return os.toByteArray();
}
private int findDexFileCount(ZFile zFile) {
for (int i = 2; i < 30; i++) {
for (int i = 2; ; i++) {
if (zFile.get("classes" + i + ".dex") == null)
return i - 1;
}
throw new IllegalStateException("wtf");
}
// Use the current timestamp as the name of the build file
@SuppressWarnings("SimpleDateFormat")
private String currentTimeStr() {
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");
return df.format(new Date());
private void signApkUsingAndroidApksigner(File workingDir, File apkPath, File outputPath) throws PatchError, IOException {
ArrayList<String> commandList = new ArrayList<>();
var keyStoreFile = new File(workingDir, "keystore");
try (InputStream is = getClass().getClassLoader().getResourceAsStream("assets/keystore");
FileOutputStream os = new FileOutputStream(keyStoreFile)) {
if (is == null)
throw new PatchError("Fail to save keystore file");
IOUtils.copy(is, os);
}
commandList.add("sign");
commandList.add("--ks");
commandList.add(keyStoreFile.getAbsolutePath());
commandList.add("--ks-key-alias");
commandList.add("key0");
commandList.add("--ks-pass");
commandList.add("pass:" + 123456);
commandList.add("--key-pass");
commandList.add("pass:" + 123456);
commandList.add("--out");
commandList.add(outputPath.getAbsolutePath());
commandList.add("--v1-signing-enabled");
commandList.add(Boolean.toString(v1));
commandList.add("--v2-signing-enabled"); // v2签名不兼容android 6
commandList.add(Boolean.toString(v2));
commandList.add("--v3-signing-enabled"); // v3签名不兼容android 6
commandList.add(Boolean.toString(v3));
commandList.add(apkPath.getAbsolutePath());
try {
ApkSignerTool.main(commandList.toArray(new String[0]));
} catch (Exception e) {
throw new PatchError("Failed to sign apk: " + e.getMessage());
}
}
}

View File

@ -1,98 +0,0 @@
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 java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.util.ArrayList;
/**
* Created by Wind
*/
public class BuildAndSignApkTask implements Runnable {
private final boolean keepUnsignedApkFile;
private final String signedApkPath;
private final String unzipApkFilePath;
private final String unsignedApkPath;
public BuildAndSignApkTask(boolean keepUnsignedApkFile, String unzipApkFilePath, String unsignedApkPath, String signedApkPath) {
this.keepUnsignedApkFile = keepUnsignedApkFile;
this.unsignedApkPath = unsignedApkPath;
this.unzipApkFilePath = unzipApkFilePath;
this.signedApkPath = signedApkPath;
}
@Override
public void run() {
try {
File unzipApkPathFile = new File(unzipApkFilePath);
File keyStoreFile = new File(unzipApkPathFile, "keystore");
String keyStoreAssetPath;
keyStoreAssetPath = "assets/keystore";
try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(keyStoreAssetPath);
FileOutputStream out = new FileOutputStream(keyStoreFile)) {
IOUtils.copy(inputStream, out);
}
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) {
if (!unsignedApkFile.delete()) {
throw new IllegalStateException("wtf");
}
}
}
catch (Exception err) {
throw new IllegalStateException("wtf", err);
}
}
private boolean signApk(String apkPath, String keyStorePath, String signedApkPath) {
return signApkUsingAndroidApksigner(apkPath, keyStorePath, signedApkPath, "123456");
}
private boolean signApkUsingAndroidApksigner(String apkPath, String keyStorePath, String signedApkPath, String keyStorePassword) {
ArrayList<String> commandList = new ArrayList<>();
commandList.add("sign");
commandList.add("--ks");
commandList.add(keyStorePath);
commandList.add("--ks-key-alias");
commandList.add("key0");
commandList.add("--ks-pass");
commandList.add("pass:" + keyStorePassword);
commandList.add("--key-pass");
commandList.add("pass:" + keyStorePassword);
commandList.add("--out");
commandList.add(signedApkPath);
commandList.add("--v1-signing-enabled");
commandList.add("true");
commandList.add("--v2-signing-enabled"); // v2签名不兼容android 6
commandList.add("false");
commandList.add("--v3-signing-enabled"); // v3签名不兼容android 6
commandList.add("false");
commandList.add(apkPath);
try {
ApkSignerTool.main(commandList.toArray(new String[0]));
}
catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
}

View File

@ -72,7 +72,6 @@ public class ApkSignatureHelper {
}
}
jarFile.close();
System.out.println("getApkSignInfo result: " + certs[0]);
return new String(toChars(certs[0].getEncoded()));
} catch (Exception e) {
e.printStackTrace();

View File

@ -3,6 +3,7 @@ package org.lsposed.patch.util;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import wind.android.content.res.AXmlResourceParser;
import wind.v1.XmlPullParser;
@ -13,23 +14,12 @@ import wind.v1.XmlPullParserException;
*/
public class ManifestParser {
/**
* Get the package name and the main application name from the manifest file
* */
public static Pair parseManifestFile(String filePath) {
public static Pair parseManifestFile(InputStream is) throws IOException {
AXmlResourceParser parser = new AXmlResourceParser();
File file = new File(filePath);
String packageName = null;
String applicationName = null;
if (!file.exists()) {
System.out.println(" manifest file not exist!!! filePath -> " + filePath);
return null;
}
FileInputStream inputStream = null;
try {
inputStream = new FileInputStream(file);
parser.open(inputStream);
parser.open(is);
while (true) {
int type = parser.next();
@ -64,20 +54,21 @@ public class ManifestParser {
}
}
} catch (XmlPullParserException | IOException e) {
e.printStackTrace();
System.out.println("parseManifestFile failed, reason --> " + e.getMessage());
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
return new Pair(packageName, applicationName);
}
/**
* Get the package name and the main application name from the manifest file
*/
public static Pair parseManifestFile(String filePath) throws IOException {
File file = new File(filePath);
try (var is = new FileInputStream(file)) {
return parseManifestFile(is);
}
}
public static class Pair {
public String packageName;
public String applicationName;