Un fuzzer guidé par la couverture de code pour les interpréteurs de langages dynamiques basé sur un langage intermédiaire personnalisé ("FuzzIL") qui peut être muté et traduit en JavaScript.
Utilisation
Les étapes de base pour utiliser ce fuzzer sont :
Télécharger le code source de l'un des moteurs JavaScript pris en charge. Voir le répertoire Targets/ pour la liste des moteurs JavaScript supportés.
Appliquer les correctifs correspondants depuis le répertoire de la cible. Consultez également le README.md dans ce répertoire.
Compiler le moteur avec l'instrumentation de couverture (nécessite clang >= 4.0) comme décrit dans le README.
Compiler le fuzzer : swift build [-c release].
Exécuter le fuzzer : swift run [-c release] FuzzilliCli --profile=<profile> [autres options cli] /chemin/vers/jsshell. Voir aussi swift run FuzzilliCli --help.
La construction et l'exécution de Fuzzilli et des moteurs JavaScript pris en charge dans Docker et sur Google Compute Engine sont également supportées.
Développement
Consultez main.swift pour voir un exemple d'utilisation de la bibliothèque Fuzzilli et jouer avec les différentes options de configuration. Ensuite, jetez un œil à Fuzzer.swift pour la logique de fuzzing de haut niveau. De là, plongez dans toute partie qui vous semble intéressante.
Il serait grandement apprécié que vous envoyiez une courte note (incluant éventuellement un numéro CVE) à [email protected] ou ouvriez une pull request pour toute vulnérabilité découverte avec l'aide de ce projet afin qu'elle puisse être incluse dans la section démonstration de bogues. Autrement, vous pouvez bien sûr réclamer toute prime de bogue, crédits CVE, etc. pour les vulnérabilités :)
Concept
Lors du fuzzing pour des bogues de cœur d'interpréteur, par exemple dans les compilateurs JIT, la correction sémantique des programmes générés devient une préoccupation. Cela contraste avec la plupart des autres scénarios, par exemple le fuzzing des API d'exécution, où la correction sémantique peut facilement être contournée en enveloppant le code généré dans des constructions try-catch. Il existe différentes possibilités pour atteindre un taux acceptable d'échantillons sémantiquement corrects, dont l'une est une approche mutationnelle où tous les échantillons du corpus sont également sémantiquement valides. Dans ce cas, chaque mutation a seulement une faible chance de transformer un échantillon valide en un échantillon invalide.
Pour implémenter un fuzzer JavaScript basé sur les mutations, il faut définir des mutations du code JavaScript. Au lieu de muter l'AST ou d'autres éléments syntaxiques d'un programme, un langage intermédiaire personnalisé (IL) est défini sur lequel les mutations du flux de contrôle et de données d'un programme peuvent être effectuées plus directement. Cet IL est ensuite traduit en JavaScript pour exécution. Le langage intermédiaire ressemble approximativement à ceci :
Un programme FuzzIL est simplement une liste d'instructions.
Une instruction FuzzIL est une opération avec des variables d'entrée et de sortie et potentiellement un ou plusieurs paramètres (entre guillemets simples dans la notation ci-dessus).
Les entrées des instructions sont toujours des variables, il n'y a pas de valeurs immédiates.
Chaque sortie d'une instruction est une nouvelle variable, et les variables existantes ne peuvent être réaffectées que par des opérations dédiées telles que l'instruction Reassign.
Chaque variable est définie avant d'être utilisée.
Un certain nombre de mutations peuvent ensuite être effectuées sur ces programmes :
InputMutator : remplace les variables d'entrée des instructions par des différentes pour muter le flux de données du programme.
CodeGenMutator : génère du code et l'insère quelque part dans le programme muté. Le code est généré soit en exécutant un générateur de code, soit en copiant certaines instructions d'un autre programme du corpus (épissage).
CombineMutator : insère un programme du corpus dans une position aléatoire dans le programme muté.
OperationMutator : mute les paramètres des opérations, par exemple en remplaçant une constante entière par une autre.
et plus encore...
Une discussion beaucoup plus approfondie sur le fonctionnement de Fuzzilli peut être trouvée ici.
Implémentation
Le fuzzer est implémenté en Swift, avec certaines parties (par exemple les mesures de couverture, les interactions socket, etc.) implémentées en C.
Architecture
Une instance de fuzzer (implémentée dans Fuzzer.swift) est composée des composants centraux suivants :
MutationFuzzer : produit de nouveaux programmes à partir de programmes existants en appliquant des mutations. Ensuite, exécute les échantillons produits et les évalue.
ScriptRunner : exécute les programmes du langage cible.
Corpus : stocke les échantillons intéressants et les fournit au fuzzer principal.
Environment : a connaissance de l'environnement d'exécution, par exemple les builtins disponibles, les noms de propriétés et les méthodes.
Minimizer : minimise les programmes crashants et intéressants.
Evaluator : évalue si un échantillon est intéressant selon une certaine métrique, par exemple la couverture de code.
Lifter : traduit un programme FuzzIL vers le langage cible (JavaScript).
De plus, un certain nombre de modules sont optionnellement disponibles :
Statistics : rassemble diverses informations statistiques.
NetworkSync : synchronise plusieurs instances sur le réseau.
ThreadSync : synchronise plusieurs instances au sein du même processus.
Storage : stocke les programmes crashants sur le disque.
Le fuzzer est piloté par événements, la plupart des interactions entre différentes classes se produisant via des événements. Les événements sont déclenchés par exemple suite à un crash ou à la découverte d'un programme intéressant, à l'exécution d'un nouveau programme, à la génération d'un message de log, etc. Voir Events.swift pour la liste complète des événements. Le mécanisme d'événements découple efficacement les différents composants du fuzzer et facilite l'implémentation de modules supplémentaires.
Un programme FuzzIL peut être construit à l'aide d'une instance ProgramBuilder. Un ProgramBuilder fournit des méthodes pour créer et ajouter de nouvelles instructions, ajouter des instructions d'un autre programme, récupérer des variables existantes, interroger le contexte d'exécution à la position actuelle (par exemple si elle se trouve à l'intérieur d'une boucle), et plus encore.
Exécution
Fuzzilli utilise un mode d'exécution personnalisé appelé REPRL (read-eval-print-reset-loop). Pour cela, le moteur cible est modifié pour accepter une entrée de script via des pipes et/ou une mémoire partagée, l'exécuter, puis réinitialiser son état interne et attendre le prochain script. Cela supprime le surcoût lié à la création de processus et en grande partie à l'initialisation du moteur.
Scalabilité
Il y a une instance Fuzzer par processus cible. Cela permet une exécution synchrone des programmes et simplifie ainsi l'implémentation de divers algorithmes tels que les mutations consécutives et la minimisation. De plus, cela évite de devoir implémenter un accès thread-safe à l'état interne, par exemple le corpus. Chaque instance de fuzzer a sa propre DispatchQueue, correspondant conceptuellement à un seul thread. En règle générale, toute interaction avec une instance Fuzzer doit se produire sur la file d'attente de répartition de cette instance. Cela garantit la sécurité des threads car la file d'attente est série. Pour plus de détails, voir la documentation.
Pour passer à l'échelle, les instances de fuzzer peuvent former une hiérarchie arborescente, dans laquelle elles signalent les nouveaux échantillons intéressants et les crashs à leur nœud parent. En retour, un nœud parent synchronise son corpus avec ses nœuds enfants. La communication entre les nœuds de l'arbre peut se faire de différentes manières, chacune implémentée comme un module :
Communication inter-thread : synchronise les instances dans le même processus en mettant des tâches en file d'attente dans la DispatchQueue de l'autre fuzzer.
Cette conception permet au fuzzer de passer à l'échelle vers plusieurs cœurs sur une seule machine ainsi que vers de nombreuses machines différentes. Comme un nœud parent peut rapidement devenir surchargé si trop d'instances lui envoient des programmes, il est possible de configurer plusieurs niveaux d'instances, par exemple une instance racine, 16 nœuds intermédiaires connectés à la racine, et 256 "feuilles" connectées aux nœuds intermédiaires. Voir le répertoire Cloud/ pour plus d'informations sur le fuzzing distribué.
Ressources
Ressources supplémentaires sur ce fuzzer :
Une présentation sur Fuzzilli donnée à l'Offensive Con 2019.
Le mémoire de master pour lequel l'implémentation initiale a été réalisée.
Un article de blog par Sensepost sur l'utilisation de Fuzzilli pour trouver un bogue dans V8.
Un article de blog par Doyensec sur le fuzzing du moteur JerryScript avec Fuzzilli.
Un article du NDSS Symposium 2023 sur Fuzzilli et sa comparaison avec d'autres fuzzers.
Démonstration de bogues
La liste suivante contient certains des bogues découverts avec l'aide de Fuzzilli. Seuls les bogues ayant un impact sur la sécurité et présents dans au moins une version bêta du logiciel concerné doivent être inclus dans cette liste. Étant donné que Fuzzilli est souvent utilisé pour des tests de fuzzing continus pendant le développement, de nombreux problèmes découverts par lui ne sont pas inclus dans cette liste car ils sont généralement trouvés avant que le code vulnérable n'atteigne une version bêta. Une liste de tous les problèmes récemment trouvés par Fuzzilli dans V8 peut cependant être trouvée ici.
Remerciements particuliers à tous les utilisateurs de Fuzzilli qui ont signalé des bogues découverts par celui-ci !
WebKit/JavaScriptCore
Issue 185328 : Le compilateur DFG utilise un registre de sortie incorrect pour l'opération NumberIsInteger
CVE-2018-4299 : performProxyCall fuit un objet interne vers le script
CVE-2018-4359 : compileMathIC produit un code machine incorrect
CVE-2019-8518 : Accès hors limites dans le JIT FTL dû à LICM déplaçant un accès à un tableau avant la vérification des limites
CVE-2019-8558 : Use-after-free de CodeBlock dû à des Watchpoints pendant
CVE-2019-8611 : L'optimisation AIR supprime incorrectement l'affectation à un registre
CVE-2019-8623 : Le mouvement de code invariant de boucle (LICM) dans le JIT DFG laisse une variable de pile non initialisée
CVE-2019-8622 : doesGC() du DFG est incorrect concernant le comportement de l'opération HasIndexedProperty sur les StringObjects
CVE-2019-8671 : DFG : Le mouvement de code invariant de boucle (LICM) laisse un accès à une propriété d'objet non gardé
CVE-2019-8672 : Use-after-free de JSValue dans les ValueProfiles
CVE-2019-8678 : JSC échoue à exécuter haveABadTime() lorsque certains prototypes sont modifiés, conduisant à des confusions de type
CVE-2019-8685 : JSPropertyNameEnumerator utilise des ID de structure incorrects
CVE-2019-8765 : Confusion de type GetterSetter lors de la compilation DFG
CVE-2019-8820 : Confusion de type lors du bailout lors de la reconstruction des objets arguments
CVE-2019-8844 : ObjectAllocationSinkingPhase ne devrait pas insérer de hints pour les allocations qui ne sont plus valides
CVE-2020-3901 : Confusion de type GetterSetter dans le code JIT FTL (dû à un LICM pas toujours sûr)
CVE-2021-30851 : Verrou manquant lors d'une recherche simultanée dans une HashTable
CVE-2021-30818 : Confusion de type lors de la reconstruction des arguments sur la sortie OSR du DFG
CVE-2022-46696 : Échec d'assertion dû à une vérification d'exception manquante dans le code compilé JIT
CVE-2022-46699 : Échec d'assertion dû à une mise en cache incorrecte de propriétés spéciales dans les ICs
CVE-2022-46700 : Intl.Locale.prototype.hourCycles fuit un JSValue vide vers le script
CVE-2025-43214 : Corruption mémoire lors de JSToWasmEntry lors de l'itération sur la pile
CVE-2025-43213 : Typage invalide de l'opération NewRegExpUntyped
Gecko/Spidermonkey
CVE-2018-12386 : Bogue d'allocation de registres dans IonMonkey conduit à des confusions de type
CVE-2019-9791 : L'inférence de type d'IonMonkey est incorrecte pour les constructeurs entrés via OSR
CVE-2019-9792 : IonMonkey fuit la valeur magique JS_OPTIMIZED_OUT vers le script
CVE-2019-9816 : ObjectGroup inattendu dans l'opération ObjectGroupDispatch
CVE-2019-9813 : Le code compilé par IonMonkey ne met pas à jour les types de propriétés inférés, conduisant à des confusions de type
CVE-2019-11707 : IonMonkey prédit incorrectement le type de retour de Array.prototype.pop, conduisant à des confusions de type
CVE-2020-15656 : Confusion de type pour les arguments spéciaux dans IonMonkey