看到这篇技术文章皆是缘分。本人在一家研运一体的游戏公司做安卓游戏SDK,并不是安卓逆向从业人员。工作中经常使用Apktool工具,写这一篇技术文纯粹是好奇心作祟,好奇这东西是什么原理,怎么做到把Apk拆解成最原始的样子。
Apktool它是一个开源的逆向工具,Java写出来的。多么强大我就不多说,能找到这里说明你应该知道它是做什么的。在写文之前看过很多,也搜过很多技术大佬的文章,分析的过程并没有让我的好奇心得到满足。于是我把源码Clone下来分析一下。单纯的记录一下分析过程还有产生的疑问,没准那一次忘了,回头看看自己写的文章会有不同的感受。
新版官网Apk构建流程(常识部分)
构建流程涉及许多将项目转换成 Android 应用软件包 (APK) 的工具和流程。构建流程非常灵活,因此了解它的一些底层工作原理会很有帮助。(熟悉流程的可以忽略此部分,继续往下观看)
Android 应用模块的构建流程如上图所示,按照如上常规步骤执行:(如下步骤均来源于安卓官网,我只是做一个搬运工)
-
编译器将您的源代码转换成 DEX 文件(Dalvik 可执行文件,其中包括在 Android 设备上运行的字节码),并将其他所有内容转换成编译后的资源。
-
APK 打包器将 DEX 文件和编译后的资源组合成单个 APK。不过,必须先为 APK 签名,然后才能将应用安装并部署到 Android 设备上。
-
APK 打包器使用调试或发布密钥库为 APK 签名:
-
如果你构建的是调试版应用(即专用于测试和分析的应用),则打包器会使用调试密钥库为应用签名。Android Studio 会自动使用调试密钥库配置新项目。
-
如果你构建的是打算对外发布的发布版应用,则打包器会使用发布密钥库为应用签名。
-
-
在生成最终 APK 之前,打包器会使用 zipalign 工具对应用进行优化,以减少其在设备上运行时所占用的内存。
老版官网Apk构建流程(了解部分)
安卓官网只能找到新版本的apk构建流程,老版本的流程图在官网找了半天也没找到。于是求助了一下Google大佬。找到了老版本的流程图,下图就是老版本的apk构建流程。(熟悉流程的可以忽略此部分,继续往下观看)
上图可以分为七个大模块:
-
处理资源相关文件,生成R.java: aapt来打包res资源文件,生成R.java、resources.arsc和res文件
-
处理aidl文件,生成相应的Java文件 :aidl工具解析接口定义文件然后生成相应的Java代码接口供程序调用
-
编译项目源代码,生成class文件 :Java Compiler阶段。项目中所有的Java代码,包括R.java和.aidl文件,javac编译成.class文件
-
转换所有的class文件,生成classes.dex文件 :通过dx工具,将.class文件和第三方库中的.class文件处理生成classes.dex文件。(新版已经被d8代替)
-
打包生成APK文件 :通过apkbuilder工具,将aapt生成的resources.arsc和res文件、assets文件和classes.dex一起打包生成apk
-
对APK文件进行签名 :通过Jarsigner工具,对上面的apk进行debug或release签名。(新版已经被apksigner代替)
-
对签名后的APK文件进行对齐处理 :通过zipalign工具,将Android 应用 (APK) 文件提供重要的优化。其目的是要确保所有未压缩数据的开头均相对于文件开头部分执行特定的对齐。具体来说,它会使 APK 中的所有未压缩数据(例如图片或原始文件)在 4 字节边界上对齐。
敲重点:明明要说Apktool 源码?怎么单独写了一章无关紧要的apk构建流程?这并不冲突,只是做了一个铺垫。首先你要知道apk是如何构建,知道如何构建(怎么来的)才能更好理解Apktool逆向。而这一章节最关键的就是AAPT概念。后续源码中解码和构建都离不开aapt工具。那AAPT是什么呢,我做下简单介绍:
AAPT(Android 资源打包工具)是一种构建工具,Android Studio 和 Android Gradle 插件使用它来编译和打包应用的资源。AAPT 会解析资源、为资源编制索引,并将资源编译为针对 Android 平台进行过优化的二进制格式。
本次演示的源码为最新版v2.5.1 Master,大家有兴趣的话可以看下我以前分享过的文章 Apktool源码下载与编译
Apktool工程项目介绍(了解部分,不感兴趣的往下看)
Apktool源码下载我这里就不做演示,下图就是下载完成后的文件夹结构图
Apktool是java项目同时也是gradle项目,下图是项目引用的第三方库
根据上面的第三方库重要的做下简单介绍
-
baksmali :是dalvik(Android的Java VM实现)使用的dex格式的汇编程序/反汇编程序,项目中dex文件解析靠的就是它
-
commons_cli : Apache开源组织提供的用于解析命令行参数的库
-
snakeyaml :配置文件解释器。对应就是反编译后的apktool.yml文件
-
guava : Google的 Java项目广泛依赖 的核心库
-
xmlpull : xml解析框架,项目中用于解析清单文件还有xml等
Apktool工程结构图
针对上图的结构做下说明
-
brut.apktool : apktool核心工程
-
apktool-cli :这部分的工程只有一个main.java 程序的主入口
-
apktool-lib : 这部分的工程内容最为丰富,包含了apktool中所有的命令 和交互调用的类,还有项目自带的三大系统的aapt和aapt2工具,同时还包含系统框架android-framework.jar
-
-
brut.j.common : 有关异常定义和处理的工具类,服务于brut.apktool.
-
brut.j.dir : 有关文件方面工具类,服务于brut.apktool .
-
brut.j.util :工程通用工具类,服务于brut.apktool .
主函数Main逻辑(重要部分)
既然是Java项目程序入口肯定是Main(),可得知入口类为brut.apktool.Main。还有一种方式我们可以通过生成的jar包来查找程序入口(既然下载了源码,这种方式不建议了。)
上述代码为主函数中部分代码截取。为什么截取部分代码?首先Main.java代码量加上注释大约有800行代码,放上来占地不说而且看着乱。然而我觉得这样做没必要,代码大家都能下载,能进来的都是技术,有谁又看不懂呢?个人觉得主要来分析一下大体的逻辑还有一些细节,这很重要!!!
主函数(Main.java)做了什么逻辑,我从上到下给大家梳理一下
-
设置模式选项 。 针对的是日志级别打印。其中包括三项模式 默认模式(NORMAL) 、详细模式(VERBOSE) 、 静默模式(QUIET)
-
创建cli命令行解析器。包括定义阶段(创建命令行选项)、解析阶段(解析命令行参数)、询问阶段(判断命令行出现了哪个选项)、 进阶部分(获得参数值)、帮助信息等。我笼统的说了一下,英语好的同学可以去Apache Commons CLI官网去看Doc,英语不好的可以Google搜索一下文档语法。
-
自定义异常 ,针对可能出现的问题捕获异常。
-
创建Apktool 五种常规的用法(附上对应的方法名)
-
解码: cmdDecode(CommandLine cli)
-
构建 : cmdBuild(CommandLine cli)
-
安装框架: cmdInstallframework(CommandLine cli)
-
清除框架目录 : cmdEmptyframeworkDirectory(CommandLine cli)
-
列出框架目录(v2.5.0新增): cmdListframeworks(CommandLine cli)
-
其实上述五种常规用法,最核心的就是解码(反编译)和构建(回编),余下的属于附属用法。重点说一下解码和构建的逻辑 。至于框架是什么?往下看会解释。apktool设定的命令可以看一下我之前发过的博客、Apktool命令大全。一定要看 ,与下面代码讲解有关联,方便大家理解。
帮助文档(了解部分)
如下截图相信大家非常熟悉,这一部分单独写出来纯属很经典。这部分是描述apktool的帮助文档,只要输入apktool即可展示。详细可以看下代码usage()方法
解码调用逻辑
下面就是解码部分代码。传入命令行获的值、实例解码核心类ApkDecoder 、设置解码命令参数、创建输出目录、最终调用到decoder.decode()
Apk文件结构(了解部分,铺垫下文)
开篇简述了Apk文件是怎么来的。这部分章节简述一下Apk文件结构,这有助对下文的理解,熟悉这部分可以跳过。
APK是AndroidPackage的缩写,即Android安装包(apk)。APK是类似Symbian Sis或Sisx的文件格式。通过将APK文件直接传到Android模拟器或Android手机中执行即可安装。
apk文件和sis一样,把android sdk编译的工程打包成一个安装程序文件,格式为apk。APK文件其实是zip格式,但后缀名被修改为apk,通过UnZip解压后,可以看到如下:
-
assets :存放资源文件,系统在编译的时候不会编译assets下的资源文件;
-
lib或者libs :用来存放三方库的地方;
-
meta-INF :描述包信息的目录;
-
res :项目中的资源文件夹;
-
AndroidManifest.xml :功能清单文件;
-
classes.dex :包含所有class的文件,供DVM执行;
-
resources.arsc :编译后的二进制资源文件;
ApkDecoder类中的解码逻辑(耐心读完)
粗略看下整体的逻辑然后在细扒。先从hasResources()解析资源文件判断条件成立看起,hasResources()上面代码我就不说了注释非常齐全。如何判断条件是否成立呢? 逻辑很简单,该文件夹里面是否包含resources.arsc(上一章已经讲述过resources.arsc是什么)。继续往下看,有两个判断值 DECODE_RESOURCES_NONE(不需要解码资源)和DECODE_RESOURCES_FULL(完整的解码资源)。
先说DECODE_RESOURCES_NONE(不需要解码资源)判断值,正常我们输入apktool d xx.apk没有额外命令时候一定不会走这个条件值,只有加入了no-res命令才会执行。实例Androlib对象(decodeResourcesRaw方法),既然不解码资源那么直接复制"resources.arsc", “AndroidManifest.xml”, "res"三个文件里的内容到指定文件夹里。如果在额外加入了force-manifest解析清单文件命令,则会加一层判断,如果文件中包含清单文件,才能真正执行解码清单文件的逻辑。
再说下DECODE_RESOURCES_FULL(完整解码资源文件)判断值,正常我们输入apktool d xx.apk一定会走这个条件值。实例Androlib对象,这个条件值只做了两件事 ,第一件 判断文件中是否包含清单文件,有的话直接解码清单文件。第二件就是执行解码resource.arsc的逻辑。
接着看hasResources()解析资源文件不成立(else部分)执行的逻辑.主要的逻辑是针对没有resources.arsc情况,并且没有属性引用的文件。还要说一下,decodeManifestFull()和decodeManifestWithResources()有着本质的区别。虽然都是解析清单文件。decodeManifestFull方法是没有属性引用的清单文件,而decodeManifestWithResources是有属性引用的清单文件。
怎么理解呢?正常情况下清单文件会引用到系统或者当前应用的res/下的资源(也就是resources.arsc),所以解码有属性引用的清单文件一定用到decodeManifestWithResources方法。
继续看hasSources()部分代码,成立条件是否包含dex文件,判断条件值有关键的两个。第一条件判断值:DECODE_SOURCES_NONE,翻译过来就是无解码源,对应着no-src命令。如果不加上no-src命令这个判断条件一定不会执行的。做的逻辑就是复制classes.dex文件到指定目录。
第二条件判断值 :DECODE_SOURCES_SMALI或DECODE_SOURCES_SMALI_ONLY_MAIN_CLASSESZ,没有额外命令情况下正常都会走这个条件的,执行就是解码.dex格式的文件。hasMultipleSources()就是对多个dex文件进行处理。逻辑与hasSources()没什么区别,不再做解释。
下面几个方法对于理解源码也很重要,先看decodeRawFiles(),用到了Androlib类里面的方法。这个方法主要解析原生的文件,就是Android在编译apk的过程中不参与编译的文件目录,执行的逻辑直接复制Assets和libs、lib、kotlin四个文件到指定的目录。在反编译资源时候经常能看到这么一段日志,Copying assets and libs… 就是来自这个方法。
接着看decodeUnknownFiles()方法,也是用到Androlib类里面的方法。根据方法字面意思可知,解码apk未知文件,那么什么是未知文件呢?Androlib.decodeUnknownFiles()里面有一个isAPKFileNames方法,它规定了一个正常APK中应该有哪些文件。
isAPKFileNames()方法中有一个名为APK_STANDARD_ALL_FILENAMES定义,它是一个字符串数组,包含范围有"classes.dex", “AndroidManifest.xml”, “resources.arsc”, “res”, “r”, “R”,“lib”, “libs”, “assets”, “meta-INF”, “kotlin”。不再此范围的就会被划分为未知文件。
执行此方法逻辑时候会自动创建一个unknown文件夹,将筛选出来的未知文件复制到unknown文件夹内,并记录在apkool.yml文件中(下面有三张图片作为演示验证),最后在构建(回编)时候会用到。在反编译资源时候经常能看到这么一段日志,Copying unknown files… 就是来自这个方法。
接着看recordUncompressedFiles(),同样用到Androlib类里面的方法。根据方法字面意思可知,记录未压缩的文件。我先说一下方法里具体执行的逻辑。for循环遍历解压后apk所有文件,如果满足条件的文件会记录到uncompressedFilesOrExts集合中。
第一个判断条件isAPKFileNames是否是一个apk文件,这个在上面未知文件中介绍过了不在细说。第二个判断条件unk.getCompressionLevel(file)==0 表示压缩的等级,0表示不压缩。
满足上面2个条件,有符合条件的文件,直接获取文件的扩展名称。NO_COMPRESS_PATTERN是一个正则表达式,意思是无压缩模式,判断条件前面加了一个!即压缩模式。如果ext字符串为空或者是压缩过的文件走这个条件。最终将不压缩的文件扩展类型添加到uncompressedFilesOrExts集合供apktool.yml中doNotCompress字段记录。
没看完源码之前我曾有过这样的一个疑问?整个apktool源码里并没有执行真正压缩的逻辑,那压缩文件哪里来的。经查阅一番资料得知压缩逻辑源于appt,开篇时候我讲过apk生成离不开aapt打包工具,而apktool是对原始的apk进行解码。 对于压缩逻辑好奇的同学可以翻阅aapt源码,源码中能找到你想要的答案(需要有c++底子) 。其实用aapt命令就能很好理解apktool中的压缩到底是什么鬼了。输入以下命令可知!!!!
其中各字段代表的含义如下(作为了解):
-
Length:文件的长度。
-
Method:数据压缩算法,有Deflate和Stored两种类型。
-
Ratio:压缩率。
-
Size:文件压缩后节省的大小。跟压缩率有关。Size=(1-压缩率)*Length。
-
Date:日期。
-
Time:时间。
-
CRC-32:循环冗余校验,是一种加密算法。
-
Name:文件名称。
重点是看Ratio字段,其中0%就是不压缩文件,在下图中apktool.yml里doNotCompress字段里得到验证。当然大家可以自己去试验一下,来验证我说的!!!
不压缩代码部分已经分析完了,接着看下writeOriginalFiles(),写入原始数据,同样也是用到Androlib类里面的方法。执行的逻辑先创建一个original文件夹,把原始二进制清单文件、meta-INF 、meta-INF/services等文件复制到original文件夹下。在反编译资源时候经常能看到这么一段日志,Copying original files… 就是来自这个方法。很简单不细说了。
最后来看一下 writemetaFile()方法, 在反编译时候一定会有apktool.yml文件,下面方法就是有关apktool.yml全部的属性。下面注释很齐全,我们看下每个字段属性的细节。
看这部分代码前一定要看懂.arsc数据结构,不然我说的你可能不懂。推一篇不错的博文参考 ARSC 文件格式解析
按代码顺序详细说下!!!
mAndrolib.isframeworkApk(getResTable()) : 是否是一个系统apk文件;对应isframeworkApk字段
执行的逻辑,罗列出apk中所有资源包然后遍历,从ResTable()表中(先理解成解析当前应用.arsc文件,下面会详细说ResTable是什么)获取应用资源id,如果资源id<64 则认为是系统apk,反之不是。
id是int值代表的是10进制,系统应用资源id是16进制0x01开头(转换成10进制是1) ,当前应用的资源id是0x7f开头(转换成10进制是127)。正常情况下的应用该值基本都是false 。
putUsesframework(meta) : 记录使用的框架;对应usesframework字段
这个字段包含两个属性 分别是 ids 和tag。先说tag它最简单,说白了这个就是-t 或–tag 给框架加标签用的命令,区分多个厂商定制或framework。没有指定tag命令正常情况下会是null。 然后看下ids属性之所以为1 ,是因为系统应用资源id是16进制0x01开头(转换成10进制是1) ,那个不是-1 细看是有空格的应该是yml格式导致的,这个- 1的问题被骗了好多年。(断点亲测过值=1,详细看源码)。
putSdkInfo(meta): 记录sdk信息;对应sdkInfo字段
包含了两个属性minSdkVersion和targetSdkVersion,严格来说三个才对还有一个maxSdkVersion属性,源码中有写到。如果你没有在AS根录build.gradle中定义maxSdkVersion属性,apktool.yml也不会展示的。
这三个值哪里来的呢,说白了解析清单文件二进制数据格式得到的,详细见源码XmlPullStreamDecoder.parseAttr()
putPackageInfo(meta): 记录包信息;对应packageInfo字段
其中包含两个属性forcedPackageId和renameManifestPackage。最开始的时候我一直好奇forcedPackageId属性中127那里来的?在这方面下点功夫知道了怎么回事。看下图注释吧比较详细,我不细说了。非系统库和共享库条件下这个forcedPackageId基本是127 不会变。
putVersionInfo(meta): 记录版本信息;对应versionInfo字段
这个字段比较简单 ,如下图所示,看一下在AS中根目录build.gradle中设置的两个属性,最后都会被读取出来放在apktool.yml里面。具体怎么读取数值,可以看下上面putSdkInfo()说过的aapt命令。
putSharedLibraryInfo(meta): 记录是否是一个库文件信息;对应sharedLibrary字段
这个属性字段的意思是否用到系统资源共享库,说白了就是清单文件中属性 。我不做详细的解释涉及的内容较多,不是一句两句说得清楚,详细可以搜一下SharedLibrary去了解一下。我推荐一篇技术文章供大家参考,有兴趣深入理解的可以去看看写的非常不错Android资源管理中的SharedLibrary资源共享库!!!
之前我做过这样的测试清单文件添加引用库,执行反编译命令后apktool.yml属性sharedLibrary依旧是false。这让我百思不得其解,也是看了上面我推荐的博客才知道需要执行aapt中的–shared-lib命令才行,这命令的意思是要把当前应用编译资源共享库,而不是普通的APK。(犹豫本人功力不足没有吃透aapt源码整体逻辑,还是老话有能力底子深的翻阅源码。如果下载不便,可以私聊我提供!!!)
该方法执行核心逻辑可以看下ARSCDecoder中的readTablePackage方法里 mResTable.setSharedLibrary(true)。只有资源id、0x00才会走到id==0这个条件里sharedLibrary才会变成true,常规应用apk基本上是false。
也就是说 if (id == 0) {} packageId=0说明了有资源共享库id 也就是SharedLibrary,那么为什么给id重新赋值呢,是因为针对多个资源共享库,我们知道0是共享库资源,多个共享库呢 资源id怎么排列呢。难道都是0x00 ?如何区分呢? 0x01是系统资源,不能占用.所以从2(0x02)开始
下图是aapt源码中有关资源ID定义的方法和属性,这个部分很重要,appt源码中给packageId定义类型。
-
AppFeature packageId = 0x7f; 当前应用的资源包
-
System packageId = 0x01; 系统应用资源包
-
SharedLibrary packageId = 0x00; 共享库资源包
putSparseResourcesInfo(meta): 记录是否是特殊资源信息;对应sparseResources字段
sparseResources是指.arsc文件中的特殊资源布尔类型值,没有特殊资源即为false,可以到ARSCDecoder类readTableTypeSpec()查看引用关系。这个跟aapt和aapt2版本区别有关,深究的话就要看下aapt2的源码。
现在唯一能知道这个跟aapt2命令 enable-sparse-encoding 有关,该命令的意思:允许使用二进制搜索树对稀疏条目进行编码。 这有助于优化 APK 大小,但会降低资源检索性能。
putUnknownInfo(meta): 记录未知文件;对应unknownFiles字段
这个字段记录着apk中未知的文件,上面已经复述过了不细讲了。
putFileCompressionInfo(meta): 记录不压缩文件;对应doNotCompress字段
该字段描述的是不压缩文件,上面已经细说过不在复述。
小结
我们粗略的看了一下apktool解码逻辑,到这里我们可以发现,apktool在反编译的整个过程核心点按解码顺序就三个:resource.arsc文件,AndroidManifest.xml文件,dex文件。通俗点说解码主要就是解析这三种文件,当然这里面多少也有aapt的影子在里面。
解码核心逻辑(非常重要)
上面讲述内容只是粗略的过滤了一下解码的整体过程。但是解码的核心却没有细究过,解码三个文件是怎么解析的?一直提过的table是什么也没过细说过,带着问题我们详细重新捋一下逻辑。先看一下正常执行命令来反编译apk文件的输出日志!如下所示:
步骤1-从反编译第一个日志看起,开始使用apktool打印当前版本以及反编译应用apk名称 ! ! !
步骤2-加载数据表,数据表就是resources.arsc文件
这里只看完整反编译资源文件的情况,也就是DECODE_RESOURCES_FULL所在条件下面的代码。分支条件下第一行代码就是setTargetSdkVersion,我们看看setTargetSdkVersion方法的内部实现。
ApkDecoder内部是维护了一个ResTable的,我们的任何的资源数据信息都是根据ResTable来取的。当ApkDecoder发现mResTable变量是空的的时候,会对此进行初始化。
接下来我们就主要看看Androlib的getResTable方法,getRestable()函数主要创建一个ResTable类,用于表示resources.arsc反编译后的数据结构。 ResTable的数据填充主要通过loadMainPkg函数完成。loadMainPkg又通过getResPackagesFromApk函数完成。
继续接着上面的说,AndrolibResources类下getResPackagesFromApk方法。BufferedInputStream读取 resources.arsc二进制文件,层层调用到了ARSCDecoder.decode();
继续执行到ARSCDecoder类下的decode方法,ARSCDecoder调用readTableHeader,这个方法叫readTableHeader,实则把整个resources.arsc都解析了。
解析完成后,代码层层返回得到ResTable数据,ResTable数据得到填充。解析resources.arsc先说到这里,细说起来不是一句两句的事情,已经给大家开了一个头,下面内容就是按照resources.ars数据格式解析二进制数据。有关resources.arsc格式可以看一下博文ARSC 文件格式解析 能帮助大家更好的理解接下来的代码 !!!
步骤3-解码AndroidManifest.xml文件与资源…
该到解析清单文件这一环节了,先看完整解码清单文件的条件。Androlib 类/decodeManifestWithResources(),继续走到AndrolibResources类decodeManifestWithResources()。接着走到了ResFileDecoder类下的decodeManifest(),最后执行到XmlPullStreamDecoder/decode函数。
发现很有意思事情,经过层层逻辑调用,解析清单文件(.xml)采用的是Pull解析法,封装了一层,直接把Android中的一些方法copy过来了。有关清单文件的二进制格式可以看下我写过的博客AndroidManifest.xml 文件格式解析有助于帮助你理解apktool源码中有关解析二进制清单文件格式(偷个懒看),解析清单文件到此为止往下看!!!!
步骤4-加载系统框架资源表
文中提了不少关于框架文件这一词,框架文件是什么呢?给大家解释一下。你应该知道,Android应用程序利用了Android OS本身上的代码和资源。这些被称为框架资源,Apktool依靠它们来正确解码和构建apk。
每个Apktool版本在发布时内部都包含最新的AOSP框架。这使你可以毫无问题地解码和构建大多数apk。但是,制造商(国内的小米 华为 魅族这种定制的厂商)除了常规的AOSP文件外,还添加了自己的框架文件。要针对这些制造商apk使用apktool,必须首先安装制造商框架文件。(工作中只要不涉及到厂商应用或者系统级别应用。正常用不到这部分知识,可以作为知道部分)。
倒叙看下面代码的逻辑吧,首先我们要找到系统框架所在路径,根据所使用的操作系统,框架存储在不同的位置 ! ! !
-
Unix - $HOME/.local/share/apktool/framework/
-
Windows - %UserProfile%rAppDataLocalapktoolframework
-
Mac - $HOME/Library/apktool/framework/
如果这些目录不存在,它会默认创建一个临时目录。你也可以利用该参数–frame-path为框架文件选择备用文件夹。
由于这些位置有时位于隐藏的目录中,因此管理这些框架成为一个麻烦事。在v2.2.1中添加一个简单的帮助命令,可以运行apktool empty-framework-dir以达到清空框架的目的。
框架的路径获取到了,接着继续看。frametag是跟框架的-tag命令有关,没有指定 tag命令,frametag就是一个null字符串。先看frametag不为空情况,会创建一个id-tag.apk文件,然后return(通常这个逻辑很少执行)。
frametag为空的条件下会根据id创建一个id.apk的文件,正常情况下就是id=1,至于id为什么是1上面有说过系统框架的资源id 0x01。id=1证明是系统框架,会把aapt内置的brut/androlib/android-framework.jar复制到指定路径下,例如我的路径 /Users/LongFei/Library/apktool/framework/1.apk。如果有用到厂商的框架文件,例如htc的框架就会变成2.apk,依此类推,1.apk和2.apk是共存的。
获取到的系统框架apk文件了,输出Loading resource table from file: … 我们熟悉的日志,最终逻辑执行到了getResPackagesFromApk(),这个方法应该不陌生在说解析resource.asrc文件中说过,不做说明。步骤3,简单说就是把关联的系统框架资源解析了一下。
步骤5-规范清单文件包名
这部分没什么好说,重点看adjustPackageManifest()函数,分别将解析完成的resources.arsc和清单文件进行包名比较。如果包名一致,打印 LOGGER.info(“Regular manifest package…”);日志!至于为什么包名一致,跟步骤6关系有关。解析完resources.arsc和清单文件后,生成资源文件也就是res文件,资源文件要有包名才能生成资源。
步骤6和7-生成res文件
开始我有个误区一直以为decodeResourcesFull函数就是解析resources.arsc文件,通读整个解码逻辑还有多次断点分析并不是这样。
经过前面执行的步骤,清单文件还有.arsc文件都是解析好了等待被使用的,decodeResourcesFull主要还是对当前解码后资源做处理(解码后的res文件)。执行到 mAndrolib.decodeResourcesFull函数时,getResTable()此时不为空已经有个完整的ResTable数据表了(对此有怀疑的可以断点调试一下就知道)。
这个方法其实用到的解析类和AndroidManifest.xml的解析类是一样的,因为他们都属于arsc格式,而且资源文件也是xml格式的,这里值得注意的是,会产生一个反编译中最关键的一个文件:public.xml,这个文件是在反编译之后的resvaluespublic.xml
而这里的id值是一个整型值,8个字节;由三部分组成的:
PackageId+TypeId+EntryId
-
PackageId:是包的Id值,Android中如果是第三方应用的话,这个值默认就是0x7F,系统应用的话就是0x01上面说过, PackageId 等于 id=“0x7f010000”
-
TypeId:资源的类型ID, 资源的类型有 animator、anim、color、drawable、layout、menu、raw、string 和 xml 等等若干种,每一种都会被赋予一个 ID,而且这些类型的值是从1开始逐渐递增的,而且顺序不能改变,attr=0x01,drawable=0x02…
-
Entry ID是指每一个资源在其所属的资源类型中所出现的次序。注意,不同类型的资源的Entry ID有可能是相同的,但是由于它们的类型不同,我们仍然可以通过其资源ID来区别开来。
步骤8-解码dex文件,将dex解析成smali源码
只看完整解码.dex文件函数条件,接着看重点是看 SmaliDecoder.decode函数,这个方法主要将dex文件解析成smali源码。
这部分代码主要看DexFileFactory.loadDexContainer(),这里用到了第三方的dexlib2来处理dex字节码,处理完dex文件后,交给Baksmali.disassembleDexFile()同样也是第三方库api,最后生成smali文件。
步骤9-10-11部分
这部分内容不细说了,上面已经详细介绍过了,整个逻辑已经介绍完毕。
apktool源码可读性非常高,而且写的思路非常清晰明确。内容通俗易懂,命名非常标准,一看就能理解什么意思,对各种可能出现的问题和故障做了充分的处理。
只要熟悉resource.arsc文件,AndroidManifest.xml文件,dex文件这三种类型文件,加上断点分析基本没什么太大的难度。看完apktool的源码,最大的好处就是以后出现反编译的问题可以自己动手进行调试。对安卓原生系统生成apk 拆解apk也有个深层次的理解,同时也可以研究反调试的手段及方法。
说一句实话,研究apktool源码消耗了很多私人时间来熟悉代码、分析代码。中间遇到过很多我不熟悉也没见过的知识,所幸我坚持的看完了解码整体的逻辑,并且通俗的写了出来,收获也是满多的。
原计划把上述说的三种文件都单独写一篇博文,然后把链接放到这篇博文上的,奈何私人时间上不允许。没有全部完成,所以选几篇大佬写的非常不错的文章放上去了。文章中涉及到解析.arsc文件和.dex后续有空我会补成我的链接。
文中很多话都用大白话,并没有用专业的术语来说,只是为了记录个人分析过程,不喜勿喷。这篇文章是我个人的总结和分析,不保证所有的东西都是对的,如有不对欢迎指出,我会及时更正!!!