Перейти к содержимому

Динамический анализ Android приложений с Frida: перехват криптофункций без исходников

Динамический анализ Android приложений с Frida: перехват криптофункций без исходников
Время чтения: 13 мин.

На аудите финтех-приложения без исходного кода я нашёл AES в режиме ECB с ключом, зашитым в строковые ресурсы. Три хука Frida, 40 минут работы — полная схема шифрования лежит в логе терминала. Ни строчки smali-кода (низкоуровневый байт-код Android, в который компилируется Java/Kotlin), ни часов за отладчиком. Только JavaScript-скрипты, которые цепляются к работающему приложению и показывают: что оно шифрует, каким алгоритмом, с каким ключом. Дальше — пошаговый разбор от настройки окружения до восстановления логики шифрования APK за один сеанс, с объяснением каждого шага и ожидаемого вывода.

Место в цепочке мобильного пентеста

Динамический анализ Android приложений с Frida — конкретное звено в цепочке, которая на практике выглядит так:

  1. Получение APK — скачиваем или извлекаем с устройства через adb pull /data/app/com.target.app/base.apk
  2. Статический анализ — декомпилируем APK в jadx (GUI-декомпилятор, преобразующий байт-код обратно в читаемый Java-код), ищем строки, имена классов, подозрительные паттерны
  3. Динамическая инструментация (Frida) — подключаемся к работающему процессу, перехватываем вызовы криптофункций, видим реальные данные в памяти — этот шаг разбираем ниже
  4. Анализ трафика — через Burp Suite или mitmproxy проверяем, что приложение отправляет на сервер и в каком виде
  5. Эксплуатация — используем собранные данные (ключи, токены, логику) для демонстрации уязвимости

В терминологии MITRE ATT&CK (открытая база тактик и техник атак; T-коды вроде T1179 — её идентификаторы) перехват функций соответствует технике Hooking (T1179 в Enterprise ATT&CK, deprecated; актуальные аналоги — T1056.004 Credential API Hooking в Enterprise и T1617 Hooking в Mobile ATT&CK). Извлечение логики шифрования — Deobfuscate/Decode Files or Information (T1140, тактика Defense Evasion). Работают в связке: хуки дают доступ к данным, анализ перехваченного восстанавливает криптосхему.

Зачем перехватывать именно криптофункции? Мобильные приложения шифруют токены авторизации, PIN-коды, платёжные данные. Если схема шифрования слабая — ECB вместо GCM, hardcoded-ключ, отсутствие IV — это прямое нарушение OWASP MASVS-STORAGE (стандарт безопасности мобильных приложений, требующий корректного использования криптографии). На пентесте такая находка — критическая уязвимость категории A02:2021 — Cryptographic Failures по классификации OWASP Top 10.

Требования к окружению

Прежде чем писать первую команду — проверьте, что всё на месте. Несовпадение версий Frida — причина номер один, по которой у новичков ничего не запускается.

Компонент Минимум Рекомендуется
ОС (рабочая машина) Windows 10 / Ubuntu 20.04 / macOS 12 Kali Linux 2024+ или Ubuntu 22.04
RAM рабочей машины 4 ГБ 8 ГБ (jadx на больших APK потребляет 2–4 ГБ)
Python 3.8+ 3.11+
Android-устройство Android 7+ с root (Magisk) или эмулятор с root Pixel 4a/5a с Magisk, Android 12–13
ADB Android SDK Platform Tools Актуальная версия с developer.android.com
Frida frida + frida-tools через pip Версия frida-server ДОЛЖНА совпадать с pip-пакетом
jadx 1.4+ jadx-gui 1.5+ (для графического поиска)

Без root работать можно — через frida-gadget (библиотека, встраиваемая внутрь APK). Процедура отдельная: распаковка APK через apktool, копирование .so в lib/, переподпись. Для первого знакомства с инструментацией Android приложений проще рутованное устройство или эмулятор Android Studio с Google APIs (не Google Play — там нет root-доступа).

