# 使用 Frida 匯出介面

您可以透過此功能實現將 frida RPC 匯出的方法作為 FIRERPA 介面擴充呼叫。您可以透過自行編寫功能性 Hook 程式碼實現對 APP 控制的極致。此功能需要您熟悉 frida hook 腳本的編寫，如果您已有現存腳本，您需要對您的腳本做稍微的改寫，使用我們的特定方法來實現。

```{attention}
自 9.0 開始，我們內建使用的 frida 17.x 將需要自行打包 frida-java-bridge 進腳本，否則會出現 Java not defined 相關錯誤，這項改動屬於 frida 官方的改動，依據官方的改動說明，您需要新建 nodejs 專案並引入 java bridge，詳細請參考 https://github.com/oleavr/frida-agent-example 或使用我們提供的 tools/frida_script_generate.py 對原來的 js 腳本進行二次封裝。
```

## 編寫匯出腳本

您需要編寫特定格式的腳本，匯出函數名稱必須遵循命名規範，務必是首字母小寫的 camelCase 駝峰式命名。對於定義性的名稱如 HTTP，方法命名不可使用全大寫格式，如名稱 `sendHTTPRequest`，應在匯出腳本中寫為 `sendHttpRequest` 而非使用全大寫 HTTP，以下是一個腳本應遵循的大致結構。

```js
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 呼叫程式碼區塊代表的意義分別為：

```js
return performRpcJVMCall(function() {
        // 在 JVM 執行緒上執行
})
```

程式碼區塊 performRpcJVMCall 代表在 JVM 中執行您的程式碼，您可以在區塊裡使用 JVM 相關功能，例如 Java.use 或其他涉及到應用 Java 層的操作等。

```js
return performRpcJVMCallOnMain(function() {
        // 在 UI 執行緒上執行
})
```

程式碼區塊 performRpcJVMCallOnMain，代表在 JVM 中並且在 UI 主執行緒中執行您的程式碼，對於涉及到 UI 或主執行緒的操作，您需要在這個程式碼區塊裡執行才能成功，同時需要注意您的程式碼不會阻塞主執行緒執行，否則可能會導致應用無回應甚至崩潰。

```js
return performRpcCall(function() {
        // 執行一般的 JS 程式碼
})
```

程式碼區塊 performRpcCall，您只能在此執行一些基礎的 js 程式碼，不可在此使用 Java 層的邏輯。

```{attention}
您可能仍然需要使用 Java.perform 來封裝整體程式碼區塊來確保 Java 邏輯正常執行。
```

現在您已經了解了我們程式碼的基本編寫邏輯，您可以將您的程式碼封裝成這種格式的來適配，如果只是想測試，也可以直接複製這段程式碼來開始體驗，我們下面也是利用此 Hook 程式碼進行講解。

## 注入匯出腳本

這裡的注入匯出腳本的方法和 `持久化 Frida 腳本` 章節中的其實是完全一樣的使用方法。

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

呼叫了注入介面後，您的腳本也應該就注入了，您可以執行如下呼叫來檢查腳本是否正常。

```python
app.is_script_alive()
```

## 呼叫匯出方法

呼叫腳本內的匯出方法的方式和您原生使用 FIRERPA 介面是很相似的，請看範例程式碼。

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

對於多開應用，您需要獲取多開應用的實例

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

以上的呼叫方法等價於下面的，您可以發現方法中的 `Func1` 變成了 `_func1`，這兩種都是可以的。

```python
app.example_func1("FIRE", "RPA")
```

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

## 呼叫匯出方法（HTTP介面）

除了支援您使用客戶端呼叫外，我們還支援您透過 HTTP 方式呼叫，您可以按照以下形式來使用。

```{tip}
自 8.15 版本開始，匯出方法也支援使用 jsonrpc 2.0 協定呼叫（尚不支援 MultiCall 模式）。
```

您可以像如下方式，直接使用 jsonrpclib 進行呼叫，或者，如果您熟悉 JsonRPC 2.0 協定，您也可以自行編寫程式碼進行請求，這是最規範並且可參考文件最全的一個協定實現。

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

當然，您仍然可以繼續使用以下這種之前的呼叫協定，相對來說比較簡單，但是不夠規範。

```python
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 方式存取，並且需提供憑證密碼。

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