Экспортируемые интерфейсы 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 , либо используйте предоставленный нами pack_frida_script.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() {
        // Выполняется в потоке JVM
})

Блок performRpcJVMCall означает выполнение вашего кода в JVM; внутри блока вы можете использовать функции, связанные с JVM, например Java.use или другие операции, затрагивающие Java-слой приложения.

return performRpcJVMCallOnMain(function() {
        // Выполняется в UI-потоке
})

Блок performRpcJVMCallOnMain означает выполнение вашего кода в главном UI-потоке JVM. Для операций, связанных с 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")

Эти два способа вызова эквивалентны. Вы увидите, что 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 в URL обозначает 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 происходит зависание или тайм-аут, скорее всего, приложение находится в фоновом режиме и было принудительно переведено системой в спящий режим. Поэтому необходимо, чтобы приложение всегда работало на переднем плане.