当前位置:   article > 正文

抽丝剥茧看ApkTool的反编译流程

apktool
作者简介:Serpit,Android开发工程师,2023年加入37手游技术部,目前负责国内游戏发行 Android SDK 开发。

前言

由于做需求时,最近遇到了一个apktool反编译时报错,虽然问题简单,在排查解决问题的同时,借此机会顺便学习一下apktool的源码,了解apktool是如何实现反编译的。

关于apktool的使用方式,前面已有文章有相关的介绍,链接如下,感兴趣的同学可以先学习一下~

Android 逆向入门保姆级教程 (https://juejin.cn/post/7249286405007851557)

一个APK是如何诞生的

在看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

Main方法

java程序的入口都是从Main方法开始的,因此先来看看Main方法。

  1. public static void main(String[] args) throws BrutException {
  2. // headless
  3. System.setProperty("java.awt.headless", "true");
  4. // set verbosity default
  5. Verbosity verbosity = Verbosity.NORMAL;
  6. // cli parser
  7. CommandLineParser parser = new DefaultParser();
  8. CommandLine commandLine;
  9. // load options
  10. _Options();
  11. try {
  12. commandLine = parser.parse(allOptions, args, false);
  13. if (! OSDetection.is64Bit()) {
  14. System.err.println("32 bit support is deprecated. Apktool will not support 32bit on v3.0.0.");
  15. }
  16. } catch (ParseException ex) {
  17. System.err.println(ex.getMessage());
  18. usage();
  19. System.exit(1);
  20. return;
  21. }
  22. // check for verbose / quiet
  23. if (commandLine.hasOption("-v") || commandLine.hasOption("--verbose")) {
  24. verbosity = Verbosity.VERBOSE;
  25. } else if (commandLine.hasOption("-q") || commandLine.hasOption("--quiet")) {
  26. verbosity = Verbosity.QUIET;
  27. }
  28. setupLogging(verbosity);
  29. // check for advance mode
  30. if (commandLine.hasOption("advance") || commandLine.hasOption("advanced")) {
  31. setAdvanceMode();
  32. }
  33. boolean cmdFound = false;
  34. for (String opt : commandLine.getArgs()) {
  35. if (opt.equalsIgnoreCase("d") || opt.equalsIgnoreCase("decode")) {
  36. cmdDecode(commandLine);
  37. cmdFound = true;
  38. } else if (opt.equalsIgnoreCase("b") || opt.equalsIgnoreCase("build")) {
  39. cmdBuild(commandLine);
  40. cmdFound = true;
  41. } else if (opt.equalsIgnoreCase("if") || opt.equalsIgnoreCase("install-framework")) {
  42. cmdInstallFramework(commandLine);
  43. cmdFound = true;
  44. } else if (opt.equalsIgnoreCase("empty-framework-dir")) {
  45. cmdEmptyFrameworkDirectory(commandLine);
  46. cmdFound = true;
  47. } else if (opt.equalsIgnoreCase("list-frameworks")) {
  48. cmdListFrameworks(commandLine);
  49. cmdFound = true;
  50. } else if (opt.equalsIgnoreCase("publicize-resources")) {
  51. cmdPublicizeResources(commandLine);
  52. cmdFound = true;
  53. }
  54. }
  55. // if no commands ran, run the version / usage check.
  56. if (!cmdFound) {
  57. if (commandLine.hasOption("version")) {
  58. _version();
  59. System.exit(0);
  60. } else {
  61. usage();
  62. }
  63. }
  64. }

Main方法不长,主要分为两部分

  • 加载命令行

  • 匹配命令并执行相应的命令

由于我们关注的是反编译流程,所以找到反编译相关的入口代码 cmdDecode(commandLine)

  1. if (opt.equalsIgnoreCase("d") || opt.equalsIgnoreCase("decode")){
  2. cmdDecode(commandLine);
  3. cmdFound = true;
  4. }

顺带说一句,这里也可以根据我们apktool的使用命令 apktool d xxx.apk来匹配到这一行代码

反编译的准备工作

进入cmdDecode后,代码如下

  1. private static void cmdDecode(CommandLine cli) throws AndrolibException {
  2. ApkDecoder decoder = new ApkDecoder();
  3. int paraCount = cli.getArgList().size();
  4. String apkName = cli.getArgList().get(paraCount - 1);
  5. File outDir;
  6. // check for options
  7. if (cli.hasOption("s") || cli.hasOption("no-src")) {
  8. decoder.setDecodeSources(ApkDecoder.DECODE_SOURCES_NONE);
  9. }
  10. if (cli.hasOption("only-main-classes")) {
  11. decoder.setDecodeSources(ApkDecoder.DECODE_SOURCES_SMALI_ONLY_MAIN_CLASSES);
  12. }
  13. if (cli.hasOption("d") || cli.hasOption("debug")) {
  14. System.err.println("SmaliDebugging has been removed in 2.1.0 onward. Please see: https://github.com/iBotPeaches/Apktool/issues/1061");
  15. System.exit(1);
  16. }
  17. if (cli.hasOption("b") || cli.hasOption("no-debug-info")) {
  18. decoder.setBaksmaliDebugMode(false);
  19. }
  20. if (cli.hasOption("t") || cli.hasOption("frame-tag")) {
  21. decoder.setFrameworkTag(cli.getOptionValue("t"));
  22. }
  23. if (cli.hasOption("f") || cli.hasOption("force")) {
  24. decoder.setForceDelete(true);
  25. }
  26. if (cli.hasOption("r") || cli.hasOption("no-res")) {
  27. decoder.setDecodeResources(ApkDecoder.DECODE_RESOURCES_NONE);
  28. }
  29. if (cli.hasOption("force-manifest")) {
  30. decoder.setForceDecodeManifest(ApkDecoder.FORCE_DECODE_MANIFEST_FULL);
  31. }
  32. if (cli.hasOption("no-assets")) {
  33. decoder.setDecodeAssets(ApkDecoder.DECODE_ASSETS_NONE);
  34. }
  35. if (cli.hasOption("k") || cli.hasOption("keep-broken-res")) {
  36. decoder.setKeepBrokenResources(true);
  37. }
  38. if (cli.hasOption("p") || cli.hasOption("frame-path")) {
  39. decoder.setFrameworkDir(cli.getOptionValue("p"));
  40. }
  41. if (cli.hasOption("m") || cli.hasOption("match-original")) {
  42. decoder.setAnalysisMode(true);
  43. }
  44. if (cli.hasOption("api") || cli.hasOption("api-level")) {
  45. decoder.setApiLevel(Integer.parseInt(cli.getOptionValue("api")));
  46. }
  47. if (cli.hasOption("o") || cli.hasOption("output")) {
  48. outDir = new File(cli.getOptionValue("o"));
  49. } else {
  50. // make out folder manually using name of apk
  51. String outName = apkName;
  52. outName = outName.endsWith(".apk") ? outName.substring(0,
  53. outName.length() - 4).trim() : outName + ".out";
  54. // make file from path
  55. outName = new File(outName).getName();
  56. outDir = new File(outName);
  57. }
  58. decoder.setOutDir(outDir);
  59. decoder.setApkFile(new File(apkName));
  60. try {
  61. decoder.decode();
  62. } catch (OutDirExistsException ex) {
  63. System.err
  64. .println("Destination directory ("
  65. + outDir.getAbsolutePath()
  66. + ") "
  67. + "already exists. Use -f switch if you want to overwrite it.");
  68. System.exit(1);
  69. } catch (InFileNotFoundException ex) {
  70. System.err.println("Input file (" + apkName + ") " + "was not found or was not readable.");
  71. System.exit(1);
  72. } catch (CantFindFrameworkResException ex) {
  73. System.err
  74. .println("Can't find framework resources for package of id: "
  75. + ex.getPkgId()
  76. + ". You must install proper "
  77. + "framework files, see project website for more info.");
  78. System.exit(1);
  79. } catch (IOException ex) {
  80. System.err.println("Could not modify file. Please ensure you have permission.");
  81. System.exit(1);
  82. } catch (DirectoryException ex) {
  83. System.err.println("Could not modify internal dex files. Please ensure you have permission.");
  84. System.exit(1);
  85. } finally {
  86. try {
  87. decoder.close();
  88. } catch (IOException ignored) {}
  89. }
  90. }

在这里,逻辑是读取配置,初始化ApkDecoder并设置一系列传入的参数,最后调用到decode来执行反编操作。

decoder.decode();

至此,反编译的前置流程已经结束,进入decode方法后,就是真正的反编译流程了。在开始看反编译前,简单了解一下apk里面都有哪些东西,毕竟apk文件是反编译流程的输入,还是有必要知道的。(大神可以忽略这一环节~)

APK的组成

话不多说,直接上图

758cf43db2533fa16d6e6f0e2f975ceb.jpeg

可以看到,解压APK后的文件分成:

  • AndroidManifest.xml:清单文件

  • assets:资源文件,编译的时候不会编译assets中的文件

  • lib:三方库存放的地方,一般都是so库存放的位置

  • META-INF:包体信息,跟(V1)签名相关的文件会在这里

  • res:项目中的res文件目录

  • resources.arsc:编译后的二进制资源文件

  • classes.dex:源码编译后的dex文件,供Dalvik虚拟机使用

decode

创建反编译的输出路径

  1. File outDir = getOutDir();
  2. AndrolibResources.sKeepBroken = mKeepBrokenResources;
  3. if (!mForceDelete && outDir.exists()) {
  4. throw new OutDirExistsException();
  5. }
  6. if (!mApkFile.isFile() || !mApkFile.canRead()) {
  7. throw new InFileNotFoundException();
  8. }
  9. try {
  10. OS.rmdir(outDir);
  11. } catch (BrutException ex) {
  12. throw new AndrolibException(ex);
  13. }
  14. outDir.mkdirs();
  15. LOGGER.info("Using Apktool " + Androlib.getVersion() + " on " + mApkFile.getName());

资源解析

  1. if (hasResources()) {
  2. switch (mDecodeResources) {
  3. case DECODE_RESOURCES_NONE:
  4. mAndrolib.decodeResourcesRaw(mApkFile, outDir);
  5. if (mForceDecodeManifest == FORCE_DECODE_MANIFEST_FULL) {
  6. // done after raw decoding of resources because copyToDir overwrites dest files
  7. if (hasManifest()) {
  8. mAndrolib.decodeManifestWithResources(mApkFile, outDir, getResTable());
  9. }
  10. }
  11. break;
  12. case DECODE_RESOURCES_FULL:
  13. if (hasManifest()) {
  14. mAndrolib.decodeManifestWithResources(mApkFile, outDir, getResTable());
  15. }
  16. mAndrolib.decodeResourcesFull(mApkFile, outDir, getResTable());
  17. break;
  18. }
  19. } else {
  20. // if there's no resources.arsc, decode the manifest without looking
  21. // up attribute references
  22. if (hasManifest()) {
  23. if (mDecodeResources == DECODE_RESOURCES_FULL
  24. || mForceDecodeManifest == FORCE_DECODE_MANIFEST_FULL) {
  25. mAndrolib.decodeManifestFull(mApkFile, outDir, getResTable());
  26. }
  27. else {
  28. mAndrolib.decodeManifestRaw(mApkFile, outDir);
  29. }
  30. }
  31. }

首先判断是否存在resources.arsc文件,有则判断是否包含清单文件并解码清单文件,然后接着解码resource.arsc。由于先前执行apktool的参数中没有额外的配置,因此默认都会走到DECODE_RESOURCES_FULL的case中

  1. case DECODE_RESOURCES_FULL:
  2. if (hasManifest()) {
  3. mAndrolib.decodeManifestWithResources(mApkFile, outDir, getResTable());
  4. }
  5. mAndrolib.decodeResourcesFull(mApkFile, outDir, getResTable());
  6. break;

至此,处理了两个文件resources.arscAndroidManifest.xml

源码解析

  1. if (hasSources()) {
  2. switch (mDecodeSources) {
  3. case DECODE_SOURCES_NONE:
  4. mAndrolib.decodeSourcesRaw(mApkFile, outDir, "classes.dex");
  5. break;
  6. case DECODE_SOURCES_SMALI:
  7. case DECODE_SOURCES_SMALI_ONLY_MAIN_CLASSES:
  8. mAndrolib.decodeSourcesSmali(mApkFile, outDir, "classes.dex", mBakDeb, mApiLevel);
  9. break;
  10. }
  11. }

首先判断是否有class.dex文件,然后执行解码操作,这里有两个情况,分别是DECODE_SOURCES_NONE对应no-src命令,因为默认输入的命令中无此命令,故先忽略,看下面的case,解码.dex格式的文件。

可以留意一下Androlib的decodeSourcesSmali,这个方法的作用是将dex解析成smali源码。利用的是第三方库dexlib2来处理字节码,最后生成smail文件。

处理完class.dex之后,会继续处理其余dex,调用的还是decodeSourcesSmali进行处理。

  1. if (hasMultipleSources()) {
  2. // foreach unknown dex file in root, lets disassemble it
  3. Set<String> files = mApkFile.getDirectory().getFiles(true);
  4. for (String file : files) {
  5. if (file.endsWith(".dex")) {
  6. if (! file.equalsIgnoreCase("classes.dex")) {
  7. switch(mDecodeSources) {
  8. case DECODE_SOURCES_NONE:
  9. mAndrolib.decodeSourcesRaw(mApkFile, outDir, file);
  10. break;
  11. case DECODE_SOURCES_SMALI:
  12. mAndrolib.decodeSourcesSmali(mApkFile, outDir, file, mBakDeb, mApiLevel);
  13. break;
  14. case DECODE_SOURCES_SMALI_ONLY_MAIN_CLASSES:
  15. if (file.startsWith("classes") && file.endsWith(".dex")) {
  16. mAndrolib.decodeSourcesSmali(mApkFile, outDir, file, mBakDeb, mApiLevel);
  17. } else {
  18. mAndrolib.decodeSourcesRaw(mApkFile, outDir, file);
  19. }
  20. break;
  21. }
  22. }
  23. }
  24. }
  25. }

至此,源码解析阶段处理了所有的class.dex文件。将smail源码解出来放到smali文件夹中

RawFile处理

在处理完dex后,接着处理lib/libs,assets,kotlin这几个文件夹的东西

  1. mAndrolib.decodeRawFiles(mApkFile, outDir, mDecodeAssets);
  2. public void decodeRawFiles(ExtFile apkFile, File outDir, short decodeAssetMode)
  3. throws AndrolibException {
  4. LOGGER.info("Copying assets and libs...");
  5. try {
  6. Directory in = apkFile.getDirectory();
  7. if (decodeAssetMode == ApkDecoder.DECODE_ASSETS_FULL) {
  8. if (in.containsDir("assets")) {
  9. in.copyToDir(outDir, "assets");
  10. }
  11. }
  12. if (in.containsDir("lib")) {
  13. in.copyToDir(outDir, "lib");
  14. }
  15. if (in.containsDir("libs")) {
  16. in.copyToDir(outDir, "libs");
  17. }
  18. if (in.containsDir("kotlin")) {
  19. in.copyToDir(outDir, "kotlin");
  20. }
  21. } catch (DirectoryException ex) {
  22. throw new AndrolibException(ex);
  23. }
  24. }

这部分代码还是相当简单的,就是纯拷贝出去的逻辑

处理未知文件

在Androlib中定义了正常的APK文件会有哪些文件

  1. private final static String[] APK_STANDARD_ALL_FILENAMES = new String[] {
  2. "classes.dex", "AndroidManifest.xml", "resources.arsc", "res", "r", "R",
  3. "lib", "libs", "assets", "META-INF", "kotlin" };
  1. mAndrolib.decodeUnknownFiles(mApkFile, outDir);
  2. public void decodeUnknownFiles(ExtFile apkFile, File outDir)
  3. throws AndrolibException {
  4. LOGGER.info("Copying unknown files...");
  5. File unknownOut = new File(outDir, UNK_DIRNAME);
  6. try {
  7. Directory unk = apkFile.getDirectory();
  8. // loop all items in container recursively, ignoring any that are pre-defined by aapt
  9. Set<String> files = unk.getFiles(true);
  10. for (String file : files) {
  11. if (!isAPKFileNames(file) && !file.endsWith(".dex")) {
  12. // copy file out of archive into special "unknown" folder
  13. unk.copyToDir(unknownOut, file);
  14. // lets record the name of the file, and its compression type
  15. // so that we may re-include it the same way
  16. mResUnknownFiles.addUnknownFileInfo(file, String.valueOf(unk.getCompressionLevel(file)));
  17. }
  18. }
  19. } catch (DirectoryException ex) {
  20. throw new AndrolibException(ex);
  21. }
  22. }

除了定义的文件,其余出现的文件,会被定义为unknown文件。这些文件会被复制到unknown文件夹中,并记录文件名和压缩类型,方便后续可以用相同的方式获取到它们

处理META-INF&记录信息

至此,还剩META-INF文件外,其他都得到了处理。但源码中还有一个记录的流程

  1. mUncompressedFiles = new ArrayList<>();
  2. mAndrolib.recordUncompressedFiles(mApkFile, mUncompressedFiles);
  3. mAndrolib.writeOriginalFiles(mApkFile, outDir);
  4. writeMetaFile();

首先,recordUncompressedFiles记录未压缩的文件,这里的作用主要是将未压缩的文件记录到apktool.yml中的doNotCompress字段中。

然后,writeOriginalFiles中会将META-INF文件拷贝出去。

  1. public void writeOriginalFiles(ExtFile apkFile, File outDir)
  2. throws AndrolibException {
  3. LOGGER.info("Copying original files...");
  4. File originalDir = new File(outDir, "original");
  5. if (!originalDir.exists()) {
  6. originalDir.mkdirs();
  7. }
  8. try {
  9. Directory in = apkFile.getDirectory();
  10. if (in.containsFile("AndroidManifest.xml")) {
  11. in.copyToDir(originalDir, "AndroidManifest.xml");
  12. }
  13. if (in.containsFile("stamp-cert-sha256")) {
  14. in.copyToDir(originalDir, "stamp-cert-sha256");
  15. }
  16. if (in.containsDir("META-INF")) {
  17. in.copyToDir(originalDir, "META-INF");
  18. if (in.containsDir("META-INF/services")) {
  19. // If the original APK contains the folder META-INF/services folder
  20. // that is used for service locators (like coroutines on android),
  21. // copy it to the destination folder so it does not get dropped.
  22. LOGGER.info("Copying META-INF/services directory");
  23. in.copyToDir(outDir, "META-INF/services");
  24. }
  25. }
  26. } catch (DirectoryException ex) {
  27. throw new AndrolibException(ex);
  28. }
  29. }

最后,writeMetaFile生成apktool.yml,具体对应的写入逻辑下方有备注

  1. private void writeMetaFile() throws AndrolibException {
  2. MetaInfo meta = new MetaInfo();
  3. meta.version = Androlib.getVersion();
  4. meta.apkFileName = mApkFile.getName();
  5. if (mResTable != null) {
  6. meta.isFrameworkApk = mAndrolib.isFrameworkApk(mResTable);
  7. putUsesFramework(meta); //对应usesFramework字段
  8. putSdkInfo(meta); //对应sdkInfo字段
  9. putPackageInfo(meta); //对应packageInfo字段
  10. putVersionInfo(meta); //对应versionInfo字段
  11. putSharedLibraryInfo(meta); //对应sharedLibrary字段
  12. putSparseResourcesInfo(meta); //对应sparseResources字段
  13. } else {
  14. putMinSdkInfo(meta);
  15. }
  16. putUnknownInfo(meta);
  17. putFileCompressionInfo(meta);
  18. mAndrolib.writeMetaFile(mOutDir, meta);
  19. }

放一个apktool.yml的样例

  1. !!brut.androlib.meta.MetaInfo
  2. apkFileName: demo.apk
  3. compressionType: false
  4. doNotCompress:
  5. - resources.arsc
  6. - png
  7. - mp4
  8. - ogg
  9. - assets/cache/https.__res.xxh5.z7xz.com_xxh5dev/assetsid.txt
  10. - assets/cache/https.__res.xxh5.z7xz.com_xxh5dev/filetable.bin
  11. isFrameworkApk: false
  12. packageInfo:
  13. forcedPackageId: '127'
  14. renameManifestPackage: null
  15. sdkInfo:
  16. minSdkVersion: '19'
  17. targetSdkVersion: '26'
  18. sharedLibrary: false
  19. sparseResources: false
  20. unknownFiles:
  21. xxxhttp/okhttp3/internal/publicsuffix/publicsuffixes.gz: '0'
  22. usesFramework:
  23. ids:
  24. - 1
  25. tag: null
  26. version: 2.7.0
  27. versionInfo:
  28. versionCode: '1055'
  29. versionName: 9.6.4

小结

可以看到,apktool的源码还是很简单明了的,反编译的主流程就是:读取命令参数 -> 生成ApkDecoder -> 反编译开始 -> 逐步处理各个文件夹/文件并输出到目标文件夹中 -> 生成apktool.yml

值得一提的是,每个文件夹/文件的处理核心逻辑都在Androlib类中,包括但不限于dex转smail的封装、清单文件解析等。但本文的主旨只是领略apktool的反编译流程,未深入涉及到具体的每种文件处理细节,后续会针对某种文件细节会出相应的博客分析,敬请期待~

作者:37手游移动客户端团队
链接:https://juejin.cn/post/7276999034962640956

关注我获取更多知识或者投稿

e3843b97ce2f1995a214c4dedd9345ab.jpeg

cd1445fa0bc982332a9fc3f0e9b3346a.jpeg

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/IT小白/article/detail/493380
推荐阅读