Frida エクスポートインターフェース

この機能を使用すると、frida RPC でエクスポートされたメソッドを FIRERPA インターフェースの拡張として呼び出すことができます。独自の機能的な Hook スクリプトを作成することで、アプリの完全な制御、署名のエクスポートなどが可能になります。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
        })
})

上記のサンプルスクリプトでは、3 種類のメソッド定義を確認できます。その中で、return performRpc パターンのコードブロックは、値を正しく返すために必ず使用する必要があります。これら 3 つの異なる performRpc 呼び出しコードブロックが意味するものはそれぞれ次のとおりです。

return performRpcJVMCall(function() {
        // JVM スレッドで実行
})

コードブロック performRpcJVMCall は、JVM 内でコードを実行することを意味します。ブロック内では Java.use など、アプリケーションの Java レイヤーに関わる操作を行うことができます。

return performRpcJVMCallOnMain(function() {
        // UI スレッドで実行
})

コードブロック performRpcJVMCallOnMain は、JVM の UI メインスレッドでコードを実行することを意味します。UI やメインスレッドに関わる操作は、このブロック内で実行しなければ成功しません。同時に、コードがメインスレッドをブロックしないように注意してください。ブロックするとアプリが無応答になったりクラッシュしたりする可能性があります。

return performRpcCall(function() {
        // 通常の JS コードを実行
})

コードブロック 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")

これら 2 つの呼び出し方は同等です。メソッド名の 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 経由でインターフェースを呼び出した際にフリーズやタイムアウトが発生する場合、その多くはアプリがバックグラウンドにありシステムによって強制的にスリープ状態にされていることが原因です。そのため、アプリが常にフォアグラウンドで動作していることを確認する必要があります。