Настройка Frida для Android: frida-server через adb

Установка frida-tools на рабочую машину

Frida состоит из двух частей: клиент на компьютере (frida-tools) и сервер на Android-устройстве (frida-server). Общаются они через adb — Android Debug Bridge, мост между ПК и устройством по USB или Wi-Fi.

Установка клиента — две команды: pip install frida-tools ставит CLI-утилиты (frida, frida-trace, frida-ps), а pip install frida — Python-биндинги для автоматизации скриптов. После установки выполните frida --version и запомните номер — он понадобится на следующем шаге.

Запуск frida-server на устройстве

Здесь чаще всего спотыкаются: версия frida-server обязана точно совпадать с версией клиента. Если frida --version показывает 16.5.2 — скачивайте frida-server-16.5.2-android-arm64.xz (arm64 для большинства современных телефонов, x86_64 для эмулятора); номер должен совпадать до последней цифры. Сборки лежат на GitHub в разделе releases проекта frida/frida. После скачивания распакуйте архив: unxz frida-server-*-android-arm64.xz — без этого шага вы скопируете на устройство сжатый файл, который не запустится.

Последовательность через adb:

Шаг 1. Копируем бинарник: adb push frida-server /data/local/tmp/. Ожидаемый вывод: frida-server: 1 file pushed. Если ошибка Permission denied — убедитесь, что USB-отладка включена в настройках разработчика устройства.

Шаг 2. Даём права: adb shell "chmod 755 /data/local/tmp/frida-server". Без вывода — это нормально.

Шаг 3. Запускаем сервер. Надёжнее всего — в отдельном окне терминала: adb shell, затем su, затем /data/local/tmp/frida-server &. Альтернатива одной строкой: adb shell 'su -c "/data/local/tmp/frida-server &"'. Нюанс: nohup отсутствует в toybox многих Android-сборок, поэтому при закрытии adb-сессии процесс может завершиться — держите терминал открытым или используйте setsid вместо nohup, если он доступен.

Проверка. Выполните frida-ps -U (флаг -U — USB-подключение). Если видите список процессов Android (com.android.systemui, com.google.android.gms и т.д.) — frida-server работает и клиент его видит. Ошибка Failed to enumerate processes — первым делом проверяйте совпадение версий клиента и сервера. Второе — что frida-server запущен от root (через adb shell su -c "/data/local/tmp/frida-server &" на устройствах с Magisk).

Разведка: находим крипто-классы без исходников

Прежде чем писать Frida скрипты для Android, нужно понять, какие крипто-классы использует приложение. Hooking Android приложений (перехват вызовов функций на лету — вы подменяете или дополняете поведение метода своим кодом) вслепую — пустая трата времени. Два подхода: статический через jadx и динамический через frida-trace.

Статический поиск через jadx

jadx — декомпилятор, преобразующий APK обратно в Java-код (не оригинальный исходник, а реконструкцию). Откройте APK в jadx-gui и выполните глобальный поиск (Ctrl+Shift+F) по строкам: javax.crypto.Cipher, MessageDigest, SecretKeySpec, AES, DES, RSA.

Даже если код обфусцирован (имена классов заменены ProGuard на a.b.c.d), вызовы стандартных Android-классов видны — они входят в SDK и не переименовываются. В jadx вы увидите что-то вроде Cipher.getInstance("AES/ECB/PKCS5Padding") внутри метода a.b.c.encrypt(). Имя обёртки нечитаемо, но алгоритм — как на ладони.

Динамическая разведка через frida-trace

frida-trace — утилита из frida-tools, которая автоматически создаёт хуки для заданных методов и выводит каждый вызов в реальном времени. Это разведка: вы ещё не пишете скрипты, а просто смотрите, какие крипто-методы вызываются при работе с приложением.

Команда: frida-trace -U -j 'javax.crypto.Cipher!*' com.target.app

