Frida 导出接口

您可以通过此功能将 frida RPC 导出的方法作为 FIRERPA 接口扩展调用。通过自行编写功能性的 Hook 脚本,您可以实现对 APP 的极致控制、签名导出等。需要您熟悉 frida 脚本的编写。

注意

自 9.0 版本开始,内置的 Frida 17.x 需要您自行将 frida-java-bridge 打包进脚本,否则会出现 Java not defined 相关错误,这项改动属于 Frida 官方的变更。依据官方的改动说明,您需要新建 Node.js 项目并引入 frida-java-bridge,详细请参考:https://github.com/oleavr/frida-agent-example ,或者使用我们提供的 frida_script_generate.py 对 js 脚本进行打包。

编写导出脚本

您需要编写特定格式的脚本,导出函数名必须遵循命名规范:必须使用首字母小写的驼峰 camelCase 命名法。对于缩写词如 HTTP,方法名不得使用全大写格式。例如,sendHTTPRequest 应在导出脚本中写为 sendHttpRequest。以下是一个脚本应遵循的大致结构。

Java.perform(function() {
const String = Java.use("java.lang.String")
rpc.exports.exampleFunc1 =  function (a, b) {
        return performRpcJVMCall(function() {
                return String.$new("Execute on JVM thread:" + a + b).toString()
        })
}
rpc.exports.exampleFunc2 = function (a, b) {
        return performRpcJVMCallOnMain(function() {
                return String.$new("Execute on Main UI thread:" + a + b).toString()
        })
}
rpc.exports.exampleFunc3 = function (a, b) {
        return performRpcCall(function() {
                return a + b
        })
})

在上面的示例脚本中,您可以看到三种方法定义。其中,return performRpc 模式的代码块必须使用,以确保您的值正确返回。这三种不同的 performRpc 调用代码块所代表的意义分别为:

return performRpcJVMCall(function() {
        // Execute on JVM thread
})

代码块 performRpcJVMCall 代表在 JVM 中执行您的代码,您可以在块里使用 JVM 相关功能,例如 Java.use 或其他涉及到应用 Java 层的操作。

return performRpcJVMCallOnMain(function() {
        // Execute on UI thread
})

代码块 performRpcJVMCallOnMain 代表在 JVM 的 UI 主线程中执行您的代码。对于涉及 UI 或主线程的操作,您需要在该代码块中执行才能成功。同时,请注意确保您的代码不会阻塞主线程,否则可能导致应用无响应甚至崩溃。

return performRpcCall(function() {
        // Execute Normal JS code
})

代码块 performRpcCall 只能用于执行基础 JS 代码,不可在此使用 Java 层的逻辑。

注意

您可能仍然需要使用 Java.perform 来封装整体代码块,以确保 Java 逻辑正常执行。

现在,您已了解代码的基本编写逻辑。您可以将您的代码封装成此格式进行适配。如果您只是想测试,也可以直接复制这段代码来体验。我们下面也将基于此 Hook 代码进行讲解。

注入导出脚本

这里注入导出脚本的方法与 持久化 Frida 脚本 章节中的方法完全相同。

app = d.application("com.android.settings")
app.attach_script(script, runtime=ScriptRuntime.RUNTIME_QJS, standup=5)

调用注入接口后,脚本即已注入,您可以通过以下调用检查脚本是否正常。

app.is_script_alive()

调用导出方法

调用脚本内导出方法的方式与您原生使用 FIRERPA 接口非常相似

app = d.application("com.android.settings")
app.exampleFunc1("FIRE", "RPA")

对于多开应用,您需要获取多开应用的实例:

app = d.application("com.android.settings", user=UID)
app.exampleFunc1("FIRE", "RPA")

这两个调用方式等价,您会发现方法名中的 Func1 变成了 _func1,两种方式均可。

app.example_func1("FIRE", "RPA")
>>> app.example_func1("FIRE", "RPA")
'Hello World:FIRERPA'
>>> app.example_func2("FRI", "DA")
'Hello World:FRIDA'
>>> app.example_func3("FIRE", "RPA")
'FIRERPA'

调用导出方法(HTTP 接口)

除了支持客户端调用外,我们还支持通过 HTTP 方式调用,可按以下形式使用。

您可以使用 jsonrpclib 进行调用,如下所示;或者,如果您熟悉 JSON-RPC 2.0 协议,也可以自行编写代码发送请求——这是最规范且可参考文档最全的协议实现。

import jsonrpclib
server = jsonrpclib.Server('http://192.168.0.2:65000/script/com.android.settings/0')
server.example_func1("FIRE", "RPA")

当然,您也可以继续使用之前的调用协议,相对简单,但不够规范。

import json
import requests
url = "http://192.168.0.2:65000/script/com.android.settings/exampleFunc1?user=0"
res = requests.post(url, data={"args": json.dumps(["FIRE", "RPA"])})
print(res.status_code, res.json()["result"])

链接中的查询参数 user 表示应用的 UID,默认为 0,多开应用须指定 UID。

以上代码通过 HTTP 方式调用脚本导出接口。com.android.settings 为应用包名,exampleFunc1 为导出的函数名。请求参数 args 必须使用 json.dumps 序列化。参数列表支持多个参数,由方法参数个数决定,提供空列表 [] 表示该导出函数无参数。

如果 FIRERPA 启用了接口证书,则需使用 HTTPS 方式访问,并提供证书密码。

headers = {"X-Token": "证书密码"}
res = requests.post(url, data={"args": json.dumps(["LAM", "DA"])}, headers=headers, verify=False)
print(res.status_code, res.json()["result"])

HTTP 状态码

通过 HTTP 接口调用会返回特定的状态码,可根据状态码进行状态判断。

状态码状态码描述
200一切正常
410脚本未注入或未安装
500脚本或参数异常
400参数错误

问题排查

如果您通过 API 或 HTTP 方式调用接口时出现卡死或超时,大概率是因为应用处于后台被系统强制休眠。因此,您需要确保 APP 始终处于前台运行状态。