Фаззинг без присмотра: что агент берёт на себя, а что нет

Как устроен Fuzzing Taskflow от GitHub Security Lab: контур из обвязки, MCP-инструментов и SQLite, где человек остаётся в петле принятия решений о значимости багов.

· 5 мин чтения

Непрерывный фаззинг давно известен. Запуск фуззера снова и снова, инструмент подаёт в программу полуслучайные данные и ловит падения. OSS-Fuzz работает годами, а критические баги в проектах всё равно всплывают. Причина почти всегда одна. Кто-то должен смотреть на покрытие, какие строки и ветки кода фуззер вообще зацепил. Писать обвязки, то есть harness, небольшие программы-обёртки, через которые фуззер дёргает функции библиотеки. Эти обвязки нужны для кода, до которого фуззер не добирается. И разбирать каждый креш. Автор проекта задался вопросом. Какую часть этой человеческой работы можно отдать LLM-агенту? Ответом стал Fuzzing Taskflow.

На вход подаётся ссылка на репозиторий. Дальше агент сам находит подходящие точки входа, разбирает систему сборки, пишет обвязки, запускает AFL++, читает отчёты о покрытии, улучшает обвязки, триажит каждый креш и пишет отчёт об уязвимости. Без человека, который следил бы за процессом.

Три слоя: решения отдельно, исполнение отдельно

Архитектура сознательно разделена. Первый слой — это шелл-скрипт, который склеивает стадии пайплайна. Второй слой — это набор YAML-файлов, по одному на стадию; по сути это промпты, которые говорят агенту, что делать на каждом шаге. Третий слой — это набор MCP-инструментов, Model Context Protocol (стандарт, через который модель вызывает внешние утилиты). Инструменты умеют запускать AFL, компилировать обвязку, сохранять креш, читать отчёт о покрытии. Агент никогда не зовёт AFL или clang напрямую, только собирает пайплайн из этих примитивов. Всё состояние хранится в базе SQLite fuzz_context.db. Стадии обмениваются данными только через базу, не через память. Это и есть ключевое правило. Агент принимает решения, инструменты выполняют работу.

Мелочь, которая сильно влияет на качество. Каждая обвязка собирается дважды. Бинарник .afl идёт с инструментацией AFL и санитайзерами, инструментами, которые ловят ошибки памяти прямо во время работы. Это AddressSanitizer и UndefinedBehaviorSanitizer. Он используется для фаззинга. Бинарник .cov идёт без AFL-инструментации, но с профилировкой clang. Он затем проигрывает очередь AFL и считает реальное покрытие по строкам и веткам.

Цикл покрытия вместо человека с LCOV

Ядро пайплайна — замена ручного шага: открыть LCOV-отчёт, найти непокрытые ветки, написать новую обвязку или сид. На каждой итерации агент запускает AFL на бюджет времени, проигрывает очередь на .cov-бинарнике, читает список непокрытых веток и выбирает одно из действий. Добавить новый сид, чтобы достать непокрытую ветку. Дописать вызов API в обвязку. Обогатить словарь AFL «магическими константами» из сравнений в коде. Или пропустить пробел в покрытии, если это редкая ветка ошибки или сторонний код.

Бюджеты времени удваиваются каждую итерацию: 30с → 60с → … → 960с (около 32 минут на цель). Идея в том, чтобы тратить короткие раунды, пока есть лёгкое покрытие, и длинные, когда фуззер пробивает трудную проверку. Когда две итерации подряд дают меньше настраиваемого порога (по умолчанию 1% абсолютного покрытия по строкам), срабатывает детектор плато, и цикл переходит к следующей цели.

Структурно-чувствительный фаззинг

Байтовые мутаторы AFL отлично работают с бинарными форматами и с трудом справляются с текстовыми. Классическое решение — руками писать кастомный мутатор под каждый формат. В пайплайне четыре механизма. Готовые словари и мутаторы для JSON, XML, регулярных выражений, PNG и длино-префиксных бинарных форматов. Автогенерация мутатора из исходников цели, извлекаются строковые литералы и 32-битные константы из #define, case и enum. Динамически растущий словарь AFL, который после каждой итерации пополняется токенами из сравнений рядом с непокрытыми строками. Оператор склейки файлов из корпуса — рекомбинация, которую штатный havoc AFL выполняет плохо.

Особенно интересен третий пункт. Словарь буквально растёт в сторону кода, до которого фуззер ещё не добрался.

Триаж и отчёты об уязвимостях

Найти креш — это половина работы. Дальше идёт триаж. Каждый креш минимизируется через afl-tmin, проигрывается под AddressSanitizer для снятия стека, дедуплицируется по хешу вершины стека с нормализацией шаблонов и суффиксов LTO. Потом агент читает исходник обвязки и падающую функцию, идёт по цепочке вызовов от публичного API назад и пишет markdown-отчёт с анализом корневой причины по файлу и строке, аргументом достижимости, оценкой эксплуатируемости, предлагаемым патчем в виде unified diff, формата записи изменений «до/после», и эскизом регрессионного теста. У каждого отчёта один из вердиктов: vulnerability, library_hardening, harness_bug, OOM, timeout, assertion_failure, duplicate.

Различение настоящей уязвимости (достижимой и эксплуатируемой через публичный API) и просто бага в самой обвязке — именно тот экспертный вывод, который раньше требовал вручную пройти код по вызовам. В GitHub Security Lab честно предупреждают: модель понимает целевой код ограниченно и ошибается. Вердикт становится хорошо подготовленной стартовой точкой для человека, а не финальным решением.

Как применить

  1. Запускайте пайплайн только в одноразовой среде: Codespace, временная VM, без повышенных прав. Агент запускает AFL, clang и произвольные команды сборки прямо на хосте, без контейнера, поэтому инъекция в промпт теоретически может сделать всё, что разрешено вашему пользователю.
  2. Откройте репозиторий seclab-taskflows-fuzzing и поднимите Codespace. Для короткой проверки запустите ./scripts/fuzzing/run_fuzzing.sh DaveGamble/cJSON. Для реальной кампании используйте ./scripts/fuzzing/run_fuzzing.sh tukaani-project/xz.
  3. Запустите кампанию и откройте дашборд на порту 8765, в Codespace он пробрасывается автоматически. Там видно состояние каждой обвязки, динамику покрытия со спарклайнами, тепловую карту крешей и таймлайн итераций.
  4. Делите работу. Агенту: написание обвязок, запуск AFL, чтение покрытия, триаж крешей и черновик отчёта. Человеку: финальная оценка каждого вердикта, проверка предложенного патча и решение, какие баги тянуть в апстрим.
  5. Перед длинной кампанией загляните в src/seclab_taskflows_fuzzing/configs/model_config.yaml и убедитесь, что выбранная модель подходит. В GitHub Security Lab по умолчанию используют Claude Sonnet 5, потому что он прошёл их внутренние проверки, в то время как некоторые передовые модели накладывают ограничения на вывод.