Frida 的使用

在 13.4 节和 13.5 节,我们了解了 Xposed 的基本用法,可以说只要找到位置,就能通过 Hook 拿到数据。然而 Xposed 是具有局限性的,例如它只能Hook Java 层的逻辑,不能 Hook Native 层的。另外,整个 Xposed 模块的逻辑需要使用 Java 语言实现,如果我们对 Java 不熟悉,那么实现起来会有一定难度。

什么是 Native 层的逻辑呢?简单理解这就是使用 C/C++ 编写的一些逻辑。假设某个 App 中的某些算法是用 C/C++ 实现的,它们最终会被编译到一个 so 格式的文件中,Java 层可以直接调用该 so 文件执行对应的加密算法,而无须知道文件内部的具体逻辑。Xposed 是用 Java 实现的,可以 Hook Java 层的逻辑,但对于 Hook Native 层的逻辑,就无能为力了。

本节我们就介绍另外—个简单好用的 Hook 神器——Frida! 如果要用几个词描述 Frida,那就是强大、方便、灵活。

Frida 的简介

Frida 是一个基于 Python 和 JavaScript 的 Hook 与调试框架,是一款易用的跨平台 Hook 工具,无论 Java 层的逻辑,还是 Native 层的逻辑,它都可以 Hook。Frida 可以把代码插入原生 App 的内存空间,然后动态地监视和修改其行为,支持 Windows、Mac、Linux、Android、iOS 全平台。

Frida 是使用 Python 注入 JavaScript 脚本实现的,可以通过 JavaScript 脚本操作手机上的 Java 代码,Python 脚本和 JavaScript 脚本的编写跟执行是在电脑上进行的,而且无须在手机上额外安装 APP 和插件,所以整体实现起来更加灵活和轻量级,调试起来也更加方便。而 Xposed 需要使用 Java 实现一个模块,然后编译并安装到手机上,灵活性相对差一些,但如果要做持久化的 Hook,还是推荐使用 Xposed。

下面简单列一下 Xposed 和 Frida 的优缺点:

Xposed 的优缺点

  • 优点:非常适合编写 Java 层的 Hook 逻辑,因为自己就是用 Java 语言编写的;适合一些持久化的 Hook 操作,编写完毕后可以独立且永久地运行在手机上,适用于生产实践。

  • 缺点:配置环境的过程比较烦琐,在调试过程中需要编译和重新安装 Xposed 模块,对 HookNative 层逻辑无能为力。

Frida 的优缺点

  • 优点:Java 层和 Native 层的逻辑都能 Hook;在电脑上编写和执行脚本,修改之后无须重新编译和额外在手机上安装 App,操作方便又灵活;环境配置简单,能很好地支持跨平台。

  • 缺点:是用 JavaScript 操作 Java 逻辑,所以兼容性会差一些;更适合在开发阶段调试时使用,不太适合应用于生产实践。

准备工作

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

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

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

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

具体的安装方法可以参考 https://setup.scrape.center/frida

以上准备工作做好之后,就可以在电脑上运行 frida-ps 命令查看手机上运行着的 App 进程了,命令如下:

frida-ps -U

运行结果类似图 13-66 所示的这样。

图 13-66 手机上运行着的 App 进程

控制台输出了手机上运行的进程,证明电脑和手机连接成功!

本节接下来会以两个简单的 App 为例,讲解 Frida 的基础使用方法,所以请先下载并安装这两个 App。

  • AppBasic1:https://appbasic1.scrape.center/。

  • AppBasic2:https://appbasic2.scrape.center/。

Hook Java层的逻辑

首先,我们把下载好的第一个 App 安装到模拟器上,该 App 启动后的页面如图 13-67 所示。

整个页面非常简洁,中间有一个 Test 按钮,点击该按钮,会出现 Toast 提示信息,内容为 3。这其中的逻辑是怎样的呢?我们可以直接用 jadx-gui 反编译一下 apk 文件,从源码中查找入口,如图 13-68 所示。

可以看到源码非常简单,整体逻辑就是点击按钮后触发 onClickTest 方法,然后这个方法直接调用 Toast 的 makeText 方法,显示 getMessage 方法的返回结果。这里 getMessage 方法实现的是基本的加和操作,因为在调用时传入的参数是 1 和 2,所以显示的 Toast 内容就是 3。

图13-68 反编译的结果

那怎么进行 Hook 呢,我们可以定义这样一个 JavaScript 脚本:

Java.perform(()=> {
    let MainActivity = Java.use('com.germey.appbasic1.MainActivity')
    console.log('start hook')
    MainActivity.getMessage.implementation = (arg1, arg2) => {
        send('Start Hook!')
        return '6'
    }
})

将其保存为 hook_java.js 文件。这里我们编写的是一个全局可用的 Java 对象,通过调用其 perform 方法来实现我们的 Hook 逻辑。首先调用 Java 对象的 use 方法获取指向 MainActivity 类的指针,并赋值为 MainActivity。然后改写 MainActivity 中的 getMessage 方法,由于这个方法接收两个参数,因此这里也写两个参数一一 arg1 和 arg2,分别代表源码中的 i 和 i2,但这里我们没有对 arg1 和 arg2 做加和操作,而是直接返回了数字 6,这样就完成了方法的改写一一不使用接收到的参数,直接返回数字 6。

Hook 逻辑定义好了,怎么让它生效呢?使用 Python 脚本调用即可,于是新建一个 hook_java.py 文件,文件内容如下:

这里我们首先读出刚编写的 JavaScript 代码,并赋值为 CODE 变量,即把代码转成了 Python 字符串,然后声明了一个包名,并赋值为 PROCESS_NAME 变量。

