Экспорт интерфейсов с помощью Frida

Эта функция позволяет вам экспортировать методы, определённые через Frida RPC, и вызывать их как расширения интерфейса FIRERPA. Вы можете написать собственный код для хуков, чтобы достичь максимального контроля над приложением. Эта функция требует знакомства с написанием скриптов-хуков для Frida. Если у вас уже есть готовые скрипты, вам потребуется немного их изменить, используя наши специальные методы.

Внимание

Начиная с версии 9.0, встроенная версия Frida 17.x требует самостоятельной упаковки `frida-java-bridge` в скрипт, иначе возникнут ошибки, связанные с `Java not defined`. Это изменение было внесено официально командой Frida. Согласно их инструкциям, вам необходимо создать проект Node.js и подключить java bridge. Для получения подробной информации обратитесь к https://github.com/oleavr/frida-agent-example или используйте наш инструмент `tools/frida_script_generate.py` для повторной упаковки ваших существующих js-скриптов.

Написание скрипта для экспорта

Вам необходимо написать скрипт в определённом формате. Имена экспортируемых функций должны соответствовать соглашению об именовании camelCase, то есть начинаться с маленькой буквы. Для определительных имён, таких как HTTP, в названии метода нельзя использовать полностью заглавные буквы. Например, имя sendHTTPRequest в скрипте экспорта должно быть записано как sendHttpRequest, а не с использованием заглавных букв HTTP. Ниже приведена общая структура, которой должен следовать скрипт.

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 означает, что ваш код будет выполняться в JVM и в основном UI-потоке. Операции, связанные с UI или основным потоком, должны выполняться внутри этого блока для успешного завершения. При этом необходимо следить, чтобы ваш код не блокировал выполнение основного потока, иначе это может привести к зависанию или сбою приложения.

return performRpcCall(function() {
        // Выполняется обычный JS-код
})

В блоке кода performRpcCall вы можете выполнять только базовый JS-код и не можете использовать логику Java-уровня.

Внимание

Вам все еще может потребоваться обернуть весь блок кода в `Java.perform`, чтобы обеспечить корректное выполнение Java-логики.

Теперь, когда вы ознакомились с основной логикой написания нашего кода, вы можете адаптировать свой код под этот формат. Если вы просто хотите протестировать, вы можете скопировать этот код, чтобы начать. В дальнейшем мы будем использовать этот же код хука для объяснений.

Внедрение скрипта для экспорта

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

Совет

Начиная с версии 8.15, экспортированные методы также поддерживают вызовы по протоколу jsonrpc 2.0 (режим MultiCall пока не поддерживается).

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