赞
踩
作者简介:Serpit,Android开发工程师,2023年加入37手游技术部,目前负责国内游戏发行 Android SDK 开发。
由于做需求时,最近遇到了一个apktool反编译时报错,虽然问题简单,在排查解决问题的同时,借此机会顺便学习一下apktool的源码,了解apktool是如何实现反编译的。
关于apktool的使用方式,前面已有文章有相关的介绍,链接如下,感兴趣的同学可以先学习一下~
Android 逆向入门保姆级教程 (https://juejin.cn/post/7249286405007851557)
在看apktool源码之前,我们需要了解一下APK是如何通过我们的项目代码编程一个APK文件的,使得后续了解反编译过程更简单。主要步骤如下:
资源处理,利用aapt打包生成res资源文件,生成resources.arsc、R.java和res文件
源代码通过编译生成class文件进而转化成dex文件
通过apkbuilder工具将resources.arsc、res文件、assets文件和dex文件打包生成apk
签名
签名后文件对齐
由于团队中使用的apktool 2.7.0版本,所以本文阅读的源码也基于2.7.0。源码链接如下:
https://github.com/iBotPeaches/Apktool/tree/v2.7.0
java程序的入口都是从Main方法开始的,因此先来看看Main方法。
- public static void main(String[] args) throws BrutException {
-
-
- // headless
- System.setProperty("java.awt.headless", "true");
-
-
- // set verbosity default
- Verbosity verbosity = Verbosity.NORMAL;
-
-
- // cli parser
- CommandLineParser parser = new DefaultParser();
- CommandLine commandLine;
-
-
- // load options
- _Options();
-
-
- try {
- commandLine = parser.parse(allOptions, args, false);
-
-
- if (! OSDetection.is64Bit()) {
- System.err.println("32 bit support is deprecated. Apktool will not support 32bit on v3.0.0.");
- }
- } catch (ParseException ex) {
- System.err.println(ex.getMessage());
- usage();
- System.exit(1);
- return;
- }
-
-
- // check for verbose / quiet
- if (commandLine.hasOption("-v") || commandLine.hasOption("--verbose")) {
- verbosity = Verbosity.VERBOSE;
- } else if (commandLine.hasOption("-q") || commandLine.hasOption("--quiet")) {
- verbosity = Verbosity.QUIET;
- }
- setupLogging(verbosity);
-
-
- // check for advance mode
- if (commandLine.hasOption("advance") || commandLine.hasOption("advanced")) {
- setAdvanceMode();
- }
-
-
- 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")) {
- cmdInstallFramework(commandLine);
- cmdFound = true;
- } else if (opt.equalsIgnoreCase("empty-framework-dir")) {
- cmdEmptyFrameworkDirectory(commandLine);
- cmdFound = true;
- } else if (opt.equalsIgnoreCase("list-frameworks")) {
- cmdListFrameworks(commandLine);
- cmdFound = true;
- } else if (opt.equalsIgnoreCase("publicize-resources")) {
- cmdPublicizeResources(commandLine);
- cmdFound = true;
- }
- }
-
-
- // if no commands ran, run the version / usage check.
- if (!cmdFound) {
- if (commandLine.hasOption("version")) {
- _version();
- System.exit(0);
- } else {
- usage();
- }
- }
- }
Main方法不长,主要分为两部分
加载命令行
匹配命令并执行相应的命令
由于我们关注的是反编译流程,所以找到反编译相关的入口代码 cmdDecode(commandLine)
- if (opt.equalsIgnoreCase("d") || opt.equalsIgnoreCase("decode")){
- cmdDecode(commandLine);
- cmdFound = true;
- }
顺带说一句,这里也可以根据我们apktool的使用命令 apktool d xxx.apk
来匹配到这一行代码
进入cmdDecode后,代码如下
- private static void cmdDecode(CommandLine cli) throws AndrolibException {
- ApkDecoder decoder = new ApkDecoder();
-
-
- int paraCount = cli.getArgList().size();
- String apkName = cli.getArgList().get(paraCount - 1);
- File outDir;
-
-
- // check for options
- if (cli.hasOption("s") || cli.hasOption("no-src")) {
- decoder.setDecodeSources(ApkDecoder.DECODE_SOURCES_NONE);
- }
- if (cli.hasOption("only-main-classes")) {
- decoder.setDecodeSources(ApkDecoder.DECODE_SOURCES_SMALI_ONLY_MAIN_CLASSES);
- }
- if (cli.hasOption("d") || cli.hasOption("debug")) {
- System.err.println("SmaliDebugging has been removed in 2.1.0 onward. Please see: https://github.com/iBotPeaches/Apktool/issues/1061");
- System.exit(1);
- }
- if (cli.hasOption("b") || cli.hasOption("no-debug-info")) {
- decoder.setBaksmaliDebugMode(false);
- }
- if (cli.hasOption("t") || cli.hasOption("frame-tag")) {
- decoder.setFrameworkTag(cli.getOptionValue("t"));
- }
- if (cli.hasOption("f") || cli.hasOption("force")) {
- decoder.setForceDelete(true);
- }
- if (cli.hasOption("r") || cli.hasOption("no-res")) {
- decoder.setDecodeResources(ApkDecoder.DECODE_RESOURCES_NONE);
- }
- if (cli.hasOption("force-manifest")) {
- decoder.setForceDecodeManifest(ApkDecoder.FORCE_DECODE_MANIFEST_FULL);
- }
- if (cli.hasOption("no-assets")) {
- decoder.setDecodeAssets(ApkDecoder.DECODE_ASSETS_NONE);
- }
- if (cli.hasOption("k") || cli.hasOption("keep-broken-res")) {
- decoder.setKeepBrokenResources(true);
- }
- if (cli.hasOption("p") || cli.hasOption("frame-path")) {
- decoder.setFrameworkDir(cli.getOptionValue("p"));
- }
- if (cli.hasOption("m") || cli.hasOption("match-original")) {
- decoder.setAnalysisMode(true);
- }
- if (cli.hasOption("api") || cli.hasOption("api-level")) {
- decoder.setApiLevel(Integer.parseInt(cli.getOptionValue("api")));
- }
- if (cli.hasOption("o") || cli.hasOption("output")) {
- outDir = new File(cli.getOptionValue("o"));
- } else {
- // make out folder manually using name of apk
- String outName = apkName;
- outName = outName.endsWith(".apk") ? outName.substring(0,
- outName.length() - 4).trim() : outName + ".out";
-
-
- // make file from path
- outName = new File(outName).getName();
- outDir = new File(outName);
- }
-
-
- decoder.setOutDir(outDir);
- decoder.setApkFile(new File(apkName));
-
-
- try {
- decoder.decode();
- } catch (OutDirExistsException ex) {
- System.err
- .println("Destination directory ("
- + outDir.getAbsolutePath()
- + ") "
- + "already exists. Use -f switch if you want to overwrite it.");
- System.exit(1);
- } catch (InFileNotFoundException ex) {
- System.err.println("Input file (" + apkName + ") " + "was not found or was not readable.");
- System.exit(1);
- } catch (CantFindFrameworkResException ex) {
- System.err
- .println("Can't find framework resources for package of id: "
- + ex.getPkgId()
- + ". You must install proper "
- + "framework files, see project website for more info.");
- System.exit(1);
- } catch (IOException ex) {
- System.err.println("Could not modify file. Please ensure you have permission.");
- System.exit(1);
- } catch (DirectoryException ex) {
- System.err.println("Could not modify internal dex files. Please ensure you have permission.");
- System.exit(1);
- } finally {
- try {
- decoder.close();
- } catch (IOException ignored) {}
- }
- }
在这里,逻辑是读取配置,初始化ApkDecoder
并设置一系列传入的参数,最后调用到decode来执行反编操作。
decoder.decode();
至此,反编译的前置流程已经结束,进入decode方法后,就是真正的反编译流程了。在开始看反编译前,简单了解一下apk里面都有哪些东西,毕竟apk文件是反编译流程的输入,还是有必要知道的。(大神可以忽略这一环节~)
话不多说,直接上图
可以看到,解压APK后的文件分成:
AndroidManifest.xml:清单文件
assets:资源文件,编译的时候不会编译assets中的文件
lib:三方库存放的地方,一般都是so库存放的位置
META-INF:包体信息,跟(V1)签名相关的文件会在这里
res:项目中的res文件目录
resources.arsc:编译后的二进制资源文件
classes.dex:源码编译后的dex文件,供Dalvik虚拟机使用
- File outDir = getOutDir();
- AndrolibResources.sKeepBroken = mKeepBrokenResources;
- if (!mForceDelete && outDir.exists()) {
- throw new OutDirExistsException();
- }
- if (!mApkFile.isFile() || !mApkFile.canRead()) {
- throw new InFileNotFoundException();
- }
-
-
- try {
- OS.rmdir(outDir);
- } catch (BrutException ex) {
- throw new AndrolibException(ex);
- }
- outDir.mkdirs();
-
-
- LOGGER.info("Using Apktool " + Androlib.getVersion() + " on " + mApkFile.getName());
- if (hasResources()) {
- switch (mDecodeResources) {
- case DECODE_RESOURCES_NONE:
- mAndrolib.decodeResourcesRaw(mApkFile, outDir);
- if (mForceDecodeManifest == FORCE_DECODE_MANIFEST_FULL) {
- // done after raw decoding of resources because copyToDir overwrites dest files
- if (hasManifest()) {
- mAndrolib.decodeManifestWithResources(mApkFile, outDir, getResTable());
- }
- }
- break;
- case DECODE_RESOURCES_FULL:
- if (hasManifest()) {
- mAndrolib.decodeManifestWithResources(mApkFile, outDir, getResTable());
- }
- 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);
- }
- }
- }
首先判断是否存在resources.arsc
文件,有则判断是否包含清单文件并解码清单文件,然后接着解码resource.arsc。由于先前执行apktool的参数中没有额外的配置,因此默认都会走到DECODE_RESOURCES_FULL
的case中
- case DECODE_RESOURCES_FULL:
- if (hasManifest()) {
- mAndrolib.decodeManifestWithResources(mApkFile, outDir, getResTable());
- }
- mAndrolib.decodeResourcesFull(mApkFile, outDir, getResTable());
- break;
至此,处理了两个文件resources.arsc
和AndroidManifest.xml
- if (hasSources()) {
- switch (mDecodeSources) {
- case DECODE_SOURCES_NONE:
- mAndrolib.decodeSourcesRaw(mApkFile, outDir, "classes.dex");
- break;
- case DECODE_SOURCES_SMALI:
- case DECODE_SOURCES_SMALI_ONLY_MAIN_CLASSES:
- mAndrolib.decodeSourcesSmali(mApkFile, outDir, "classes.dex", mBakDeb, mApiLevel);
- break;
- }
- }
首先判断是否有class.dex文件,然后执行解码操作,这里有两个情况,分别是DECODE_SOURCES_NONE
对应no-src命令,因为默认输入的命令中无此命令,故先忽略,看下面的case,解码.dex格式的文件。
可以留意一下Androlib的decodeSourcesSmali,这个方法的作用是将dex解析成smali源码。利用的是第三方库dexlib2来处理字节码,最后生成smail文件。
处理完class.dex之后,会继续处理其余dex,调用的还是decodeSourcesSmali
进行处理。
- 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")) {
- 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, mApiLevel);
- break;
- case DECODE_SOURCES_SMALI_ONLY_MAIN_CLASSES:
- if (file.startsWith("classes") && file.endsWith(".dex")) {
- mAndrolib.decodeSourcesSmali(mApkFile, outDir, file, mBakDeb, mApiLevel);
- } else {
- mAndrolib.decodeSourcesRaw(mApkFile, outDir, file);
- }
- break;
- }
- }
- }
- }
- }
至此,源码解析阶段处理了所有的class.dex文件。将smail源码解出来放到smali文件夹中
在处理完dex后,接着处理lib/libs,assets,kotlin这几个文件夹的东西
- mAndrolib.decodeRawFiles(mApkFile, outDir, mDecodeAssets);
-
-
-
-
- public void decodeRawFiles(ExtFile apkFile, File outDir, short decodeAssetMode)
- throws AndrolibException {
- LOGGER.info("Copying assets and libs...");
- try {
- Directory in = apkFile.getDirectory();
-
-
- if (decodeAssetMode == ApkDecoder.DECODE_ASSETS_FULL) {
- if (in.containsDir("assets")) {
- in.copyToDir(outDir, "assets");
- }
- }
- if (in.containsDir("lib")) {
- in.copyToDir(outDir, "lib");
- }
- if (in.containsDir("libs")) {
- in.copyToDir(outDir, "libs");
- }
- if (in.containsDir("kotlin")) {
- in.copyToDir(outDir, "kotlin");
- }
- } catch (DirectoryException ex) {
- throw new AndrolibException(ex);
- }
- }
这部分代码还是相当简单的,就是纯拷贝出去的逻辑
在Androlib中定义了正常的APK文件会有哪些文件
- private final static String[] APK_STANDARD_ALL_FILENAMES = new String[] {
- "classes.dex", "AndroidManifest.xml", "resources.arsc", "res", "r", "R",
- "lib", "libs", "assets", "META-INF", "kotlin" };
- mAndrolib.decodeUnknownFiles(mApkFile, outDir);
-
-
- public void decodeUnknownFiles(ExtFile apkFile, File outDir)
- throws AndrolibException {
- LOGGER.info("Copying unknown files...");
- File unknownOut = new File(outDir, UNK_DIRNAME);
- try {
- Directory unk = apkFile.getDirectory();
-
-
- // loop all items in container recursively, ignoring any that are pre-defined by aapt
- Set<String> files = unk.getFiles(true);
- for (String file : files) {
- if (!isAPKFileNames(file) && !file.endsWith(".dex")) {
-
-
- // copy file out of archive into special "unknown" folder
- unk.copyToDir(unknownOut, file);
- // lets record the name of the file, and its compression type
- // so that we may re-include it the same way
- mResUnknownFiles.addUnknownFileInfo(file, String.valueOf(unk.getCompressionLevel(file)));
- }
- }
- } catch (DirectoryException ex) {
- throw new AndrolibException(ex);
- }
- }
除了定义的文件,其余出现的文件,会被定义为unknown文件。这些文件会被复制到unknown文件夹中,并记录文件名和压缩类型,方便后续可以用相同的方式获取到它们
至此,还剩META-INF文件外,其他都得到了处理。但源码中还有一个记录的流程
- mUncompressedFiles = new ArrayList<>();
- mAndrolib.recordUncompressedFiles(mApkFile, mUncompressedFiles);
- mAndrolib.writeOriginalFiles(mApkFile, outDir);
- writeMetaFile();
首先,recordUncompressedFiles
记录未压缩的文件,这里的作用主要是将未压缩的文件记录到apktool.yml中的doNotCompress字段中。
然后,writeOriginalFiles
中会将META-INF文件拷贝出去。
- public void writeOriginalFiles(ExtFile apkFile, File outDir)
- throws AndrolibException {
- LOGGER.info("Copying original files...");
- File originalDir = new File(outDir, "original");
- if (!originalDir.exists()) {
- originalDir.mkdirs();
- }
-
-
- try {
- Directory in = apkFile.getDirectory();
- if (in.containsFile("AndroidManifest.xml")) {
- in.copyToDir(originalDir, "AndroidManifest.xml");
- }
- if (in.containsFile("stamp-cert-sha256")) {
- in.copyToDir(originalDir, "stamp-cert-sha256");
- }
- if (in.containsDir("META-INF")) {
- in.copyToDir(originalDir, "META-INF");
-
-
- if (in.containsDir("META-INF/services")) {
- // If the original APK contains the folder META-INF/services folder
- // that is used for service locators (like coroutines on android),
- // copy it to the destination folder so it does not get dropped.
- LOGGER.info("Copying META-INF/services directory");
- in.copyToDir(outDir, "META-INF/services");
- }
- }
- } catch (DirectoryException ex) {
- throw new AndrolibException(ex);
- }
- }
最后,writeMetaFile
生成apktool.yml,具体对应的写入逻辑下方有备注
- private void writeMetaFile() throws AndrolibException {
- MetaInfo meta = new MetaInfo();
- meta.version = Androlib.getVersion();
- meta.apkFileName = mApkFile.getName();
-
-
- if (mResTable != null) {
- meta.isFrameworkApk = mAndrolib.isFrameworkApk(mResTable);
- putUsesFramework(meta); //对应usesFramework字段
- putSdkInfo(meta); //对应sdkInfo字段
- putPackageInfo(meta); //对应packageInfo字段
- putVersionInfo(meta); //对应versionInfo字段
- putSharedLibraryInfo(meta); //对应sharedLibrary字段
- putSparseResourcesInfo(meta); //对应sparseResources字段
- } else {
- putMinSdkInfo(meta);
- }
- putUnknownInfo(meta);
- putFileCompressionInfo(meta);
-
-
- mAndrolib.writeMetaFile(mOutDir, meta);
- }
放一个apktool.yml的样例
!!brut.androlib.meta.MetaInfo apkFileName: demo.apk compressionType: false doNotCompress: - resources.arsc - png - mp4 - ogg - assets/cache/https.__res.xxh5.z7xz.com_xxh5dev/assetsid.txt - assets/cache/https.__res.xxh5.z7xz.com_xxh5dev/filetable.bin isFrameworkApk: false packageInfo: forcedPackageId: '127' renameManifestPackage: null sdkInfo: minSdkVersion: '19' targetSdkVersion: '26' sharedLibrary: false sparseResources: false unknownFiles: xxxhttp/okhttp3/internal/publicsuffix/publicsuffixes.gz: '0' usesFramework: ids: - 1 tag: null version: 2.7.0 versionInfo: versionCode: '1055' versionName: 9.6.4
可以看到,apktool的源码还是很简单明了的,反编译的主流程就是:读取命令参数 -> 生成ApkDecoder -> 反编译开始 -> 逐步处理各个文件夹/文件并输出到目标文件夹中 -> 生成apktool.yml
值得一提的是,每个文件夹/文件的处理核心逻辑都在Androlib
类中,包括但不限于dex转smail的封装、清单文件解析等。但本文的主旨只是领略apktool的反编译流程,未深入涉及到具体的每种文件处理细节,后续会针对某种文件细节会出相应的博客分析,敬请期待~
作者:37手游移动客户端团队
链接:https://juejin.cn/post/7276999034962640956
关注我获取更多知识或者投稿
{x}--{y} {{x}}---{{y}}
赞
踩
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。