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 始終處於前景執行狀態。