接着我们使用 frida 包中的 get_usb_device 方法获取了当前连接的设备,并调用设备的 attach 方法挂载了对应的进程,该进程被赋值为 process 变量。之后我们调用 process 变量的 create_script 方法往进程中注入了 Hook 脚本(就是传入 CODE 变量),并将返回结果赋值为 script 变量。

对于 script 变量,我们可以设置事件监听和回调方法,例如这里监听 message 事件,回调方法设置为 on_message,这样一来,JavaScript 代码中任何通过 send 方法发送的数据,on_message 方法都会接收到对应的内容,这就实现了 JavaScript 到 Python 的消息通信。最后,调用 script 变量的 load 方法注入脚本。

接下来我们先启动 AppBasic1,再启动编写的 Python 脚本:

python3 hook_java.py

此时点击 TEST 按钮,页面如图 13-69 所示。

图 13-69 Hook 操作后的 AppBasic1 启动页面

可以看到这里显示的 Toast 信息是 6,正是我们在 JavaScript 代码中定义的返回值,证明 Hook 成功了!同时观察一下电脑上的控制台,显示的内容如图 13-70 所示。

从这里可以看到,每点击一次按钮,控制台就会输出一行代码,代码内容为:

{'type': 'send', 'payload': 'StartHook!'}

这里 payload 的内容就是我们在 JavaScript 代码中使用 send 方法发送的消息内容,代表我们在 Python 脚本中成功接收到了这个消息,实现了 JavaScript 脚本与 Python 脚本的通信。

如果我们能 Hook 某个方法的执行结果,然后通过 JavaScript 代码把它保存为某个变量,再利用 send 方法把这个变量发送给 Python 脚本,Python 就能成功获取代码的返回结果了,之后对结果进行处理和保存,数据爬取就完成了。

Hook Native层的逻辑

现在我们尝试用 Frida 工具 Hook Native 层的代码,即 so 文件中的方法。先来看一下 AppBasic2 在 Hook 之前的启动页面,如图 13-71 所示。

图13-70 控制台显示的内容 图13-71 AppBasic2的启动页面

同样地,使用 jadx-gui 反编译 apk 文件,查看逻辑入口,如图13-72所示。

图13-72 反编译的结果

可以看到 MainActivity 类中声明了一个 native 方法,叫作 getMessage,其参数也是 i 和 i2,但是这里并没有它的具体实现。紧接着的实现也很关键:

static {
    System.loadLibrary("native");
}

这里通过 System 类的 loadLibrary 方法加载了一个 native 库,其实就是加载了一个 Native 层的 so 文件,所以源码中应该有对应的 so 文件,在源码中仔细找一下,是可以找到的,如图 13-73 所示。

图13-73 Native层的so文件

可以看到这里有好几个 so 文件,它们适用于不同平台,名字都是 libnative.so。对于 so 文件,jadx-gui 就无能为力了,因为这是由 C/C++ 编译成的文件,jadx-gui 没法通过反编译得到其源码。

那能用 Frida 进行 Hook 吗?能!我们来修改一下 getMessage 方法的返回结果。同样先实现一个 JavaScript 脚本:

将其保存为 hook_native.js 文件。跟 Hook Java 层时的逻辑不同,要 Hook Native 层,需要利用 Interceptor 对象的 attach 方法,其第一个参数是指向 Native 方法的指针,第二个参数是 Hook 逻辑的实现。

  • 对于第一个参数,这里直接调用 Module 对象的 findExportByName 方法获取了指针,该方法的第一个参数是 so 文件的名称,这里就是 libnative.so;第二个参数是符合一定命名规范的方法路径,开头是 Java,然后是包名,注意包名中间的连接字符变成了下划线,接着是被 Hook 方法所在的 Activity 的名称,这里就是 MainActivity,最后就是方法名称,这些内容都通过下划线连接。

  • 对于第二个参数,这里我们定义了两个 Hook 方法,其中 onEnter 代表被 Hook 方法执行前的逻辑,onLeave 代表被 Hook 方法执行后的逻辑。onLeave 方法的参数是 val,代表被 Hook 的方法,即 getMessage。根据图 13-71,getMessage 原本的返回结果是 3,这里我们调用 val 的 replace 方法,将其替换成了 5,实现了返回结果的修改。

然后调用这个脚本,新建一个 hook_native.py 文件:

这里跟 Hook Java 层时的不同体现在 JavaScript 文件的路径和 App 的包名上,其他完全一样,这里不再展开讲解。

重新启动 AppBasic2,同时启动该 Python 脚本:

python3 hook_native.py

此时点击 TEST 按钮,页面如图 13-74 所示。

可以看到 Toast 信息变成了 5,同时控制台的输出内容如图 13-75 所示。

图13-74 Hook 操作后的 AppBasic2 启动页面 图13-75 控制台的输出内容

一样地,这里的 payload 值就是我们在 JavaScript 脚本中使用 send 方法发送的消息内容,我们在 onEnter 方法中调用 send 方法,发送了 arg1 和 arg2 的值,然后 Python 脚本成功接收到了这个消息,实现了 JavaScript 脚本与 Python 脚本的通信。

总结

本节我们使用 Frida Hook 了 Java 层和 Native 层的逻辑,通过这两个基本的案例,相信大家可以初步体会到 Frida 的基本操作和 API 的编写方法。当然,Frida 能做的远远不止这些,更多的 API 使用方法可以参考官方文档 https://frida.re/docs/home/

本节内容的参考来源。

  • Frida 官方文档。

  • CSDN 网站上 “Android逆向之旅—Hook神器 Frida 使用详解” 文章。

最后,如果你想深入学习 Frida,这里推荐一本书——陈佳林(网名 r0ysue)的《安卓Frida逆向与抓包实战》,这本书讲述了利用 Frida 进行 Android App 逆向和抓包的相关知识,可以学习一下。