基于Frida-RPC模拟执行so文件

在 13.8 节中,我们使用 IDA Pro 对 so 文件进行了逆向处理,还原了其中的一些逻辑,把汇编代码转化为了可读性更好的 C/C++ 代码,再加以适当的动态调试,便找出了 so 文件中隐含的加密算法。

但 so 文件本身也可以设置一定的保护措施。我们已经在 11.1 节了解了 JavaScript 的混淆机制,混淆之后的 JavaScript 代码可读性变得非常差,会给我们分析带来很大的难度。同理,如果在 so 文件中添加了一些混淆机制,那么 so 文件内部的代码逻辑也会进一步变得不可读,即使把其内容转化为 C/C++ 代码,也难以阅读和分析,这就是 Native 层的混淆。

在 Native 层实现混淆,常用的技术是 OLLVM,即针对 LLVM 的代码混淆工具。

OLLVM 的简介

OLLVM 是 Obfuscator-LLVM 的简称,是瑞士西北应用科技大学安全实验室于 2010 年 6 月发起的一个项目,该项目旨在提供一套开源的针对 LLVM 的代码混淆工具,以增加对逆向工程的难度。项目地址是 https://github.com/obfuscator-llvm/obfuscator ,目前的最新版本是 4.0。

到这里大家可能还是一头雾水,OLLVM 看起来是在 LLVM 上增加了一些混淆机制的结果,那 LLVM 文是什么?LLVM 就是一个编译器架构,是模块化、可重用的编译器和工具链技术的集合,功能是把源代码(如 C/C++ 代码)转化成目标机器能执行的代码。

如图 13-131 所示,整个 LLVM 架构从广义上分为三部分一一前端、优化器、后端。前端会用到一个叫作 Clang 的套件,Clang 是 LLVM 项目的一个子项目,负责完成一些代码的词法分析、语法分析和语义分析、生成中间代码。之后的代码优化和生成目标程序可以归类为 LLVM 后端,可以将前端生成的中间代码转化为机器码。

图13-131 LLVM 架构的示意图

我们深入了解一下这个架构中的中间代码生成的过程,这个过程中会用到一些 LLVM Pass 模块内部架构如图 13-132 所示。

图13-132 生成自标程序的示意图

OLLVM 的核心原理就是修改 Pass 模块,对中间代码进行混淆,这样后端依据中间代码生成的目标程序也会相应被混淆。因此,LLVM 和 OLLVM 最大的区别就是 Pass 模块不同。

OLLVM 支持 LLVM 支持的所有前端语言(C、C++、Objective-C、Fortran 等)和所有目标平台(x86、x86-64、PowerPC、PowerPC-64,、ARM、Thumb、MIPS 等),具有三大功能,分别是 Instructions Substitution(指令替换)、Bogus Control Flow(混淆控制流)和 Control Flow Flattening(控制流平展),具体可以参考 https://github.com/obfuscator-llvm/obfuscator/wiki/Features 中的介绍。通过这些混淆功能,原本的目标程序会被混得更加复杂,使我们难以分析,从而增加了逆向难度。

案例介绍

本节我们介绍一个在 Native 层进行 OLLVM 混淆的案例,示例 App 安装包的下载地址为 https://app9.scrape.center 。如果用 IDA Pro 对这个 App 中的 so 文件进行逆向,就可以看到它的混淆效果。

App9 是在 App8 的基础上增加 OLLVM 混淆得到的,所以我们可以对比一下 App8 和 App9 的不同。在 IDA Pro 中,打开 encrypt 方法的 Graph Overview 面板,可以大致看出这个方法内部的调用逻辑层级。

在混淆之前,so 文件(即 App8 中的 so 文件)的 Graph Overview 面板是图 13-133 展示的这样。

图13-133 App8 中 so 文件的 Graph Overview 面板

可以看到,整个调用逻辑还是相对清晰的一一层层嵌套,没有复杂的依赖关系。在混淆之后,SO 文件(即 App9 中的 so 文件)的 Graph Overview 面板是图 13-134 展示的这样。

图13-134 App9 中 so 文件的 Graph Overview 面板

可以看到,这里 encrypt 方法的调用逻辑就复杂了很多,经过一些混淆之后,方法内部的调用关系和依赖关系变得愈发复杂,此时我们想深入分析方法内部的逻辑,已经变得十分困难。

解决思路

对于刚提出的问题,如果再像 13.8 节那样逆向 so 文件和通过动态调试进行分析,那就是难上加难。有没有什么方法可以使这个流程更加简单,或者说有没有可以绕过这个流程的方法呢?

答案当然是有,解决思路通常有两种。

  • 直接硬刚:和 13.8 节的内容类似,通过各种辅助调试工具和追踪工具找出 so 文件中隐含的关键加密逻辑。

  • 模拟执行:不关心 so 文件的内部逻辑,通过某种方式直接调用 so 文件,传入对应的参数,得到执行结果,即纯黑盒调用。

