
Mecanismo de fuzzing distribuído orientado por cobertura, compatível com alvos libFuzzer; escala para milhares de tarefas concorrentes, usa sanitizers e destilação de corpus para revelar bugs.
Por que não? Atualmente, estamos tentando fuzzar alguns alvos muito grandes e muito lentos para os quais o libFuzzer, o AFL e afins não escalam necessariamente bem. Para um dos nossos exemplos motivadores, veja SiliFuzz. Ao trabalhar no Centipede, planejamos experimentar novas abordagens para fuzzing diferencial em escala massiva, que os mecanismos de fuzzing existentes não tentam fazer.
Recursos notáveis:
Suporte pronto para uso para fuzz targets baseados em libFuzzer. Para usar o seu
LLVMFuzzerTestOneInput() favorito, você só precisa compilar seu alvo com as
opções de compilador e linker do Centipede.
Em desenvolvimento. Testamos o centipede em uma pequena equipe com alguns alvos. A menos que você faça parte do projeto Centipede ou queira nos ajudar, provavelmente você ainda não quer continuar lendo.
Escala. A intenção é poder executar qualquer número de jobs simultaneamente, com muito pouca sobrecarga de comunicação. Atualmente testamos com 100 jobs locais e com 10 mil jobs em um cluster.
Fora do processo. O alvo é executado em um processo separado. Qualquer crash nele não afetará o fuzzer. O Centipede também pode ser usado em processo, mas esse modo não é o objetivo principal. Se o seu alvo for pequeno e rápido, você provavelmente ainda prefere o libFuzzer.
A integração com os sanitizers é alcançada por meio de builds separados. Se durante o fuzzing você quiser encontrar bugs com ASAN, MSAN ou TSAN, você precisará fornecer binários separados para cada sanitizer, além de um binário principal para o próprio Centipede. O binário principal não deve usar nenhum dos sanitizers.
Nenhuma parte da interface interna é estável. Tudo pode mudar nesta fase.
Um programa que produz um fluxo infinito de entradas para um alvo e orquestra a execução.
Um binário, uma biblioteca, uma API, ou melhor, qualquer coisa que possa consumir bytes como entrada e produzir algum tipo de dados de cobertura como saída. Um alvo do libFuzzer pode ser um alvo do Centipede. Leia mais aqui.
Uma sequência de bytes que pode ser fornecida a um alvo. A entrada pode ser um saco arbitrário de bytes ou algum dado estruturado, por exemplo, um proto serializado.
Um número que representa algum comportamento único do alvo. Por exemplo, uma feature 1234567 pode representar o fato de que o bloco básico número 987 no alvo foi executado 7 vezes. Ao executar uma entrada com o alvo, o fuzzer coleta as features que foram observadas durante a execução.
Um conjunto de features associado a uma entrada específica.
Algumas informações sobre o comportamento do alvo quando ele executa uma determinada entrada. A cobertura geralmente é representada como o conjunto de features que a entrada acionou no alvo.
Uma função que recebe bytes como entrada e produz uma pequena mutação aleatória da entrada. Veja também: fuzzing ciente de estrutura.
Uma função que sabe como alimentar uma entrada em um alvo e obter cobertura em troca (ou seja, executar).
Um mecanismo de fuzzing personalizável que permite ao usuário substituir o Mutator e o Executor.
Uma biblioteca que implementa a interface de executor esperada pelo fuzzer Centipede. O runner sabe como executar um alvo instrumentado com sancov, coletar a cobertura resultante e passá-la de volta ao Centipede. Futuros fuzz targets do Centipede podem ser linkados com essa biblioteca para se tornarem executáveis pelo Centipede.
Um conjunto de entradas.
Um processo de escolha de um subconjunto de um corpus maior, de modo que o subconjunto tenha as mesmas features de cobertura do corpus original.
Um arquivo que representa um subconjunto do corpus e outro arquivo que representa os conjuntos de features para esse mesmo subconjunto do corpus.
Mesclar o shard B no shard A significa: para cada entrada no shard B que tenha features ausentes no shard A, adicionar essa entrada a A.
Um único processo de fuzzer. Um job escreve apenas em um shard, mas pode ler vários shards.
Um diretório local ou remoto que contém dados produzidos ou consumidos por um fuzzer.
git clone https://github.com/google/centipede.git
cd centipede
CENTIPEDE_SRC=`pwd`
BIN_DIR=$CENTIPEDE_SRC/bazel-bin
bazel build -c opt :all
O que você precisará para as etapas seguintes:
$BIN_DIR/centipede - o binário do mecanismo (o fuzzer).$BIN_DIR/libcentipede_runner.pic.a - a biblioteca que você precisa linkar com seu fuzz target (o runner).$CENTIPEDE_SRC/clang-flags.txt - flags de compilação recomendadas do clang para o alvo.Você pode manter esses arquivos onde estão ou copiá-los para algum lugar.
Fornecemos dois exemplos de compilação do alvo: um alvo minúsculo de arquivo único e o libpng. Depois de compilar seu alvo, prossiga para a etapa de execução do fuzz target.
Este exemplo usa um dos fuzz targets de exemplo simples, também conhecidos como puzzles, incluídos no repositório do Centipede.
NOTA: Os comandos abaixo usam as flags de $CENTIPEDE_SRC/clang-flags.txt. Você pode optar por usar outro conjunto de flags de instrumentação: clang-flags.txt apenas fornece uma opção padrão simples.
FUZZ_TARGET=byte_cmp_4 # or any other source under $CENTIPEDE_SRC/puzzles
clang++ @$CENTIPEDE_SRC/clang-flags.txt -c $CENTIPEDE_SRC/puzzles/$FUZZ_TARGET.cc -o $BIN_DIR/$FUZZ_TARGET.o
Esta etapa linka o fuzz target recém-compilado com libcentipede_runner.pic.a e outras bibliotecas necessárias.
clang++ $BIN_DIR/$FUZZ_TARGET.o $BIN_DIR/libcentipede_runner.pic.a \
-ldl -lrt -lpthread -o $BIN_DIR/$FUZZ_TARGET
Pule para a etapa de execução.
LIBPNG_BRANCH=v1.6.37 # You can experiment with other branches if you'd like
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
Executar localmente não dará a escala completa, mas pode ser útil durante a fase de desenvolvimento do fuzzer. Recomendamos que tanto o fuzzer quanto o alvo sejam copiados para um diretório local antes da execução, a fim de evitar sobrecarregar um sistema de arquivos de rede.
WD=$HOME/centipede_run
mkdir -p $WD
NOTA: Talvez seja necessário adicionar o
llvm-symbolizer
ao seu $PATH para que algumas funcionalidades do Centipede funcionem. O
simbolizador pode ser instalado como parte da distribuição do LLVM:
sudo apt install llvm
which llvm-symbolizer # normally /usr/bin/llvm-symbolizer
rm -rf $WD/*
$BIN_DIR/centipede --binary=$BIN_DIR/$FUZZ_TARGET --workdir=$WD --num_runs=100
Veja o que há no diretório de trabalho
tree $WD
...
├── <fuzz target name>-d9d90139ee2ccc687f7c9d5821bcc04b8a847df5
│ └── features.0
└── corpus.0
AVISO: Não exceda o número de núcleos da sua máquina para a flag --j.
rm -rf $WD/*
$BIN_DIR/centipede --binary=$BIN_DIR/$FUZZ_TARGET --workdir=$WD --num_runs=100 --j=5
Veja o que há no diretório de trabalho:
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
Cada shard do Centipede normalmente não cobre todas as features que o corpus inteiro cobre. Para destilar o corpus, um processo do Centipede precisará ler todos os shards. Atualmente, a destilação funciona assim:
--distill_shards=N. Isso
fará com que os primeiros N jobs produzam N arquivos de corpus destilado independentes
(um por job). Cada um dos corpora destilados deve ter as mesmas features do
corpus completo, mas as entradas podem ser bem diferentes entre esses corpora destilados.Se você também precisar exportar o corpus destilado para um diretório no estilo libFuzzer
(diretório local com um arquivo por entrada), adicione --corpus_dir=DIR.
O Centipede gera um relatório de cobertura simples na forma de um arquivo de texto. O shard
123 gera um arquivo workdir/coverage-report-BINARY.000123.txt
antes do fuzzing real começar, ou seja, o relatório reflete a cobertura observada pelo
shard 123 após carregar o corpus.
O relatório mostra funções totalmente cobertas (todas as arestas do fluxo de controle são observadas pelo menos uma vez), não cobertas ou parcialmente cobertas. Para funções parcialmente cobertas, o relatório contém informações simbólicas para todas as arestas cobertas e não cobertas.
O relatório será algo assim:
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