Разбор аргументов: -U — USB-подключение, -j — Java-метод (не нативный), 'javax.crypto.Cipher!*' — перехватить все методы класса Cipher (звёздочка), com.target.app — имя пакета целевого приложения (узнать через frida-ps -Ua, где -a показывает только приложения).

Откройте приложение, потыкайте типичные действия — логин, отправка данных, переход между экранами — и наблюдайте терминал. Если видите вызовы Cipher.getInstance("AES/ECB/PKCS5Padding"), Cipher.init(1, ...), Cipher.doFinal(...) — приложение шифрует данные и вы уже знаете алгоритм. Параметр 1 в init — целочисленная константа Cipher.ENCRYPT_MODE, 2DECRYPT_MODE, 3WRAP_MODE, 4UNWRAP_MODE (константы класса javax.crypto.Cipher).

Если в выводе мелькает AES/ECB — приложение использует электронную кодовую книгу (ECB), где одинаковые блоки открытого текста дают одинаковые блоки шифротекста. Это криптографический антипаттерн: по шифротексту можно распознать структуру данных. Классический пример — «пингвин ECB», где зашифрованное изображение сохраняет контуры оригинала.

Перехват криптофункций Android: Cipher, MessageDigest, SecretKeySpec

От разведки — к полноценному перехвату. Здесь пишем Frida скрипты для Android, которые показывают алгоритм, ключ и данные на каждом этапе шифрования.

Hooking javax.crypto.Cipher

Скрипт перехватывает два метода: Cipher.getInstance() (какой алгоритм выбран) и Cipher.doFinal() (какие данные проходят через шифрование). У doFinal несколько перегрузок — ниже показана только doFinal(byte[]). Если приложение вызывает doFinal(byte[], int, int) или update()+doFinal() без аргументов, хук не сработает. Для полного покрытия переберите все перегрузки в цикле (Cipher.doFinal.overloads.forEach(...)) или добавьте отдельные .overload для каждой сигнатуры. Для схемы update()+doFinal() без аргументов нужно также перехватывать Cipher.update, накапливая входные байты в замыкании по экземпляру Cipher:

Java.perform(function() {
  var Cipher = Java.use('javax.crypto.Cipher');
  Cipher.getInstance.overload('java.lang.String').implementation = function(algo) {
    console.log('[Cipher] algorithm: ' + algo);
    return this.getInstance(algo);
  };
  Cipher.doFinal.overload('[B').implementation = function(data) {
    console.log('[Cipher] input (' + data.length + ' bytes): ' + toHex(data));
    var result = this.doFinal(data);
    console.log('[Cipher] output (' + result.length + ' bytes): ' + toHex(result));
    return result;
  };
});

