JEB的使用
上一节中我们了解了 jadx 的基本使用方法,体验了其强大的功能。
当然,类似提供反编译功能的工具还有很多,JEB 就是一个。本节我们会结合一个案例学习使用 JEB 反编译和分析 apk 文件的过程。
JEB 的简介
JEB 是由 PNF 软件(PNF Software)机构开发的一款专业的反编译 Android App 的工具,适用于逆向和审计工程,功能非常强大。相比 jadx,JEB 除了支持 apk 文件的反编译和 Android App 的动态调试,还支持 ARM、MIPS、AVR、Intel-x86、WebAssembly、Ethereum(以太坊)等程序的反编译、反汇编、动态调试等。另外,JEB 能解析和处理一些 PDF 文件,是一个极其强大的综合性逆向和审计工具。
由于本章主要讲 Android 逆向相关的内容,所以多关注它和 Android 相关的功能。对于 Android App,JEB 主要提供如下功能。
-
可以对 Android App 和 Dalvik(Android 虚拟机,类似 Java 中的 JVM)字节码执行精确和快速的反编译操作。
-
内置的分析模块可以对高度混淆的代码提供虚拟层次化重构,对分析混淆代码很有帮助。
-
可以对接 JEB API 来执行一些逆向任务,支持用 Java 和 Python 编写自动化逆向脚本。
JEB 支持 Windows、Linux、Mac 三大平台,目前主要分为三个版本:JEB CE(社区版)、JEB Android(Androld版)、JEB Pro(专业版)。JEB CE 提供一些基础的功能,如反编译 dex 文件,反编译和反汇编 Intel-x86,但不支持反编译 Dalvik 字节码。JEB Android 则更专注于 Android 系统,支持反编译 dex 文件,也支持反编译和反汇编 Dalvik 字节码。JEB Pro 则是 “完全体”,支持官网介绍的所有功能。三个版本的具体功能对比可以参考官网( https://www.pnfsoftware.com/jeb )的介绍。JEB CE 是免费的,JEB Android 和 JEB Pro 都是收费的,需要购买许可证才可以使用。
准备工作
本节我们要使用 JEB(JEB Android 或 JEB Pro)来反编译和动态调试一个 Android App,关于 JEB 的下载地址和安装方式可以参考 https://setup.scrape.center/jeb 。
安装好 JEB 之后,需要下载示例 apk 文件,地址为 https://app5.scrape.center/ ,下载好后保存为 scrape-app5.apk 文件即可。
然后准备好一部 Android 手机,真机和模拟器都可以,在手机上安装好刚下载的 apk 文件并启动 App。另外还需要确保在电脑上能使用 adb 命令正常连接到手机。
实战
打开 JEB,把示例 apk 文件直接拖到窗口里,经过一段时间的处理,JEB就完成了反编译,如图 13-23所示。
图13-23 JEB 的界面
从左侧的 Bytecode/Hierarchy 部分可以看到,反编译后的代码以一个个包的形式组织在一起,右侧显示的则是 Smali 代码(Dalvik 的反汇编程序实现,类似 x86 平台下的汇编语言),通过这个代码、我们大体能够看出一些执行逻辑和数据操作的过程。
虽然我们得到了 Smali 代码,但这似乎不是用 Java 语言编写的,我们从哪个地方入手呢?由于我们要找的是 URL 中加密参数 token 的位置,因此最简单的方式当然是借助 API 的一些标志字符串查找了。
我们知道示例 App 在启动的时候会开始请求数据,请求 URL 里包含关键字 /api/movie,以及 offset、token 等参数,具体的抓包过程这里就不再赞述了,可以参考 12.3 节的内容。
因为 API 的路径通常是用字符串定义的,而且一般写死在 App 代码里,所以我们可以尝试使用 /api/movie 来搜索,看看是否能搜索到相关的逻辑。点击菜单中的 “Edit”一→“Find”,打开 JEB 的查找功能,输入 “/api/movie”,如图 13-24 所示。
点击 “Find” 按钮,可以看到 JEB 帮我们找到了对应的 Smali 代码,如图 13-25 所示。
图 13-24 在 JEB 工具中搜索 /api/movie 图 13-25 /api/movie 对应的 Smali 代码
这里其实是声明了一个静态不可变的字符串,叫作 indexPath。但这里是 Smali 代码,我们如何找到对应的源码位置呢?可以先选中该字符串,然后右击,在菜单中选择 “Decompile”,如图 13-26 所示。
图13-26选择 “Decompile”
之后就成功定位到了 Java 代码的声明处,如图 13-27 所示。
图13-27 声明 indexPath 字符串的源码
这里就是 indexPath 的原始声明,同时还能看到 index 方法的声明,它包含 3 个参数:offset、limit 和 token。由此可以发现,这里的参数和声明恰好跟请求 URL 的格式是相同的。
我们可以在 Java 代码处再次选择 “Decompile”,即可回到对应的 Smali 代码处,如图 13-28 所示。
图13-28 回到 Smali 代码
可以看到 Smali 代码的定义和 Java 代码的定义一一对应。但这里似乎仅仅定义了 API,并没有真正的实现,因此我们可以接着搜索引用 /api/movie 的位置,如图 13-29 所示。
图13-29 引用 /api/movie 的位置
同样在查找结果处右击,在打开的菜单中选择 “Decompile”,就跳转到了对应的 Java 代码处,如图 13-30 所示。
图13-30 引用 /api/movie 的 Java 代码
很明显,这里就是逻辑实现相关的代码了。稍微读一下这里的 Java 代码,大致是调用了一个 apiService 对象的 index 方法,并传入了几个参数,第一个参数是 arg6 和 arg7 计算后的结果,第二个参数是 arg7,第三个参数是 encrypt 方法返回的结果(这个方法的参数还是一个包含 /api/movie 字符串的 ArrayList 对象)。
这里看起来似乎是请求 API 的一个操作,但是我们也不确定是不是真的是这个位置。为了更好地确定这里是不是我们想要的数据加载入口,下面尝试使用 JEB 的动态调试功能验证一下,例如在刚才的代码位置添加一个断点,然后滑动 App 加载数据,看在运行到断点的位置时是否停止了运行,如果停止了,就证明我们找的这个位置是正确的,否则继续寻找。
那怎么动态调试呢?其实操作很简单,首先确保本节的示例 App 已经安装在了手机上,并且能在电脑上通过 adb 命令与手机连接。然后运行 adb 命令:
adb shell am start -D -n com.goldze.mvvmhabit/.ui.MainActivity
这条命令的功能就是让 App 以调试模式启动,-D 指定了 App 以调式模式启动,-n 指定了启动入口,这里设置为示例 App 的包名和 MainActivity。运行这条命令后,可以看到手机上显示如图 13-31 所示的字样。
图13-31 App 正在等待 Debugger 的连接
这时回到 JEB 的界面,点击工具栏里的 “Debug” 按钮,如图 13-32 所示。
图13-32 点击 “Debug” 按钮
之后会检测出正在运行的 Android 设备,如图 13-33 所示。
图13-33 显示正在运行的 Android 设备
点击下方的 “Attach” 按钮,Debugger 就成功挂载了手机上的 App 进程,JEB 的界面变成图 13-34 所示的这样,弹出了几个调试窗口。
图13-34 现在的 JEB 界面
与此同时,手机上的 “Waiting for Debugger” 提示也消失了,示例 App 正常运行并加载出了第一页数据,如图 13-35 所示。
这证明挂载成功了。在 JEB 中,我们可以选中想要调试的 Smali 代码,然后点击菜单栏中的 “Debugger” → “ToggleBreakpoint” 来添加断点,如图 13-36 所示。
图13-35 第一页电影数据 图13-36 点击 “Toggle Breakpoint”
例如在刚才的 /api/movie 对应的 Smali 代码处添加一个断点,效果见图 13-37。
图13-37 添加一个断点
这时再次滑动 App 触发数据的加载,然后神奇的事情发生了—— JEB 显示代码执行到断点的位置时停下来了,如图 13-38 所示。
这说明什么?说明数据加载的过程正是调用这个断点位置处代码的过程,即数据加载入口找对了。我们可以点击 “Step Over” 按钮尝试逐行执行此处的代码,如图 13-39 所示。
图13-38 执行至断点位置 图13-39 逐行执行数据加载入口处的代码
在执行的过程中,我们可以观察 “VM/Locals” 窗口,这里显示了各个变量的类型和对应的值如图 13-40 所示。
图13-40 “VM/Locals” 窗口中的内容
另外,可以点击工具栏中的 “Run” 按钮继续执行到下一个断点,如图 13-41 所示
图13-41 “Run” 按钮
如果没有下一个断点,会直接完成数据请求,App 中加载出下一页数据。
经过多次的数据加载和调试,以及观察对比 “VM/Locals” 窗口中的各个变量(如果有必要,还可以用抓包软件验证),最终不难发现,变量 v2 就对应 Java 源代码里面的 ArrayList 对象,vo 对应 offset 参数的值,v7 对应 limit 参数的值,v3 对应 GET 请求过程中的 token 参数。
另外,可以推测 v3,即 token 参数的值就是刚才图 13-30 中 encrypt 方法返回的结果,也就是 token 字符串的生成逻辑包含在这个 encrypt 方法里。
我们先详细看看 encrypt 方法是怎么定义的,再单独对这个方法进行反编译操作,如图 13-42 所示。
图13-42 反编译 encrypt 方法
找到对应的 encrypt 方法后,再定位到 Java 代码中它的声明处,如图 13-43 所示。
其实很明显了,我们分析一下这段 Java 代码,传入 encrypt 方法的参数是 arg7,经过刚才的分析可知,arg7 其实是一个长度为 1 的列表,其内容是 ["/api/movie"]。方法中先定义了一个叫作 arg1 的字符串,其实就是获取的时间戳信息;然后把 arg1 添加到 arg7 中,现在 arg7 里就有两个内容了,一个是字符串 /api/movie,一个是时间戳信息;接着声明了 sign 变量,可以看出其是用逗号把 arg7 中的两个内容拼接在一起,外层再调用 shaEncrypt 方法的结果(经过观察,shaEncrypt 方法其实实现的就是 SHA1 算法);后面又声明了一个 ArrayList 对象,赋值给 temp 变量,并把 sign 和 arg1 的值添加进去,再把 temp 中的内容使用逗号拼接起来,最后进行 Base64 编码及返回。
那么现在 token 字符串的整体加密逻辑就清楚了。
模拟
了解了基本的算法流程后,我们门可以用 Python 代码实现这个流程:
这里最关键的就是 token 字符串的生成过程,我们定义了一个 get_token 方法来实现,整体思路就是上面梳理的内容:
-
在列表中加入当前时间戳;
-
将列表内容用逗号拼接起来;
-
对拼接结果进行 SHA1 编码;
-
将编码结果和时间戳再次拼接;
-
对拼接后的结果进行 Base64 编码。
最后的运行结果如下:
这样我们就成功爬取到示例 App 的数据了。
总结
本节我们通过一个案例讲解了比较基本的 App 逆向过程,包括 JEB 工具的使用方法、动态调试和代码追踪操作等,还通过分析代码厘清了基本逻辑,最后模拟实现了 API 的参数构造和请求发送,得到最终的数据。
当然本节介绍的内容仅是 JEB 所有功能的冰山一角,更多关于 JEB 的使用教程可以参考其官方文档 https://www.pnfsoftware.com/jeb/manual/ 。