Le fuzzing est depuis longtemps utilisé pour tester la robustesse des systèmes. On les soumet pendant de longues périodes à l'assaut de requêtes aléatoires pour évaluer leur résistance. On peut ainsi évaluer une probabilité d'erreur dans le système en fonction de la couverture estimée du test. C'est relativement simple et efficace... Tant qu'on n'essaie pas d'identifier la cause d'un plantage donné... Or c'est précisément le but de la recherche de faille.

Constater le matin au réveil que le système cible est mort suite au test de la nuit passée ne vous avancera pas à grand chose. Vous avez en effet besoin de savoir quelle requête, ou enchaînement de requêtes, a précisément entraîné le crash constaté. Il est donc nécessaire de tracer très précisément tout ce qui a été envoyé. Condition nécessaire, mais certainement pas suffisante : si vous avez généré cinquante gigaoctets de trafic, trouver votre bonheur dans la capture relève de la gageure. Chercher une aiguille dans une meule de foin n'est pas vraiment la meilleure façon de gagner du temps ou d'économiser votre énergie. Vous devez pouvoir associer l'émission des stimuli à l'observation de la cible pour d'une part détecter les problèmes quand ils apparaissent et d'autre part en identifier les causes probables automatiquement, ce qui est moins trivial qu'il n'y parait. Les problèmes déclenchés peuvent en effet se manifester différemment : selon le type de d'erreur que vous atteignez par exemple, ou encore le type de fonctionnalité que vous testez. Il ne faut pas être sorti de la cuisse de Jupiter pour convenir qu'une erreur dans un traitement de texte ne se manifestera pas de la même manière qu'une erreur dans un driver réseau, et on conçoit aisément qu'un travail préparatoire soit nécessaire pour identifier les évènements importants et se donner les moyens de les détecter.

Une fois l'erreur détectée et sa cause isolée, il ne vous reste plus qu'à vous lancer dans l'analyse de la situation. Et pour cela, vous avez besoin d'un maximum d'informations sur le système au moment du crash. Naïvement, on se dit qu'il suffit de l'observer, puisse qu'il est planté, là, juste sous vos yeux. Certes. Mais il serait quand même bien plus intéressant d'avoir une liste de tous les plantages à l'issu du fuzzing, plutôt que d'avoir à relancer le test après chaque erreur. Facile, il suffit de réinitialiser la cible et relancer automatiquement le processus ! Sauf qu'en réinitialisation, on perd de précieuses informations. Damned. Il va donc falloir les extraire et les garder au chaud pour la suite. On pourrait certes les obtenir en reproduisant le problème constaté, mais autant en avoir le maximum sous le coude quand on se lance dans l'analyse.

La génération des stimuli n'est pas non plus triviale. On ne génére pas son trafic n'importe comment non plus. Forcément. Voulant obtenir une couverture maximale, c'est à dire l'envoi de tous les cas possibles, on va naturellement se tourner vers des approches systématiques faisant varier les paramètres séquentiellement plutôt que vers une approche aléatoire susceptible de rejouer du trafic inutilement. Est-ce pour autant suffisant ? Non. D'abord parce qu'il est probable que vous ne disposiez pas du temps nécessaire pour tester toutes les possibilités. Il va donc falloir choisir en faisant appel à votre savoir et votre expérience. En outre, cette approche ne tient pas compte de l'état de la cible lorsqu'elle reçoit le stimuli. Prenons l'exemple tout bête d'une connexion TCP. Si vous recevez un SYN/ACK sur un port fermé, rien ne se passe. Si vous recevez le même paquet sur un port ouvert, vous émettrez un RST/ACK. Selon son état, qui dépend en outre des stimuli précédemment reçus, la cible ne réagit donc pas de la même manière. La génération de vos paquets dépend donc du diagramme d'état de la fonction que vous désirez tester, si vous voulez le faire bien.

Cette dernière observation n'est pas des moindres. L'expérience montrent d'ailleurs que la plupart des résultats obtenus par fuzzing le sont sur des points où on n'a pas besoin de tenir compte de l'état de la cible :

  • le chargement puis le déchargement d'un système de fichier ramène la cible dans le même état (sauf si ça plante, forcément) ;
  • le lancement d'une application sur un fichier donné puis sa fermeture permet de revenir également à au même état initial ;
  • quand aux drivers Wi-Fi, les publications portent sur la gestion des probes dans l'état par défaut d'un driver actif.

Autant de cas où la cible est dans son état initial, et où un test n'entrainant pas d'erreur la ramène dans ce même état. Si on veut tester d'autres états, il est nécessaire de construire non plus des stimuli individuels, mais des séquences de stimuli qui respectent le diagramme d'état du composant ciblé. On imagine aisément que cela prend nettement plus de temps aussi bien en préparation qu'en exécution. Un protocole compliqué, genre IPSEC ou SNMP, va se révéler plus hardu à tester, sans parler des difficultés liée à l'implémentation du fuzzer lui-même. Ironiquement, ce sont ces protocoles qui produisent des implémentations les plus buggées... À tel point que des tests, même surfaciques, remontent pas mal de matière !


On voit donc bien que rien que dans sa mise en œuvre, le fuzzing présente des difficultés certaines qui ne le rendent pas applicable à toutes les situations aussi facilement qu'on veut bien le croire. Certains cas s'y prêtent à merveille, d'autres moins, voire pas du tout. Et comme beaucoup d'autres techniques, la qualité de ses résultats dépend énormément des compétences de la personne qui exécute le test, tout comme leur interprétation. Ce n'est décidemment pas encore cette fois qu'on rendra les kiddies intelligents.