
Motore di fuzzing distribuito guidato dalla copertura, compatibile con i target libFuzzer; scalabile fino a migliaia di job concorrenti, utilizza sanitizer e distillazione del corpus per scoprire bug.
Perché no? Stiamo attualmente cercando di fare fuzzing su alcuni target molto grandi e molto lenti per i quali libFuzzer, AFL e simili non scalano necessariamente bene. Per uno dei nostri esempi motivazionali vedi SiliFuzz. Mentre lavoriamo su Centipede, intendiamo sperimentare nuovi approcci al fuzzing differenziale su larga scala che i motori di fuzzing esistenti non provano a fare.
Caratteristiche notevoli:
Supporto immediato per i fuzz target basati su libFuzzer. Per usare la tua LLVMFuzzerTestOneInput() preferita devi solo compilare il target con le opzioni del compilatore e del linker di Centipede.
Lavoro in corso. Testiamo Centipede all'interno di un piccolo team su un paio di target. A meno che tu non faccia parte del progetto Centipede o voglia aiutarci, probabilmente non vuoi ancora leggere oltre.
Scala. L'intento è poter eseguire un numero qualsiasi di job contemporaneamente, con un overhead di comunicazione molto ridotto. Attualmente testiamo con 100 job locali e con 10k job su un cluster.
Fuori processo. Il target gira in un processo separato. Eventuali crash in esso non influenzeranno il fuzzer. Centipede può essere usato anche in-process, ma questa modalità non è l'obiettivo principale. Se il tuo target è piccolo e veloce, probabilmente vuoi ancora libFuzzer.
L'integrazione con i sanitizer è ottenuta tramite build separate. Se durante il fuzzing vuoi trovare bug con ASAN, MSAN o TSAN, dovrai fornire binari separati per ogni sanitizer e un binario principale per Centipede stesso. Il binario principale non dovrebbe usare nessuno dei sanitizer.
Nessuna parte dell'interfaccia interna è stabile. Qualsiasi cosa può cambiare in questa fase.
Un programma che produce un flusso infinito di input per un target e ne orchestra l'esecuzione.
Un binario, una libreria, un'API, o piuttosto qualsiasi cosa che possa consumare byte come input e produrre una qualche forma di dati di copertura come output. Un target di libFuzzer può essere un target di Centipede. Leggi di più qui.
Una sequenza di byte che può essere fornita a un target. L'input può essere un insieme arbitrario di byte o alcuni dati strutturati, ad esempio un proto serializzato.
Un numero che rappresenta un comportamento unico del target. Ad esempio, una feature 1234567 può rappresentare il fatto che il blocco di base numero 987 nel target è stato eseguito 7 volte. Quando esegue un input con il target, il fuzzer raccoglie le feature osservate durante l'esecuzione.
Un insieme di feature associate a un input specifico.
Alcune informazioni sul comportamento del target quando esegue un dato input. La copertura è solitamente rappresentata come l'insieme di feature che l'input ha attivato nel target.
Una funzione che prende byte come input e restituisce una piccola mutazione casuale dell'input. Vedi anche: structure-aware fuzzing.
Una funzione che sa come fornire un input a un target e ottenere in cambio la copertura (cioè eseguire).
Un motore di fuzzing personalizzabile che consente all'utente di sostituire il Mutator e l'Executor.
Una libreria che implementa l'interfaccia dell'executor prevista dal fuzzer Centipede. Il runner sa come eseguire un target instrumentato con sancov, raccogliere la copertura risultante e restituirla a Centipede. I potenziali fuzz target di Centipede possono essere linkati con questa libreria per renderli eseguibili da Centipede.
Un insieme di input.
Un processo di selezione di un sottoinsieme di un corpus più grande, tale che il sottoinsieme abbia le stesse feature di copertura del corpus originale.
Un file che rappresenta un sottoinsieme del corpus e un altro file che rappresenta gli insiemi di feature per quello stesso sottoinsieme del corpus.
Unire lo shard B nello shard A significa: per ogni input nello shard B che ha feature mancanti nello shard A, aggiungere quell'input ad A.
Un singolo processo del fuzzer. Un job scrive solo su uno shard, ma può leggere più shard.
Una directory locale o remota che contiene dati prodotti o consumati da un 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
Cosa ti servirà per i passaggi successivi:
$BIN_DIR/centipede - il binario del motore (il fuzzer).$BIN_DIR/libcentipede_runner.pic.a - la libreria con cui devi linkare il tuo fuzz target (il runner).$CENTIPEDE_SRC/clang-flags.txt - le flag di compilazione clang consigliate per il target.Puoi tenere questi file dove sono o copiarli altrove.
Forniamo due esempi di compilazione del target: un minuscolo target a file singolo e libpng. Una volta compilato il target, procedi al passaggio di esecuzione del fuzz target.
Questo esempio usa uno dei semplici fuzz target di esempio, noti anche come puzzle, inclusi nel repository Centipede.
NOTA: I comandi seguenti usano le flag da $CENTIPEDE_SRC/clang-flags.txt. Puoi scegliere di usare un altro insieme di flag di strumentazione: clang-flags.txt fornisce solo una semplice opzione predefinita.
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
Questo passaggio collega (linka) il fuzz target appena compilato con libcentipede_runner.pic.a e altre librerie richieste.
clang++ $BIN_DIR/$FUZZ_TARGET.o $BIN_DIR/libcentipede_runner.pic.a \
-ldl -lrt -lpthread -o $BIN_DIR/$FUZZ_TARGET
Vai al passaggio di esecuzione.
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
Eseguire localmente non darà la scala completa, ma può essere utile durante la fase di sviluppo del fuzzer. Raccomandiamo di copiare sia il fuzzer che il target in una directory locale prima dell'esecuzione per evitare di sollecitare un file system di rete.
WD=$HOME/centipede_run
mkdir -p $WD
NOTA: Potrebbe essere necessario aggiungere llvm-symbolizer al tuo $PATH affinché alcune funzionalità di Centipede funzionino. Il symbolizer può essere installato come parte della distribuzione 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
Guarda cosa c'è nella directory di lavoro
tree $WD
...
├── <fuzz target name>-d9d90139ee2ccc687f7c9d5821bcc04b8a847df5
│ └── features.0
└── corpus.0
ATTENZIONE: Non superare il numero di core della tua macchina con la flag --j.
rm -rf $WD/*
$BIN_DIR/centipede --binary=$BIN_DIR/$FUZZ_TARGET --workdir=$WD --num_runs=100 --j=5
Guarda cosa c'è nella directory di lavoro:
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
Ogni shard di Centipede tipicamente non copre tutte le feature coperte dall'intero corpus. Per distillare il corpus, un processo Centipede dovrà leggere tutti gli shard. Attualmente, la distillazione funziona così:
--distill_shards=N. Questo farà sì che i primi N job producano N file di corpus distillato indipendenti (uno per job). Ciascuno dei corpus distillati dovrebbe avere le stesse feature dell'intero corpus, ma gli input potrebbero essere molto diversi tra questi corpus distillati.Se devi anche esportare il corpus distillato in una directory in stile libFuzzer (directory locale con un file per input), aggiungi --corpus_dir=DIR.
Centipede genera un semplice report di copertura sotto forma di file di testo. Lo shard 123 genera un file workdir/coverage-report-BINARY.000123.txt prima che inizi il fuzzing vero e proprio, cioè il report riflette la copertura osservata dallo shard 123 dopo il caricamento del corpus.
Il report mostra le funzioni completamente coperte (tutti gli archi del flusso di controllo sono osservati almeno una volta), non coperte, o parzialmente coperte. Per le funzioni parzialmente coperte, il report contiene informazioni simboliche per tutti gli archi coperti e non coperti.
Il report sarà simile a questo:
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
Da definire