Apktool源码解析
apktool是一个第三方的反编译工具,它可以将资源解码为几乎原始的形式,并在进行修改后进行回编——官方介绍
因为工作原因,我需要经常使用apktool对apk包进行反编译和回编,也会在使用中碰到一些坑。这里对apktool源码进行一次梳理
apktool官网地址
apktool Github
截止到目前官网最新版本为:2.5.0, github上最新版本为:2.5.1-SNAPSHOT
目录介绍
目录.png从github下载完源码,导入idea
- apktool主目录
- main函数入口
- 反编译和回编业务代码
- 常量声明,现在主要是异常
- 压缩文件处理
- 工具类、cmd
- apktool在各个系统中的执行脚本
源码解析
首先从入口函数main开始
Main.java
public static void main(String[] args) throws IOException, InterruptedException, BrutException {
...
commandLine = parser.parse(allOptions, args, false);
// 设置日志输出等级
if (commandLine.hasOption("-v") || commandLine.hasOption("--verbose")) {
//详细信息
verbosity = Verbosity.VERBOSE;
} else if (commandLine.hasOption("-q") || commandLine.hasOption("--quiet")) {
//不输出日志
verbosity = Verbosity.QUIET;
}
setupLogging(verbosity);
boolean cmdFound = false;
for (String opt : commandLine.getArgs()) {
if (opt.equalsIgnoreCase("d") || opt.equalsIgnoreCase("decode")) {
//反编译
cmdDecode(commandLine);
cmdFound = true;
} else if (opt.equalsIgnoreCase("b") || opt.equalsIgnoreCase("build")) {
//回编
cmdBuild(commandLine);
cmdFound = true;
} else if (opt.equalsIgnoreCase("if") || opt.equalsIgnoreCase("install-framework")) {
//安装framework.apk
cmdInstallFramework(commandLine);
cmdFound = true;
} else if (opt.equalsIgnoreCase("empty-framework-dir")) {
//删除framework目录
cmdEmptyFrameworkDirectory(commandLine);
cmdFound = true;
} else if (opt.equalsIgnoreCase("list-frameworks")) {
//输出所有framework文件名称
cmdListFrameworks(commandLine);
cmdFound = true;
} else if (opt.equalsIgnoreCase("publicize-resources")) {
//公共资源
cmdPublicizeResources(commandLine);
cmdFound = true;
}
}
}
对于我们重点关注反编译和回编,安装fragmework.apk,主要在appt/appt2回编资源、生成R文件和resources.arsc用。就是android资源编译版本,这个可以在sdk\platforms\android-(android版本如:28)\android.jar这里找到,默认的在项目apktool-lib/src/main/resources/androlib/android-framework.jar.
反编译
命令: java -jar apktool.jar d [option] <apkFile>
在cmdDecode方法中检查参数后使用ApkDecoder进行解码
ApkDecoder.java
public void decode() throws AndrolibException, IOException, DirectoryException {
try {
...
//资源文件解码
//是否有resources.arsc文件
if (hasResources()) {
//资源解码模式
switch (mDecodeResources) {
//不解码资源使用“-r”设置
//这个模式不解码资源,不解码资源,会节约反编译和回编时间
case DECODE_RESOURCES_NONE:
//解码资源文件,实际上就是拷贝resources.arsc、AndroidManifest.xml、res和zip方式打开一样
mAndrolib.decodeResourcesRaw(mApkFile, outDir);
if (mForceDecodeManifest == FORCE_DECODE_MANIFEST_FULL) {
setTargetSdkVersion();
setAnalysisMode(mAnalysisMode, true);
// done after raw decoding of resources because copyToDir overwrites dest files
if (hasManifest()) {
mAndrolib.decodeManifestWithResources(mApkFile, outDir, getResTable());
}
}
break;
//将二进制的资源解码
case DECODE_RESOURCES_FULL:
setTargetSdkVersion();
setAnalysisMode(mAnalysisMode, true);
if (hasManifest()) {
//根据resources.arsc解码androidmanifest
mAndrolib.decodeManifestWithResources(mApkFile, outDir, getResTable());
}
//根据resources.arsc和apk,生成values目录下的资源文件(attrs.xml、colors.xml、dimens.xml、ids.xml、、、)
mAndrolib.decodeResourcesFull(mApkFile, outDir, getResTable());
break;
}
} else {
// if there's no resources.arsc, decode the manifest without looking
// up attribute references
if (hasManifest()) {
if (mDecodeResources == DECODE_RESOURCES_FULL
|| mForceDecodeManifest == FORCE_DECODE_MANIFEST_FULL) {
mAndrolib.decodeManifestFull(mApkFile, outDir, getResTable());
}
else {
mAndrolib.decodeManifestRaw(mApkFile, outDir);
}
}
}
//代码部分解码
//判断主dex是否存在
if (hasSources()) {
//dex解码模式
switch (mDecodeSources) {
//直接拷贝dex不解码,使用“-s”设置
case DECODE_SOURCES_NONE:
mAndrolib.decodeSourcesRaw(mApkFile, outDir, "classes.dex");
break;
//解码主dex,
case DECODE_SOURCES_SMALI:
case DECODE_SOURCES_SMALI_ONLY_MAIN_CLASSES:
//将dex解码成smali
mAndrolib.decodeSourcesSmali(mApkFile, outDir, "classes.dex", mBakDeb, mApi);
break;
}
}
//是否有分包dex文件, 如:classes1.dex、classes2.dex
if (hasMultipleSources()) {
// foreach unknown dex file in root, lets disassemble it
Set<String> files = mApkFile.getDirectory().getFiles(true);
for (String file : files) {
if (file.endsWith(".dex") && file.startsWith("classes")) {
if (! file.equalsIgnoreCase("classes.dex")) {
switch(mDecodeSources) {
case DECODE_SOURCES_NONE:
mAndrolib.decodeSourcesRaw(mApkFile, outDir, file);
break;
case DECODE_SOURCES_SMALI:
mAndrolib.decodeSourcesSmali(mApkFile, outDir, file, mBakDeb, mApi);
break;
case DECODE_SOURCES_SMALI_ONLY_MAIN_CLASSES:
if (file.startsWith("classes") && file.endsWith(".dex")) {
mAndrolib.decodeSourcesSmali(mApkFile, outDir, file, mBakDeb, mApi);
} else {
mAndrolib.decodeSourcesRaw(mApkFile, outDir, file);
}
break;
}
}
}
}
}
//将apk包中的lib、assets、libs、kotlin文件解压
mAndrolib.decodeRawFiles(mApkFile, outDir, mDecodeAssets);
//将apk中未知的文件拷贝出来如okhttp
mAndrolib.decodeUnknownFiles(mApkFile, outDir, mResTable);
mUncompressedFiles = new ArrayList<String>();
//记录上面拷贝的未知文件
mAndrolib.recordUncompressedFiles(mApkFile, mUncompressedFiles);
mAndrolib.writeOriginalFiles(mApkFile, outDir);
//将记录的信息写入到apktool.yml
writeMetaFile();
} catch (Exception ex) {
throw ex;
} finally {
try {
mApkFile.close();
} catch (IOException ignored) {}
}
}
整个反编译流程已经结束完了,可以对反编译后的资源或字节码进行操作
- “-r”命令忽略资源的反编译
- "-s"命令忽略dex文件的反编译
Dex2Smali:使用的是baksmali,如果有需求将其它jar包反编译后加入app的smali里面,最好使用apktool同一一个版本baksmali
反编译目录
反编译目录.png方便生成的目录大概就是这样,
1|2|5|6|8|9:apk的assets|动态链接库|res资源文件(布局资源配置)|smali字节码(代码部分)|androidmanifest|apktool反编译配置信息,回编的时候需要读取
3|4||7译未识别文件,但是包体里存在的文件,直接拷贝到目录
配置apktool.yml
!!brut.androlib.meta.MetaInfo
apkFileName: apk名称,回编时候生成的文件名称
compressionType: 默认false
doNotCompress:不进行压缩的文件列表
...
isFrameworkApk: false
packageInfo:
forcedPackageId: '127'
renameManifestPackage: null
sdkInfo:SDK信息
minSdkVersion: '15' apk适配最低版本
targetSdkVersion: '23' apk适配版本,这里修改包体适配版本,回编后生效
sharedLibrary: false
sparseResources: false
unknownFiles:
android-support-multidex.version.txt: '8'
sources.list: '8'
usesFramework:
ids:
- 1
tag: null
version: apktool版本
versionInfo: apk版本信息,这里修改apk版本信息,对应的包体也会修改,回编后生效
versionCode: '202'
versionName: 2.1.0
回编
命令:java -jar apktool.jar -b [option] <apkSourcesPath>
Androidlib.ava
public void build(ExtFile appDir, File outFile)
throws BrutException {
//读取反编译的本地配置信息
MetaInfo meta = readMetaFile(appDir);
...
//将smali文件回编成dex
//主dex回编
buildSources(appDir);
//除主dex外的其他dex回编
buildNonDefaultSources(appDir);
//检查androidManifest
buildManifestFile(appDir, manifest, manifestOriginal);
//将res目录下的文件回编成二进制文件
buildResources(appDir, meta.usesFramework);
//添加动态链接库
buildLibs(appDir);
//处理original目录
buildCopyOriginalFiles(appDir);
//生成apk
buildApk(appDir, outFile);
//未识别的添加到apk中
buildUnknownFiles(appDir, outFile, meta);
}
public void buildSources(File appDir)
throws AndrolibException {
//两个判断条件,第一个是未将dex反编译成smali的,直接拷贝到回编目录,第二个是将smali回编成dex,使用的是smali(一个第三方库,将smali回编成dex)工具
if (!buildSourcesRaw(appDir, "classes.dex") && !buildSourcesSmali(appDir, "smali", "classes.dex")) {
LOGGER.warning("Could not find sources");
}
}
//调用第三方工具smali把smali文件回编成dex文件
public boolean buildSourcesSmali(File appDir, String folder, String filename)
throws AndrolibException {
ExtFile smaliDir = new ExtFile(appDir, folder);
if (!smaliDir.exists()) {
return false;
}
File dex = new File(appDir, APK_DIRNAME + "/" + filename);
if (! apkOptions.forceBuildAll) {
LOGGER.info("Checking whether sources has changed...");
}
if (apkOptions.forceBuildAll || isModified(smaliDir, dex)) {
LOGGER.info("Smaling " + folder + " folder into " + filename + "...");
dex.delete();
SmaliBuilder.build(smaliDir, dex, apkOptions.forceApi > 0 ? apkOptions.forceApi : mMinSdkVersion);
}
return true;
}
这里咋们来看看资源如何回编
public void buildResources(ExtFile appDir, UsesFramework usesFramework)
throws BrutException {
//第一个判断,如果资源未反编译,直接拷贝编译目录
//第二个判断资源反编译了,进行编译
if (!buildResourcesRaw(appDir) && !buildResourcesFull(appDir, usesFramework)
&& !buildManifest(appDir, usesFramework)) {
LOGGER.warning("Could not find resources");
}
}
public boolean buildResourcesFull(File appDir, UsesFramework usesFramework)
throws AndrolibException {
...
使用appt对资源进行编译
mAndRes.aaptPackage(apkFile, new File(appDir,
"AndroidManifest.xml"), new File(appDir, "res"),
ninePatch, null, parseUsesFramework(usesFramework));
}
AndroidLibResources.java
public void aaptPackage(File apkFile, File manifest, File resDir, File rawDir, File assetDir, File[] include)
throws AndrolibException {
String aaptPath = apkOptions.aaptPath;
boolean customAapt = !aaptPath.isEmpty();
List<String> cmd = new ArrayList<String>();
try {
//获取aapt路径,项目中apktool-lib/src/resources/prebuilt下
String aaptCommand = AaptManager.getAaptExecutionCommand(aaptPath, getAaptBinaryFile());
cmd.add(aaptCommand);
} catch (BrutException ex) {
LOGGER.warning("aapt: " + ex.getMessage() + " (defaulting to $PATH binary)");
cmd.add(AaptManager.getAaptBinaryName(getAaptVersion()));
}
if (apkOptions.isAapt2()) {
//aapt2编译
aapt2Package(apkFile, manifest, resDir, rawDir, assetDir, include, cmd, customAapt);
return;
}
//aapt1编译
aapt1Package(apkFile, manifest, resDir, rawDir, assetDir, include, cmd, customAapt);
}
apktool的反编译和回编就已经完成了。这里有aapt2和aapt1编译资源,如果有使用aapt生成R文件的需求,最好使用aapt2,因为aapt2支持的版本比aapt1高
-
brut.androlib.AndrolibException: brut.common.BrutException异常(坑)
资源回编我们使用的aapt cmd命令的方式,windows下命令最大长度8191个字符,如果超过就会报上面这个问题,这时我们就需要消减命令长度,我发现回编事会吧assets下的文件一起加入命令。去掉就行了 -
把assets下的dex文件给反编译了,移动安全联盟的SDK下有(坑)
在反编译时指定apk下dex文件就行了