Le jour où je suis tombé amoureux du fuzzing

En 2007, j'ai écrit quelques outils de modding pour le simulateur d'espace Freelancer . Les ressources du jeu sont stockées au format «binaire INI» ou «BINI». Le format binaire a probablement été choisi pour des raisons de performances: ces fichiers sont plus rapides à charger et à lire qu'un texte arbitraire au format INI.

La plupart du contenu du jeu peut être modifié directement à partir de ces fichiers, en changeant les noms, les prix des produits, les statistiques des vaisseaux spatiaux ou même en ajoutant de nouveaux navires. Les fichiers binaires sont difficiles à modifier directement, donc l'approche naturelle est de les convertir en texte INI, d'apporter des modifications dans un éditeur de texte, puis de les reconvertir au format BINI et de remplacer les fichiers dans le répertoire du jeu.

Je n'ai pas analysé le format BINI, et je ne suis pas le premier à apprendre à les éditer. Mais je n'aimais pas les outils existants et j'avais ma propre vision de la façon dont ils devraient fonctionner. Je préfère une interface de style Unix, bien que le jeu lui-même fonctionne sous Windows.

A cette époque, je venais juste de me familiariser avec les outils yacc (en fait Bison ) et lex (en fait flex ), ainsi qu'avec Autoconf, donc je les ai utilisés exactement. C'était intéressant d'essayer ces utilitaires dans la pratique, bien que j'imite servilement d'autres projets open source, ne comprenant pas pourquoi tout a été fait de cette façon, en aucune autre manière. En raison de l'utilisation de yacc / lex et de la création de scripts de configuration, un système complet de type Unix était requis. Tout cela est visible dans la version originale des programmes .

Le projet s'est avéré être un succès: j'ai moi-même utilisé avec succès ces outils, et ils sont apparus dans différentes collections pour le modding Freelancer.

Refactoring


Mi-2018, je suis revenu sur ce projet. Avez-vous déjà regardé votre ancien code avec la pensée: qu'avez-vous même pensé? Mon format INI s'est avéré être beaucoup plus rigide et strict que nécessaire, les binaires ont été enregistrés de manière douteuse et l'assemblage n'a même pas fonctionné normalement.

Grâce à dix ans d'expérience supplémentaire, je savais avec certitude que j'écrirais beaucoup mieux ces outils maintenant. Et je l'ai fait en quelques jours, en les réécrivant à partir de zéro. Ce nouveau code est maintenant dans le thread principal sur Github.

J'aime rendre tout aussi simple que possible , alors je me suis débarrassé de l'autoconf au profit d'un Makefile plus simple et plus portable . Plus de yacc ou de lex, mais l'analyseur est écrit à la main. Seul le C portable approprié est utilisé. Le résultat est si simple que j'assemble le projet avec une courte commande de Visual Studio , donc le Makefile n'est pas vraiment nécessaire. Si vous remplacez stdint.h par typedef , vous pouvez même créer et exécuter binitools sous DOS .

La nouvelle version est plus rapide, plus compacte, plus propre et plus facile. Il est beaucoup plus flexible en ce qui concerne l'entrée INI, il est donc plus facile à utiliser. Mais est-ce vraiment correct?

Fuzzing


Je m'intéresse au fuzz depuis de nombreuses années, en particulier l' af (american fuzzy lop). Mais il ne l'a jamais maîtrisé, bien qu'il ait testé certains des outils que j'utilise régulièrement. Mais le fuzzing n'a rien trouvé de remarquable, du moins avant que j'abandonne. J'ai testé ma bibliothèque JSON et pour une raison quelconque, je n'ai rien trouvé non plus. Il est clair que mon analyseur JSON ne pouvait pas être aussi fiable, non? Mais le flou ne montre rien. (Il s'est avéré que ma bibliothèque JSON est assez fiable, grâce en grande partie aux efforts de la communauté!)

Mais maintenant, j'ai un analyseur INI relativement nouveau. Bien qu'il puisse analyser et assembler correctement l'ensemble original de fichiers BINI dans le jeu, sa fonctionnalité n'a pas vraiment été testée. Ici, le fuzzing trouvera sûrement quelque chose. De plus, vous n'avez pas besoin d'écrire une seule ligne pour exécuter afl sur ce code. Les outils par défaut fonctionnent avec une entrée standard, ce qui est idéal.

En supposant que vous ayez installé les outils nécessaires (make, gcc, afl), voici comment le fuzzing binitools démarre facilement:

 $ make CC=afl-gcc $ mkdir in out $ echo '[x]' > in/empty $ afl-fuzz -i in -o out -- ./bini 

