TA的每日心情 | 开心 昨天 00:08 |
---|
签到天数: 362 天 [LV.8]以坛为家I
管理员
  
- 积分
- 160408
|
前言
h+ v" ~& o7 t
1 |; r8 h+ r+ C& t8 c9 F' H( F看到这篇技术文章皆是缘分。本人在一家研运一体的游戏公司做安卓游戏SDK,并不是安卓逆向从业人员。工作中经常使用Apktool工具,写这一篇技术文纯粹是好奇心作祟,好奇这东西是什么原理,怎么做到把Apk拆解成最原始的样子。" S) U3 O" G/ S* P- k
Apktool它是一个开源的逆向工具,Java写出来的。多么强大我就不多说,能找到这里说明你应该知道它是做什么的。在写文之前看过很多,也搜过很多技术大佬的文章,分析的过程并没有让我的好奇心得到满足。于是我把源码Clone下来分析一下。单纯的记录一下分析过程还有产生的疑问,没准那一次忘了,回头看看自己写的文章会有不同的感受。- Q" o6 }. b, \' f1 v; g
Apk的构建流程6 b& b7 @9 U% i$ n, ] \
$ h: S6 k/ C8 F9 f' q# j新版官网Apk构建流程(常识部分)
) v g% [: n1 V+ D4 K( W# m' E
* n4 k* F# [/ X: D2 L7 z6 c. Z3 E构建流程涉及许多将项目转换成 Android 应用软件包 (APK) 的工具和流程。构建流程非常灵活,因此了解它的一些底层工作原理会很有帮助。(熟悉流程的可以忽略此部分,继续往下观看)
5 p- a# X4 A8 b5 ^1 f4 m: g9 yAndroid 应用模块的构建流程如上图所示,按照如上常规步骤执行:(如下步骤均来源于安卓官网,我只是做一个搬运工)
9 {4 ]& i% X7 ?+ b
4 I4 C; _$ h. M/ L" U0 N- 编译器将您的源代码转换成 DEX 文件(Dalvik 可执行文件,其中包括在 Android 设备上运行的字节码),并将其他所有内容转换成编译后的资源。
/ L7 t3 P5 ?; Z - APK 打包器将 DEX 文件和编译后的资源组合成单个 APK。不过,必须先为 APK 签名,然后才能将应用安装并部署到 Android 设备上。+ E+ \4 {9 e; ?4 P. |4 L
- APK 打包器使用调试或发布密钥库为 APK 签名:
) u& A8 F, e5 @& i7 W1 w, }1 o* u8 t! A
- 如果你构建的是调试版应用(即专用于测试和分析的应用),则打包器会使用调试密钥库为应用签名。Android Studio 会自动使用调试密钥库配置新项目。: t0 j/ ?& W4 b: ~
- 如果你构建的是打算对外发布的发布版应用,则打包器会使用发布密钥库为应用签名。; Z; {7 l& k) S2 a7 |* v
# s+ X3 ]7 H8 p
- 在生成最终 APK 之前,打包器会使用 zipalign 工具对应用进行优化,以减少其在设备上运行时所占用的内存。$ _5 d, s9 B5 ?8 ]( r+ J+ w" M
老版官网Apk构建流程(了解部分)' L9 E. C6 N1 G2 V5 ]8 Z
; D% }) [1 U$ p/ h
安卓官网只能找到新版本的apk构建流程,老版本的流程图在官网找了半天也没找到。于是求助了一下Google大佬。找到了老版本的流程图,下图就是老版本的apk构建流程。(熟悉流程的可以忽略此部分,继续往下观看)
. [8 B; B+ @0 c' j上图可以分为七个大模块:
! N# Y, e1 a; T/ K2 _
* V( a+ T4 @6 v; i: q |$ b) u- 处理资源相关文件,生成R.java: aapt来打包res资源文件,生成R.java、resources.arsc和res文件
& [1 b( X- C6 C - 处理aidl文件,生成相应的Java文件 :aidl工具解析接口定义文件然后生成相应的Java代码接口供程序调用
X6 k9 x) T2 [. b/ G - 编译项目源代码,生成class文件 :Java Compiler阶段。项目中所有的Java代码,包括R.java和.aidl文件,javac编译成.class文件: R* U* J4 }1 h' C7 L, {
- 转换所有的class文件,生成classes.dex文件 :通过dx工具,将.class文件和第三方库中的.class文件处理生成classes.dex文件。(新版已经被d8代替)8 p( a. o9 f3 ]# d4 x+ R ?' s
- 打包生成APK文件 :通过apkbuilder工具,将aapt生成的resources.arsc和res文件、assets文件和classes.dex一起打包生成apk
, R& R7 }' B! O. P; d1 r - 对APK文件进行签名 :通过Jarsigner工具,对上面的apk进行debug或release签名。(新版已经被apksigner代替)
0 K* q+ @: d* F; k5 }) r - 对签名后的APK文件进行对齐处理 :通过zipalign工具,将Android 应用 (APK) 文件提供重要的优化。其目的是要确保所有未压缩数据的开头均相对于文件开头部分执行特定的对齐。具体来说,它会使 APK 中的所有未压缩数据(例如图片或原始文件)在 4 字节边界上对齐。
- L) z% K# I) p4 g# R2 v5 {9 L7 K 敲重点:明明要说Apktool 源码?怎么单独写了一章无关紧要的apk构建流程?这并不冲突,只是做了一个铺垫。首先你要知道apk是如何构建,知道如何构建(怎么来的)才能更好理解Apktool逆向。而这一章节最关键的就是AAPT概念。后续源码中解码和构建都离不开aapt工具。那AAPT是什么呢,我做下简单介绍:
% U. |) l% O% u* zAAPT(Android 资源打包工具)是一种构建工具,Android Studio 和 Android Gradle 插件使用它来编译和打包应用的资源。AAPT 会解析资源、为资源编制索引,并将资源编译为针对 Android 平台进行过优化的二进制格式。
3 g* F; q9 N) G s$ f9 XApktool源码解析. T/ d% Z4 Q7 K; g- C; R% x5 G
! ~, J. m+ @1 I+ M+ B: B- `' ]
本次演示的源码为最新版v2.5.1 Master,大家有兴趣的话可以看下我以前分享过的文章 Apktool源码下载与编译
/ x* C& B k( hApktool工程项目介绍(了解部分,不感兴趣的往下看)
" \: _( c# ~8 |3 D
- _; a4 b0 E! |7 r: ^) @. h+ R3 W7 DApktool源码下载我这里就不做演示,下图就是下载完成后的文件夹结构图
5 i3 a5 e0 s* L0 h" mApktool是java项目同时也是gradle项目,下图是项目引用的第三方库. S1 C- r8 b3 r4 ?
//全局自定义部分 ext { depends = [ baksmali: 'org.smali:baksmali:2.4.0', commons_cli: 'commons-cli:commons-cli:1.4', commons_io: 'commons-io:commons-io:2.4', commons_lang: 'org.apache.commons:commons-lang3:3.1', guava: 'com.google.guava:guava:14.0', junit: 'junit:junit:4.12', proguard_gradle: 'com.guardsquare:proguard-gradle:7.0.0', snakeyaml: 'org.yaml:snakeyaml:1.18:android', smali: 'org.smali:smali:2.4.0', xmlpull: 'xpp3:xpp3:1.1.4c', xmlunit: 'xmlunit:xmlunit:1.6', ] }//依赖部分 api project(':brut.j.dir'), project(':brut.j.util'), project(':brut.j.common')implementation depends.baksmali, depends.smali, depends.snakeyaml, depends.xmlpull, depends.guava, depends.commons_lang, depends.commons_io根据上面的第三方库重要的做下简单介绍! S, }4 ]/ f, d P# p/ ]% o
7 h* |: d- z- p$ H4 X. q) K
- baksmali :是dalvik(Android的Java VM实现)使用的dex格式的汇编程序/反汇编程序,项目中dex文件解析靠的就是它
* q. Y+ \0 ~8 `; W! t - commons_cli : Apache开源组织提供的用于解析命令行参数的库
. f' Z8 Y, J# `5 T - snakeyaml :配置文件解释器。对应就是反编译后的apktool.yml文件
6 h( |7 D7 Q7 m. h$ t, @: @ - guava : Google的 Java项目广泛依赖 的核心库. I+ G, t) T3 L) G' c L4 T' ^
- xmlpull : xml解析框架,项目中用于解析清单文件还有xml等
5 `6 S/ w$ z: W8 q Apktool工程结构图
( @0 {! [) @' H, l* O针对上图的结构做下说明1 _& H7 }/ l7 f3 }: h; r
9 ^& i; |: |' B- brut.apktool : apktool核心工程
7 m: T w* r4 J0 i: D# T
/ `# _$ }8 Y5 v1 ?9 P/ A. a- apktool-cli :这部分的工程只有一个main.java 程序的主入口
& y; e. |% R3 A3 P% }, c$ d2 M - apktool-lib : 这部分的工程内容最为丰富,包含了apktool中所有的命令 和交互调用的类,还有项目自带的三大系统的aapt和aapt2工具,同时还包含系统框架android-framework.jar+ v- v' |0 z0 a2 v- X2 Y% d) H2 {
; j$ l& z) z# p2 D% x4 b% q7 R6 H5 G% q - brut.j.common : 有关异常定义和处理的工具类,服务于brut.apktool.7 l/ p" p7 x9 y) d: \+ B
- brut.j.dir : 有关文件方面工具类,服务于brut.apktool .
+ O8 y/ ~6 X6 K" ~: g - brut.j.util :工程通用工具类,服务于brut.apktool .0 \+ z% t0 Z8 ]# J1 a" F
主函数Main逻辑(重要部分)- T! Y% T; _. J: Q) {
; a" R5 g* q7 i; z2 n
既然是Java项目程序入口肯定是Main(),可得知入口类为brut.apktool.Main。还有一种方式我们可以通过生成的jar包来查找程序入口(既然下载了源码,这种方式不建议了。)
: }' \2 ~ r. u& p public static void main(String[] args) throws IOException, InterruptedException, BrutException { //通俗的解释这句话的意思就是:开启了headless模式,就不要指望硬件帮忙,你需要自立更生,依靠系统的计算能力模拟出这些特性 System.setProperty("java.awt.headless", "true"); //设置为默认模式 Verbosity verbosity = Verbosity.NORMAL; //创建cli命令行解释器 CommandLineParser parser = new DefaultParser(); CommandLine commandLine; // 加载命令行选项 _Options(); try { //参数1:全局option 用来遍历参数是否包含此选项 // 参数2:解析命令行输入的参数 // 参数3:布尔类型的参数,意思无法识别的参数将抛出异常(可以去看源码参数描述) commandLine = parser.parse(allOptions, args, false); //根据指定的选项解析参数 if (! OSDetection.is64Bit()) { //如果不是64位操作系统 抛出异常 System.err.println("32 bit support is deprecated. Apktool will not support 32bit on v2.6.0.");//简单说就是不支持32位系统 } } catch (ParseException ex) { //出现解析异常打印帮助消息 System.err.println(ex.getMessage()); usage(); //出现异常则打印帮助信息 System.exit(1); //异常下退出程序 return; } /** * 检查命令行是否有详细模式或者静默模式命令 * hasOption() 判断是否含有指定的参数,判断命令行是否出现该命令。出现则返回true,否则返回false */ if (commandLine.hasOption("-v") || commandLine.hasOption("--verbose")) { verbosity = Verbosity.VERBOSE; //详细模式展示命令行 } else if (commandLine.hasOption("-q") || commandLine.hasOption("--quiet")) { verbosity = Verbosity.QUIET; //详细模式展示命令行 } setupLogging(verbosity); //设置日志模式 //检测高级模式,如果输入的命令是apktool advance或者apktool advanced 那么就设置成高级选项模式 if (commandLine.hasOption("advance") || commandLine.hasOption("advanced")) { setAdvanceMode(); } boolean cmdFound = false; for (String opt : commandLine.getArgs()) { //例如 执行apktool d or decode xx.apk时 命令行参数大小写都可以 //equalsIgnoreCase() 方法用于将字符串与指定的对象比较,不考虑大小写。 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; } } //如果未运行任何命令,请运行aktool -version 命令检查使用情况 if (!cmdFound) { if (commandLine.hasOption("version")) { //如果输入命令 apktool -version 展示apktool版本号 _version(); System.exit(0); } else { usage(); //展示参数详细用法 } } } //省略部分。。。。。 上述代码为主函数中部分代码截取。为什么截取部分代码?首先Main.java代码量加上注释大约有800行代码,放上来占地不说而且看着乱。然而我觉得这样做没必要,代码大家都能下载,能进来的都是技术,有谁又看不懂呢?个人觉得主要来分析一下大体的逻辑还有一些细节,这很重要!!!" I5 D5 y* ^. Y! u
主函数(Main.java)做了什么逻辑,我从上到下给大家梳理一下" [ U" [ Z8 C- W+ Q" w" x
6 ~8 q4 n. g4 x$ X! r- 设置模式选项 。 针对的是日志级别打印。其中包括三项模式 默认模式(NORMAL) 、详细模式(VERBOSE) 、 静默模式(QUIET)& ^( P( T) f8 S2 K, r7 A
- 创建cli命令行解析器。包括定义阶段(创建命令行选项)、解析阶段(解析命令行参数)、询问阶段(判断命令行出现了哪个选项)、 进阶部分(获得参数值)、帮助信息等。我笼统的说了一下,英语好的同学可以去Apache Commons CLI官网去看Doc,英语不好的可以Google搜索一下文档语法。2 _" s( C1 f) F5 ^ T5 m
( e$ ~0 N3 U- ^1 H1 ~% E/ L: M- 自定义异常 ,针对可能出现的问题捕获异常。2 T) E- B. B& D( ^4 h
2 B, _" p2 ?9 e3 ^- 创建Apktool 五种常规的用法(附上对应的方法名)
( U2 f3 s4 c _, ~4 ~
( w$ | f/ H) n- a9 s& C& q9 c, k5 U- 解码: cmdDecode(CommandLine cli)0 H$ K5 d* g. ~6 q# E
- 构建 : cmdBuild(CommandLine cli)* k' ^9 J0 R7 b; u
- 安装框架: cmdInstallFramework(CommandLine cli)
2 C B5 s6 p3 r5 y0 h - 清除框架目录 : cmdEmptyFrameworkDirectory(CommandLine cli)
/ ]( j8 T! A! N" h. } - 列出框架目录(v2.5.0新增): cmdListFrameworks(CommandLine cli)
! [8 p Z- x" @" G5 a+ C% ?
4 y, s+ q+ I' n% L( K( X P F 其实上述五种常规用法,最核心的就是解码(反编译)和构建(回编),余下的属于附属用法。重点说一下解码和构建的逻辑 。至于框架是什么?往下看会解释。apktool设定的命令可以看一下我之前发过的博客、Apktool命令大全。一定要看 ,与下面代码讲解有关联,方便大家理解。
" t8 [, }+ K/ O0 i* t帮助文档(了解部分)
9 V7 }) i9 |( H/ S8 @: T; x y ?
如下截图相信大家非常熟悉,这一部分单独写出来纯属很经典。这部分是描述apktool的帮助文档,只要输入apktool即可展示。详细可以看下代码usage()方法
" X( [; s, m8 [6 |' A解码调用逻辑5 ^& t$ v& v0 B5 r
& c, e6 ?. o/ s/ B+ y7 i8 S
下面就是解码部分代码。传入命令行获的值、实例解码核心类ApkDecoder 、设置解码命令参数、创建输出目录、最终调用到decoder.decode()' n5 y* }: S5 `
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; //.........省略部分 String outName = apkName; //使用apk名称创建一个新文件夹 outName = outName.endsWith(".apk") ? outName.substring(0, outName.length() - 4).trim() : outName + ".out"; //从路径创建文件 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: " + String.valueOf(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) {} }Apk文件结构(了解部分,铺垫下文), S7 w' C7 m* h* D& [' F+ {
* A6 N) ~, k" p9 l3 h/ _5 e开篇简述了Apk文件是怎么来的。这部分章节简述一下Apk文件结构,这有助对下文的理解,熟悉这部分可以跳过。$ d2 K1 j- j& S7 _
APK是AndroidPackage的缩写,即Android安装包(apk)。APK是类似Symbian Sis或Sisx的文件格式。通过将APK文件直接传到Android模拟器或Android手机中执行即可安装。/ ~# i9 u6 m, o
apk文件和sis一样,把android sdk编译的工程打包成一个安装程序文件,格式为apk。APK文件其实是zip格式,但后缀名被修改为apk,通过UnZip解压后,可以看到如下:6 v' B5 r! y7 h" ]
9 p3 D9 z* ^) H, K$ X$ Q6 O @1 w' @+ h4 S
- assets :存放资源文件,系统在编译的时候不会编译assets下的资源文件;0 P8 a6 M4 t8 B- T# ^
- lib或者libs :用来存放三方库的地方;
7 V! o' O* \: D O6 n - META-INF :描述包信息的目录;
/ j, Y; {" H7 ?: i* w( M7 }( A/ q - res :项目中的资源文件夹;
! W+ b2 J9 P5 h. G5 U - AndroidManifest.xml :功能清单文件; |. L# [ _2 H+ t
- classes.dex :包含所有class的文件,供DVM执行;
+ \9 k6 `" Y- C* Y4 N4 w' m - resources.arsc :编译后的二进制资源文件;
. I0 x! c( R* W" C1 V( J+ B' g' R2 }
6 a( K, H: y/ Q8 W- {# W
# v/ X' W. E1 z# l6 v' RApkDecoder类中的解码逻辑(耐心读完)
4 T+ ?6 ?. N/ ?" \
: X; u k% |: t1 j3 Z8 i0 t: e粗略看下整体的逻辑然后在细扒。先从hasResources()解析资源文件判断条件成立看起,hasResources()上面代码我就不说了注释非常齐全。如何判断条件是否成立呢? 逻辑很简单,该文件夹里面是否包含resources.arsc(上一章已经讲述过resources.arsc是什么)。继续往下看,有两个判断值 DECODE_RESOURCES_NONE(不需要解码资源)和DECODE_RESOURCES_FULL(完整的解码资源)。
) X& k u H/ ]# p* Y' i /** * 执行反编译主逻辑 * @throws AndrolibException * @throws IOException * @throws DirectoryException */ public void decode() throws AndrolibException, IOException, DirectoryException { try { //获取输出目录。执行的逻辑:如果指定了具体路径 (-o命令)用指定目录,没有指定就用默认目录。可看下Main代码.setOutDir() File outDir = getOutDir(); AndrolibResources.sKeepBroken = mKeepBrokenResources; //keep-broken-res跟这个命令有关 ,可以看下对应引用关系 if (!mForceDelete && outDir.exists()) { //如果当前反编译apk名称目录存在 ,抛出异常 throw new OutDirExistsException(); } if (!mApkFile.isFile() || !mApkFile.canRead()) { //找不到反编译apk文件或apk文件不可读 抛异常 对应的可以去看Main.java throw new InFileNotFoundException(); } try { OS.rmdir(outDir); // 如果文件夹不存在return,存在递归删除 } catch (BrutException ex) { throw new AndrolibException(ex); } outDir.mkdirs(); //创建多层文件夹 LOGGER.info("Using Apktool " + Androlib.getVersion() + " on " + mApkFile.getName());先说DECODE_RESOURCES_NONE(不需要解码资源)判断值,正常我们输入apktool d xx.apk没有额外命令时候一定不会走这个条件值,只有加入了no-res命令才会执行。实例Androlib对象(decodeResourcesRaw方法),既然不解码资源那么直接复制"resources.arsc", "AndroidManifest.xml", "res"三个文件里的内容到指定文件夹里。如果在额外加入了force-manifest解析清单文件命令,则会加一层判断,如果文件中包含清单文件,才能真正执行解码清单文件的逻辑。. N( f; K; }2 B2 \7 c( S
再说下DECODE_RESOURCES_FULL(完整解码资源文件)判断值,正常我们输入apktool d xx.apk一定会走这个条件值。实例Androlib对象,这个条件值只做了两件事 ,第一件 判断文件中是否包含清单文件,有的话直接解码清单文件。第二件就是执行解码resource.arsc的逻辑。3 ]4 N, S7 Z+ Z0 \0 W
接着看hasResources()解析资源文件不成立(else部分)执行的逻辑.主要的逻辑是针对没有resources.arsc情况,并且没有属性引用的文件。还要说一下,decodeManifestFull()和decodeManifestWithResources()有着本质的区别。虽然都是解析清单文件。decodeManifestFull方法是没有属性引用的清单文件,而decodeManifestWithResources是有属性引用的清单文件。
& b6 a& N/ [0 m" `& t怎么理解呢?正常情况下清单文件会引用到系统或者当前应用的res/下的资源(也就是resources.arsc),所以解码有属性引用的清单文件一定用到decodeManifestWithResources方法。
' ?) p- x# F. ]2 I" w if (hasResources()) { //含有resources.arsc资源 switch (mDecodeResources) { //反编译资源 case DECODE_RESOURCES_NONE: //没有解码资源,针对 no-res命令 mAndrolib.decodeResourcesRaw(mApkFile, outDir); //不反编译资源文件直接copy("resources.arsc", "AndroidManifest.xml", "res"保持原封不动) if (mForceDecodeManifest == FORCE_DECODE_MANIFEST_FULL) { //对应force-manifest命令 强制解析清单文件 setTargetSdkVersion(); setAnalysisMode(mAnalysisMode, true); if (hasManifest()) { //如果含有清单文件 //开始解析清单文件二进制数据 mAndrolib.decodeManifestWithResources(mApkFile, outDir, getResTable()); } } break; case DECODE_RESOURCES_FULL: //正常执行反编译资源逻辑 setTargetSdkVersion(); setAnalysisMode(mAnalysisMode, true); //设置为分析模式 if (hasManifest()) {//如果是清单文件 mAndrolib.decodeManifestWithResources(mApkFile, outDir, getResTable()); //开始解析清单文件二进制数据 } mAndrolib.decodeResourcesFull(mApkFile, outDir, getResTable());//解析 resource.arsc文件 break; } } else { //如果没有resources.arsc文件,则在不查找属性引用下的解码清单文件 if (hasManifest()) {//如果有清单文件 if (mDecodeResources == DECODE_RESOURCES_FULL //如果想完整的反编译资源并且强制解析清单文件 || mForceDecodeManifest == FORCE_DECODE_MANIFEST_FULL) { mAndrolib.decodeManifestFull(mApkFile, outDir, getResTable()); } else { mAndrolib.decodeManifestRaw(mApkFile, outDir); //执行的逻辑是直接复制清单文件 } } }继续看hasSources()部分代码,成立条件是否包含dex文件,判断条件值有关键的两个。第一条件判断值:DECODE_SOURCES_NONE,翻译过来就是无解码源,对应着no-src命令。如果不加上no-src命令这个判断条件一定不会执行的。做的逻辑就是复制classes.dex文件到指定目录。- A0 |* x# F( c
第二条件判断值 :DECODE_SOURCES_SMALI或DECODE_SOURCES_SMALI_ONLY_MAIN_CLASSESZ,没有额外命令情况下正常都会走这个条件的,执行就是解码.dex格式的文件。hasMultipleSources()就是对多个dex文件进行处理。逻辑与hasSources()没什么区别,不再做解释。" m; w$ e, T9 _
if (hasSources()) { //文件夹 含有单个dex文件 switch (mDecodeSources) { case DECODE_SOURCES_NONE://不需要解析dex文件,对应no-src 命令 mAndrolib.decodeSourcesRaw(mApkFile, outDir, "classes.dex"); //这一步做的逻辑,直接复制classes.dex文件 break; case DECODE_SOURCES_SMALI: case DECODE_SOURCES_SMALI_ONLY_MAIN_CLASSES: //对应only-main-classes 命令 mAndrolib.decodeSourcesSmali(mApkFile, outDir, "classes.dex", mBakDeb, mApi); //反编译把dex文件转换成smali文件 很关键这部分 break; } } if (hasMultipleSources()) { //文件夹含有多个dex文件,跟hasSources()里面逻辑基本一致不复述 // foreach unknown dex file in root, lets disassemble it Set 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, mApi);//反编译把dex文件转换成smali文件 很关键这部分 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; } } } } }下面几个方法对于理解源码也很重要,先看decodeRawFiles(),用到了Androlib类里面的方法。这个方法主要解析原生的文件,就是Android在编译apk的过程中不参与编译的文件目录,执行的逻辑直接复制Assets和libs、lib、kotlin四个文件到指定的目录。在反编译资源时候经常能看到这么一段日志,Copying assets and libs... 就是来自这个方法。
( h0 @* A( |" ?8 l/ Q0 V1 t mAndrolib.decodeRawFiles(mApkFile, outDir, mDecodeAssets); //解析原始的文件,Assets和libs、lib、kotlin四个文件不需要处理,直接copy mAndrolib.decodeUnknownFiles(mApkFile, outDir, mResTable); //处理未知文件。未知类型指的是非apk固定的文件。接着看decodeUnknownFiles()方法,也是用到Androlib类里面的方法。根据方法字面意思可知,解码apk未知文件,那么什么是未知文件呢?Androlib.decodeUnknownFiles()里面有一个isAPKFileNames方法,它规定了一个正常APK中应该有哪些文件。
4 e1 d1 W A: F+ `) ~' ?' S) p7 @6 disAPKFileNames()方法中有一个名为APK_STANDARD_ALL_FILENAMES定义,它是一个字符串数组,包含范围有"classes.dex", "AndroidManifest.xml", "resources.arsc", "res", "r", "R","lib", "libs", "assets", "META-INF", "kotlin"。不再此范围的就会被划分为未知文件。" E6 [1 T3 z2 k
执行此方法逻辑时候会自动创建一个unknown文件夹,将筛选出来的未知文件复制到unknown文件夹内,并记录在apkool.yml文件中(下面有三张图片作为演示验证),最后在构建(回编)时候会用到。在反编译资源时候经常能看到这么一段日志,Copying unknown files... 就是来自这个方法。
: {1 }- X2 z: p接着看recordUncompressedFiles(),同样用到Androlib类里面的方法。根据方法字面意思可知,记录未压缩的文件。我先说一下方法里具体执行的逻辑。for循环遍历解压后apk所有文件,如果满足条件的文件会记录到uncompressedFilesOrExts集合中。
) ~; R. C' _9 S3 c8 x第一个判断条件isAPKFileNames是否是一个apk文件,这个在上面未知文件中介绍过了不在细说。第二个判断条件unk.getCompressionLevel(file)==0 表示压缩的等级,0表示不压缩。! m# M* C0 [) p
满足上面2个条件,有符合条件的文件,直接获取文件的扩展名称。NO_COMPRESS_PATTERN是一个正则表达式,意思是无压缩模式,判断条件前面加了一个!即压缩模式。如果ext字符串为空或者是压缩过的文件走这个条件。最终将不压缩的文件扩展类型添加到uncompressedFilesOrExts集合供apktool.yml中doNotCompress字段记录。+ L/ B! P; N" P5 q
//不压缩模式正则private final static Pattern NO_COMPRESS_PATTERN = Pattern.compile("(" + "jpg|jpeg|png|gif|wav|mp2|mp3|ogg|aac|mpg|mpeg|mid|midi|smf|jet|rtttl|imy|xmf|mp4|" + "m4a|m4v|3gp|3gpp|3g2|3gpp2|amr|awb|wma|wmv|webm|mkv)$");
& a; p2 g ?( `" b: s) A i6 O% ~8 x; ~; C2 v) M% Y5 o
没看完源码之前我曾有过这样的一个疑问?整个apktool源码里并没有执行真正压缩的逻辑,那压缩文件哪里来的。经查阅一番资料得知压缩逻辑源于appt,开篇时候我讲过apk生成离不开aapt打包工具,而apktool是对原始的apk进行解码。 对于压缩逻辑好奇的同学可以翻阅aapt源码,源码中能找到你想要的答案(需要有c++底子) 。其实用aapt命令就能很好理解apktool中的压缩到底是什么鬼了。输入以下命令可知!!!!
/ W3 e' e" M3 B# R$ w9 s- ]: p其中各字段代表的含义如下(作为了解):* }& H7 G4 [6 s5 [
+ L8 O# v( T q
- Length:文件的长度。* e! u* `9 [9 H x! a
- Method:数据压缩算法,有Deflate和Stored两种类型。
3 L: _8 x8 c# W8 c) r - Ratio:压缩率。" `3 k9 m$ z+ Z, ?/ Q, N% g. Y9 _) l2 r
- Size:文件压缩后节省的大小。跟压缩率有关。Size=(1-压缩率)*Length。# H2 ?' [! P" g# ~6 {, a" l5 z
- Date:日期。
- O& f/ K- R! M# R$ k - Time:时间。
2 A) F; z9 @" @5 q C \: M% [ - CRC-32:循环冗余校验,是一种加密算法。, O, T2 E C6 t( t \
- Name:文件名称。
' ?4 y. _ H1 m' H- M# w 重点是看Ratio字段,其中0%就是不压缩文件,在下图中apktool.yml里doNotCompress字段里得到验证。当然大家可以自己去试验一下,来验证我说的!!!
5 `; q# f- }* X" N4 X# }/ s- s; e) s- ^ mUncompressedFiles = new ArrayList(); mAndrolib.recordUncompressedFiles(mApkFile, mUncompressedFiles);//记录未压缩的文件 mAndrolib.writeOriginalFiles(mApkFile, outDir);//具体逻辑已经对该方法加说明了 不压缩代码部分已经分析完了,接着看下writeOriginalFiles(),写入原始数据,同样也是用到Androlib类里面的方法。执行的逻辑先创建一个original文件夹,把原始二进制清单文件、META-INF 、META-INF/services等文件复制到original文件夹下。在反编译资源时候经常能看到这么一段日志,Copying original files... 就是来自这个方法。很简单不细说了。
! ]1 S# F$ B' I% C0 x; j2 t最后来看一下 writeMetaFile()方法, 在反编译时候一定会有apktool.yml文件,下面方法就是有关apktool.yml全部的属性。下面注释很齐全,我们看下每个字段属性的细节。
" ^" [# b2 k& F' T9 o$ e+ j//writeMetaFile代码部分 private void writeMetaFile() throws AndrolibException { MetaInfo meta = new MetaInfo(); meta.version = Androlib.getVersion(); //获取apktool版本号 meta.apkFileName = mApkFile.getName(); //获取文件名 //如果要反编译资源并且有resources.arsc文件或者manifest文件,执行条件内的逻辑 if (mDecodeResources != DECODE_RESOURCES_NONE && (hasManifest() || hasResources())) { //apktool.yml中 isFrameworkApk字段布尔类型 是否引用了系统apk,可以看下方法注释 meta.isFrameworkApk = mAndrolib.isFrameworkApk(getResTable()); putUsesFramework(meta); //放置是否使用系统框架 putSdkInfo(meta); //放置sdkInfo信息 putPackageInfo(meta); //放置PackageInfo信息 putVersionInfo(meta); //放置versionCode 和versionName putSharedLibraryInfo(meta); //记录是否是库文件信息 putSparseResourcesInfo(meta); //特殊资源(看方法注释) } putUnknownInfo(meta); //记录未知文件 putFileCompressionInfo(meta); //设置文件压缩信息(也是列出不压缩的文件) mAndrolib.writeMetaFile(mOutDir, meta); //写入Meta文件数据 }!!brut.androlib.meta.MetaInfoapkFileName: demo.apkcompressionType: falsedoNotCompress:- resources.arsc- META-INF/android.arch.core_runtime.version- assets/AssetBundles/app_version.bytes......省略若干isFrameworkApk: falsepackageInfo: forcedPackageId: '127' renameManifestPackage: nullsdkInfo: minSdkVersion: '19' targetSdkVersion: '29'sharedLibrary: falsesparseResources: falseunknownFiles: firebase-measurement-connector.properties: '8' firebase-messaging.properties: '8'......省略若干usesFramework: ids: - 1 tag: nullversion: 2.4.1versionInfo: versionCode: '2' versionName: 2.0.0看这部分代码前一定要看懂.arsc数据结构,不然我说的你可能不懂。推一篇不错的博文参考 ARSC 文件格式解析
5 s3 c8 c3 p; X' X! q# S+ [. q" F按代码顺序详细说下!!!
2 }8 \% D9 K; U" G3 smAndrolib.isFrameworkApk(getResTable()) : 是否是一个系统apk文件;对应isFrameworkApk字段
8 |. V6 r7 y9 Q# _) ^: n
- H$ B: e% P( p执行的逻辑,罗列出apk中所有资源包然后遍历,从ResTable()表中(先理解成解析当前应用.arsc文件,下面会详细说ResTable是什么)获取应用资源id,如果资源id 1) { pkg = selectPkgWithMostResSpecs(pkgs); } else if (pkgs.length == 0) { throw new AndrolibException("Arsc files with zero or multiple packages"); } else { pkg = pkgs[0]; } if (pkg.getId() != id) { throw new AndrolibException("Expected pkg of id: " + String.valueOf(id) + ", got: " + pkg.getId()); } resTable.addPackage(pkg, false); return pkg; }步骤5-规范清单文件包名
* }* ^4 s) A7 W! K这部分没什么好说,重点看adjustPackageManifest()函数,分别将解析完成的resources.arsc和清单文件进行包名比较。如果包名一致,打印 LOGGER.info("Regular manifest package...");日志!至于为什么包名一致,跟步骤6关系有关。解析完resources.arsc和清单文件后,生成资源文件也就是res文件,资源文件要有包名才能生成资源。
* D9 ]) t4 D" Y /** * 有属性引用解析清单文件核心类 * @param resTable * @param apkFile * @param outDir * @throws AndrolibException */ public void decodeManifestWithResources(ResTable resTable, ExtFile apkFile, File outDir) throws AndrolibException { Duo duo = getResFileDecoder(); ResFileDecoder fileDecoder = duo.m1; //res文件解码 ResAttrDecoder attrDecoder = duo.m2.getAttrDecoder(); //res资源属性解码 attrDecoder.setCurrentPackage(resTable.listMainPackages().iterator().next()); Directory inApk, in = null, out; try { inApk = apkFile.getDirectory(); out = new FileDirectory(outDir); LOGGER.info("Decoding AndroidManifest.xml with resources..."); fileDecoder.decodeManifest(inApk, "AndroidManifest.xml", out, "AndroidManifest.xml"); //删除versionName / versionCode属性(Aapt API 16) if (!resTable.getAnalysisMode()) { //检查resources.arsc包与AndroidManifest中列出的包之间是否不匹配 // 从清单中删除android属性 versionCode / versionName进行重建,这是一项必需的更改,以防止出现有关版本冲突的警告 //它将通过apktool.yml作为参数传递给aapt,例如“ --min-sdk-version” adjustPackageManifest(resTable, outDir.getAbsolutePath() + File.separator + "AndroidManifest.xml"); ResXmlPatcher.removeManifestVersions(new File( outDir.getAbsolutePath() + File.separator + "AndroidManifest.xml")); mPackageId = String.valueOf(resTable.getPackageId()); } } catch (DirectoryException ex) { throw new AndrolibException(ex); } } public void adjustPackageManifest(ResTable resTable, String filePath) throws AndrolibException { //将resources.arsc包名与AndroidManifest中的包名进行比较 ResPackage resPackage = resTable.getCurrentResPackage(); String pkgOriginal = resPackage.getName(); mPackageRenamed = resTable.getPackageRenamed(); resTable.setPackageId(resPackage.getId()); resTable.setPackageOriginal(pkgOriginal); // 1) Check if pkgOriginal === mPackageRenamed // 2) Check if pkgOriginal is ignored via IGNORED_PACKAGES if (pkgOriginal.equalsIgnoreCase(mPackageRenamed) || (Arrays.asList(IGNORED_PACKAGES).contains(pkgOriginal))) { LOGGER.info("Regular manifest package..."); } else { LOGGER.info("Renamed manifest package found! Replacing " + mPackageRenamed + " with " + pkgOriginal); ResXmlPatcher.renameManifestPackage(new File(filePath), pkgOriginal); } }步骤6和7-生成res文件8 Z) t- B& A4 v. Q& r7 Q& W( k
开始我有个误区一直以为decodeResourcesFull函数就是解析resources.arsc文件,通读整个解码逻辑还有多次断点分析并不是这样。2 ?+ y' H E* R2 X0 U6 T; N3 f
经过前面执行的步骤,清单文件还有.arsc文件都是解析好了等待被使用的,decodeResourcesFull主要还是对当前解码后资源做处理(解码后的res文件)。执行到 mAndrolib.decodeResourcesFull函数时,getResTable()此时不为空已经有个完整的ResTable数据表了(对此有怀疑的可以断点调试一下就知道)。8 Z% Z- Z1 v! k6 X( Q2 k- a6 W
mAndrolib.decodeResourcesFull(mApkFile, outDir, getResTable()); //解码完整的资源。 public ResTable getResTable() throws AndrolibException { if (mResTable == null) { boolean hasResources = hasResources(); //是否包含arsc文件 boolean hasManifest = hasManifest(); //是否包含清单文件 if (! (hasManifest || hasResources)) { //都不存在抛异常 throw new AndrolibException( "Apk doesn't contain either AndroidManifest.xml file or resources.arsc file"); } mResTable = mAndrolib.getResTable(mApkFile, hasResources); //ResTable的生成和数据结构的填充主要是通过Androlib.getResTable方法。 } return mResTable; }这个方法其实用到的解析类和AndroidManifest.xml的解析类是一样的,因为他们都属于arsc格式,而且资源文件也是xml格式的,这里值得注意的是,会产生一个反编译中最关键的一个文件:public.xml,这个文件是在反编译之后的res\values\public.xml% \! X' K7 C) l1 ?
public void decodeResourcesFull(ExtFile apkFile, File outDir, ResTable resTable) throws AndrolibException { mAndRes.decode(resTable, apkFile, outDir); } public void decode(ResTable resTable, ExtFile apkFile, File outDir) throws AndrolibException { Duo duo = getResFileDecoder(); ResFileDecoder fileDecoder = duo.m1; ResAttrDecoder attrDecoder = duo.m2.getAttrDecoder(); attrDecoder.setCurrentPackage(resTable.listMainPackages().iterator().next()); Directory inApk, in = null, out; try { out = new FileDirectory(outDir); inApk = apkFile.getDirectory(); //获取apk文件目录 out = out.createDir("res"); //创建res目录 if (inApk.containsDir("res")) { in = inApk.getDir("res"); } if (in == null && inApk.containsDir("r")) { in = inApk.getDir("r"); } if (in == null && inApk.containsDir("R")) { in = inApk.getDir("R"); } } catch (DirectoryException ex) { throw new AndrolibException(ex); } ExtMXSerializer xmlSerializer = getResXmlSerializer(); for (ResPackage pkg : resTable.listMainPackages()) { attrDecoder.setCurrentPackage(pkg); LOGGER.info("Decoding file-resources..."); for (ResResource res : pkg.listFiles()) { fileDecoder.decode(res, in, out); } LOGGER.info("Decoding values */* XMLs..."); for (ResValuesFile valuesFile : pkg.listValuesFiles()) { generateValuesFile(valuesFile, out, xmlSerializer); //生成values文件 } generatePublicXml(pkg, out, xmlSerializer); //生成res/values/public.xml文件 } AndrolibException decodeError = duo.m2.getFirstError(); if (decodeError != null) { throw decodeError; } }而这里的id值是一个整型值,8个字节;由三部分组成的:0 Q( ^. U, b2 D
PackageId+TypeId+EntryId
; L# O% |3 e* R
, D7 O1 e1 O# o$ z/ ]2 w- PackageId:是包的Id值,Android中如果是第三方应用的话,这个值默认就是0x7F,系统应用的话就是0x01上面说过, PackageId 等于 id="0x7f010000"
# x: ^4 d$ g: C - TypeId:资源的类型ID, 资源的类型有 animator、anim、color、drawable、layout、menu、raw、string 和 xml 等等若干种,每一种都会被赋予一个 ID,而且这些类型的值是从1开始逐渐递增的,而且顺序不能改变,attr=0x01,drawable=0x02.... f. d4 {* I7 {8 x1 j
- Entry ID是指每一个资源在其所属的资源类型中所出现的次序。注意,不同类型的资源的Entry ID有可能是相同的,但是由于它们的类型不同,我们仍然可以通过其资源ID来区别开来。! ?/ Z0 z& r; H/ j/ x0 x2 p" u
步骤8-解码dex文件,将dex解析成smali源码
1 N" z6 E; b5 H" G; w0 a! G9 X只看完整解码.dex文件函数条件,接着看重点是看 SmaliDecoder.decode函数,这个方法主要将dex文件解析成smali源码。! W2 ?2 I3 d, ?0 M) \. {
mAndrolib.decodeSourcesSmali(mApkFile, outDir, "classes.dex", mBakDeb, mApi); //反编译把dex文件转换成smali文件 很关键这部分//Androlib类下的decodeSourcesSmali函数。 /** * 反编译把dex文件转换成smali文件 * @param apkFile * @param outDir * @param filename * @param bakdeb * @param api * @throws AndrolibException */ public void decodeSourcesSmali(File apkFile, File outDir, String filename, boolean bakdeb, int api) throws AndrolibException { try { File smaliDir; if (filename.equalsIgnoreCase("classes.dex")) { //如果文件名是classes.dex smaliDir = new File(outDir, SMALI_DIRNAME); //创建一个smali文件夹 } else { smaliDir = new File(outDir, SMALI_DIRNAME + "_" + filename.substring(0, filename.indexOf("."))); } OS.rmdir(smaliDir); // 如果文件夹不存在return,存在递归删除 smaliDir.mkdirs(); //创建多层文件 LOGGER.info("Baksmaling " + filename + "..."); SmaliDecoder.decode(apkFile, smaliDir, filename, bakdeb, api); //具体执行 } catch (BrutException ex) { throw new AndrolibException(ex); } }这部分代码主要看DexFileFactory.loadDexContainer(),这里用到了第三方的dexlib2来处理dex字节码,处理完dex文件后,交给Baksmali.disassembleDexFile()同样也是第三方库api,最后生成smali文件。, e1 k A0 y2 p' [& c1 \
// SmaliDecoder类中的decode()方法 private void decode() throws AndrolibException { try { final BaksmaliOptions options = new BaksmaliOptions(); // options省略....... int jobs = Runtime.getRuntime().availableProcessors(); //返回可用处理器的Java虚拟机的数量。 if (jobs > 6) { jobs = 6; } // 创建容器,loadDexContainer()加载包含1个或多个dex文件的文件 MultiDexContainer |
|