Фаззинг
Фаззинг (от англ. fuzz — «пух», «нечёткий») — это автоматизированная техника тестирования программного обеспечения, заключающаяся в подаче на вход программы некорректных, неожиданных или случайных данных (фазз-данных) с целью выявления ошибок, уязвимостей и сбоев в работе. Фаззинг относится к классу методов динамического анализа и широко применяется в области информационной безопасности, разработки и обеспечения качества ПО.
История
Истоки фаззинга восходят к началу 1950-х годов, когда в ходе разработки ранних компьютерных систем программисты случайно обнаруживали ошибки при вводе нестандартных данных. Однако систематическое применение метода началось в 1988 году, когда американский учёный Бартон Миллер из Висконсинского университета в Мадисоне разработал первую программу-фаззер. Миллер тестировал утилиты командной строки UNIX, подавая на их вход случайные символы, и обнаружил, что до 25–33 % стандартных утилит (например, cat, grep, ls) аварийно завершались или зависали при обработке такого ввода. Это исследование, опубликованное в 1990 году, заложило основы современного фаззинга.
В 1990-е годы фаззинг использовался преимущественно в академической среде и в рамках тестирования коммерческого ПО, но не получил широкого распространения. Перелом произошёл в 2000-х годах, когда рост числа кибератак и уязвимостей в популярных приложениях (например, в Microsoft Office, Adobe Reader, браузерах) стимулировал интерес к автоматизированным методам поиска ошибок. В 2002 году компания Microsoft начала внедрять фаззинг в процесс разработки Windows, а в 2004 году исследователь Дэвид Айтел создал открытый фаззер zzuf, который модифицировал системные вызовы для тестирования приложений.
Значительный прогресс произошёл в 2010-х годах с появлением эффективных алгоритмов, таких как AFL (American Fuzzy Lop), разработанный Михалом Залевски в 2013 году. AFL использовал генетические алгоритмы и покрытие кода для генерации более релевантных тестовых данных, что позволило находить уязвимости в десятках тысяч программ. В 2014 году команда Google запустила проект OSS-Fuzz — платформу для непрерывного фаззинг-тестирования открытого ПО, которая к 2024 году выявила более 30 000 уязвимостей в тысячах проектов. С 2020-х годов фаззинг стал стандартной практикой в разработке критически важного ПО, включая ядра операционных систем, веб-браузеры и криптографические библиотеки.
Классификация
Фаззинг классифицируется по нескольким критериям: степени знания о структуре входных данных, способу генерации тестов и типу целевого приложения.
По степени знания о входных данных
- Мутационный фаззинг (mutational fuzzing) — основан на модификации существующих корректных входных данных (сидов). Программа-фаззер случайным образом изменяет биты, байты, строки или структуры в сидах, создавая новые варианты. Этот метод прост в реализации, но может быть неэффективен для форматов со сложными проверками (например, ZIP-архивов с контрольными суммами).
- Генеративный фаззинг (generational fuzzing) — создаёт входные данные с нуля, используя формальное описание формата (например, грамматику, BNF-нотацию, схемы). Такой подход позволяет генерировать синтаксически корректные, но семантически некорректные данные, что повышает вероятность прохождения проверок и достижения глубоких участков кода. Примеры: фаззеры для протоколов HTTP, SQL-запросов, файлов PDF.
- Гибридный фаззинг (hybrid fuzzing) — комбинирует мутацию и генерацию, часто с использованием символьного выполнения (concolic testing) для решения сложных ограничений на пути выполнения. Например, фаззер Driller (2016) чередует мутационный поиск и символьное выполнение для обхода проверок.
По способу генерации тестов
- Случайный фаззинг (random fuzzing) — генерирует данные полностью случайным образом. Прост, но имеет низкую эффективность для сложных форматов.
- Покрытие-ориентированный фаззинг (coverage-guided fuzzing) — использует информацию о покрытии кода (например, количество выполненных базовых блоков, ветвей) для выбора наиболее перспективных мутаций. AFL, libFuzzer и Honggfuzz — типичные представители. Этот метод доминирует в современном фаззинге.
- Фаззинг на основе грамматики (grammar-based fuzzing) — генерирует данные по заданной грамматике, что позволяет тестировать парсеры и интерпретаторы.
- Фаззинг на основе мутации протокола (protocol fuzzing) — модифицирует пакеты сетевых протоколов, сохраняя их структуру.
По типу целевого приложения
- Фаззинг файловых форматов — подача на вход программы модифицированных файлов (изображения, документы, архивы).
- Фаззинг сетевых протоколов — отправка некорректных пакетов на серверы (например, HTTP, DNS, SSH).
- Фаззинг API и библиотек — вызов функций с некорректными аргументами (например, через fuzz-тестирование в Rust или Go).
- Фаззинг ядра ОС — тестирование системных вызовов и драйверов (например, syzkaller для Linux).
- Фаззинг веб-приложений — подача некорректных данных через HTTP-запросы (SQL-инъекции, XSS).
Устройство и принцип работы
Типичный фаззинг-цикл состоит из следующих этапов:
- Выбор целевого приложения — определяется программа или библиотека для тестирования. Для фаззинга требуется, чтобы приложение могло принимать входные данные из внешнего источника (файл, stdin, сокет, аргумент командной строки).
- Подготовка сидов — создаётся набор корректных входных данных (например, несколько валидных PNG-файлов для тестирования библиотеки обработки изображений). Сиды служат отправной точкой для мутаций.
- Генерация тестовых данных — фаззер применяет мутации (перестановка битов, вставка/удаление байтов, замена строк, слияние фрагментов) или генерирует данные по грамматике. В покрытие-ориентированных фаззерах мутации выбираются на основе предыдущих результатов.
- Запуск целевого приложения — фаззер запускает приложение с сгенерированными данными и отслеживает его поведение (аварийное завершение, зависание, превышение лимитов памяти или времени).
- Мониторинг и сбор информации — если приложение завершается аварийно (например, с сигналом SIGSEGV, SIGABRT), фаззер сохраняет входные данные, вызвавшие сбой, для последующего анализа. В покрытие-ориентированных фаззерах также собирается информация о покрытии кода (например, через инструментирование бинарного кода или использование аппаратных счётчиков).
- Обратная связь — на основе покрытия фаззер выбирает наиболее перспективные входные данные для дальнейших мутаций, что позволяет постепенно углубляться в код.
Современные фаззеры, такие как AFL, libFuzzer и Honggfuzz, используют инструментирование (вставку дополнительных инструкций в бинарный код) для отслеживания покрытия. Инструментирование может быть статическим (на этапе компиляции) или динамическим (через эмуляцию, например, QEMU). В 2020-х годах получили распространение фаззеры на основе аппаратных средств, таких как Intel Processor Trace (PT), которые позволяют записывать трассы выполнения без значительных накладных расходов.
Применение
Фаззинг применяется в различных областях, связанных с разработкой и безопасностью ПО:
- Поиск уязвимостей — основное применение. Фаззинг позволяет находить ошибки переполнения буфера, использования после освобождения (use-after-free), целочисленные переполнения, ошибки форматирования строк и другие типы уязвимостей, которые могут быть использованы злоумышленниками. Например, в 2014 году с помощью фаззинга была обнаружена уязвимость Heartbleed в OpenSSL, а в 2021 году — уязвимость Log4Shell в библиотеке Log4j.
- Тестирование качества ПО — выявление крашей, зависаний и некорректного поведения при обработке нестандартных данных. Используется на этапах интеграционного и регрессионного тестирования.
- Верификация безопасности — в рамках процессов разработки (например, в Microsoft SDL — Security Development Lifecycle) фаззинг является обязательным этапом для критически важных компонентов.
- Исследование протоколов и форматов — фаззинг помогает выявить скрытые особенности реализации протоколов, которые не описаны в спецификациях.
Крупные компании активно используют фаззинг. Google в рамках OSS-Fuzz тестирует тысячи открытых проектов, включая OpenSSL, libpng, FFmpeg, SQLite. Microsoft встроила фаззинг в Visual Studio и Azure DevOps. Apple использует фаззинг для тестирования iOS и macOS. В России фаззинг применяется в разработке защищённого ПО, в том числе в компаниях «Лаборатория Касперского» и «ИнфоТеКС», а также в государственных программах по импортозамещению.
Примеры инструментов
- AFL (American Fuzzy Lop) — открытый покрытие-ориентированный фаззер, использующий генетические алгоритмы. Поддерживает инструментирование через GCC/Clang и QEMU. Широко используется в академических и коммерческих проектах.
- libFuzzer — библиотека для фаззинга, входящая в состав Clang/LLVM. Интегрируется непосредственно в тестируемое приложение, что обеспечивает высокую производительность. Используется в OSS-Fuzz.
- Honggfuzz — открытый фаззер с поддержкой аппаратного трассирования (Intel PT), работающий на Linux, macOS и Windows. Отличается низкими накладными расходами.
- Syzkaller — фаззер для ядра Linux, разработанный Google. Генерирует системные вызовы с произвольными аргументами и находит уязвимости в драйверах и подсистемах ядра.
- OSS-Fuzz — не инструмент, а платформа Google для непрерывного фаззинг-тестирования открытого ПО. Предоставляет инфраструктуру для запуска фаззеров на облачных серверах и автоматического уведомления разработчиков о найденных ошибках.
- Peach Fuzzer — коммерческий фаззер для тестирования протоколов и файловых форматов. Поддерживает создание пользовательских моделей данных.
- Fuzzilli — фаззер для JavaScript-движков, разработанный Google. Использует мутации на уровне AST и находит уязвимости в V8, SpiderMonkey и JavaScriptCore.
Критика и ограничения
Несмотря на эффективность, фаззинг имеет ряд ограничений:
- Невозможность гарантировать полноту — фаззинг не может доказать отсутствие ошибок, так как тестирует лишь конечное подмножество входных данных. Для сложных программ с большим пространством состояний (например, компиляторов, СУБД) полное покрытие практически недостижимо.
- Сложность тестирования программ с проверками — если программа проверяет контрольные суммы, цифровые подписи или сложные инварианты, случайные мутации часто отвергаются на ранних этапах. Гибридные методы (например, с символьным выполнением) частично решают эту проблему, но требуют значительных вычислительных ресурсов.
- Зависимость от качества сидов — плохой выбор начальных сидов может привести к тому, что фаззер будет «застревать» в поверхностных участках кода, не достигая глубоких ветвей.
- Высокие ресурсозатраты — для эффективного фаззинга требуется много процессорного времени и памяти. В облачных средах (например, OSS-Fuzz) используются кластеры из сотен машин.
- Ложные срабатывания — некоторые аварийные завершения могут быть вызваны не ошибками в коде, а, например, переполнением стека из-за рекурсии, что требует ручного анализа.
- Проблемы с недетерминированным поведением — программы, использующие случайные числа, многопоточность или асинхронные операции, могут вести себя по-разному при одних и тех же входных данных, что затрудняет воспроизведение ошибок.
Интересные факты
- Термин «фаззинг» (fuzzing) был предложен Бартоном Миллером в 1988 году, первоначально как «fuzz testing». Название отражает «нечёткость» (fuzziness) входных данных.
- В 2016 году исследователи из Google использовали фаззинг для тестирования компилятора C++ и обнаружили более 1000 ошибок в Clang и GCC.
- Фаззинг применяется не только для программного обеспечения, но и для аппаратных устройств, например, для тестирования прошивок IoT-устройств или контроллеров автомобилей.
- В 2020 году проект OSS-Fuzz достиг отметки в 20 000 найденных уязвимостей, а к 2024 году — более 30 000.
- В России фаззинг активно используется в рамках программы «Цифровая экономика» для тестирования отечественного ПО, в том числе операционных систем «Астра Linux» и «РЕД ОС».
BFOmetr — база данных и аналитика по компаниям России.
На главную BFOmetr →