Жили мы все спокойно, вяло сражались с мелкими проблемами, которые подкидывала нам тетя Frida и даже почти научились с ними быстро справляться. Когда в один прекрасный день, 17 мая 2025 года, мир изменился и никогда больше не будет прежним….
DBI фреймворк Frida обновился до следующей мажорной версии 17.0.0
и началось планомерное разрушение всего того доброго, разумного и вечного, что было наработано непосильным трудом =) Поломались хуки и инструменты на базе Frida, а из-за отсутствия нормального гайда по миграции пришлось (о ужас) следить за релизноутами и читать issue на гитхабе, чтобы понять что изменилось и продолжает изменяться в течении новых минорных итераций релиза #17. Я не претендую на роль человека, который сейчас расскажет как все починить, но какие-то свои наработки и соображения покажу. Возможно они будут полезны будущим желателям поправить развалившиеся инструменты.
Важные замечания:
- Все тесты проводились на Frida
17.3.1
, Python3.13.2
и nodejsv22.14.0
- Приводимые примеры кода являются самодостаточными, их можно полностью копировать и они должны работать
Чиним python
Начну с того, что вызвало у меня больше всего проблем - с биндингов питона. Наверное, если бы я регулярно читал релизноуты, то не столкнулся бы с этими проблемами, но много ли среди вас тех, кто регулярно читает релизноуты всех тулзов которыми пользуется?
Начну с базового примера обвязки на питоне для хуков:
import argparse
import sys
import frida
hook = """
console.log("[~] Start script")
if (Java.available) {
Java.perform(function() {
console.log(`Android version: ${Java.androidVersion}`)
})
}
"""
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('package', help='Spawn a new process and attach')
args = parser.parse_args()
pid = frida.get_usb_device().spawn(args.package)
session = frida.get_usb_device().attach(pid)
script = session.create_script(hook)
script.load()
frida.get_usb_device().resume(pid)
print('')
sys.stdin.read()
Этот код прекрасно работает на 16-й версии, но если его запустить на 17, то будет выведен только текст [~] Start script
. И самое плохое, что не появится никаких ошибок. Вообще никаких. Если скрипт не сильно зависит от общения с питоном, то можно попытаться понять хоть что-то запустив его напрямую через консоль. Для этого сохраним содержимое скрипта в файл hook.js
и запустим:
$ frida -Uf com.android.chrome -l hook.js
Пу-пу-пу-пу….. Работает. А через биндинги питона нет. Почему так? А ответ как раз в релизноутах. Если коротко: бриджи (bridge) к языкам Objective-C, Swift и Java больше не являются частью рантайма GumJS. Теперь это самостоятельные компоненты, которые нужно подключать если есть намерение их использовать. При этом, они все еще вкючены во Frida REPL и frida-trace
, что и было продемонстрировано выше когда скрипт заработал при запуске через REPL.
Теперь, для использования хуков из питона есть пара опций:
- Использовать транспилированные js-хуки с добавлением всех нужных бриджей
- Транспилировать такие хуки на лету из скрипта на python
Для тех кто ничего не понял, дам краткое пояснение: уже довольно давно существует возможность писать хуки на TypeScript и транспилировать их в JavaScript. Мне кажется этим мало кто пользуется в обычной жизни, потому что это во-первых требует всяких дополнительных телодвижений в виде настройки рабочего окружения, а во-вторых, гораздо проще набросать пару хуков в файлике и запустить чем заниматься какой-то там разработкой. Но теперь, по всей видимости, пользователей этой штуки станет больше.
Для того чтобы заставить наши хуки работать из питона, прямо в директории с проектом (или скриптом, если у вас один файл) создадим и настроим проект агента:
$ frida-create -t agent -n android-printer -o android-printer
$ cd android-printer && npm install && npm install frida-java-bridge
Далее просто переносим скрипт из переменной hook
в файл index.ts
и добавляем импорт для получения доступа к классу Java
:
import Java from "frida-java-bridge";
console.log("[~] Start script")
if (Java.available) {
Java.perform(function() {
console.log(`Android version: ${Java.androidVersion}`)
})
}
Теперь в директории с агентом выполняем команду npm run build
, успешным результатом выполнения которой должно стать появление файла _agent.js
. Это и есть наш траспилированный хук со всеми необходимыми зависимостями. Осталось немного модифицировать скрипт на питоне чтобы подключить этот хук:
|
|
Запуск ожидаемо приводит к успеху. Таким образом мы рассмотрели первую опцию - запуск предварительно транспилированных скриптов.
Альтернативный способ, который может быть более удобен в некоторых случаях - транспиляция на лету. Суть его в создании объекта компилятора с передачей в него “главного” скрипта на языке TypeScript. В нашем примере это будет выглядеть так:
|
|
Про бриджи теперь есть отдельный раздел в документации, который тоже можно почитать. Но не стоит ждать от него каких-то откровений, там все еще типичная документация фриды. Если вы понимаете о чем я =)
Чиним JS
Из-за изменения имен некоторых функций, обычные хуки тоже сломались. Описание в релизноутах лично мне показалось несколько мутным, хотя там написано, что оно “straight-forward” =) В целом, рефакторинг хуков сводится к замене одних функций на другие и изменению самих вызовов в некоторых случаях. Приведу базовую таблицу от которой можно отталкиваться при рефакторинге:
<17 | >= 17 |
---|---|
Module.getGlobalExportByName(null, “open”); | Module.getGlobalExportByName(“open”); |
Module.findExportByName(null, “open”); | Module.getGlobalExportByName(“open”); |
Module.getSymbolByName(null, ‘open’) | Module.getGlobalExportByName(‘open’) |
Module.getExportByName(’libc.so’, ‘open’) | Process.getModuleByName(’libc.so’).getExportByName(‘open’) |
Module.getBaseAddress(“libc.so”) | Process.getModuleByName(’libc.so’).base |
Memory.readCString(somePtr) | somePtr.readCString() |
Memory.readUtf8String(somePtr) | somePtr.readUtf8String() |
Memory.readUtf16String(somePtr) | somePtr.readUtf16String() |
Memory.readAnsiString(somePtr) | somePtr.readAnsiString() |
Memory.readInt(somePtr) | somePtr.readInt() |
Memory.writeUInt(somePtr) | somePtr.writeUInt() |
Здесь приведены не все функции для работы с указателями, но рефачить все остальные нужно схожим образом. Например:
var ptr = Memory.alloc(Process.pointerSize);
Memory.writePointer(ptr, NULL);
После рефакторинга станет:
var ptr = Memory.alloc(Process.pointerSize);
ptr.writePointer(NULL);
Выводы
Вооружившись этими знаниями, сделать миграцию вполне реально за довольно короткий срок. Рефакторинг JS-а можно вообще заскриптовать (скриньте мне если напишете такой скрипт плиз 🌚). Но судя по динамике выхода новых минорных релизов 17-й фриды - нас ждет еще много сюрпризов и нестабильной работы. Впрочем, мы к этому уже давно привыкли 💪