Что происходит в каждой строке:

  • Java.perform() — выполняет код внутри виртуальной машины Android (ART). Без этой обёртки обращение к Java-классам невозможно.
  • Java.use('javax.crypto.Cipher') — получает ссылку на класс Cipher в памяти процесса. Класс не импортируется, а берётся из уже загруженного приложения.
  • .overload('[B') — указывает конкретную перегрузку метода doFinal, принимающую массив байт ([B — нотация Java для byte[]). У doFinal несколько перегрузок; без .overload Frida выдаст Error: doFinal(): has more than one overload.
  • this.doFinal(data) — вызов оригинального метода. Без него приложение сломается: данные не зашифруются и логика нарушится.

Функция toHex() — утилита для конвертации массива байт в читаемую hex-строку. Frida не включает её по умолчанию: напишите цикл по массиву с .toString(16) для каждого элемента или возьмите готовую реализацию из Frida CodeShare.

Запуск: frida -U -l cipher_hook.js com.target.app. Флаг -l загружает скрипт из файла. После запуска выполните действие в приложении (логин, отправка формы) — в терминале появится [Cipher] algorithm: AES/ECB/PKCS5Padding, затем hex входных и выходных данных.

Hooking MessageDigest и SecretKeySpec

Следующий шаг — перехватить создание ключа и хэширование. MessageDigest отвечает за хэши (SHA-256, MD5), SecretKeySpec — за создание криптографического ключа из сырого массива байт:

Java.perform(function() {
  var MD = Java.use('java.security.MessageDigest');
  MD.digest.overload('[B').implementation = function(input) {
    console.log('[Hash] input: ' + toHex(input));
    var out = this.digest(input);
    console.log('[Hash] output (' + out.length + ' bytes): ' + toHex(out));
    return out;
  };
  var SKS = Java.use('javax.crypto.spec.SecretKeySpec');
  SKS.$init.overload('[B', 'java.lang.String').implementation = function(key, algo) {
    console.log('[Key] algo: ' + algo + ' | key: ' + toHex(key));
    return this.$init(key, algo);
  };
});

Конструктор в Frida вызывается через $init — особенность API, потому что в JavaScript нельзя обратиться к конструктору как к обычному методу. Когда приложение выполняет new SecretKeySpec(keyBytes, "AES"), хук выведет сам ключ в hex.

Ожидаемый вывод при перехвате криптофункций Android в типичном приложении:

  • [Key] algo: AES | key: 0123456789abcdef0123456789abcdef — 32 hex-символа, 16 байт = AES-128
  • [Hash] input: 70617373776f7264 — hex-представление строки «password»
  • [Hash] output (32 bytes): 5e884898da28... — SHA-256 хэш

Если ключ создаётся из пароля через хэш — видна вся цепочка: пароль → SHA-256 → AES-ключ.

Восстановление логики шифрования за один сеанс

Объединяем данные всех хуков в последовательность — это и есть reverse engineering без исходников Android:

  1. Приложение берёт пароль пользователя (или hardcoded-строку)
  2. Хэширует через SHA-256 — видно в логе [Hash]
  3. Использует первые 16 байт хэша как AES-ключ — видно в логе [Key]
  4. Шифрует JSON с данными через AES/ECB/PKCS5Padding — видно в логе [Cipher]
  5. Отправляет шифротекст на сервер — проверяем через перехват трафика

На практике восстановление происходит за один сеанс: запустили скрипты, потыкали приложение, прочитали лог. Никакого ручного разбора smali, никакого графа вызовов в дизассемблере.

Типичные находки при анализе шифрования APK:

  • Hardcoded ключи — ключ зашит в код, а не хранится в Android Keystore. Нарушение OWASP MASVS-STORAGE: ключ извлекается из APK за минуты
  • ECB вместо GCM/CBC — одинаковые блоки дают одинаковый шифротекст. Можно распознать структуру данных без расшифровки
  • MD5 для деривации ключа — MD5 криптографически сломан с 2004 года, коллизии генерируются за секунды на обычном ноутбуке
  • Отсутствие IV (Initialization Vector) — без вектора инициализации одинаковые сообщения дают одинаковый шифротекст, что позволяет обнаруживать паттерны

Hex-данные из лога конвертируются в читаемый вид через CyberChef (веб-инструмент для преобразования данных) или в терминале: echo "7b22757365726e616d65223a..." | xxd -r -p покажет JSON {"username":"..."}.

Ограничения Frida и когда техника не работает

Frida — отличный инструмент динамической инструментации, но не серебряная пуля. Конкретные ситуации, где стандартный подход ломается:

Преимущества Ограничения
Не нужен исходный код приложения Требуется root или внедрение frida-gadget в APK
Перехват данных в реальном времени Версия frida-server обязана совпадать с клиентом
JavaScript-скрипты — быстрое прототипирование Детектируется приложениями (порт 27042, /proc/self/maps)
Работает с обфусцированным Java-кодом Java.use не находит ProGuard-имена без точного имени класса
Поддержка Java и нативного слоя Нативный hooking требует знания C-сигнатур и указателей

Anti-Frida и anti-root проверки. Приложения, соответствующие OWASP MASVS-RESILIENCE (стандарт устойчивости к реверс-инжинирингу), проверяют наличие Frida: сканируют открытые порты (27042 — порт по умолчанию), ищут строку frida в /proc/self/maps, проверяют наличие frida-server в файловой системе. В терминах MITRE ATT&CK это Debugger Evasion (T1622). По данным Approov (вендор защиты мобильных приложений), детектирование Frida реализовано в большинстве финтех-приложений коммерческого уровня.

Обходы есть: смена порта frida-server через frida-server -l 127.0.0.1:1337 (после чего подключаться через adb forward tcp:1337 tcp:1337 и frida -H 127.0.0.1:1337 <package> вместо -U; используйте 127.0.0.1 вместо 0.0.0.0, чтобы не открывать порт наружу через Wi-Fi — 0.0.0.0 оправдан только при прямом Wi-Fi-подключении без adb forward; меняет только listen-порт, но не обходит детект по строкам frida в /proc/self/maps и D-Bus handshake), использование frida-gadget вместо frida-server, перекомпиляция Frida с заменой magic-строк (frida-agent, gum-js-loop и др.) для обхода сигнатурного детекта в memory maps. Каждое приложение реализует свой набор проверок — универсального обхода нет, но для большинства приложений из bug bounty программ хватает базовых методов.

Нативный код (C/C++). Если криптография реализована в .so-библиотеке (скомпилированный нативный код через Android NDK), Java.use бесполезен. Для перехвата нативных функций используйте Interceptor.attach с Module.findExportByName — принцип тот же, но вместо Java-классов работаете с экспортами C-библиотек. На практике это нужно реже: большинство Android-приложений используют стандартные Java-классы javax.crypto даже при наличии нативного слоя.

Обфусцированные имена классов. ProGuard и R8 переименовывают com.app.CryptoUtils в a.b.c. Попытка Java.use('com.app.CryptoUtils') вернёт ошибку. Решение — перехватывайте стандартные классы (javax.crypto.Cipher, java.security.MessageDigest), они не обфусцируются. Через call stack в Frida (добавьте console.log(Java.use('android.util.Log').getStackTraceString(Java.use('java.lang.Exception').$new()))) определите, из какого обфусцированного класса пришёл вызов.

Статус проекта: Frida активно развивается — на GitHub 14 000+ звёзд, коммиты ежемесячно. Текущая стабильная ветка — 16.x. При обновлении проверяйте changelog: ломающие изменения в JavaScript API случаются между мажорными версиями.

Год за годом наблюдаю смещение: разработчики с бюджетом на безопасность переносят криптографию в нативный слой (NDK, C++), а поверх навешивают anti-tampering проверки промышленного класса — DexGuard, Arxan. Java-хуки через javax.crypto по-прежнему рабочий вектор для 80% приложений в bug bounty, но для финтех-приложений топ-уровня этого уже мало. Через год-два чистый Java-hooking останется применимым только к приложениям без бюджета на защиту.

Настоящая ценность навыка — не в конкретных скриптах из этой статьи, а в понимании архитектуры: где приложение создаёт ключ, как передаёт данные между Java- и нативным слоем, какой алгоритм выбирает и почему. Три точки перехвата, которые мы разобрали (Cipher — алгоритм и данные, SecretKeySpec — ключ, MessageDigest — деривация), позволяют восстановить любую схему шифрования, построенную на стандартных классах Android. Скрипт адаптируется за пять минут, архитектурное мышление нарабатывается месяцами. Формула на бумаге понятна, но по-настоящему перехват криптофункций ощущается только когда сам проходишь от frida-ps до расшифрованного JSON в терминале. На HackerLab.pro есть категории web и mobile с задачами разного уровня — стоит попробовать, когда руки дойдут до практики. Если тема зацепила и хочется разобраться в безопасности системно — на IB Basics берут с любого старта и ведут до первых реальных задач.

Эту тему и смежные навыки разбирают на практике в курсе «Профессия Реверс-инженер» Codeby Academy.