Фаззинг с помощью AFL++
Что такое фаззинг?
Фаззинг — если абстракно: дайте программе множество неожиданных входных данных и посмотрите, что произойдет. Вы не пытаетесь доказать правильность программы, вы исследуете, как она не работает. Если программа падает, зависает или странно ведет себя при странных входных данных, то такой сбой часто указывает на ошибку (а иногда и на уязвимость в системе безопасности). Фаззинг автоматизирует этот процесс и может опробовать миллионы входных данных гораздо быстрее, чем человек.

Как это работает на практике: вы выбираете цель (программу, библиотечную функцию, сетевой сервис), подаете ей входные данные, которые случайным образом мутируют (обратите внимание: случайные мутации и рандомные входные данные — это не одно и то же) или генерируются на основе корректных примеров, и отслеживаете сбои. Современные фаззеры включают в себя цикл обратной связи, измеряя покрытие кода или другие сигналы времени выполнения и предпочитая входы, которые проверяют новые пути, тем самым сохраняя «интересные» случаи в качестве seed для дальнейшей мутации. Их часто сочетают с санитайзерами (например, ASan), чтобы превратить тонкие ошибки памяти в явные, воспроизводимые сбои.
Цель фаззинга:
- Уязвимости и ошибки безопасности (переполнение буфера, SQL-инъекция, межсайтовый скриптинг).
- Сбои (повреждение нулевого указателя, деление на ноль, необработанные исключения)
- Неожиданное поведение (повреждение данных, бесконечные циклы, некорректные результаты)
Типы фаззинга
- Фаззинг веб-приложений часто выглядит как автоматическое обнаружение каталогов или параметров. Фаззер многократно проверяет сайт с помощью множества тщательно мутированных или перегруженных URL и шаблонов запросов, чтобы обнаружить скрытые конечные точки, панели администратора или забытые ресурсы. На практике это может привести к обнаружению страниц, которые должны быть доступны только после входа в систему, или выявить неправильную конфигурацию веб-сервера, которая приводит к утечке конфиденциальной функциональности. То, что начинается как поток безобидных на вид запросов, может быстро составить карту поверхности приложения и указать на отсутствующие средства контроля доступа.
- Фаззинг форматов файлов нацелен на парсеры и вьюверы — программы, принимающие файлы на вход. Типичным сценарием является подача в программу чтения PDF-файлов неправильно сформированных или намеренно поврежденных PDF-файлов, сгенерированных фаззером. Поскольку парсеры обычно делают предположения о структуре и длине, неправильно сформированный файл может вызвать сбой, повреждение памяти или другое неопределенное поведение. Такие сбои особенно важны: они часто указывают на ошибки, которые можно использовать, и могут привести к реальным эксплойтам, если злоумышленник сможет отправить жертве вредоносный документ.
- Фаззинг сетевых протоколов фокусируется на данных, которые сервер потребляет по сети. В этом случае фаззер генерирует неожиданные или неправильно сформированные пакеты и отправляет их в службу, реализующую тот или иной протокол. Серверы, не обеспечивающие надежную проверку входящих пакетов, могут упасть, зависнуть или повести себя непредсказуемо, столкнувшись с такими входными данными. Такое поведение обычно сигнализирует о переполнении буфера или логических ошибках в обработке протокола — уязвимостях, которые злоумышленники могут использовать удаленно.
Во всех трех случаях картина одна и та же: автоматизированное входных данных позволяет выявить ветки кода, которые программное обеспечение делает, но никогда не проверяет. Практические результаты могут быть самыми разнообразными: от обнаружения неправильно сконфигурированных конечных веб-узлов до инициирования сбоев в программном обеспечении и удаленно эксплуатируемых уязвимостей в сетевых сервисах. Вот почему фаззинг является важным инструментом как для защитников, и почему интеграция фаззинга в процессы разработки и тестирования безопасности значительно повышает устойчивость программного обеспечения.
Категории фаззинга
- Фаззинг, основанный на мутациях, начинает с уже существующих входных данных, а затем вносит небольшие изменения, например, переворачивает биты, вставляет дополнительные байты или переставляет поля. Поскольку он работает на основе существующих данных, он быстр и прост, и часто обнаруживает ошибки, вызванные неожиданными крайними случаями.
- Фаззинг на основе генерации идет другим путем, создавая входные данные с нуля с помощью модели или грамматики. Это делает его особенно эффективным для сложных форматов файлов или протоколов, где структура имеет значение, а случайные изменения иначе были бы отвергнуты слишком рано.
- Фаззинг, управляемый по фидбеку, добавляет еще один уровень интеллекта, наблюдая за тем, какие части программы выполняются, и отдавая предпочтение входам, которые исследуют новые или ранее нетронутые пути кода. Обучаясь на основе обратной связи во время выполнения, он значительно повышает эффективность. В реальном мире эти техники часто комбинируются, чтобы сбалансировать скорость, достоверность входных данных и тщательность исследования, что в конечном итоге повышает шансы на обнаружение серьезных уязвимостей.
Фаззинг с помощью AFL++
Установка AFL++
Существует несколько способов установки AFL++, но я предпочитаю использовать сборку из исходного кода, без использования Docker или какой-либо виртуализации.
ПРИМЕЧАНИЕ: в зависимости от версии Debian/Ubuntu/Kali/…, замените -14 на любую доступную версию llvm.
sudo apt-get update
sudo apt-get install -y build-essential python3-dev automake cmake git flex bison libglib2.0-dev libpixman-1-dev python3-setuptools cargo libgtk-3-dev
# try to install llvm 14 and install the distro default if that fails
sudo apt-get install -y lld-14 llvm-14 llvm-14-dev clang-14 || sudo apt-get install -y lld llvm llvm-dev clang
sudo apt-get install -y gcc-$(gcc --version|head -n1|sed 's/\..*//'|sed 's/.* //')-plugin-dev libstdc++-$(gcc --version|head -n1|sed 's/\..*//'|sed 's/.* //')-dev
sudo apt-get install -y ninja-build # for QEMU mode
sudo apt-get install -y cpio libcapstone-dev # for Nyx mode
sudo apt-get install -y wget curl # for Frida mode
sudo apt-get install -y python3-pip # for Unicorn mode
git clone https://github.com/AFLplusplus/AFLplusplus
cd AFLplusplus
make distrib
sudo make install
make distrib также собирает режим FRIDA, режим QEMU, unicorn_mode и другие. Если вам нужен только простой AFL++, то делайте make all.
Перейдем к примеру, который предоставил Алекс Малено. https://github.com/alex-maleno/Fuzzing-Module.git
Первый шаг: клонируем репозиторий в целевую папку.
git clone https://github.com/alex-maleno/Fuzzing-Module.git
Следующий шаг: заходим в папку exercise1 и добавляем команду AFL++ tooling, а также команду build:
cd exercise1
mkdir build; cd build
CC=$YOUR_PATH/AFLplusplus/afl-clang-fast CXX=$YOUR_PATH/AFLplusplus/afl-clang-fast++ cmake ..
Мы устанавливаем переменные окружения CC и CXX в значения afl-clang-fast и afl-clang-fast++ соответственно. Эти переменные окружения указывают системе сборки использовать модифицированные компиляторы Clang от AFL++ вместо стандартных компиляторов для кода на C и C++.
cmake .. указывает CMake (запускаемому из каталога сборки) сгенерировать систему сборки, используя дерево исходных текстов в родительской папке. Если указать CMake на использование обёрток компиляторов AFL++, он создаст Make-файлы (или Ninja-файлы), которые скомпилируют ваш код с инструментарием. Эти компиляторы AFL++ инструментируют программу во время компиляции, вставляя легкие хуки, чтобы фаззер мог наблюдать за путями выполнения и направлять мутации.
Компиляция и сборка
После того как CMake написал файлы сборки, вызовите make , чтобы скомпилировать проект. Поскольку цепочка инструментов настроена на использование afl-clang-fast / afl-clang-fast++, результирующий бинарник будет собран с инструментарием AFL++. Для этого примера сборка создает исполняемый файл simple_crash, как указано в CMakeLists.txt(add_executable(simple_crash simple_crash.cpp)). После завершения работы make в ./simple_crash будет запущена скомпилированная, инструментированная фаззером; теперь она готова к управлению AFL++ для фаззинга.
Seed
В AFL++ seed — это любой начальный входной файл, который вы помещаете в папку -i. Фаззер мутирует эти файлы для генерации тестовых примеров. Хорошие seed — это небольшие, корректные примеры, которые отрабатывают реальные пути кода (минимальный корректный файл и несколько разнообразных реальных вариантов); они придают мутациям полезную структуру, чтобы фаззер мог добраться до более глубокого кода. Избегайте огромных, не связанных между собой файлов или чисто случайных фрагментов.
Совет: держите горстку компактных, разнообразных семплов и добавьте словарь важных лексем, если формат использует узнаваемые ключевые слова. Это ускорит поиск интересных сбоев.
mkdir seeds; cd seeds
for i in {0..4}; do dd if=/dev/urandom of=seed_$i bs=64 count=10; done
Это комаеда создает seed для нашей программы exercise1
Фаззинг цели
Чтобы приступить к фаззингу, вам нужно начать с afl-fuzz, с входными seed и скомпилированным бинарным файлом.
$YOUR_PATH/AFLplusplus/afl-fuzz -i /build/seeds/ -o out -m none -- /build/simple_crash

