30 KiB
Процесс 11: Динамический анализ кода программы
Дата публикации: 3 апреля
Время на чтение: ~16 мин
ТГ канал: Про_SEC_со
Представьте ситуацию. Релиз прошёл, в CI всё зелёное, в merge request два апрува — жизнь прекрасна. SAST (автоматическая проверка исходников без запуска программы) молчит, потому что смотрит на исходный код, а не на то, что случится, когда этот текст превратится в машинный код и встретит живые данные.
Через неделю в прод прилетает багрепорт: парсер входного файла — кусок приложения, который по правилам формата раскладывает файл на поля — падает с segfault. Простыми словами: процесс полез в память туда, куда ему нельзя, ОС прислала сигнал SIGSEGV и приложение умерло. Триггер — «кривое» вложение внутри файла. Это ещё не эксплойт с шеллом на сервере, но сервис лежит — для пользователя разницы мало.
Разработчик лезет в код и видит: границы буфера никто не сверял с тем, сколько байт реально читают. Статический анализатор на этом участке молчит — указатели прыгают так, что анализатор запутался, плюс компилятор при оптимизации переписал логику так, что «на бумаге» и в бинарнике не одно и то же. Пока программу не прогнали с реальными байтами — руками атакующего или хотя бы машины, которая их генерит, — проблема не всплыла.
Именно об этом раздел 5.11 ГОСТ Р 56939-2024. Статика смотрит в редактор; динамика смотрит, что программа делает в рантайме (пока она уже работает — скомпилирована или крутится в интерпретаторе). А фаззинг мы разберём чуть ниже отдельно — без этого слово «фаззер» звучит как заклинание.
Суть процесса простыми словами
Динамический анализ — это когда вы не спорите с кодом на бумаге, а запускаете его и смотрите, что произойдёт. Подсунули битый вход — посмотрели реакцию. Включили санитайзеры — компилятор вшил в программу сторожей: вылез за массив, обратился к памяти после free — сторож орёт. В C/C++/Rust это часто ASan (ловит выход за границы, use-after-free), MSan (неинициализированная память), UBSan (неопределённое поведение по стандарту). В примечании к 5.11.2.5 ГОСТ перечисляет ещё и отладку, профилирование, поиск секретов в памяти, инструментирование бинарников — всё, что помогает увидеть баг в исполнении.
Коротко: если статика отвечает на вопрос «код похож на опасный?», динамика отвечает на «где приложение реально сломается?».
Что такое фаззинг?
Фаззинг в быту — это «стресс-тест для входа». Вы не придумываете вручную десять тысяч тест-кейсов. Вы даёте программе много разных входов подряд — случайных, мутированных, собранных из маленьких «зёрнышек» (сидов) — и смотрите: упадёт, зависнет, выдаст странный ответ или упрётся в таймаут. Цель одна: найти место, где программа ведёт себя не так, как ожидали разработчики.
Звучит как магия? На самом деле это упорный перебор с умом. Два частых лица:
-
Нативный код, парсеры, сетевые форматы. Вход — поток байтов. Инструменты вроде libFuzzer (часто в связке с LLVM) кормят вашу функцию кусками байтов тысячи раз в секунду. Упал процесс — получили стек, воспроизвели, починили.
-
Веб и API. Вход — HTTP-запрос: путь, параметры, заголовки. Инструменты вроде ffuf подставляют в URL и query варианты из вордлистов — тот же дух фаззинга, другая форма входа. «Авария» здесь –– это ответ 500, лишний открытый путь или таймаут — то, что потом превращается в баг или дыру в конфигурации.
ГОСТ называет это фаззинг-тестирование и отдельно требует коллекции входов для мутаций (5.11.2.8): не один случайный файл, а набор, который задевает разные ветки логики. Иначе фаззер долго крутится на месте, как колесо в снегу — шум есть, пользы мало.
Мутации — это когда из почти нормального входа делают чуть-чуть кривой: перевернули байт, удлинили строку, сменили кодировку.
Как это стыкуется с процессами 9 и 10
Не путать с соседними процессами по стандарту:
- Процесс 10 (статика) — код не запускается. Хорошо ловит типовые паттерны в тексте, плохо понимает, что будет в железе.
- Процесс 9 (экспертиза) — человек читает смысл. Он не заменит ночной прогон фаззера на миллионе входов и не поймает все отложенные баги памяти одним взглядом.
- Процесс 11 — машина гоняет программу и собирает факты: упало, утекло, ответило не тем кодом.
Три слоя друг друга не отменяют. Зелёный SAST не значит «можно не гонять под ASan». Зелёное ревью не значит «фаззер ничего не найдёт». И наоборот: фаззер нашёл краш — без человека вы не поймёте, баг это или ожидаемая защита.
Требование 5.11.2.1: Регламент динамического анализа
Разработать регламент проведения динамического анализа кода ПО.
«Ну мы как-то прогоняли Valgrind… в прошлом квартале… кажется, Василий на макбуке». Без бумаги (или хотя бы страницы в Confluence или его аналоге) процесс 11 растворяется в личной памяти людей. ГОСТ просит наоборот: чтобы было ясно кто жмёт кнопку, чем гоняют, когда повторять прогон и что делать с красными отчётами.
Артефакт 5.11.3.1: что должно быть в регламенте
По смыслу регламент — это ответы на вопросы нового инженера в понедельник утром. Ниже — чеклист того, что ГОСТ хочет видеть записанным:
- Роли и обязанности — кто готовит стенд, кто запускает сканеры и фаззеры, кто разбирает краши, кто утверждает закрытие.
- Критерии выбора инструментов динамического анализа и фаззинга (язык, платформа, тип ПО — сервис, библиотека, десктоп).
- Критерии выбора методов динамического анализа. Например: прогон под санитайзерами (отладочная сборка с ASan/UBSan — медленнее, зато ловит классы багов памяти) против прогона чистого релиз-билда (как у пользователя в проде — без лишних проверок; нужен для сравнения производительности и поведения, но многие ошибки памяти так не всплывут). Отдельно — нужна ли DBI (dynamic binary instrumentation: анализ «в рантайме» в уже готовом бинарнике через внедрение инструментовочного кода — DynamoRIO, QEMU user-mode, коммерческие решения; уместно, когда нет исходников или нужен эксперимент без пересборки).
- Критерии выбора модулей — что обязательно гоняем динамикой и фаззингом (обычно парсеры, протоколы, нативные обработчики, всё на границе доверия).
- Правила обработки срабатываний — аварийная остановка, зависание, что считается инцидентом теста, что — шумом.
- Процедуры устранения ошибок — тикеты, приоритеты, повторный прогон после фикса.
- Периодичность динамического анализа и события для повторного прогона (релиз, крупный рефакторинг границы ввода, смена компилятора).
- Периодичность фаззинга и критерии завершения кампании (например: N часов без новых уникальных крашей, или бюджет CPU, или покрытие — как договоритесь, но критерий должен быть записан).
Связка с другими артефактами ГОСТа (5.6.3.2, 5.7.3.x) — ваша модель угроз и требования подсказывают, где динамика критичнее всего; регламент переводит это в операционный режим.
Пример: выдержка из регламента
1. Роли
- Владелец процесса 11: команда AppSec / выделенный инженер
- Исполнитель прогона: разработчик модуля + CI (ночные джобы)
- Разбор крашей: разработчик + при эскалации — AppSec
2. Повторный динамический анализ (5.11.2.6)
- Обязательно после исправления каждого краша/утечки, зафиксированного
в отчёте 5.11.3.5 или 5.11.3.6
- Полный цикл перед минорным релизом для модулей из перечня 5.11.3.3
3. Критерии завершения фаззинг-кампании
- 24 часа непрерывного прогона на типовом CI-раннере
без новых уникальных стеков падения; либо достижение лимита
итераций 10^7 — что наступит раньше, с фиксацией в отчёте
Не обязательно оформлять ровно так — важно, чтобы договорённости существовали и их можно было показать аудитору без танцев с бубном.
Требования 5.11.2.2–5.11.2.4: Инструменты, модули, сценарии
5.11.2.2 Определить инструменты динамического анализа и фаззинг-тестирования, порядок их применения.
5.11.2.3 Определить перечень модулей (компонентов) ПО, которые необходимо подвергнуть динамическому анализу, включая фаззинг-тестирование.
5.11.2.4 Определить сценарии проведения тестирования для каждого исследуемого модуля … средствами динамического анализа, включая инструменты проведения фаззинг-тестирования.
Три шага, как рецепт: сначала выбираете инструменты, потом список модулей, которые им доверяете, потом для каждого модуля — сценарий «как запускать и когда считать, что закончили». Без третьего шага первые два превращаются в красивую табличку, которую никто не исполняет.
Артефакты 5.11.3.2 и 5.11.3.3
Перечень инструментов (5.11.3.2) — не просто имена. Нужны версии, соответствие исследуемым модулям (x86_64 Linux-сервис, ARM-встраиваемка, JVM — разные тулзы), параметры эксплуатации (флаги компилятора для ASan, образ Docker с нужным libc).
Перечень модулей (5.11.3.3) — для каждого: наименование и идентификатор (из вашей CMDB, архитектурной схемы или реестра компонентов — главное, чтобы однозначно ссылаться в сценариях и отчётах).
Пример фрагмента перечня инструментов:
| Инструмент | Версия | Модуль (ID) | Назначение |
|---|---|---|---|
| clang + ASan/UBSan | 17.x | SVC-PARSER-001 | Переполнения, UB при парсинге |
| libFuzzer + санитайзер | из LLVM 17 | SVC-PARSER-001 | Фаззинг сырых байтов в парсер |
| ffuf | 2.x | SVC-API-GW-003 | Фаззинг HTTP: пути, параметры, заголовки |
| Valgrind memcheck | 3.22 | SVC-CORE-002 | Утечки на долгоживущем демоне |
Артефакт 5.11.3.4: сценарий на модуль
Для каждого модуля сценарий включает:
- идентификатор модуля
- наименование инструмента
- параметры настройки
- критерии запуска и остановки (что считается началом прогона, что — успешным окончанием или аварией)
Пример сценария (нативный парсер — байты в API):
Модуль: SVC-PARSER-001 (бинарный парсер пакетов protobuf поверх TCP)
Инструмент: libFuzzer, target fuzz_parse_packet
Параметры: -max_total_time=86400 -rss_limit_mb=4096
корпус сидов: /fuzz/seeds/parser_v3/
Запуск: по событию — merge в main затронул parser/*; еженедельно ночью.
Остановка: истечение max_total_time ИЛИ ручной стоп при критическом
дефиците ресурсов CI; результаты — артефакт job.
Пример сценария (HTTP/API — ffuf): формально тот же процесс 11, только «вход» у программы не файл, а запрос в сеть. ffuf (fuzz faster u fool) подставляет в URL, заголовки и тело куски из вордлистов. Вы не ждёте обязательно segfault — часто ценнее странный 500, открытый debug-путь или ответ, где в JSON торчит кусок стека. Для бизнеса это тоже «сломалось».
Модуль: SVC-API-GW-003 (публичный REST за API-gateway)
Инструмент: ffuf
Параметры (синтаксис ffuf 2.x): точка подстановки **FUZZ** в URL совпадает с меткой у `-w file:FUZZ`
ffuf -u https://staging.example.com/FUZZ -w /fuzz/wordlists/api-paths-min.txt:FUZZ
-H "Authorization: Bearer $STAGING_TOKEN" -X GET
-fc 404 -t 40 -rate 30
Отдельный прогон для query: -u "https://staging.example.com/api/v1/items?FUZZ=1"
-w /fuzz/wordlists/api-params.txt:FUZZ
(-fc 404 — не показывать ответы «страница не найдена», чтобы не засорять отчёт)
Коллекция входов (5.11.2.8): вордлисты как сиды — минимальный набор путей
(/api/v1/, /admin/, /debug/, …) и имён параметров (id, user, debug, …),
чтобы покрыть разные обработчики; при необходимости — мутации внешним скриптом.
Запуск: после изменений в маршрутизации gateway; ежемесячно.
Остановка: исчерпание вордлистов или лимит времени job; отчёт — JSON ffuf + журнал аномалий.
Требование 5.11.2.5: Проводить динамический анализ
Проводить динамический анализ с использованием инструментов динамического анализа.
Формулировка короткая, смысл жёсткий: не «у нас в политике написано», а реально запускали и что-то зафиксировали. Примечание к пункту в ГОСТе разрешает широкую палитру — битые входы, профилирование, отладка, поиск секретов в памяти, санитайзеры, вмешательство в бинарник на лету — лишь бы это помогало увидеть проблему в момент работы программы.
На практике чаще всего сталкивают в одну связку:
- Санитайзеры (Material Symbols:
memory— удобная метафора раздела про память в документации) — быстрый feedback в CI на C/C++/Rust - Fuzzing harness для нативного кода — обёртка
LLVMFuzzerTestOneInputили аналог, которая кормит ваш API сырыми байтами - ffuf (и аналоги) для веб/API — перебор сегментов URL, параметров, иногда тел запросов по заранее подготовленным спискам; ищете аномалии в ответах сервера, не только «падение процесса», но и 500, странные задержки, утечки в JSON ошибки
- Для managed-кода без своего нативного парсера — динамические проверки рантайма, нагрузочные и хаос-тесты плюс ffuf на край HTTP, если сервис наружу смотрит REST/GraphQL
Требование 5.11.2.6: Повторный динамический анализ
Проводить повторный динамический анализ модулей (компонентов) ПО с целью контроля устранения ошибок.
Починили — покажите тем же тестом, что починка сработала. Иначе история выглядит как «закрыли тикет, потому что надоело». Повторный прогон с теми же параметрами — это и контроль качества, и спокойствие перед аудитом: цепочка «нашли — исправили — проверили» замкнулась.
Требования 5.11.2.7–5.11.2.8: Фаззинг и коллекции входов
5.11.2.7 Проводить фаззинг-тестирование.
5.11.2.8 При проведении фаззинг-тестирования использовать тестовые коллекции входных данных, подлежащие дальнейшим мутациям … вызывающие использование различных функциональных возможностей тестируемого модуля …
Здесь ГОСТ как бы говорит: фаззинг — не волшебная кнопка «найти всё». Если дать фаззеру один файл или один шаблон URL, он долго будет крутиться вокруг одной ветки кода и греть процессор впустую. Нужны сиды — маленькие, но разнообразные заготовки: чтобы мутации цеплялись за разные ручки логики.
Что класть в коллекцию:
- для бинарного парсера — по одному крошечному валидному образцу на каждый тип сообщения, который вы реально поддерживаете
- для JSON/XML — не только «хороший объект», а примитивы, вложенность, странный юникод — всё, что заставляет парсер идти по разным дорожкам
- для ffuf — вордлисты путей и имён параметров, которые бьют в разные обработчики за gateway, плюс при необходимости короткие значения
Идея простая: не один «боевой дамп размером с Вселенную», а набор ключей, которыми вы открываете разные двери в коде. Тогда фаззинг перестаёт быть лотереей и становится поиском.
Требование 5.11.2.9: Устранение ошибок по процедурам
Устранять выявленные в процессе динамического анализа, включая фаззинг-тестирование, ошибки в соответствии с принятыми процедурами устранения …
Отчёты 5.11.3.5 и 5.11.3.6 — это мост между «нашли» и «починили». Красный лог без тикета и владельца — просто шум в почте. В регламенте (5.11.3.1) заранее пропишите: кто заводит задачу, какие сроки, когда звать AppSec. Иначе получится знакомая картина: «потом разберём» тянется кварталами, пока тот же баг не всплывёт у клиента громче.
Артефакт 5.11.3.5: отчёт по динамическому анализу
Должен включать:
- срабатывания инструментов
- результаты обработки для типов ошибок, которые регламент требует разбирать (падения, зависания и т.п.) — то есть не сырой дамп, а классификация: дубликат, ложняк, баг, нужен CVE, назначен исполнитель
Пример фрагмента:
Прогон: ASan, модуль SVC-PARSER-001, сборка #4821, 2026-04-02
Срабатывания:
1. heap-buffer-overflow в parse_length() parser.c:112
Статус обработки: подтверждён баг, JIRA SEC-8842, исправление в PR !1203
2. leak в init_context() — подтверждена ложная тревога (известный баг трекера ASan #…)
Артефакт 5.11.3.6: отчёт по фаззингу
Должен включать:
- сведения о работе инструментов: длительность, число аварийных завершений, число найденных путей выполнения (или аналоги метрик вашего фаззера)
- результаты анализа каждого аварийного завершения — воспроизводимость, стек, связь с тикетом, статус
Пример (libFuzzer — падения процесса):
Кампания: libFuzzer SVC-PARSER-001, 2026-03-28 – 2026-03-29
Длительность: 26 ч
Уникальных крашей (по стеку): 3
Итераций: 4.2e8
Пути (coverage counter): рост до плато с 18-го часа
Обработка аварий:
CRASH-001: integer overflow → fix в v1.3.2, повторный прогон — зелёный
CRASH-002: timeout 5s — воспроизводится, в работе
CRASH-003: duplicate CRASH-001 после ребейза — объединено с SEC-8842
Пример (ffuf — аномалии HTTP; «аварии» в отчёте — не только segfault, но и нештатные ответы):
Кампания: ffuf SVC-API-GW-003, staging, 2026-04-01
Длительность: 42 мин
Запросов: 18 400 (вордлисты путей + параметров)
Инструмент: ffuf v2.1, вывод json
Результаты работы (метрики 5.11.3.6):
- ответы 500: 7 уникальных URL (стек в теле у 2 — утечка пути к файлу)
- ответы 200 на /internal/health-debug — не должен быть доступен снаружи
- таймауты >10s: 1 эндпоинт (возможный DoS при длинном query)
Обработка:
HTTP-500-A: дубликат известного бага SEC-9011
HTTP-500-B: новый тикет SEC-9150, исправление маршрутизации gateway
INFO-LEAK: убрать stack trace из prod-конфига ошибок
Как это работает вместе
Процесс 11 не отменяет статику и ревью — он добавляет то, что эти два слоя по определению плохо видят: что случится, когда биты пойдут по проводам и памяти по-настоящему.
Регламент (5.11.3.1) — чтобы никто не полагался на «ну мы же помним». Перечни инструментов и модулей (5.11.3.2–5.11.3.3) — чтобы через год не гадать, чем вы гоняли сервис весной. Сценарии (5.11.3.4) — чтобы запуск был не ритуалом одного героя, а повторяемой инструкцией. Отчёты (5.11.3.5–5.11.3.6) — чтобы красное стало работой, а не папкой «почитать потом». Повтор после фикса (5.11.2.6) и устранение по процедуре (5.11.2.9) — чтобы цикл закрылся.
Если совсем упростить для себя и для коллеги из соседнего отдела: статика спрашивает у кода «ты выглядишь подозрительно?». Ревью спрашивает «ты делаешь то, что задумывали?». Динамика и фаззинг спрашивают «ты сломаешься, если я сделаю с входом вот так?». ГОСТ как раз про то, чтобы последний вопрос задавали системно, а не в день инцидента.