Développé à l'origine par Michal Zalewski [email protected].
Consultez QuickStartGuide.txt si vous n'avez pas le temps de lire ce fichier.
Le fuzzing est l’une des stratégies les plus puissantes et éprouvées pour identifier les problèmes de sécurité dans les logiciels du monde réel ; il est responsable de la grande majorité des bogues d’exécution de code à distance et d’élévation de privilèges trouvés à ce jour dans les logiciels critiques pour la sécurité.
Malheureusement, le fuzzing est également relativement superficiel ; des mutations aveugles et aléatoires rendent très improbable l’atteinte de certains chemins de code dans le code testé, laissant certaines vulnérabilités fermement hors de portée de cette technique.
Il y a eu de nombreuses tentatives pour résoudre ce problème. L’une des premières approches – lancée par Tavis Ormandy – est la distillation de corpus. La méthode s'appuie sur des signaux de couverture pour sélectionner un sous-ensemble de graines intéressantes à partir d'un corpus massif et de haute qualité de fichiers candidats, puis les fuzzer par des moyens traditionnels. L’approche fonctionne exceptionnellement bien, mais nécessite qu’un tel corpus soit facilement disponible. De plus, les mesures de couverture de blocs ne fournissent qu’une compréhension très simpliste de l’état du programme et sont moins utiles pour guider les efforts de fuzzing à long terme.
D'autres recherches, plus sophistiquées, se sont concentrées sur des techniques telles que l'analyse du flux de programme (« exécution concolique »), l'exécution symbolique ou l'analyse statique. Toutes ces méthodes sont extrêmement prometteuses dans des contextes expérimentaux, mais ont tendance à souffrir de problèmes de fiabilité et de performances dans les utilisations pratiques - et n'offrent actuellement pas d'alternative viable aux techniques de fuzzing « stupides ».
American Fuzzy Lop est un fuzzer à force brute couplé à un algorithme génétique extrêmement simple mais solide, guidé par des instruments. Il utilise une forme modifiée de couverture périphérique pour détecter sans effort les changements subtils à l'échelle locale dans le flux de contrôle du programme.
En simplifiant un peu, l’algorithme global peut être résumé comme suit :
Charger les cas de tests initiaux fournis par l'utilisateur dans la file d'attente,
Prenez le fichier d'entrée suivant de la file d'attente,
Essayez de réduire le scénario de test à la taille la plus petite qui ne modifie pas le comportement mesuré du programme,
Mutez le fichier à plusieurs reprises en utilisant une variété équilibrée et bien documentée de stratégies de fuzzing traditionnelles,
Si l’une des mutations générées a entraîné une nouvelle transition d’état enregistrée par l’instrumentation, ajoutez la sortie mutée en tant que nouvelle entrée dans la file d’attente.
Allez au 2.
Les cas de test découverts sont également périodiquement sélectionnés pour éliminer ceux qui ont été rendus obsolètes par des découvertes plus récentes et à couverture plus élevée ; et subir plusieurs autres étapes de minimisation des efforts basées sur l'instrumentation.
Résultat secondaire du processus de fuzzing, l'outil crée un petit corpus autonome de cas de test intéressants. Ceux-ci sont extrêmement utiles pour lancer d'autres régimes de tests exigeants en main-d'œuvre ou en ressources - par exemple, pour tester les navigateurs, les applications bureautiques, les suites graphiques ou les outils fermés.
Le fuzzer est minutieusement testé pour offrir des performances prêtes à l'emploi bien supérieures aux outils de fuzzing aveugle ou de couverture uniquement.
Lorsque le code source est disponible, l'instrumentation peut être injectée par un outil complémentaire qui remplace gcc ou clang dans tout processus de construction standard de code tiers.
L'instrumentation a un impact assez modeste sur les performances ; en conjonction avec d'autres optimisations mises en œuvre par afl-fuzz, la plupart des programmes peuvent être fuzzés aussi rapidement, voire plus rapidement, que possible avec les outils traditionnels.
La manière correcte de recompiler le programme cible peut varier en fonction des spécificités du processus de construction, mais une approche presque universelle serait la suivante :
$ CC=/path/to/afl/afl-gcc ./configure
$ make clean all
Pour les programmes C++, vous souhaiterez également définir CXX=/path/to/afl/afl-g++
.
Les wrappers clang (afl-clang et afl-clang++) peuvent être utilisés de la même manière ; Les utilisateurs de clang peuvent également choisir d'exploiter un mode d'instrumentation plus performant, comme décrit dans llvm_mode/README.llvm.
Lorsque vous testez des bibliothèques, vous devez trouver ou écrire un programme simple qui lit les données depuis stdin ou depuis un fichier et les transmet à la bibliothèque testée. Dans un tel cas, il est essentiel de lier cet exécutable à une version statique de la bibliothèque instrumentée, ou de s'assurer que le bon fichier .so est chargé au moment de l'exécution (généralement en définissant LD_LIBRARY_PATH
). L'option la plus simple est une construction statique, généralement possible via :
$ CC=/path/to/afl/afl-gcc ./configure --disable-shared
Définir AFL_HARDEN=1
lors de l'appel de 'make' amènera le wrapper CC à activer automatiquement les options de renforcement du code qui facilitent la détection de simples bogues de mémoire. Libdislocator, une bibliothèque d'assistance incluse avec AFL (voir libdislocator/README.dislocator) peut également aider à découvrir les problèmes de corruption du tas.
PS. Il est conseillé aux utilisateurs d'ASAN de consulter le fichier notes_for_asan.txt pour connaître les mises en garde importantes.
Lorsque le code source n'est PAS disponible, le fuzzer offre un support expérimental pour une instrumentation rapide et à la volée des binaires boîte noire. Ceci est accompli avec une version de QEMU fonctionnant dans le mode « émulation de l'espace utilisateur » moins connu.
QEMU est un projet distinct d'AFL, mais vous pouvez facilement créer la fonctionnalité en procédant :
$ cd qemu_mode
$ ./build_qemu_support.sh
Pour des instructions et des mises en garde supplémentaires, consultez qemu_mode/README.qemu.
Le mode est environ 2 à 5 fois plus lent que l'instrumentation au moment de la compilation, est moins propice à la parallélisation et peut présenter d'autres bizarreries.
Pour fonctionner correctement, le fuzzer nécessite un ou plusieurs fichiers de démarrage contenant un bon exemple des données d'entrée normalement attendues par l'application ciblée. Il y a deux règles de base :
Gardez les fichiers petits. Moins de 1 Ko est idéal, bien que ce ne soit pas strictement nécessaire. Pour savoir pourquoi la taille est importante, consultez perf_tips.txt.
Utilisez plusieurs scénarios de test uniquement s’ils sont fonctionnellement différents les uns des autres. Cela ne sert à rien d’utiliser cinquante photos de vacances différentes pour brouiller une bibliothèque d’images.
Vous pouvez trouver de nombreux bons exemples de fichiers de démarrage dans le sous-répertoire testcases/ fourni avec cet outil.
PS. Si un vaste corpus de données est disponible pour le filtrage, vous souhaiterez peut-être utiliser l'utilitaire afl-cmin pour identifier un sous-ensemble de fichiers fonctionnellement distincts qui exercent différents chemins de code dans le binaire cible.
Le processus de fuzzing lui-même est effectué par l'utilitaire afl-fuzz. Ce programme nécessite un répertoire en lecture seule avec les cas de test initiaux, un emplacement séparé pour stocker ses résultats, ainsi qu'un chemin d'accès au binaire à tester.
Pour les binaires cibles qui acceptent les entrées directement depuis stdin, la syntaxe habituelle est :
$ ./afl-fuzz -i testcase_dir -o findings_dir /path/to/program [...params...]
Pour les programmes qui acceptent les entrées d'un fichier, utilisez '@@' pour marquer l'emplacement dans la ligne de commande de la cible où le nom du fichier d'entrée doit être placé. Le fuzzer vous remplacera par ceci :
$ ./afl-fuzz -i testcase_dir -o findings_dir /path/to/program @@
Vous pouvez également utiliser l'option -f pour écrire les données mutées dans un fichier spécifique. Ceci est utile si le programme attend une extension de fichier particulière.
Les binaires non instrumentés peuvent être fuzzés en mode QEMU (ajoutez -Q dans la ligne de commande) ou en mode traditionnel de fuzzer aveugle (spécifiez -n).
Vous pouvez utiliser -t et -m pour remplacer le délai d'expiration et la limite de mémoire par défaut pour le processus exécuté ; de rares exemples de cibles pouvant nécessiter que ces paramètres soient modifiés incluent les compilateurs et les décodeurs vidéo.
Des conseils pour optimiser les performances de fuzzing sont abordés dans perf_tips.txt.
Notez qu'afl-fuzz commence par effectuer une série d'étapes de fuzzing déterministes, qui peuvent prendre plusieurs jours, mais ont tendance à produire des cas de test soignés. Si vous voulez des résultats rapides et sales tout de suite - comme zzuf et autres fuzzers traditionnels - ajoutez l'option -d à la ligne de commande.
Consultez le fichier status_screen.txt pour plus d'informations sur la façon d'interpréter les statistiques affichées et de surveiller l'état du processus. Assurez-vous de consulter ce fichier, surtout si des éléments de l'interface utilisateur sont surlignés en rouge.
Le processus de fuzzing se poursuivra jusqu'à ce que vous appuyiez sur Ctrl-C. Au minimum, vous souhaitez permettre au fuzzer de terminer un cycle de file d'attente, ce qui peut prendre de quelques heures à une semaine environ.
Il existe trois sous-répertoires créés dans le répertoire de sortie et mis à jour en temps réel :
queue/ - cas de test pour chaque chemin d'exécution distinctif, ainsi que tous les fichiers de démarrage fournis par l'utilisateur. Il s'agit du corpus synthétisé mentionné dans la section 2. Avant d'utiliser ce corpus à d'autres fins, vous pouvez le réduire à une taille plus petite à l'aide de l'outil afl-cmin. L'outil trouvera un sous-ensemble plus petit de fichiers offrant une couverture de bord équivalente.
crashes/ - cas de test uniques qui amènent le programme testé à recevoir un signal fatal (par exemple, SIGSEGV, SIGILL, SIGABRT). Les entrées sont regroupées par signal reçu.
hangs/ - cas de test uniques qui entraînent l'expiration du délai d'attente du programme testé. Le délai par défaut avant qu'un élément soit classé comme blocage est le plus grand entre 1 seconde et la valeur du paramètre -t. La valeur peut être affinée en définissant AFL_HANG_TMOUT, mais cela est rarement nécessaire.
Les plantages et les blocages sont considérés comme « uniques » si les chemins d'exécution associés impliquent des transitions d'état non observées dans les pannes précédemment enregistrées. Si un seul bug peut être atteint de plusieurs manières, il y aura une certaine inflation des comptes au début du processus, mais cela devrait rapidement diminuer.
Les noms de fichiers pour les plantages et les blocages sont corrélés aux entrées de file d'attente parentes et non fautives. Cela devrait aider au débogage.
Lorsque vous ne parvenez pas à reproduire un crash détecté par afl-fuzz, la cause la plus probable est que vous ne définissez pas la même limite de mémoire que celle utilisée par l'outil. Essayer:
$ LIMIT_MB=50
$ ( ulimit -Sv $[LIMIT_MB << 10] ; /path/to/tested_binary ... )
Modifiez LIMIT_MB pour qu'il corresponde au paramètre -m transmis à afl-fuzz. Sur OpenBSD, remplacez également -Sv par -Sd.
Tout répertoire de sortie existant peut également être utilisé pour reprendre des tâches interrompues ; essayer:
$ ./afl-fuzz -i- -o existing_output_dir [...etc...]
Si gnuplot est installé, vous pouvez également générer de jolis graphiques pour toute tâche de fuzzing active en utilisant afl-plot. Pour un exemple de ce à quoi cela ressemble, voir http://lcamtuf.coredump.cx/afl/plot/.
Chaque instance d'afl-fuzz occupe environ un cœur. Cela signifie que sur les systèmes multicœurs, la parallélisation est nécessaire pour utiliser pleinement le matériel. Pour obtenir des conseils sur la façon de fuzzer une cible commune sur plusieurs cœurs ou plusieurs machines en réseau, veuillez vous référer à parallel_fuzzing.txt.
Le mode de fuzzing parallèle offre également un moyen simple d'interfacer AFL à d'autres fuzzers, à des moteurs d'exécution symboliques ou concoliques, etc. ; encore une fois, consultez la dernière section de parallel_fuzzing.txt pour obtenir des conseils.
Par défaut, le moteur de mutation afl-fuzz est optimisé pour les formats de données compacts : par exemple, images, multimédia, données compressées, syntaxe d'expression régulière ou scripts shell. Il est un peu moins adapté aux langages au verbiage particulièrement verbeux et redondant, notamment HTML, SQL ou JavaScript.
Pour éviter les tracas liés à la création d'outils prenant en compte la syntaxe, afl-fuzz fournit un moyen d'amorcer le processus de fuzzing avec un dictionnaire facultatif de mots-clés de langage, d'en-têtes magiques ou d'autres jetons spéciaux associés au type de données ciblé - et de l'utiliser pour reconstruire la grammaire sous-jacente en déplacement :
http://lcamtuf.blogspot.com/2015/01/afl-fuzz-making-up-grammar-with.html
Pour utiliser cette fonctionnalité, vous devez d'abord créer un dictionnaire dans l'un des deux formats abordés dansdictionnaires/README.dictionaries ; puis pointez le fuzzer dessus via l'option -x dans la ligne de commande.
(Plusieurs dictionnaires courants sont également déjà fournis dans ce sous-répertoire.)
Il n'existe aucun moyen de fournir des descriptions plus structurées de la syntaxe sous-jacente, mais le fuzzer en comprendra probablement une partie en se basant uniquement sur les retours de l'instrumentation. Cela fonctionne réellement dans la pratique, disons :
http://lcamtuf.blogspot.com/2015/04/finding-bugs-in-sqlite-easy-way.html
PS. Même lorsqu'aucun dictionnaire explicite n'est fourni, afl-fuzz tentera d'extraire les jetons de syntaxe existants dans le corpus d'entrée en surveillant de très près l'instrumentation lors des retournements d'octets déterministes. Cela fonctionne pour certains types d'analyseurs et de grammaires, mais n'est pas aussi performant que le mode -x.
S'il est vraiment difficile de trouver un dictionnaire, une autre option consiste à laisser AFL fonctionner pendant un certain temps, puis à utiliser la bibliothèque de capture de jetons fournie en tant qu'utilitaire complémentaire à AFL. Pour cela, voir libtokencap/README.tokencap.
Le regroupement des accidents basé sur la couverture produit généralement un petit ensemble de données qui peut être rapidement trié manuellement ou avec un script GDB ou Valgrind très simple. Chaque crash est également traçable jusqu'à son scénario de test parent sans crash dans la file d'attente, ce qui facilite le diagnostic des pannes.
Cela dit, il est important de reconnaître que certains crashs de fuzzing peuvent être difficiles à évaluer rapidement en termes d'exploitabilité sans beaucoup de travail de débogage et d'analyse de code. Pour vous aider dans cette tâche, afl-fuzz prend en charge un mode « exploration de crash » tout à fait unique, activé avec l'option -C.
Dans ce mode, le fuzzer prend en entrée un ou plusieurs cas de test de plantage et utilise ses stratégies de fuzzing basées sur les commentaires pour énumérer très rapidement tous les chemins de code pouvant être atteints dans le programme tout en le maintenant dans l'état de plantage.
Les mutations qui n’entraînent pas de crash sont rejetées ; il en va de même pour toutes les modifications qui n’affectent pas le chemin d’exécution.
Le résultat est un petit corpus de fichiers qui peut être examiné très rapidement pour voir quel degré de contrôle l'attaquant a sur l'adresse défaillante, ou s'il est possible de dépasser une lecture initiale hors limites - et voir ce qui se cache en dessous. .
Oh, encore une chose : pour la minimisation des cas de test, essayez afl-tmin. L'outil peut être utilisé de manière très simple :
$ ./afl-tmin -i test_case -o minimized_result -- /path/to/program [...]
L’outil fonctionne avec des cas de test avec et sans crash. En mode crash, il acceptera volontiers les binaires instrumentés et non instrumentés. En mode sans crash, le minimiseur s'appuie sur l'instrumentation AFL standard pour simplifier le fichier sans modifier le chemin d'exécution.
Le minimiseur accepte les syntaxes -m, -t, -f et @@ d'une manière compatible avec afl-fuzz.
Un autre ajout récent à l'AFL est l'outil afl-analyze. Il prend un fichier d'entrée, tente d'inverser séquentiellement les octets et observe le comportement du programme testé. Il code ensuite par couleur l'entrée en fonction des sections qui semblent critiques et de celles qui ne le sont pas ; bien qu'il ne soit pas à l'épreuve des balles, il peut souvent offrir un aperçu rapide des formats de fichiers complexes. Plus d'informations sur son fonctionnement peuvent être trouvées vers la fin du fichier technique_details.txt.
Le fuzzing est également une technique merveilleuse et sous-utilisée pour découvrir des erreurs de conception et d’implémentation qui ne plantent pas. De nombreux bogues intéressants ont été découverts en modifiant les programmes cibles pour appeler abort() lorsque, par exemple :
Deux bibliothèques bignum produisent des sorties différentes lorsqu'elles reçoivent la même entrée générée par le fuzzer,
Une bibliothèque d'images produit différentes sorties lorsqu'on lui demande de décoder la même image d'entrée plusieurs fois de suite,
Une bibliothèque de sérialisation/désérialisation ne parvient pas à produire des sorties stables lors de la sérialisation et de la désérialisation itératives des données fournies par le fuzzer,
Une bibliothèque de compression produit une sortie incompatible avec le fichier d’entrée lorsqu’on lui demande de compresser puis de décompresser un blob particulier.
La mise en œuvre de ces contrôles ou d’autres contrôles similaires prend généralement très peu de temps ; si vous êtes le responsable d'un paquet particulier, vous pouvez rendre ce code conditionnel avec #ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
(un indicateur également partagé avec libfuzzer) ou #ifdef __AFL_COMPILER
(celui-ci est uniquement pour AFL).
Veuillez garder à l'esprit que, comme pour de nombreuses autres tâches gourmandes en calcul, le fuzzing peut mettre à rude épreuve votre matériel et votre système d'exploitation. En particulier:
Votre processeur chauffera et aura besoin d’un refroidissement adéquat. Dans la plupart des cas, si le refroidissement est insuffisant ou cesse de fonctionner correctement, la vitesse du processeur sera automatiquement limitée. Cela dit, surtout lors du fuzz sur du matériel moins adapté (ordinateurs portables, smartphones, etc.), il n'est pas totalement impossible que quelque chose explose.
Les programmes ciblés peuvent finir par récupérer de manière erratique des gigaoctets de mémoire ou remplir l'espace disque avec des fichiers indésirables. AFL essaie de faire respecter les limites de mémoire de base, mais ne peut pas empêcher tous les incidents possibles. En fin de compte, vous ne devriez pas vous tromper sur les systèmes où la perspective de perte de données ne constitue pas un risque acceptable.
Le fuzzing implique des milliards de lectures et d’écritures sur le système de fichiers. Sur les systèmes modernes, cela sera généralement fortement mis en cache, ce qui entraînera des E/S « physiques » assez modestes – mais de nombreux facteurs peuvent modifier cette équation. Il est de votre responsabilité de surveiller les problèmes potentiels ; avec des E/S très lourdes, la durée de vie de nombreux disques durs et SSD peut être réduite.
Un bon moyen de surveiller les E/S disque sous Linux est la commande « iostat » :
$ iostat -d 3 -x -k [...optional disk ID...]
Voici quelques-unes des mises en garde les plus importantes concernant l’AFL :
AFL détecte les défauts en vérifiant si le premier processus généré meurt à cause d'un signal (SIGSEGV, SIGABRT, etc.). Les programmes qui installent des gestionnaires personnalisés pour ces signaux peuvent avoir besoin que le code correspondant soit commenté. Dans la même veine, les erreurs dans l'enfant traité générées par la cible fuzzée peuvent échapper à la détection à moins que vous n'ajoutiez manuellement du code pour les détecter.
Comme tout autre outil de force brute, le fuzzer offre une couverture limitée si le cryptage, les sommes de contrôle, les signatures cryptographiques ou la compression sont utilisés pour envelopper entièrement le format de données réel à tester.
Pour contourner ce problème, vous pouvez commenter les vérifications pertinentes (voir experimental/libpng_no_checksum/ pour vous inspirer) ; si cela n'est pas possible, vous pouvez également écrire un post-processeur, comme expliqué dans experimental/post_library/.
Il existe des compromis malheureux entre ASAN et les binaires 64 bits. Cela n'est pas dû à un défaut spécifique d'afl-fuzz ; voir notes_for_asan.txt pour des conseils.
Il n'existe pas de prise en charge directe des services réseau fuzzing, des démons d'arrière-plan ou des applications interactives qui nécessitent une interaction avec l'interface utilisateur pour fonctionner. Vous devrez peut-être apporter de simples modifications au code pour qu’ils se comportent de manière plus traditionnelle. Preeny peut également proposer une option relativement simple - voir : https://github.com/zardus/preeny
Quelques conseils utiles pour modifier les services basés sur le réseau sont également disponibles sur : https://www.fastly.com/blog/how-to-fuzz-server-american-fuzzy-lop
L'AFL ne génère pas de données de couverture lisibles par l'homme. Si vous souhaitez surveiller la couverture, utilisez afl-cov de Michael Rash : https://github.com/mrash/afl-cov
Parfois, des machines sensibles s’élèvent contre leurs créateurs. Si cela vous arrive, veuillez consulter http://lcamtuf.coredump.cx/prep/.
Au-delà de cela, voir INSTALL pour des conseils spécifiques à la plate-forme.
La plupart des améliorations apportées à afl-fuzz ne seraient pas possibles sans les commentaires, les rapports de bogues ou les correctifs de :
Jann Horn Hanno Boeck
Felix Groebert Jakub Wilk
Richard W. M. Jones Alexander Cherepanov
Tom Ritter Hovik Manucharyan
Sebastian Roschke Eberhard Mattes
Padraig Brady Ben Laurie
@dronesec Luca Barbato
Tobias Ospelt Thomas Jarosch
Martin Carpenter Mudge Zatko
Joe Zbiciak Ryan Govostes
Michael Rash William Robinet
Jonathan Gray Filipe Cabecinhas
Nico Weber Jodie Cunningham
Andrew Griffiths Parker Thompson
Jonathan Neuschfer Tyler Nighswander
Ben Nagy Samir Aguiar
Aidan Thornton Aleksandar Nikolich
Sam Hakim Laszlo Szekeres
David A. Wheeler Turo Lamminen
Andreas Stieger Richard Godbee
Louis Dassy teor2345
Alex Moneger Dmitry Vyukov
Keegan McAllister Kostya Serebryany
Richo Healey Martijn Bogaard
rc0r Jonathan Foote
Christian Holler Dominique Pelle
Jacek Wielemborek Leo Barnes
Jeremy Barnes Jeff Trull
Guillaume Endignoux ilovezfs
Daniel Godas-Lopez Franjo Ivancic
Austin Seipp Daniel Komaromy
Daniel Binderman Jonathan Metzman
Vegard Nossum Jan Kneschke
Kurt Roeckx Marcel Bohme
Van-Thuan Pham Abhik Roychoudhury
Joshua J. Drake Toby Hutton
Rene Freingruber Sergey Davidoff
Sami Liedes Craig Young
Andrzej Jackowski Daniel Hodson
Merci!
Des questions ? Des inquiétudes ? Des rapports de bugs ? Veuillez utiliser GitHub.
Il existe également une liste de diffusion pour le projet ; pour vous inscrire, envoyez un e-mail à [email protected]. Ou, si vous préférez parcourir les archives en premier, essayez : https://groups.google.com/group/afl-users.