Created 2026-04-03

0 stars
Code

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 ГОСТ перечисляет ещё и отладку, профилирование, поиск секретов в памяти, инструментирование бинарников — всё, что помогает увидеть баг в исполнении.

Коротко: если статика отвечает на вопрос «код похож на опасный?», динамика отвечает на «где приложение реально сломается?».


Что такое фаззинг?

Фаззинг в быту — это «стресс-тест для входа». Вы не придумываете вручную десять тысяч тест-кейсов. Вы даёте программе много разных входов подряд — случайных, мутированных, собранных из маленьких «зёрнышек» (сидов) — и смотрите: упадёт, зависнет, выдаст странный ответ или упрётся в таймаут. Цель одна: найти место, где программа ведёт себя не так, как ожидали разработчики.

Звучит как магия? На самом деле это упорный перебор с умом. Два частых лица:

  1. Нативный код, парсеры, сетевые форматы. Вход — поток байтов. Инструменты вроде libFuzzer (часто в связке с LLVM) кормят вашу функцию кусками байтов тысячи раз в секунду. Упал процесс — получили стек, воспроизвели, починили.

  2. Веб и 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: что должно быть в регламенте

По смыслу регламент — это ответы на вопросы нового инженера в понедельник утром. Ниже — чеклист того, что ГОСТ хочет видеть записанным:

  1. Роли и обязанности — кто готовит стенд, кто запускает сканеры и фаззеры, кто разбирает краши, кто утверждает закрытие.
  2. Критерии выбора инструментов динамического анализа и фаззинга (язык, платформа, тип ПО — сервис, библиотека, десктоп).
  3. Критерии выбора методов динамического анализа. Например: прогон под санитайзерами (отладочная сборка с ASan/UBSan — медленнее, зато ловит классы багов памяти) против прогона чистого релиз-билда (как у пользователя в проде — без лишних проверок; нужен для сравнения производительности и поведения, но многие ошибки памяти так не всплывут). Отдельно — нужна ли DBI (dynamic binary instrumentation: анализ «в рантайме» в уже готовом бинарнике через внедрение инструментовочного кода — DynamoRIO, QEMU user-mode, коммерческие решения; уместно, когда нет исходников или нужен эксперимент без пересборки).
  4. Критерии выбора модулей — что обязательно гоняем динамикой и фаззингом (обычно парсеры, протоколы, нативные обработчики, всё на границе доверия).
  5. Правила обработки срабатываний — аварийная остановка, зависание, что считается инцидентом теста, что — шумом.
  6. Процедуры устранения ошибок — тикеты, приоритеты, повторный прогон после фикса.
  7. Периодичность динамического анализа и события для повторного прогона (релиз, крупный рефакторинг границы ввода, смена компилятора).
  8. Периодичность фаззинга и критерии завершения кампании (например: 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) — чтобы цикл закрылся.

Если совсем упростить для себя и для коллеги из соседнего отдела: статика спрашивает у кода «ты выглядишь подозрительно?». Ревью спрашивает «ты делаешь то, что задумывали?». Динамика и фаззинг спрашивают «ты сломаешься, если я сделаю с входом вот так?». ГОСТ как раз про то, чтобы последний вопрос задавали системно, а не в день инцидента.