Вы можете остановить фаззинг с помощью CTRL+C после 3 сбоев программы (crash).
В выходном каталоге фаззера хранятся все данные, полученные в ходе выполнения:
out/default/crashes/— входные данные, которые привели к сбою завершению программы (возможные ошибки).
out/default/hangs/— входные данные, которые привели к таймингу или зависанию (проблемы с производительностью или бесконечным циклом).
out/default/hqueue/— текущий корпус входов, которые вызывают уникальные пути кода (начальные seed + обнаруженные случаи).
Это файлы, которые вы будете проверять, и использовать для отладки.
Triage
Поскольку инструментарий AFL++ добавляет дополнительные инструкции в двоичный код, лучше всего использовать чистую сборку без инструментария при отладке. В этом простом случае вы можете не изменять CMakeLists.txt и скомпилировать исходный код непосредственно из командной строки с включенными отладочными символами.
Сначала удалите сборку и перекомпилируйте ее с помощью gcc с флагом отладки.
rm -rf /build
sudo apt update && sudo apt install -y g++
g++ -g simple_crash.cpp -o simple_crash_debug
Я буду использовать VSCode в качестве триажного тестового случая, потому что он более удобен в использовании; вы можете выбрать любую IDE, например Eclipse.
В папке Fuzzing-Module/exercise1 командой открываем VSCode:
code Fuzzing-Module/exercise1
Если VS Code спросит «Yes, I trust the authors?«, нажмите Yes.
Далее создаем отладочную конфигурацию, как показано:
- Перейдите в меню Run → Add Configuration…
- Выберите C/C++: (gdb) Launch.
Ниже приведен полный пример launch.json. Вероятно, вам потребуется скорректировать пути program и args в соответствии с вашим рабочим пространством и “сбой” файлом, который вы хотите отлаживать.
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "(gdb) Launch",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/simple_crash_debug",
"args": ["<", "${workspaceFolder}/out/default/crashes/id:000000,sig:06,src:000004,time:30,execs:235,op:quick,pos:131"],
"stopAtEntry": false,
"cwd": "${fileDirname}",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
},
{
"description": "Set Disassembly Flavor to Intel",
"text": "-gdb-set disassembly-flavor intel",
"ignoreFailures": true
}
]
}
]
}
После сохранения конфигурации откройте simple_crash.cpp и запустите отладчик (нажмите F5 или выберите Run → Start Debugging), если отладчик остановится на main, нажмите continue. Если программа или аргументы не соответствуют вашим настройкам, отредактируйте поля program и args перед запуском.

Как видно на скриншоте, фаззер мутировал seed и выполнил функцию abort().
Это была простая вводная статья в блоге. В следующем посте мы обсудим более глубокие концепции AFL++, процесс оптимизации, покрытие кода и написание AFL++ harness.