这两种思路各有优劣,如果我们采用第二种,那么确实可以免去一些复杂的分析过程。但这并不是万能的,因为某些 App 的 so 文件中包含一些风控检测,例如检测外部执行环境是不是正常等,如果检测有异常,就可能拒绝返回数据或者返回假数据等。所以有时候,这种思路不一定有效,这时需要我们深人 so 文件中找出核心问题并解决。

本节示例 App 中的 so 文件没有设置任何风控检测,所以我们可以采取 “模拟执行” 的方式调用 so 文件,得到对应的执行结果。

其实模拟执行 so 文件的方法有很多,有 Frida-RPC、AndServer-RPC、unidbg 等。本节我们先介绍利用 Frida-RPC 模拟执行 so 文件的过程。

准备工作

请确保已经正确配置好了 Frida 的环境,并能成功在电脑上用 Frida 连接到手机,具体有如下几个要求。

  • 在电脑上安装好 frida-tools 并可以成功导入使用。

  • 在手机上下载并运行 frida-server 文件,即在手机上启动一个 Frida 服务,以便电脑上的 Frida 客户端可以与之连接。

  • 让电脑和手机连在同一局域网下,并且能在电脑上用 adb 命令成功连接到手机。

具体的配置可以参考 13.5 节的内容。

另外我们需要在手机上安装 App9,并确保数据可以正常加载,运行效果和之前各个示例 App 的效果是一样的。

实战

首先我们可以使用 jadx-gui 和 IDA Pro 对整个 App9 进行反编译和反汇编分析,分析过程可以参考 13.8 节的内容,这里不再展开。分析之后,可以得到如下信息。

  • 关键的 token 参数的加密逻辑是在 Native 层实现的,即隐含在 so 文件中。

  • 调用 so 文件的过程是通过调用 NativeUtils 类的 encrypt 方法实现的,也就是说是在 Java 层实现的。

  • encrypt 方法接收两个参数,第一个参数是一个字符串,自目前是固定的 /api/movie,第二个参数是数据偏移量。

基于这些信息,我们可以使用 Frida-RPC 实现对 encrypt 方法的调用,首先新建一个 rpc.js 文件,其内容如下:

这里在最外层使用 rpc.exports 导出了一个 encrypt 方法的定义,encrypt 方法接收两个参数,一个是 string,另一个是 offset,方法内部的实现逻辑我们也了解过了。这里还是使用了 Java 对象的 perform 方法,调用 use 方法初始化了 NativeUtils 类,并赋值为 util 变量,接着调用这个变量的 encrypt 方法得到执行结果并赋值为 token 变量,最后返回这个变量。所以,最后 encrypt 方法的返回结果就是 NativeUtils 类中 encrypt 方法的执行结果。

现在我们已经在 Frida 脚本中声明了对应的 RPC 方法,那怎么调用它呢?很简单,使用一个 Python 脚本调用即可,脚本内容如下:

这里和之前一样,首先声明了几个常量。

  • BASE_URL:请求电影数据的 API 的前缀。

  • INDEX_URL:请求电影数据列表的 API 的完整 URL,这里预留了几个占位符,limit 是每次请求要获取的数据量,offset 是数据偏移量,token 是加密参数 token。

  • MAX_PAGE:最大的页码数。

  • LIMIT:就是 INDEX_URL 中的 limit 参数,是一个常量。

接着新建了一个 session 对象,这里依然是使用 attach 方法将其关联到了当前执行的包名上,然后读取了并加载刚才定义的 JavaScript 脚本,将脚本赋值为 script 变量。

随后定义了一个 get_token 方法,它接收两个参数,这两个参数和刚才 encrypt 方法的参数一一对应,方法中有一个关键的调用声明,是 script.exports,其返回结果和刚才 JavaScript 脚本中的 rpc.exports 是对应的,由于我们在 rpc.exports 中声明了 encrypt 方法,所以在 script.exports 里就能调用 encrypt 方法,传入对应的参数后,就能得到 JavaScript 脚本中 encrypt 方法的返回结果。

之后遍历了所有的电影数据列表页,构造好 offset,得到对应的 token 值,最后用 limit、offset、 token 拼接成完整的 API URL,并调用 requests 库的方法请求这个 URL。

运行结果如下:

可以看到,我们成功模拟执行了 so 文件,直接得到了加密参数 token 的值,然后构造请求实现了数据的爬取。

总结

本节我们介绍了利用 Frida-RPC 技术模拟执行 so 文件的过程,在这个过程中我们不需要关心 so 文件内部的混淆机制,对 so 文件纯黑盒调用即可得到关键信息。

本节的一些对 OLLVM 和 LLVM 的概念介绍,部分参考自下面两个内容。

  • 看雪论坛上的 “ollvm 快速学习” 文章。

  • CSDN 网站上的 “OLLVM 环境搭建、源码分析及使用” 文章。