L'utilitaire bini accepte INI en entrée et émet BINI, il est donc beaucoup plus intéressant de le vérifier que la procédure unbini inverse. Comme unbini analyse des données binaires relativement simples, le (probablement) fuzzer n'a rien à rechercher. Cependant, juste au cas où, je l'ai quand même vérifié.



Dans cet exemple, j'ai changé le compilateur par défaut en shell GCC pour afl ( CC=afl-gcc ). Ici, afl appelle GCC en arrière-plan, mais il ajoute sa propre boîte à outils au binaire. Lors du fuzzing, afl-fuzz utilise cette boîte à outils pour surveiller le chemin d'exécution d'un programme. La documentation afl explique les détails techniques.

J'ai également créé les répertoires d'entrée et de sortie en mettant dans le répertoire d'entrée un exemple de travail minimal qui donne à afl un point de départ. Lorsqu'il démarre, il mute la file d'attente de données d'entrée et surveille les modifications pendant l'exécution du programme. Le répertoire de sortie contient les résultats et, plus important encore, le corps des données d'entrée qui provoquent des chemins d'exécution uniques. En d'autres termes, de nombreuses entrées sont traitées à la sortie floue, vérifiant de nombreux scénarios de bordure différents.

Le résultat le plus intéressant et le plus effrayant est un plantage complet du programme. Lorsque j'ai démarré pour la première fois le fuzzer pour binitools, bini montré de nombreux plantages de ce type. En quelques minutes, afl a découvert un certain nombre d'erreurs subtiles et intéressantes dans mon programme, ce qui était incroyablement utile. Fazzer a même trouvé un bug improbable d'un pointeur obsolète , vérifiant l'ordre différent des différentes allocations de mémoire. Ce bug particulier a été un tournant qui m'a fait prendre conscience de la valeur du fuzzing.

Toutes les erreurs trouvées n'ont pas conduit à des échecs. J'ai également étudié la sortie et regardé quelle entrée a donné un résultat positif et qui ne l'a pas fait, et j'ai regardé comment le programme a géré divers cas extrêmes. Elle a rejeté certaines entrées que je pensais qu'elle traiterait. Et vice versa, elle a traité certaines données que je considérais incorrectes et a interprété certaines données de manière inattendue pour moi. Donc, même après avoir corrigé des bogues avec des plantages de programmes, j'ai toujours changé les paramètres de l'analyseur pour corriger chacun de ces cas désagréables.

Créer une suite de tests


Dès que j'ai corrigé toutes les erreurs détectées par le fuzzer et ajusté l'analyseur dans toutes les situations de frontière, j'ai fait un ensemble de tests à partir du paquet de données du fuzzer - mais pas directement.

Tout d'abord, j'ai exécuté le fuzzer en parallèle - ce processus est expliqué dans la documentation afl - j'ai donc eu beaucoup d'entrées redondantes. Par redondance, je veux dire que l'entrée est différente mais a le même chemin d'exécution. Heureusement, afl dispose d'un outil pour y faire face: afl-cmin , un outil pour minimiser le shell. Il élimine les entrées inutiles.

Deuxièmement, beaucoup de ces entrées étaient plus longues que nécessaire pour invoquer leur chemin d'exécution unique. afl-tmin , un minimiseur de cas de test qui a réduit le cas de test, a aidé afl-tmin .

J'ai séparé les entrées valides et invalides - et les ai vérifiées dans le référentiel. Jetez un œil à toutes ces entrées stupides inventées par le fuzzer basé sur une seule entrée minimale:


En fait, ici, l'analyseur est figé dans un état, et un ensemble de tests garantit qu'un build particulier se comporte d'une manière très spécifique. Cela est particulièrement utile pour garantir que les assemblages créés par d'autres compilateurs sur d'autres plates-formes se comportent de la même manière en ce qui concerne leur sortie. Ma suite de tests a même détecté une erreur dans la bibliothèque dietlibc car binitools n'a pas réussi les tests après y avoir établi un lien. Si vous deviez apporter des modifications non triviales à l'analyseur syntaxique, alors vous devriez essentiellement abandonner l'ensemble actuel de tests et recommencer à nouveau pour que afl génère un tout nouveau corps pour le nouvel analyseur.

Bien sûr, le fuzzing s'est imposé comme une technique puissante. Il a trouvé un certain nombre d'erreurs que je n'aurais jamais pu découvrir par moi-même. Depuis lors, j'ai commencé à l'utiliser avec plus de compétence pour tester d'autres programmes - pas seulement le mien - et j'ai trouvé de nombreux nouveaux bugs. Maintenant, Fuzzer a pris une place permanente parmi les outils de mon kit de développement.

Source: https://habr.com/ru/post/fr438662/


All Articles