
Распределённый фаззинг-движок с управлением покрытием, совместимый с целями libFuzzer; масштабируется до тысяч параллельных задач, использует санитайзеры и дистилляцию корпуса для обнаружения ошибок.
А почему бы и нет? Мы сейчас пытаемся фаззить очень большие и очень медленные цели, для которых libFuzzer, AFL и подобные не всегда хорошо масштабируются. Один из мотивирующих примеров — SiliFuzz. Работая над Centipede, мы планируем экспериментировать с новыми подходами к массовому дифференциальному фаззингу, которые существующие движки фаззинга не пытаются реализовать.
Примечательные особенности:
Поддержка fuzz-целей на основе libFuzzer «из коробки». Чтобы использовать вашу любимую функцию LLVMFuzzerTestOneInput(), вам нужно лишь собрать цель с опциями компилятора и компоновщика Centipede.
В разработке. Мы тестируем Centipede внутри небольшой команды на паре целей. Если вы не участник проекта Centipede и не хотите нам помочь, вам, вероятно, пока не стоит читать дальше.
Масштабируемость. Предполагается возможность запускать любое количество параллельных заданий с очень небольшими накладными расходами на взаимодействие. Сейчас мы тестируем 100 локальных заданий и 10 тысяч заданий на кластере.
Внешний процесс. Цель выполняется в отдельном процессе. Любые сбои в ней не повлияют на фаззер. Centipede также можно использовать внутри процесса, но этот режим не является основной целью. Если ваша цель небольшая и быстрая, вам, вероятно, по-прежнему стоит использовать libFuzzer.
Интеграция с санитайзерами достигается с помощью отдельных сборок. Если во время фаззинга вы хотите находить ошибки с помощью , или , вам потребуется предоставить отдельные бинарные файлы для каждого санитайзера, а также один основной бинарный файл для самого Centipede. Основной бинарный файл не должен использовать ни один из санитайзеров.
Никакая часть внутреннего интерфейса не стабильна. На этом этапе может измениться что угодно.
Программа, которая генерирует бесконечный поток входных данных для цели и управляет выполнением.
Бинарный файл, библиотека, API или вообще что угодно, что может принимать байты на вход и выдавать какие-либо данные о покрытии на выходе. Цель libFuzzer может быть целью Centipede. Подробнее читайте здесь.
Последовательность байтов, которую можно подать цели. Входные данные могут быть произвольным набором байтов или структурированными данными, например сериализованным прото.
Число, представляющее некоторое уникальное поведение цели. Например, признак 1234567 может означать, что базовый блок номер 987 в цели был выполнен 7 раз. При выполнении входных данных с целью фаззер собирает признаки, наблюдавшиеся во время выполнения.
Набор признаков, связанный с одним конкретным входным набором данных.
Некоторая информация о поведении цели при выполнении заданных входных данных. Покрытие обычно представляется как набор признаков, которые входные данные вызвали в цели.
Функция, которая принимает байты на вход и выдаёт небольшую случайную мутацию входных данных. См. также: структурно-зависимый фаззинг.
Функция, которая умеет подавать входные данные цели и получать взамен покрытие (то есть выполнять).
Настраиваемый движок фаззинга, который позволяет пользователю заменять мутатор и исполнитель.
Библиотека, реализующая интерфейс исполнителя, ожидаемый фаззером Centipede. Runner умеет запускать цель, инструментированную sancov, собирать результирующее покрытие и передавать его обратно в Centipede. Потенциальные fuzz-цели Centipede могут быть слинкованы с этой библиотекой, чтобы их можно было запускать через Centipede.
Набор входных данных.
Процесс выбора подмножества большего корпуса такого, что подмножество имеет те же признаки покрытия, что и исходный корпус.
Файл, представляющий подмножество корпуса, и другой файл, представляющий наборы признаков для того же подмножества корпуса.
Слить шард B в шард A означает: для каждого входного набора данных в шарде B, признаки которого отсутствуют в шарде A, добавить этот входной набор данных в A.
Один процесс фаззера. Одно задание записывает только в один шард, но может читать несколько шардов.
Локальная или удалённая директория, содержащая данные, создаваемые или потребляемые фаззером.
git clone https://github.com/google/centipede.git
cd centipede
CENTIPEDE_SRC=`pwd`
BIN_DIR=$CENTIPEDE_SRC/bazel-bin
bazel build -c opt :all
Что понадобится для последующих шагов:
$BIN_DIR/centipede — бинарный файл движка (фаззера).$BIN_DIR/libcentipede_runner.pic.a — библиотека, которую нужно слинковать с вашей fuzz-целью (runner).$CENTIPEDE_SRC/clang-flags.txt — рекомендуемые флаги компиляции clang для цели.Вы можете оставить эти файлы на месте или скопировать куда-нибудь.
Мы приводим два примера сборки цели: один крошечный однострочный файл и libpng. После сборки цели переходите к шагу запуска fuzz-цели.
В этом примере используется одна из простых примерных fuzz-целей, также известных как puzzles (головоломки), включённых в репозиторий Centipede.
ПРИМЕЧАНИЕ. В командах ниже используются флаги из $CENTIPEDE_SRC/clang-flags.txt. Вы можете выбрать другой набор флагов инструментирования: clang-flags.txt предоставляет лишь простой вариант по умолчанию.
FUZZ_TARGET=byte_cmp_4 # или любой другой исходник из $CENTIPEDE_SRC/puzzles
clang++ @$CENTIPEDE_SRC/clang-flags.txt -c $CENTIPEDE_SRC/puzzles/$FUZZ_TARGET.cc -o $BIN_DIR/$FUZZ_TARGET.o
На этом шаге только что собранная fuzz-цель связывается с libcentipede_runner.pic.a и другими необходимыми библиотеками.
clang++ $BIN_DIR/$FUZZ_TARGET.o $BIN_DIR/libcentipede_runner.pic.a \
-ldl -lrt -lpthread -o $BIN_DIR/$FUZZ_TARGET
Переходите к шагу запуска.
LIBPNG_BRANCH=v1.6.37 # Вы можете поэкспериментировать с другими ветками, если хотите
git clone --branch $LIBPNG_BRANCH --single-branch https://github.com/glennrp/libpng.git
cd libpng
CC=clang CFLAGS=@$CENTIPEDE_SRC/clang-flags.txt ./configure --disable-shared
make -j
FUZZ_TARGET=libpng_read_fuzzer
clang++ -include cstdlib \
./contrib/oss-fuzz/$FUZZ_TARGET.cc \
./.libs/libpng16.a \
$BIN_DIR/libcentipede_runner.pic.a \
-ldl -lrt -lpthread -lz \
-o $BIN_DIR/$FUZZ_TARGET
Локальный запуск не даст полного масштаба, но может быть полезен на этапе разработки фаззера. Мы рекомендуем скопировать и фаззер, и цель в локальную директорию перед запуском, чтобы не нагружать сетевую файловую систему.
WD=$HOME/centipede_run
mkdir -p $WD
ПРИМЕЧАНИЕ. Возможно, вам потребуется добавить llvm-symbolizer в ваш $PATH, чтобы работали некоторые функции Centipede. Символизатор можно установить как часть дистрибутива LLVM:
sudo apt install llvm
which llvm-symbolizer # обычно /usr/bin/llvm-symbolizer
rm -rf $WD/*
$BIN_DIR/centipede --binary=$BIN_DIR/$FUZZ_TARGET --workdir=$WD --num_runs=100
Посмотрите, что находится в рабочей директории
tree $WD
...
├── <fuzz target name>-d9d90139ee2ccc687f7c9d5821bcc04b8a847df5
│ └── features.0
└── corpus.0
ПРЕДУПРЕЖДЕНИЕ. Не превышайте количество ядер вашей машины для флага --j.
rm -rf $WD/*
$BIN_DIR/centipede --binary=$BIN_DIR/$FUZZ_TARGET --workdir=$WD --num_runs=100 --j=5
Посмотрите, что находится в рабочей директории:
tree $WD
...
├── <fuzz target name>-d9d90139ee2ccc687f7c9d5821bcc04b8a847df5
│ ├── features.0
│ ├── features.1
│ ├── features.2
│ ├── features.3
│ └── features.4
├── corpus.0
├── corpus.1
├── corpus.2
├── corpus.3
└── corpus.4
Обычно каждый шард Centipede не покрывает всех признаков, которые покрывает весь корпус. Чтобы дистиллировать корпус, процессу Centipede потребуется прочитать все шарды. Сейчас дистилляция работает следующим образом:
--distill_shards=N. Это приведёт к тому, что первые N заданий создадут N независимых файлов дистиллированного корпуса (по одному на задание). Каждый из дистиллированных корпусов должен иметь те же признаки, что и полный корпус, но входные данные в этих дистиллированных корпусах могут сильно различаться.Если вам также нужно экспортировать дистиллированный корпус в директорию в стиле libFuzzer (локальная директория с одним файлом на входной набор данных), добавьте --corpus_dir=DIR.
Centipede генерирует простой отчёт о покрытии в виде текстового файла. Шард 123 создаёт файл workdir/coverage-report-BINARY.000123.txt перед началом собственно фаззинга, то есть отчёт отражает покрытие, наблюдаемое шардом 123 после загрузки корпуса.
В отчёте показаны функции, покрытые полностью (все рёбра потока управления наблюдаются хотя бы один раз), не покрытые или покрытые частично. Для частично покрытых функций отчёт содержит символическую информацию обо всех покрытых и непокрытых рёбрах.
Отчёт будет выглядеть примерно так:
FULL: FUNCTION_A a.cc:1:0
NONE: FUNCTION_BB bb.cc:1:0
PARTIAL: FUNCTION_CCC ccc.cc:1:0
+ FUNCTION_CCC ccc.cc:1:0
- FUNCTION_CCC ccc.cc:2:0
- FUNCTION_CCC ccc.cc:3:0
TBD