Каждому, кому доводилось тестировать безопасность Android-приложений, неминуемо приходилось сталкиваться с несколькими проблемами:
Нашел потенциальную уязвимость? Думаешь, как ее лучше подтвердить.
Проверка может быть вызвана только из кода? Пишешь приложение.
Находишь новые места с потенциальными уязвимостями? Дорабатываешь приложение или пишешь новое.
Внезапно вместо проверок ты уже подрабатываешь разработчиком… снова и снова.
Всем привет, меня зовут Антон Хазов, и мы в команде SecWare очень часто сталкиваемся с тестированием Android-приложений на уязвимости. Однажды я посчитал, сколько времени мне, как исследователю безопасности мобильного ПО, приходится тратить на написание кода. После осознания этой страшной цифры мне и пришла идея попробовать создать PoC-конструктор для таких проверок. Вот что у меня вышло.
Почему обычные инструменты не справляются?
Проведу краткий теоретический экскурс (те, кто уже в теме, раздел могут смело пропустить). Android-приложения и его экспортируемые компоненты (Activity, Service, ContentProvider, BroadcastReceiver) часто доступны извне только формально – совсем не редкость, когда логика использования завязана на внутренностях самого приложения, где можно встретить сложные Intent’ы с большим набором параметров или кастомный Parcelable-объект с прямой зависимостью от внутренних классов приложения. А существующий универсальный инструмент отладки ADB при всей своей полезности покрывает только базовые сценарии, и передать что-то сложнее примитивных типов становится либо неудобно, либо вовсе невозможно (и снова привет Parcelable-объектам).
Да, многие могут отметить, что большинство потенциальных проблем можно решить с помощью такого мощного инструмента, как Frida – и это действительно так. Но тогда речь пойдет о динамическом вмешательстве в приложение, а этим всем не хочется оперировать, когда мы хотим построить Proof-Of-Concept, где, честно говоря, даже включенный режим разработчика (а значит и ADB) отдаляет нас от боевых условий sandbox.

Тут то мы и приходим к выводу, что максимально приближает нас к боевым условиям только написание отдельного приложения с необходимой нагрузкой в коде. Однако, на практике борьба с необъятным Android Studio может отнять неприличное количество времени на выверенный PoC. Сборщики Maven и Gradle, различные менеджеры отладки, эмуляции, управления версиями SDK, визуальные редакторы XML, сотни Toolbar переключателей и т.д. – все это кажется избыточным, когда речь идет о написании малютки-кода в ~25 строк. И, как показывает практика, даже для одного приложения приходится производить десятки таких проверок.

От десятков APK к одному инструменту
Когда я понял, что на каждую гипотезу приходится разрабатывать отдельное приложение, стало очевидно, что данный процесс требует автоматизации. Задача стояла в упрощении подхода до написания небольшого Java-кода, компиляции и выполнении прямо на устройстве, минуя сборку самого приложения. Идея пришла быстро и была простой как три копейки – представляю вам решение из двух частей:
DexRunner: Android-приложение с базой для загрузки и исполнения кода.
DEXLab: VS Code расширение для упрощения сборки и доставки нагрузки.

В основе DexRunner лежит стандартный механизм исполнения DEX-файлов на Android – метод DexClassLoader, а DEXLab завязан на использовании окружения Android Studio. Для DEXLab я даже постарался нарисовать иконку.
Первый блин был вайб-комом, но работающим! Изначально я обратился за помощью к Claude AI – круто, что вайб-код позволяет сэкономить огромное количество времени на разработку, но нужно было всегда следить, чтоб не терялись детали, поэтому приходилось все равно шлифовать код самому. Да и в самом начале мне хотелось только проверить, что ничего не будет мешать работе. Архитектура нагрузки в этот момент состояла из:
Файла конфигурации config.json, который хранил информацию о том, кому передать управление:
{ "entryClass": "payload.Payload", "entryMethod": "run", }
DEX-нагрузки, сборка которого имела пайплайн: компиляция Java с android.jar --> JAR --> DEX.
И вроде пора открывать шампанское, но тут важно вспомнить, что нагрузка может иметь зависимости исследуемого приложения (его классы, методы или еще какие-то данные). На этом этапе я вручную через dex2jar конвертировал все DEX-файлы из исследуемого приложения в JAR, который затем использовал при сборке нагрузки. Но помимо сборки, чтобы не потерять зависимости, перед запуском нагрузки нужно передать DEX-файлы исследуемого приложения в DexRunner. Самих DEX-файлов у приложения может быть много, а какие из них требуются для запуска нагрузки быстро понять сложно.

