jadx 的使用
我们知道,每个 Android App 都有对应的安装包,是以 apk 为名字后缀的文件,App 的实现逻辑都包含在这个文件中。apk 文件往往包含资源文件(如图标、字体等),由 Java 代码编译而成的 dex 文件(可通过反编译 dex 文件得到 Java 代码),和一些相关的配置文件(如 AndroidManifest.xml 文件)。关于其中的细节,这里就不再一一展开介绍了,如果想了解更多内容可以学习 Android 开发相关的基本知识。
逆向中关键的一步就是反编译 apk 文件,将其还原成可读性高的 Java 代码,在多数情况下,我们通过观察并分析这个 Java 代码就能找到想要的核心逻辑。工欲善其事,必先利其器。用来反编译 apk 文件的工具有很多,例如 jadx、JEB、Apktool 等,不同工具的用法和定位也有所不同。
本节我们先来了解一下 jadx 的使用方法。
jadx 的简介
jadx 是一款使用广泛的反编译工具,可以一键把 apk 文件还原成 Java 代码,使用起来简单,功能强大,还具有—些附加功能可以辅助代码追查。其 GitHub 地址为 https://github.com/skylot/jadx 。主要具有如下几个功能:
-
除了反编译
apk文件,还可以反编译jar、class、dex、aar等文件和zip文件中的Dalvik字节码。 -
解码
AndroidManifest.xml文件和一些来自resources.arsc中的资源文件。 -
一些 apk 文件在打包过程中增加了 Java 代码的混淆机制,对比
jadx提供反混淆的支持。
jadx 本身是一个命令行工具,仅仅通过 jadx 这个命令就可以反编译一个 apk 文件。除此之外,它也有配套的图形界面工具——jadx-gui,这个使用起来更加方便,能直接以图形界面的方式打开一个 apk 文件。同时,jadx-gui 对反编译后得到的 Java 代码和其他资源文件增加了高亮支持(就像在 IDE 中打开这些内容一样),还具有快速定位、引用搜索、全文搜索等功能。所以,我们往往直接使用 jadx-gui 完成一些反编译操作。
准备工作
本节会以一个 App 为示例,介绍 jadx 的命令和 jadx-gui 的使用方法。在开始之前,请先安装好这两个工具,jadx 的安装方法可以参考 https://setup.scrape.center/jadx。
然后提前下载好示例 App 对应的 apk 文件( https://app5.scrape.center/ ),下载下来的文件保存为 scrape-app5.apk 。
jadx 的命令
使用 jadx 的命令执行文件的反编译操作,主要是指定一些输入参数和输出参数,这些参数的设置细节直接参考参数说明即可。运行 jadx -h 命令,查看 jadx 命令的用法:
jadx[-gui] [命令] [选项] <输入文件> (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc, .aab, .xapk, .apkm, .jadx.kts)
命令 (使用 '<command> --help' 查看命令选项):
plugins - 管理 jadx 插件
选项:
-d, --output-dir - 输出目录
-ds, --output-dir-src - 源码输出目录
-dr, --output-dir-res - 资源输出目录
-r, --no-res - 不反编译资源
-s, --no-src - 不反编译源代码
-j, --threads-count - 处理线程数,默认值: 4
--single-class - 反编译单个类,完整名称、原始名称或别名
--single-class-output - 反编译单个类时的输出文件或目录
--output-format - 可以是 'java' 或 'json',默认值: java
-e, --export-gradle - 保存为 Gradle 项目 (将 '--export-gradle-type' 设置为 'auto')
--export-gradle-type - 导出 Gradle 项目模板:
'auto' - 自动检测
'android-app' - Android 应用 (apk)
'android-library' - Android 库 (aar)
'simple-java' - 简单 Java
-m, --decompilation-mode - 代码输出模式:
'auto' - 尝试最佳选项 (默认)
'restructure' - 恢复代码结构 (正常的 Java 代码)
'simple' - 简化指令 (线性,带 goto)
'fallback' - 未修改的原始指令
--show-bad-code - 显示不一致的代码 (反编译不正确)
--no-xml-pretty-print - 不美化 XML
--no-imports - 禁用 import 语句的使用,始终写入完整的包名
--no-debug-info - 禁用调试信息解析和处理
--add-debug-lines - 如果可用,添加带调试行号的注释
--no-inline-anonymous - 禁用匿名类内联
--no-inline-methods - 禁用方法内联
--no-move-inner-classes - 禁用将内部类移至父类
--no-inline-kotlin-lambda - 禁用 Kotlin lambda 内联
--no-finally - 不提取 finally 块
--no-restore-switch-over-string - 不恢复字符串 switch 语句
--no-replace-consts - 不用匹配的常量字段替换常量值
--escape-unicode - 在字符串中转义非拉丁字符 (使用 \u)
--respect-bytecode-access-modifiers - 不改变原始访问修饰符
--mappings-path - 去混淆映射文件或目录。允许的格式: Tiny 和 Tiny v2 (均为 '.tiny'),Enigma (.mapping) 或 Enigma 目录
--mappings-mode - 设置处理去混淆映射文件模式:
'read' - 只读取,用户可以随时手动保存 (默认)
'read-and-autosave-every-change' - 读取并在每次更改后自动保存
'read-and-autosave-before-closing' - 在退出应用或关闭项目前读取并自动保存
'ignore' - 不读取也不保存 (可用于跳过加载项目中引用的映射文件)
--deobf - 激活去混淆
--deobf-min - 名称的最小长度,如果更短则重命名,默认值: 3
--deobf-max - 名称的最大长度,如果更长则重命名,默认值: 64
--deobf-whitelist - 以空格分隔的类 (完整名称) 和包 (以 '.*' 结尾) 列表,用于排除去混淆,默认值: android.support.v4.* android.support.v7.* android.support.v4.os.* android.support.annotation.Px androidx.core.os.* androidx.annotation.Px
--deobf-cfg-file - 用于 JADX 自动生成名称的去混淆映射文件 (JOBF 文件格式),默认值: 与输入文件在同一目录且同名,扩展名为 '.jobf'
--deobf-cfg-file-mode - 设置处理 JADX 自动生成名称的去混淆映射文件模式:
'read' - 如果找到则读取,不保存 (默认)
'read-or-save' - 如果找到则读取,否则保存 (不覆盖)
'overwrite' - 不读取,始终保存
'ignore' - 不读取也不保存
--deobf-res-name-source - 更好的资源名称来源:
'auto' - 自动选择最佳名称 (默认)
'resources' - 使用资源名称
'code' - 使用 R 类字段名称
--use-source-name-as-class-name-alias - 使用源名称作为类名别名:
'always' - 如果可用,始终使用源名称
'if-better' - 如果源名称看起来比当前名称更好,则使用源名称
'never' - 永不使用源名称,即使它可用
--source-name-repeat-limit - 允许使用源名称,如果其出现次数少于限制数,默认值: 10
--use-kotlin-methods-for-var-names - 使用 Kotlin 内在方法重命名变量,值: disable, apply, apply-and-hide, 默认值: apply
--rename-flags - 修复选项 (逗号分隔列表):
'case' - 修复大小写敏感问题 (根据 --fs-case-sensitive 选项),
'valid' - 重命名 Java 标识符以使其有效,
'printable' - 从标识符中移除不可打印字符,
或单个 'none' - 禁用所有重命名
或单个 'all' - 启用所有 (默认)
--integer-format - 整数显示方式:
'auto' - 自动选择 (默认)
'decimal' - 使用十进制
'hexadecimal' - 使用十六进制
--fs-case-sensitive - 将文件系统视为大小写敏感,默认为 false
--cfg - 将方法控制流图保存为 dot 文件
--raw-cfg - 保存方法控制流图 (使用原始指令)
-f, --fallback - 将 '--decompilation-mode' 设置为 'fallback' (已弃用)
--use-dx - 使用 dx/d8 转换 Java 字节码
--comments-level - 设置代码注释级别,值: error, warn, info, debug, user-only, none, 默认值: info
--log-level - 设置日志级别,值: quiet, progress, error, warn, info, debug, 默认值: progress
-v, --verbose - 详细输出 (将 --log-level 设置为 DEBUG)
-q, --quiet - 关闭输出 (将 --log-level 设置为 QUIET)
--disable-plugins - 以逗号分隔的要禁用插件 ID 列表,默认值:
--version - 打印 jadx 版本
-h, --help - 打印此帮助
插件选项 (-P<名称>=<值>):
dex-input: 加载 .dex 和 .apk 文件
- dex-input.verify-checksum - 加载前验证 dex 文件校验和,值: [yes, no], 默认值: yes
java-convert: 将 .class, .jar 和 .aar 文件转换为 dex
- java-convert.mode - 转换模式,值: [dx, d8, both], 默认值: both
- java-convert.d8-desugar - 在 d8 中使用 desugar,值: [yes, no], 默认值: no
kotlin-metadata: 使用 kotlin.Metadata 注解进行代码生成
- kotlin-metadata.class-alias - 重命名类别名,值: [yes, no], 默认值: yes
- kotlin-metadata.method-args - 重命名函数参数,值:: [yes, no], 默认值: yes
- kotlin-metadata.fields - 重命名字段,值: [yes, no], 默认值: yes
- kotlin-metadata.companion - 重命名伴生对象,值: [yes, no], 默认值: yes
- kotlin-metadata.data-class - 添加数据类修饰符,值: [yes, no], 默认值: yes
- kotlin-metadata.to-string - 使用 toString 重命名字段,值: [yes, no], 默认值: yes
- kotlin-metadata.getters - 将简单 getter 重命名为字段名,值: [yes, no], 默认值: yes
kotlin-smap: 使用 kotlin.SourceDebugExtension 注解重命名类别名
- kotlin-smap.class-alias-source-dbg - 从 SourceDebugExtension 重命名类别名,值: [yes, no], 默认值: no
rename-mappings: 支持各种映射
- rename-mappings.format - 映射格式,值: [AUTO, TINY_FILE, TINY_2_FILE, ENIGMA_FILE, ENIGMA_DIR, SRG_FILE, XSRG_FILE, JAM_FILE, CSRG_FILE, TSRG_FILE, TSRG_2_FILE, PROGUARD_FILE, INTELLIJ_MIGRATION_MAP_FILE, RECAF_SIMPLE_FILE, JOBF_FILE], 默认值: AUTO
- rename-mappings.invert - 加载时反转映射,值: [yes, no], 默认值: no
smali-input: 加载 .smali 文件
- smali-input.api-level - Android API 级别,默认值: 27
环境变量:
JADX_DISABLE_XML_SECURITY - 设置为 'true' 禁用 XML 文件的所有安全检查
JADX_DISABLE_ZIP_SECURITY - 设置为 'true' 禁用 zip 文件的所有安全检查
JADX_ZIP_MAX_ENTRIES_COUNT - zip 文件中允许的最大条目数 (默认值: 100 000)
JADX_CONFIG_DIR - 自定义配置目录,默认使用系统目录
JADX_CACHE_DIR - 自定义缓存目录,默认使用系统目录
JADX_TMP_DIR - 自定义临时目录,默认使用系统目录
示例:
jadx -d out classes.dex
jadx --rename-flags "none" classes.dex
jadx --rename-flags "valid, printable" classes.dex
jadx --log-level ERROR app.apk
jadx -Pdex-input.verify-checksum=no app.apk
可以看到,参数 <input files> 就是输入文件的路径,其他参数如 -d 可以指定反编译后输出文件的路径,-r 可以指定不解析资源文件(能够提升整体反编译的速度)。于是我们可以使用下面的命令对已经下载好的 scrape-app5.apk 文件进行反编译:
jadx scrape-app5.apk -d scrape-app5
运行完毕后,本地会生成一个 scrape-app5 文件夹,其内容如图 13-1 所示。
图13-1反编译生成的文件夹
从中可以看到,AndroidManifest.xml 文件、资源文件和原始的 java 文件等都成功还原出来了。例如 com.glodze.mvvmhabit.ui.MainActivity.java 文件的内容还原结果如下:
可见还原效果还是比较理想的。就这样,我们只用一条简单的命令就完成了对 apk 文件的反编译,其中 Java 代码的逻辑一览无遗。
jadx-gui 的使用方法
jadx-gui 是一个图形界面工具,它就像一个 IDE,支持很多方便快捷的交互式操作(例如把一个 apk 文件拖到 jadx-gui 后,它会直接打开这个文件,之后高亮显示反编译后的代码),以及代码搜索、定位等。相比 jadx,我个人更推荐直接使用 jadx-gui 反编译 apk 文件。
下面就来了解一下 jadx-gui 的用法。
启动和反编译
安装 jadx-gui 工具后,可以直接使用命令启动它:
jadx-gui
运行该命令后,jadx-gui 便启动了,这时我们看到的界面类似图13-2所示的这样。
图13-2 jadx-gui启动后的界面
可以通过文件路径打开示例 apk 文件,也可以直接将 apk 文件拖到 jadx-gui 的窗口中,还可以从菜单栏中的 “文件”一→“打开文件” 调出资源管理器来打开 apk 文件。文件打开之后,稍等片刻,反编译就完成了,这时看到的界面如图 13-3 所示。
图13-3 反编译后的界面
从界面的左侧可以发现,反编译后的 Java 源代码以一个个包的形式组织在一起,另外还有资源文件,其中包括图片文件、布局文件和 AndroidManifest.xml 文件(内含 apk 文件的基本信息)等。在左侧展开想要查看的包,右侧就会出现对应的 Java 源代码,如图13-4所示。
图13-4查看Java源代码
可以看出,Java 源代码的还原度还是很高的。
保存为 Gradle 项目
我们也可以把反编译后的文件另存为 Gradle 项目,Gradle 项目就是开发版本的 Android 项目,如图 13-5 所示。
导出后,会发现项目的目录结构如图 13-6 所示。
图13-5另存为Gradle项目 图13-6项目的目录结构
导出后的项目目录结构和我们在 jadx-gui 界面里看到的结构基本一致,这个项目是可以被 Android Studio 工具打开的,打开的界面如图 13-7 所示。
图13-7 在 AndroidStudio 工具中打开项目
打开之后的代码一般没法直接运行,因为毕竟整个项目是反编译出来的,我们不太可能完全还原出开发版本的 Android 项目。如果你对 Android 开发比较了解,可以试着修改一下源码和 Gradle 配置,是可以使项目正常运行的。即使不能运行也没有关系,因为我们的目的并不是运行这个代码,而是分析其中的逻辑,所以要把目光聚焦在查找和定位目标方法与逻辑定义上,Android Studio 能够帮我们更方便地完成这些操作。
当然,jadx-gui 也提供了查找和定位的相关功能,现在我们回到 jadx-gui,了解一下它的其他常见用法。
文本搜索
在 12.3 节,我们已经分析过本节的示例 App,并对其进行了抓包处理,知道了该 App 在启动阶段会请求 /api/movie 这个 API 获取数据,同时在请求的过程中会带加密参数 token,完整的请求 URL 是 https://app5.scrape.center/api/movie/?offset=30&limit=10&token=M2U5NjYxZjEwNmQyMD1kYmYyNTIzzGFkYmZkYzdiNT hlYTgzOWQoMywxNjIxNzAzMDA5%oA 。
学习本章内容之前,我们只能通过抓包知道最终的 token 参数取什么值,现在不一样了,我们已经成功反编译了 apk 文件,得到了 Java 源代码,就有办法找出这个 token 的生成逻辑。可以先寻找一些突破口,例如搜索固定的字符串,像这里 URL 中的 /api/movie 和 token 这个字符串都是可以的,因为在构造 URL 的时候,它们经常就是写死的常量,如果能找到对应的字符串,就可以顺藤摸瓜找到 token 的生成逻辑。
那我们尝试在源代码中搜索一下 URL 中的 /api/movie 字符串,可以使用 jadx-gui 提供的搜索功能,打开菜单栏里的 “导航”一→“搜索文本”,如图 13-8 所示。
这时 jadx-gui 会显示一个搜索框,如图 13-9 所示。在 “搜索文本:” 下方填入 “/api/movie”,同时可以从类名、方法名、变量名和代码中选择搜索位置,自行勾选即可,下方会显示搜索结果。
图13-9 设置搜索细节
可以看到,搜索到了两处包含 /api/movie 字符串的位置,可以依次看一下这两处的内容,先选中第一个搜索结果,然后点击 “转到” 按钮,即可跳转到对应的代码处,如图 13-10 所示。
图13-10第一个搜索结果对应的代码
可以看到,图13-10中有一个名为 i 的类,里面有一个 index 方法,该方法接收两个参数,分别是 i 和 i2,自前我们还不知道它们代表什么。
不妨先看一下 index 方法的基本逻辑吧。方法内首先构造了一个 ArrayList 对象并赋值给 arrayList 变量,这相当于 Python 中初始化的空列表;然后调用 add 方法往 arrayList 中添加了一个字符串 /api/movie;接着调用了一个 encrypt 方法,并传入参数 arrayList,通过名字可以大致推测 encrypt 方法实现的是加密过程,加密后的结果被赋值为 encrypt 变量;最后调用 index 方法本身,并传入参数 i 和 i2 的组合计算结果以及刚刚得到的 encrypt 变量,并返回最终的结果。
现在,我们大致了解了整个流程,但对其中的一些参数和调用过程还是一头雾水,难道这里就包含着 token 的加密逻辑吗?似乎也不好确认。
逆向过程其实就是包含一些不确定性的,在查到一些蛛丝马迹之后,如果不确定查到的内容是不是我们想要的,就继续深入研究,这就是一个推敲和追查的过程。
图13-11 跳到encrypt方法的声明处
查找方法和声明
我们可以试着寻找一下 encrypt 方法的声明,右击 encrypt 方法名,会打开一个菜单,选择 “跳到声明”,如图 13-11 所示。
这时就会跳转到声明 encrypt 方法的位置,如图 13-12 所示。
图 13-12 中显示了 encrypt 方法的源代码,初步观察其逻辑是传入一个包含字符串的 List 对象,然后经过一些加密处理返回一个高度疑似 Base64 编码的字符串,而我们之前看到的 token 字符串的格式也符合 Base64 的编码格式,至于这里究竟是不是 token 的生成过程,我们会在 13.2 节做一步验证,这里先主要了解 jadx-gui 的一些用法。
图13-12encrypt方法的声明
刚才我们通过 “跳到声明” 选项到了声明 encrypt 方法的位置,那能不能通过该声明,找到调用 encrypt 方法的位置呢?那当然是可以的。
查找用例
右击声明处的 encrypt 方法名,在打开的菜单中可以看到一个 “查找用例” 的选项,如图 13-13 所示。
图13-13 “查找用例” 选项
点击之后,查找的结果如图 13-14 所示。
图13-14 调用 encrypt 方法的代码
搜索结果有两处,直接双击结果,或者先选中结果再点击 “转到” 按钮,都可以跳转到对应的代码处。我们看第一个结果,可以发现这就是之前调用 encrypt 方法的位置,如图 13-15 所示。
图13-15 第一个搜索结果
反混淆
jadx-gui 还有一个强大的功能,就是反混淆。从图 13-16 中,我们看到 encrypt 方法所在的类是 i,它实现了一个接口 h,仅从这些字母并不好推测究竟是什么意思,这是 App 在编译和打包阶段做了一些混淆操作导致的结果,和 JavaScript 中的变量混淆非常相似。
图13-16 类i和接口h
针对这个问题,jadx-gui 具有反混淆功能。我们可以打开反混淆开关,点击菜单中的 “工具”→“反混淆”,如图 13-17 所示。
图13-17 反混淆功能
神奇的事情发生了!原来的类名、接口名都还原出来了,如图 13-18 所示。
图13-18 真实的类名和接口名
代码可读性大大增强!反混淆能够进一步提升代码的还原度,从而让我们更方便地推敲代码中的逻辑。
常见问题
如果有些 apk 文件比较大,jadx-gui 反编译所需的时间和消耗的资源就会更多,所以有时候在反编译过程中会提示如下错误:
这里报了一个 OutOfMemoryError 错误,代表内存溢出,对于一些比较大的 apk 文件,是会出现这种问题的,我们可以尝试用如下两个方案解决。
-
增加 JVM 的最大内存:设置 JVM_OPTS,把 JVM 的最大内存调大,之后内存溢出的问题自然可以得到有效解决。
-
减小线程数:线程多了,反编译过程消耗的内存自然也会增多,可以在运行 jadx的命令时通过 -j 命令适当将线程数量设置为更小的值。
详细的设置方法可以参考 https://setup.scrape.center/jadx,本节不深入讲解。