
Moteur de fuzzing distribué guidé par la couverture, compatible avec les cibles libFuzzer ; passe à l'échelle jusqu'à des milliers de tâches concurrentes, utilise les sanitizers et la distillation de corpus pour découvrir des bugs.
Pourquoi pas ? Nous essayons actuellement de fuzzer des cibles très volumineuses et très lentes pour lesquelles libFuzzer, AFL et autres ne passent pas nécessairement à l'échelle. Pour l'un de nos exemples motivants, voir SiliFuzz. En travaillant sur Centipede, nous prévoyons d'expérimenter de nouvelles approches de fuzzing différentiel à grande échelle, que les moteurs de fuzzing existants ne tentent pas.
Fonctionnalités notables :
Prise en charge immédiate des cibles de fuzzing basées sur libFuzzer. Pour utiliser votre LLVMFuzzerTestOneInput() préféré, il vous suffit de compiler votre cible avec les options de compilateur et de linker de Centipede.
Travail en cours. Nous testons Centipede au sein d'une petite équipe sur quelques cibles. Sauf si vous faites partie du projet Centipede, ou si vous voulez nous aider, vous ne voulez probablement pas lire la suite pour l'instant.
Passage à l'échelle. L'intention est de pouvoir exécuter un nombre quelconque de tâches en parallèle, avec très peu de surcharge de communication. Nous testons actuellement avec 100 tâches locales et 10 000 tâches sur un cluster.
Hors processus. La cible s'exécute dans un processus séparé. Tout crash dans celle-ci n'affectera pas le fuzzer. Centipede peut également être utilisé en mode in-process, mais ce mode n'est pas l'objectif principal. Si votre cible est petite et rapide, vous voulez probablement encore libFuzzer.
L'intégration avec les sanitizers est réalisée via des compilations séparées. Si pendant le fuzzing vous voulez trouver des bugs avec ASAN, MSAN ou TSAN, vous devrez fournir des binaires séparés pour chaque sanitizer ainsi qu'un binaire principal pour Centipede lui-même. Le binaire principal ne doit utiliser aucun des sanitizers.
Aucune partie de l'interface interne n'est stable. Tout peut changer à ce stade.
Un programme qui produit un flux infini d'entrées pour une cible et orchestre l'exécution.
Un binaire, une bibliothèque, une API, ou plus généralement tout ce qui peut consommer des octets en entrée et produire une forme de données de couverture en sortie. Une cible libFuzzer peut être une cible de Centipede. En savoir plus ici.
Une séquence d'octets qui peut être fournie à une cible. L'entrée peut être un ensemble arbitraire d'octets, ou des données structurées, par exemple un proto sérialisé.
Un nombre qui représente un comportement unique de la cible. Par exemple, une caractéristique 1234567 peut représenter le fait qu'un bloc de base numéro 987 de la cible a été exécuté 7 fois. Lors de l'exécution d'une entrée avec la cible, le fuzzer collecte les caractéristiques observées pendant l'exécution.
Un ensemble de caractéristiques associé à une entrée spécifique.
Des informations sur le comportement de la cible lorsqu'elle exécute une entrée donnée. La couverture est généralement représentée comme l'ensemble de caractéristiques que l'entrée a déclenché dans la cible.
Une fonction qui prend des octets en entrée et produit une petite mutation aléatoire de l'entrée. Voir aussi : fuzzing conscient de la structure.
Une fonction qui sait comment fournir une entrée à une cible et obtenir une couverture en retour (c'est-à-dire exécuter).
Un moteur de fuzzing personnalisable qui permet à l'utilisateur de remplacer le mutateur et l'exécuteur.
Une bibliothèque qui implémente l'interface d'exécuteur attendue par le fuzzer Centipede. Le runner sait comment exécuter une cible instrumentée avec sancov, collecter la couverture résultante et la retransmettre à Centipede. Les futures cibles de fuzzing Centipede peuvent être liées à cette bibliothèque pour être exécutables par Centipede.
Un ensemble d'entrées.
Un processus de sélection d'un sous-ensemble d'un corpus plus large, de sorte que le sous-ensemble possède les mêmes caractéristiques de couverture que le corpus d'origine.
Un fichier représentant un sous-ensemble du corpus et un autre fichier représentant les ensembles de caractéristiques pour ce même sous-ensemble du corpus.
Fusionner le shard B dans le shard A signifie : pour chaque entrée du shard B qui possède des caractéristiques absentes du shard A, ajouter cette entrée à A.
Un processus de fuzzing unique. Une tâche n'écrit que dans un seul shard, mais peut lire plusieurs shards.
Un répertoire local ou distant qui contient des données produites ou consommées par 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
Ce dont vous aurez besoin pour les étapes suivantes :
$BIN_DIR/centipede - le binaire du moteur (le fuzzer).$BIN_DIR/libcentipede_runner.pic.a - la bibliothèque que vous devez lier à votre cible de fuzzing (le runner).$CENTIPEDE_SRC/clang-flags.txt - les drapeaux de compilation clang recommandés pour la cible.Vous pouvez garder ces fichiers là où ils sont ou les copier ailleurs.
Nous fournissons deux exemples de compilation de la cible : une petite cible monofichier et libpng. Une fois votre cible compilée, passez à l'étape d'exécution de la cible de fuzzing.
Cet exemple utilise l'une des cibles de fuzzing d'exemple simples, aussi appelées puzzles, incluses dans le dépôt Centipede.
NOTE : Les commandes ci-dessous utilisent les drapeaux de $CENTIPEDE_SRC/clang-flags.txt. Vous pouvez choisir d'utiliser un autre ensemble de drapeaux d'instrumentation : clang-flags.txt ne fournit qu'une simple option par défaut.
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
Cette étape lie la cible de fuzzing tout juste compilée avec libcentipede_runner.pic.a et les autres bibliothèques requises.
clang++ $BIN_DIR/$FUZZ_TARGET.o $BIN_DIR/libcentipede_runner.pic.a \
-ldl -lrt -lpthread -o $BIN_DIR/$FUZZ_TARGET
Passez à l'étape d'exécution.
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
L'exécution locale ne permet pas d'atteindre la pleine échelle, mais elle peut être utile pendant la phase de développement du fuzzer. Nous recommandons de copier le fuzzer et la cible dans un répertoire local avant l'exécution afin d'éviter de solliciter un système de fichiers réseau.
WD=$HOME/centipede_run
mkdir -p $WD
NOTE : Vous devrez peut-être ajouter llvm-symbolizer à votre $PATH pour que certaines fonctionnalités de Centipede fonctionnent. Le symboliseur peut être installé dans le cadre de la distribution 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
Voyez ce qu'il y a dans le répertoire de travail
tree $WD
...
├── <fuzz target name>-d9d90139ee2ccc687f7c9d5821bcc04b8a847df5
│ └── features.0
└── corpus.0
AVERTISSEMENT : Ne dépassez pas le nombre de cœurs de votre machine pour le drapeau --j.
rm -rf $WD/*
$BIN_DIR/centipede --binary=$BIN_DIR/$FUZZ_TARGET --workdir=$WD --num_runs=100 --j=5
Voyez ce qu'il y a dans le répertoire de travail :
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
Chaque shard Centipede ne couvre généralement pas toutes les caractéristiques couvertes par l'ensemble du corpus. Pour distiller le corpus, un processus Centipede devra lire tous les shards. Actuellement, la distillation fonctionne comme ceci :
Exécutez le fuzzing comme décrit ci-dessus, afin que tous les shards aient leurs ensembles de caractéristiques calculés. Arrêtez le fuzzing.
Ensuite, exécutez les mêmes tâches de fuzzing, mais avec --distill_shards=N. Cela fera en sorte que les N premières tâches produisent N fichiers de corpus distillé indépendants (un par tâche). Chacun des corpus distillés devrait avoir les mêmes caractéristiques que le corpus complet, mais les entrées peuvent être très différentes entre ces corpus distillés.
Si vous avez également besoin d'exporter le corpus distillé vers un répertoire de style libFuzzer (répertoire local avec un fichier par entrée), ajoutez --corpus_dir=DIR.
Centipede génère un simple rapport de couverture sous la forme d'un fichier texte. Le shard 123 génère un fichier workdir/coverage-report-BINARY.000123.txt avant le début du fuzzing proprement dit, c'est-à-dire que le rapport reflète la couverture observée par le shard 123 après le chargement du corpus.
Le rapport montre les fonctions entièrement couvertes (toutes les arêtes du graphe de flot de contrôle sont observées au moins une fois), non couvertes, ou partiellement couvertes. Pour les fonctions partiellement couvertes, le rapport contient des informations symboliques pour toutes les arêtes couvertes et non couvertes.
Le rapport ressemblera à ceci :
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
À définir