
american fuzzy lop — фаззер, ориентированный на безопасность
Изначально разработан Михалом Залевски (Michal Zalewski) [email protected].
Если у вас нет времени читать этот файл, см. QuickStartGuide.txt.
Фаззинг — одна из самых мощных и проверенных стратегий выявления проблем безопасности в реальном программном обеспечении; именно с его помощью была найдена подавляющая часть ошибок удалённого выполнения кода и повышения привилегий, обнаруженных на сегодняшний день в критически важном с точки зрения безопасности ПО.
К сожалению, фаззинг также относительно поверхностен: слепые случайные мутации крайне редко позволяют достичь определённых путей выполнения в тестируемом коде, оставляя некоторые уязвимости вне досягаемости этого метода.
Предпринималось множество попыток решить эту проблему. Один из ранних подходов, пионером которого стал Тэвис Орманди (Tavis Ormandy), — дистилляция корпуса. Метод опирается на сигналы покрытия, чтобы отобрать подмножество интересных начальных входных данных из огромного качественного корпуса файлов-кандидатов, а затем фаззить их традиционными средствами. Этот подход работает исключительно хорошо, но требует наличия такого корпуса под рукой. Кроме того, измерения блочного покрытия дают лишь очень упрощённое понимание состояния программы и менее полезны для направления усилий фаззинга в долгосрочной перспективе.
Другие, более сложные исследования сосредоточены на таких методах, как анализ потока управления программы («конколическое выполнение»), символьное выполнение или статический анализ. Все эти методы чрезвычайно перспективны в экспериментальных условиях, но на практике страдают от проблем с надёжностью и производительностью — и в настоящее время не предлагают жизнеспособной альтернативы «глупым» методам фаззинга.
American Fuzzy Lop — это фаззер переборного типа, сочетающийся с чрезвычайно простым, но исключительно надёжным инструментируемым генетическим алгоритмом. Он использует модифицированную форму покрытия рёбер графа потока управления, чтобы без труда улавливать тонкие локальные изменения в потоке управления программы.
Если несколько упростить, общий алгоритм можно свести к следующему:
загрузить предоставленные пользователем начальные тестовые примеры в очередь;
взять следующий входной файл из очереди;
попытаться сократить тестовый пример до минимального размера, не изменяющего измеряемое поведение программы;
многократно мутировать файл, используя сбалансированный и хорошо изученный набор традиционных стратегий фаззинга;
если любая из полученных мутаций привела к новому переходу состояния, зафиксированному инструментацией, добавить мутированный результат как новую запись в очередь;
перейти к шагу 2.
Обнаруженные тестовые примеры также периодически прореживаются, чтобы исключить те, что устарели из-за более новых находок с более высоким покрытием; кроме того, применяются и другие шаги минимизации усилий под управлением инструментации.
Побочным результатом процесса фаззинга является создание небольшого самодостаточного корпуса интересных тестовых примеров. Они чрезвычайно полезны для подготовки других режимов тестирования, требующих больших трудозатрат или ресурсов, — например, для стресс-тестирования браузеров, офисных приложений, графических пакетов или закрытых инструментов.
Фаззер тщательно протестирован и обеспечивает производительность «из коробки», значительно превосходящую слепой фаззинг или инструменты, основанные только на покрытии.
Когда доступен исходный код, инструментация может быть внедрена с помощью сопутствующего инструмента, который работает как замена gcc или clang в любом стандартном процессе сборки стороннего кода.
Инструментация оказывает довольно умеренное влияние на производительность; в сочетании с другими оптимизациями, реализованными в afl-fuzz, большинство программ можно фаззить так же быстро или даже быстрее, чем с помощью традиционных инструментов.
Правильный способ перекомпиляции целевой программы может различаться в зависимости от особенностей процесса сборки, но почти универсальный подход выглядит так:```shell $ CC=/path/to/afl/afl-gcc ./configure $ make clean all
Для программ на C++ также стоит задать `CXX=/path/to/afl/afl-g++`.
Обёртки clang (`afl-clang` и `afl-clang++`) используются аналогично;
пользователи clang также могут воспользоваться более производительным режимом инструментирования,
как описано в llvm_mode/README.llvm.
При тестировании библиотек нужно найти или написать простую программу, которая читает
данные из stdin или из файла и передаёт их тестируемой библиотеке. В таком
случае крайне важно слинковать этот исполняемый файл со статической версией
инструментированной библиотеки либо убедиться, что загружается корректный файл `.so`
во время выполнения (обычно задаётся через `LD_LIBRARY_PATH`). Самый простой вариант — статическая
сборка, обычно возможная с помощью:```shell
$ CC=/path/to/afl/afl-gcc ./configure --disable-shared
Установка AFL_HARDEN=1 при вызове 'make' приведёт к тому, что обёртка CC
автоматически включит параметры усиления защиты кода, облегчающие обнаружение
простых ошибок памяти. Libdislocator, вспомогательная библиотека, входящая в состав AFL (см.
libdislocator/README.dislocator), также может помочь выявить проблемы повреждения кучи.
PS. Пользователям ASAN рекомендуется просмотреть файл notes_for_asan.txt на предмет важных предостережений.
Когда исходный код НЕ доступен, фаззер предлагает экспериментальную поддержку для быстрой инструментации бинарных «чёрных ящиков» на лету. Это достигается с помощью версии QEMU, работающей в малоизвестном режиме "эмуляции пользовательского пространства".
QEMU — это отдельный от AFL проект, но вы можете без труда собрать функцию, выполнив:```shell $ cd qemu_mode $ ./build_qemu_support.sh
Дополнительные инструкции и предостережения см. в qemu_mode/README.qemu.
Этот режим примерно в 2-5 раз медленнее инструментирования на этапе компиляции,
хуже поддается параллелизации и может иметь некоторые другие особенности.
## 5) Выбор исходных тестовых примеров
Для корректной работы фаззеру требуется один или несколько исходных файлов, которые
содержат хороший пример входных данных, обычно ожидаемых целевым
приложением. Есть два основных правила:
- Делайте файлы небольшими. Идеально — менее 1 кБ, хотя это не строго обязательно.
Обсуждение того, почему размер важен, см. в [perf_tips.txt](https://github.com/google/afl/blob/master/docs/perf_tips.txt).
- Используйте несколько тестовых примеров только в том случае, если они функционально отличаются
друг от друга. Нет смысла использовать пятьдесят разных отпускных фотографий
для фаззинга библиотеки для работы с изображениями.
Множество хороших примеров исходных файлов можно найти в подкаталоге testcases/
который поставляется вместе с этим инструментом.
PS. Если для проверки доступен большой корпус данных, возможно, вы захотите использовать
утилиту afl-cmin, чтобы определить подмножество функционально различных файлов, которые
задействуют разные пути выполнения кода в целевом бинарном файле.
## 6) Фаззинг бинарных файлов
Сам процесс фаззинга выполняется утилитой afl-fuzz. Эта программа
требует каталог только для чтения с исходными тестовыми примерами, отдельное место для
хранения результатов, а также путь к бинарному файлу для тестирования.