Миграция на Frida 17 - (не)Уникальный опыт

Миграция на Frida 17


Жили мы все спокойно, вяло сражались с мелкими проблемами, которые подкидывала нам тетя Frida и даже почти научились с ними быстро справляться. Когда в один прекрасный день, 17 мая 2025 года, мир изменился и никогда больше не будет прежним….

/img/frida17-migration/kdpv.png

DBI фреймворк Frida обновился до следующей мажорной версии 17.0.0 и началось планомерное разрушение всего того доброго, разумного и вечного, что было наработано непосильным трудом =) Поломались хуки и инструменты на базе Frida, а из-за отсутствия нормального гайда по миграции пришлось (о ужас) следить за релизноутами и читать issue на гитхабе, чтобы понять что изменилось и продолжает изменяться в течении новых минорных итераций релиза #17. Я не претендую на роль человека, который сейчас расскажет как все починить, но какие-то свои наработки и соображения покажу. Возможно они будут полезны будущим желателям поправить развалившиеся инструменты.

Важные замечания:

  1. Все тесты проводились на Frida 17.3.1, Python 3.13.2 и nodejs v22.14.0
  2. Приводимые примеры кода являются самодостаточными, их можно полностью копировать и они должны работать

Чиним 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
/img/frida17-migration/frida-cli-run.png

Пу-пу-пу-пу….. Работает. А через биндинги питона нет. Почему так? А ответ как раз в релизноутах. Если коротко: бриджи (bridge) к языкам Objective-C, Swift и Java больше не являются частью рантайма GumJS. Теперь это самостоятельные компоненты, которые нужно подключать если есть намерение их использовать. При этом, они все еще вкючены во Frida REPL и frida-trace, что и было продемонстрировано выше когда скрипт заработал при запуске через REPL.

Теперь, для использования хуков из питона есть пара опций:

  1. Использовать транспилированные js-хуки с добавлением всех нужных бриджей
  2. Транспилировать такие хуки на лету из скрипта на python

Для тех кто ничего не понял, дам краткое пояснение: уже довольно давно существует возможность писать хуки на TypeScript и транспилировать их в JavaScript. Мне кажется этим мало кто пользуется в обычной жизни, потому что это во-первых требует всяких дополнительных телодвижений в виде настройки рабочего окружения, а во-вторых, гораздо проще набросать пару хуков в файлике и запустить чем заниматься какой-то там разработкой. Но теперь, по всей видимости, пользователей этой штуки станет больше.

Для того чтобы заставить наши хуки работать из питона, прямо в директории с проектом (или скриптом, если у вас один файл) создадим и настроим проект агента:

$ frida-create -t agent -n android-printer -o android-printer
$ cd android-printer && npm install && npm install frida-java-bridge
/img/frida17-migration/install-agent.png

Далее просто переносим скрипт из переменной 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. Это и есть наш траспилированный хук со всеми необходимыми зависимостями. Осталось немного модифицировать скрипт на питоне чтобы подключить этот хук:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import argparse
import sys

import frida

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)

    hook = open("android-printer/_agent.js")
    script = session.create_script(hook.read())
    script.load()

    frida.get_usb_device().resume(pid)
    print('')
    sys.stdin.read()
/img/frida17-migration/agentjs-run.png

Запуск ожидаемо приводит к успеху. Таким образом мы рассмотрели первую опцию - запуск предварительно транспилированных скриптов.

Альтернативный способ, который может быть более удобен в некоторых случаях - транспиляция на лету. Суть его в создании объекта компилятора с передачей в него “главного” скрипта на языке TypeScript. В нашем примере это будет выглядеть так:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import argparse
import sys

import frida

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)

    compiler = frida.Compiler()
    bundle = compiler.build("android-printer/agent/index.ts")
    script = session.create_script(bundle)
    script.load()

    frida.get_usb_device().resume(pid)
    print('')
    sys.stdin.read()

Про бриджи теперь есть отдельный раздел в документации, который тоже можно почитать. Но не стоит ждать от него каких-то откровений, там все еще типичная документация фриды. Если вы понимаете о чем я =)

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


Смотрите также