Android 中动态加载
是指应用程序在运行时加载和执行 Dex 文件的过程,可以在运行时加载不同的代码或功能,而无需重新编译整个应用程序,动态加载 Dex 文件通常涉及以下步骤:
通过动态加载 Dex 文件,可以实现更加灵活和可扩展的功能,可以编写插件或模块化的代码,并在运行时根据需要加载它们。DexClassLoader
是 Android 中的一个类加载器,用于动态加载包含 Dex 文件的 jar 或 apk 文件,它的工作原理大概是:加载 Dex 文件,DexClassLoader 会从指定的路径中读取 Dex 文件,并将其加载到内存中… …无序过多描述,我们知道它可以加载 dex 文件即可。
本文讲的如下图所示:
如何从 apk 中获取特定包名下的代码并保存到 dex 文件中,然后再把此 dex 放到 assets 目录下,最后重新打包成 APK。
本次通过 Java 实现,最终产物是一个可执行的 Jar,使用方式如下所示:
java -jar xpluginJs.jar -mapping mapping.txt -apk 1.apk -dexname hello -pkg com.primer.pay.manager,com.primer.login.manager
意思是:
根据 mapping.txt 映射文件从 1.apk 安装包中提取所有 com.primer.pay.manager,com.primer.login.manager 包下的代码,将所提取代码写入 hello.dex 文件,hello.dex 文件将被放到包体内 assets 目录下。
/**
*
* 1、必须参数
* -mapping:安装包的映射文件
* -apk:待处理 apk 文件
* -dexname:处理后存放在 assets 目录下的 dex 文件名
* -pkg:需要提取的包名(混淆前的包名),多个包名用英文逗号分隔
*
*
* 2、当前目录运行环境目录结构
* tools:内置工具(apktool、smali)
* xpluginJs.jar:主程序
* mapping.txt:映射文件
* 1.apk:apk 文件
* output-dex2jar:中间产物输出目录
* output-dex2jar\apk-decode\dist\***.apk:最终产物
*/
public void run() {
System.err.println("=================== 1、Initializes the output directory");
initOutPutDir();
mDexConfig.setspecifyPkgSmailOutputPath(OUTPUT_PATH_SMAIL);
System.err.println("=================== 2、Load the local mapping configuration");
mObsMapping = loadMappingConfig(mDexConfig.getMappingFilePath());
System.err.println("=================== 3、Gets a list of mapping names for the specified package name");
List<String> specifyMappingPkgList = ObsMappingUtils.collectionSpecifyPkgMapping(mObsMapping,
mDexConfig.getSpecifiyPkgList());
System.err.println("=================== 4、Decompile Apk using apktool");
String decodeOutPath = decodeApk(mDexConfig.getApkFilePath(), mDexConfig.getApkDecodeOutputDirname());
mDexConfig.setApkDecodeOutputDirpath(decodeOutPath);
System.err.println("=================== 5、Collects and moves the smail file under the specified package name");
collectionAndMoveSpecifyPkgSmail(decodeOutPath, specifyMappingPkgList);
System.err.println("=================== 6、Package the smali file as dex");
String smailToDexPath = encodeSmailToDex(mDexConfig.getSpecifyPkgSmailOutputPath());
if (CommUtil.isEmptyOrNoExists(smailToDexPath)) {
throw new IllegalArgumentException("smailToDexPath is null");
}
System.err.println("\tsmailToDexPath: " + smailToDexPath);
System.err.println("=================== 7、dex encryption");
String encodeDexPath = encodeDex(mDexConfig.getEncodeDexFilename(), smailToDexPath);
System.err.println("\tencodeDexPath: " + encodeDexPath);
String decodeDexPath = decodeDex(mDexConfig.getEncodeDexFilename() + "-decode", encodeDexPath);
System.err.println("\tencodeDexPath: " + decodeDexPath);
System.err.println("=================== 8、copy dex encryption");
boolean isCopy = copyDexToAssets(encodeDexPath, decodeOutPath);
if (!isCopy) {
throw new IllegalArgumentException("copy dex to assets error");
}
System.err.println("=================== 9、Apk compile back");
String unsignApkFilePath = encodeApk(decodeOutPath);
System.err.println("\t [Successfully~] unsignApkFilePath: " + unsignApkFilePath);
}
按格式解析参数,收集必要的信息,这里参数解析或许有漏洞,但你可以根据自己的想法写出更好的参数解析,避免因外部使用的多样化导致内部解析异常。
package com.primer;
import java.util.Arrays;
import com.primer.bean.CmdArgs;
public class Main {
private static DexToolManager mDexToolManager;
private static CmdArgs mCmdArgs;
public static void main(String[] args) {
mCmdArgs = parserArgs(args);
initDexToolManager();
}
//解析 java -jar 传入的参数
private static CmdArgs parserArgs(String[] args) {
if (args == null || args.length == 0) {
throw new IllegalArgumentException(" argument not be empty");
}
CmdArgs cmdArgs = new CmdArgs();
String current;
for (int i = 0; i < args.length; i++) {
current = args[i];
if (current.equals("-mapping")) {
if (i + 1 >= args.length) {
throw new IllegalArgumentException("-mapping args error");
}
cmdArgs.mappingPath = args[i + 1];
} else if (current.equals("-apk")) {
if (i + 1 >= args.length) {
throw new IllegalArgumentException("-apk args error");
}
cmdArgs.apkPath = args[i + 1];
} else if (current.equals("-dexname")) {
if (i + 1 >= args.length) {
throw new IllegalArgumentException("-dexname args error");
}
cmdArgs.encodeDexFilename = args[i + 1];
} else if (current.equals("-pkg")) {
if (i + 1 >= args.length) {
throw new IllegalArgumentException("-pkg args error");
}
String str = args[i + 1];
String[] pkgList = str.split(",");
if (pkgList == null || pkgList.length == 0) {
throw new IllegalArgumentException("args error");
}
cmdArgs.specifiyPkgList = Arrays.asList(pkgList);
}
}
cmdArgs.checkArgument();
return cmdArgs;
}
private static void initDexToolManager() {
DexConfig dexConfig = new DexConfig.Builder()
.setApkFilePath(mCmdArgs.apkPath)
.setMappingFilePath(mCmdArgs.mappingPath)
.setSpecifyPkgList(mCmdArgs.specifiyPkgList)
.setEncodeDexFilename(mCmdArgs.encodeDexFilename)
.setApkDecodeOutputDirname("apk-decode")
.build();
//简单得对参数是否有效做检查
dexConfig.checkConfigIllegal();
mDexToolManager = new DexToolManager();
mDexToolManager.setDexConfig(dexConfig);
mDexToolManager.run();
}
}
如果让自己写删除目录及目录下的所有文件,很容易让我们想起了递归遍历,可以递归删除文件。
public void traversalFile(File dirFile, FileTraversal traversal) {
if (dirFile == null) {
return;
}
for (File file : dirFile.listFiles()) {
if (file.isDirectory()) {
traversalFile(file, traversal);
traversal.processDir(file);
} else {
traversal.processFile(file);
}
}
}
//使用
traversalFile(outFile, new FileTraversal() {
@Override
public void processFile(File file) {
if (file.exists()) {
file.delete();
}
}
@Override
public void processDir(File file) {
if (file.exists()) {
file.delete();
}
}
});
我们知道 build/output/**/ release/mapping.txt
就是 Android 开启混淆打包生成的映射文件,就是根据该文件的格式进行解析,可以从中解析获取混淆前类的全限定名(包名+类名),当然也可以拿到混淆前后的方法名等信息。
com.opos.mobad.service.tasks.a -> com.opos.mobad.service.tasks.a:
java.lang.String a -> a
java.io.FileFilter b -> b
java.util.HashMap getPayMap(android.content.Context,boolean,int) -> a
boolean d(android.content.Context) -> d
java.lang.String j(android.content.Context) -> j
根据映射文件我们可以这样简单写出解析存储数据的 bean 类及关系。
//一个映射文件包含很多类的映射
public class ObsMapping {
private LinkedList<ClassObsMapping> obsMapping;
//略
}
//一个类包含类名映射、多个成员变量映射、多个方法的映射(我们没有使用到成员变量,所以可以不要)
public class ClassObsMapping {
//类名映射
private MappingItem classMapping;
//方法映射
private LinkedList<MappingItem> methodsMapping;
//略
}
//映射的基本元素是混淆前后的名称
public class MappingItem {
//混淆前名称
private String originalName;
//混淆后名称
private String mappingName;
//略
}
如 com.primer.manager.A -> com.android.manager.AA,那么收集的是 com.android.manager,最终返回的是混淆后的类的包名。
简单地封装 Runtime.getRuntime(),通过调用 runtime.exec(cmdline)
执行控制台命令,这里是通过控制台执行 bat 脚本,再由脚本执行 java 命令执行 apktool。
APKTOOL_BAT_PATH 指向的一个 bat 脚本路径:
:: 参数1-apktool 参数2-apk路径 参数3-输出路径 -f:强制覆盖
java -jar %1 d %2 -o %3 -f
/**
* java -jar apktool_2.7.0.jar d [apk file] -o [out name]
*
* @param inputApkPath
* @param outDirName
* @return
*/
private String decodeApk(String inputApkPath, String outDirName) {
if (inputApkPath == null || inputApkPath.isEmpty()) {
throw new IllegalArgumentException("inputApkPath is null");
}
String cmdline;
String outApkPath = OUTPUT_PATH + File.separator + outDirName;
StringBuilder sb = new StringBuilder();
sb.append(APKTOOL_BAT_PATH)
.append(" ")
.append(APKTOOL_JAR_PATH)
.append(" ")
.append(inputApkPath)
.append(" ")
.append(outApkPath);
if (sb.toString().contains("\\") && !sb.toString().contains("\\\\")) {
cmdline = sb.toString().replace("\\", "\\\\");
} else {
cmdline = sb.toString();
}
System.out.println("decodeApk cmd: " + cmdline);
CommUtil.executeCmdline(cmdline, false);
return outApkPath;
}
通过递归遍历过了 smali 找到映射文件,再把目标文件已到指定目录待下一步处理。
private void collectionAndMoveSpecifyPkgSmail(String path, List<String> specifyPkgList) {
if (CommUtil.isEmptyOrNoExists(specifyPkgList)) {
return;
}
traversalFile(new File(path), new FileTraversal() {
@Override
public void processFile(File file) {
if (file.getName().endsWith(".smali")) {
SmailFile smailFile = splitClassPkgname(file.getAbsolutePath());
if (!CommUtil.isEmptyOrNoExists(smailFile.pakcgeName)) {
for (String pkg : specifyPkgList) {
if (pkg.equals(smailFile.pakcgeName)) {
moveAndDeleteTargetFile(file, smailFile);
break;
}
}
}
}
}
@Override
public void processDir(File file) {
}
});
}
public class SmailFile {
//映射后的文件名(类名)
public String filename;
//映射后的包名,如 com.primer.manager
public String pakcgeName;
//映射后的包路径,如 com/primer/manager
//因为提取到外部目录下也应该创建相同包名的目录,再不 smali 文件存放到该目录下,确保前后一致
public String pakagePath;
@Override
public String toString() {
return "SmailFile: " + filename + ", " + pakcgeName + ", " + pakagePath;
}
}
这里有一点需要注意的是,最新版 smali.jar 打包参数是 assemble
,好像以前的包版本打包参数是 b。
:: java -jar smali.jar b out_directory -o output.dex
:: 参数1:smali.jar
:: 参数2:out_directory smali 文件目录
java -jar %1 assemble %2
dex 文件本质上是一个二进制文件,二进制文件读取出来就是一组字节数组 byte[]
,简单的对字节数组进行特殊操作(插入偏移量等)就是对文件的加密。
这里还是先调用 bat 脚本,再由 bat 执行 java 命令执行 apktool,你也可以以自己的方式处理。
java -jar apktool_2.7.0.jar b [apk decode file path]
:: 参数1-apktool 参数2-apk路径
java -jar %1 b %2
TODO:
当然,有了想法你可以做很多诸如此类的事情!