1. 背景
由于Smali代码实在过于难写, 又没有专门学习过Smali代码, 最多看的懂一点点.
在这个前提下, 我们有两个方案可以使用:
- Smali
- Xposed
Smali
咱们不熟悉, 那么只能选择Xposed. 并且在我们的生产环境中, 不太方便安装Xposed框架, 所以,我们可以使用另外一种解决方案LSPatch
2. LSPatch
LSPatch: A non-root Xposed framework extending from LSPosed
Rootless implementation of LSPosed framework, integrating Xposed API by inserting dex and so into the target APK.
2.1 介绍
LSPatch可以在无ROOT环境下给应用使用Xposed的功能, 它是通过注入DEX和SO到目标APP中实现的
LSPatch提供了2个方案对我们目标应用打包:
- 通过
lspatch.jar
包进行注入 - 通过管理器进行注入
秉着学习的想法,我们可以对lspatch.jar
源码学习, 看看它是如何实现文件注入的
2.2 lspatch.jar文件注入流程
https://github.com/LSPosed/LSPatch/blob/master/patch/src/main/java/org/lsposed/patch/LSPatch.java
在上面的入口文件中, 我们可以看到doCommandLine
方法, 可以看到 patch
方法为注入的入口
public void doCommandLine() throws PatchError, IOException {
for (var apk : apkPaths) {
File srcApkFile = new File(apk).getAbsoluteFile();
String apkFileName = srcApkFile.getName();
var outputDir = new File(outputPath);
outputDir.mkdirs();
File outputFile = new File(outputDir, String.format(
Locale.getDefault(), "%s-%d-lspatched.apk",
FilenameUtils.getBaseName(apkFileName),
LSPConfig.instance.VERSION_CODE)
).getAbsoluteFile();
if (outputFile.exists() && !forceOverwrite)
throw new PatchError(outputPath + " exists. Use --force to overwrite");
logger.i("Processing " + srcApkFile + " -> " + outputFile);
// 注入的入口方法
patch(srcApkFile, outputFile);
}
}
patch方法主要做了以下几件事:
- ZFile读取应用信息
签名相关
- 生成签名信息
- 获取原始签名
Patch
- 根据下面的信息修改manifest信息
- useManager 是否使用管理器进行模块管理
- debuggableFlag 应用是否开启debug
- overrideVersionCode 重写VersionCode
- sigbypassLevel 过签名校验等级
- originalSignature 原始的签名信息
- appComponentFactory
保存文件到assets
- LSPatch配置到apk assets中
- 保存metaloader.dex到apk assets中
- 保存loader.dex 到assets中 (manager使用)
- 保存liblspatch.so到asset中
- 注入xposed模块
- 创建APK压缩包
- APK 4字节对齐
我们在这里不去过度关心它到底是如何解析原始APK, 给新的APK打入东西, 因为这都有LSPatch源码中ZFile
提供的能力, 我们不过于深究, 也不是我们要太了解的东西.
public void patch(File srcApkFile, File outputFile) throws PatchError, IOException {
if (!srcApkFile.exists())
throw new PatchError("The source apk file does not exit. Please provide a correct path.");
outputFile.delete();
logger.d("apk path: " + srcApkFile);
logger.i("Parsing original apk...");
try (var dstZFile = ZFile.openReadWrite(outputFile, Z_FILE_OPTIONS);
var srcZFile = dstZFile.addNestedZip((ignore) -> ORIGINAL_APK_ASSET_PATH, srcApkFile, false)) {
// sign apk
try {
var keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
if (keystoreArgs.get(0) == null) {
logger.i("Register apk signer with default keystore...");
try (var is = getClass().getClassLoader().getResourceAsStream("assets/keystore")) {
keyStore.load(is, keystoreArgs.get(1).toCharArray());
}
} else {
logger.i("Register apk signer with custom keystore...");
try (var is = new FileInputStream(keystoreArgs.get(0))) {
keyStore.load(is, keystoreArgs.get(1).toCharArray());
}
}
var entry = (KeyStore.PrivateKeyEntry) keyStore.getEntry(keystoreArgs.get(2), new KeyStore.PasswordProtection(keystoreArgs.get(3).toCharArray()));
new SigningExtension(SigningOptions.builder()
.setMinSdkVersion(28)
.setV2SigningEnabled(true)
.setCertificates((X509Certificate[]) entry.getCertificateChain())
.setKey(entry.getPrivateKey())
.build()).register(dstZFile);
} catch (Exception e) {
throw new PatchError("Failed to register signer", e);
}
String originalSignature = null;
if (sigbypassLevel > 0) {
originalSignature = ApkSignatureHelper.getApkSignInfo(srcApkFile.getAbsolutePath());
if (originalSignature == null || originalSignature.isEmpty()) {
throw new PatchError("get original signature failed");
}
logger.d("Original signature\n" + originalSignature);
}
// copy out manifest file from zlib
var manifestEntry = srcZFile.get(ANDROID_MANIFEST_XML);
if (manifestEntry == null)
throw new PatchError("Provided file is not a valid apk");
// parse the app appComponentFactory full name from the manifest file
final String appComponentFactory;
int minSdkVersion;
try (var is = manifestEntry.open()) {
var pair = ManifestParser.parseManifestFile(is);
if (pair == null)
throw new PatchError("Failed to parse AndroidManifest.xml");
appComponentFactory = pair.appComponentFactory;
minSdkVersion = pair.minSdkVersion;
logger.d("original appComponentFactory class: " + appComponentFactory);
logger.d("original minSdkVersion: " + minSdkVersion);
}
logger.i("Patching apk...");
// modify manifest
final var config = new PatchConfig(useManager, debuggableFlag, overrideVersionCode, sigbypassLevel, originalSignature, appComponentFactory);
final var configBytes = new Gson().toJson(config).getBytes(StandardCharsets.UTF_8);
final var metadata = Base64.getEncoder().encodeToString(configBytes);
try (var is = new ByteArrayInputStream(modifyManifestFile(manifestEntry.open(), metadata, minSdkVersion))) {
dstZFile.add(ANDROID_MANIFEST_XML, is);
} catch (Throwable e) {
throw new PatchError("Error when modifying manifest", e);
}
logger.i("Adding config...");
// save lspatch config to asset..
try (var is = new ByteArrayInputStream(configBytes)) {
dstZFile.add(CONFIG_ASSET_PATH, is);
} catch (Throwable e) {
throw new PatchError("Error when saving config");
}
logger.i("Adding metaloader dex...");
try (var is = getClass().getClassLoader().getResourceAsStream(Constants.META_LOADER_DEX_ASSET_PATH)) {
dstZFile.add("classes.dex", is);
} catch (Throwable e) {
throw new PatchError("Error when adding dex", e);
}
if (!useManager) {
logger.i("Adding loader dex...");
try (var is = getClass().getClassLoader().getResourceAsStream(LOADER_DEX_ASSET_PATH)) {
dstZFile.add(LOADER_DEX_ASSET_PATH, is);
} catch (Throwable e) {
throw new PatchError("Error when adding assets", e);
}
logger.i("Adding native lib...");
// copy so and dex files into the unzipped apk
// do not put liblspatch.so into apk!lib because x86 native bridge causes crash
for (String arch : ARCHES) {
String entryName = "assets/lspatch/so/" + arch + "/liblspatch.so";
try (var is = getClass().getClassLoader().getResourceAsStream(entryName)) {
dstZFile.add(entryName, is, false); // no compress for so
} catch (Throwable e) {
// More exception info
throw new PatchError("Error when adding native lib", e);
}
logger.d("added " + entryName);
}
logger.i("Embedding modules...");
embedModules(dstZFile);
}
// create zip link
logger.d("Creating nested apk link...");
for (StoredEntry entry : srcZFile.entries()) {
String name = entry.getCentralDirectoryHeader().getName();
if (name.startsWith("classes") && name.endsWith(".dex")) continue;
if (dstZFile.get(name) != null) continue;
if (name.equals("AndroidManifest.xml")) continue;
if (name.startsWith("META-INF") && (name.endsWith(".SF") || name.endsWith(".MF") || name.endsWith(".RSA"))) continue;
srcZFile.addFileLink(name, name);
}
dstZFile.realign();
logger.i("Writing apk...");
}
logger.i("Done. Output APK: " + outputFile.getAbsolutePath());
}
3. 实战
假设兄弟们都会Xposed模块的编写, 那么实在其实就没有什么好讲的了. 关于Xposed模块编写网上太多了. 我这里就不再浪费篇幅去说了.
简单的几步:
- Xposed模块编写
- 在Lsposed的机器上测试功能
- 使用Lspatch打包APK+模块(集成模式)
- 在没有安装LSPosed环境的设备测试
- 发布
流程非常简单, 全程无痛点.
再次, 感谢Lsposed团队发布这么好的工具给大家使用!!!