Когда файлов стало слишком много
В своей задумке я ожидаю от реализации PoC-конструктора два подхода:
Когда нагрузка – это полноценный PoC, который доставляется вручную через Intent.ACTION_OPEN_DOCUMENT и запускается UI-интерфейсом DexRunner, т.к. учитывается, что запуск будет происходить без режима разработчика.
Когда нагрузка – быстрая промежуточная проверка, где важно не терять время на передачу, чтоб проверки действительно были быстрые, а разработка облегченная.
Поэтому для второго случая я добавил автоматизацию доставки и запуска в DexRunner через BroadcastReceiver с командами на загрузку (LOAD_BUNDLE) и исполнение (RUN_BUNDLE). DexRunner сохраняет у себя в личном хранилище приложения DEX-файл (в папке code_cache), а при вызове RUN_BUNDLE берет из этой папки нагрузку и исполняет ее. Данный финт обусловлен обходом защиты на опасное исполнение кода из пользовательских директорий – это когда загрузить нагрузку в директорию /sdcard/ можно, но исполнить оттуда ничего нельзя (в дальнейшем DEXLab по умолчанию будет загружать нагрузку в директорию /data/local/tmp/, из которой позже ее возьмет DexRunner).
Позже эти Broadcast команды я внедрил и в DEXLab, что позволило автоматически воспроизводить полный цикл от сборки и подготовки до доставки и исполнения на устройстве одной командой из расширения. Однако, не хватало только одного – единого формата со всеми необходимыми файлами для запуска, поэтому был разработан собственный zip-формат с расширением «.dexs». Я подружил его с DexRunner и даже прикрутил подпись к этому архиву через HMAC-SHA256. И вот, в качестве примера, как собранная DEXS-нагрузка может выглядеть внутри (выделенное – конфиг и, непосредственно, нагрузка):

Какие получились результаты
Теперь хотелось бы наконец пройтись немного по использованию самого расширения DEXLab. В нем я расположил две команды на создание шаблона проекта: с исследуемым приложением в качестве таргета и без него.

После создания проекта итоговая структура шаблона включает в себя необходимые директории и файлы конфигурации с различными версиями для сборки, а также базовый payload.java файл с небольшим toast и println, чтобы можно было сразу получить проект, который успешно собирается и показывает свою активность. Команды я расположил в подменю к dexlab.config.json – файлу конфигурации проекта (не путать с конфигом, который заворачивается в DEXS-пакет). Получилось то, что можно увидеть на скриншоте ниже.

На скрине намеренно было раскрыто полностью дерево шаблона проекта и вызвана на данный момент вся возможная функциональность, чтобы можно было лицезреть то, чего я, собственно, и добивался. Внимательный читатель также заметит сходство по юзер-опыту с APKLab и оно тут неспроста – как можно было понять, я отчасти вдохновился этим расширением и даже само название DEXLab как бы намекает об этом =)
Что по командам:
Build and Run on Device – собирает и запускает одной командой
Build – собирает payload и упаковывает его в payload.dex
Bundle (.dexs) – упаковывает все DEX-файлы (payload+target) и подписывает DEXS
Disassemble DEX – декомпилирует payload.dex
Prepare Target – подготавливает исследуемое приложение, вытягивая из него все DEX-файлы и собирает их в единую JAR-библиотеку
Deploy to Device – пушит payload.dexs и подает сигнал DexRunner
Run on Device – подает сигнал DexRunner на запуск
Set Secret – отсылает секрет при сборке, если нужно
Install / Update DexRunner – скачивает и устанавливает последнюю release-сборку с github
Download baksmali – загружает baksmali
Download dex2jar – загружает пакет утилит dex2jar
Clean – отчищает сборку
Из вкусного я еще рекомендую использовать расширение Language Support for Java(TM) by Red Hat, которое позволяет красиво обращаться к классам сформированной библиотеки JAR, полученной от исследуемого приложения.

Ключевые особенности DEXLab ясны, но как выглядит сам DexRunner? Все достаточно обыденно: информация о нагрузке, кнопки ручного управления и отдельная активность с логами. Если же запустить созданный расширением DEXLab шаблон проекта в DexRunner, то получим ожидаемый короткий Toast и отладочные сообщения в логах.

Итоги
Собственно, итог у всего действа такой – я получил удобный и многофункциональный инструмент для написания PoC и исследований с возможностями включения рабочего окружения классов исследуемого приложения. Инструмент уже показал свое удобство и демонстрирует свою главную силу – возможность воспроизводить PoC без изменения окружения и без вмешательства в приложение.
Я думаю, это еще не предел его потенциальных возможностей. При тестировании может попасться что-то совсем хитрое, и это точно натолкнет на дальнейшее расширение функционала. Буду рад почитать замечания и новые идеи, а пока что ушел рисовать иконку для DexRunner =)
P.S. Свои наработки выложил на Github. Также буду рад подпискам на мой (пока еще маленький и уютный) телеграм-канал.
Samara-88
Очень полезный материал, особенно для тех, кто часто сталкивается с пентестом Android-приложений. То, что вы выложили наработки на Github — отдельный плюс в карму!)