Fuzzilli
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.
Les correctifs, ajouts, autres contributions, etc. à ce projet sont très bienvenus ! Cependant, veuillez rapidement consulter les notes pour les contributeurs. Fuzzilli suit approximativement le guide de style de code de Google pour Swift.
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 :
v0 <− LoadInteger '0'
v1 <− LoadInteger '10'
v2 <− LoadInteger '1'
v3 <− LoadInteger '0'
BeginFor v0, '<', v1, '+', v2 −> v4
v6 <− BinaryOperation v3, '+', v4
Reassign v3, v6
EndFor
v7 <− LoadString 'Result: '
v8 <− BinaryOperation v7, '+', v3
v9 <− LoadGlobal 'console'
v10 <− CallMethod v9, 'log', [v8]
Qui peut par exemple être trivialement traduit en code JavaScript suivant :
const v0 = 0;
const v1 = 10;
const v2 = 1;
let v3 = 0;
for (let v4 = v0; v4 < v1; v4 = v4 + v2) {
const v6 = v3 + v4;
v3 = v6;
}
const v7 = "Result: ";
const v8 = v7 + v3;
const v9 = console;
const v10 = v9.log(v8);
Ou au code JavaScript suivant en inlineant les expressions intermédiaires :
let v3 = 0;
for (let v4 = 0; v4 < 10; v4++) {
v3 = v3 + v4;
}
console.log("Result: " + v3);
FuzzIL a un certain nombre de propriétés :
- 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 :
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
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
- CVE-2021-29982 : Allocation de registre incorrecte (trouvé par JIT-Picker)
- CVE-2021-29984 : Le réordonnancement d'instructions combiné à un GC inattendu peut conduire à une corruption mémoire
- CVE-2022-28285 : AliasSet pour MLoadTypedArrayElementHole trop permissif
- CVE-2022-31745 : Erreur dans le GC incrémental
- CVE-2022-42928 : Annotations KeepAlive manquantes pour certaines opérations BigInt peuvent conduire à une corruption mémoire
- CVE-2022-45406 : Use-after-free d'un Realm JavaScript
- CVE-2023-4577 : Corruption mémoire due à l'interaction du GC et des expressions régulières
Chromium/v8* Issue 939316: Turbofan peut lire un pointeur Map hors limites lors de l'optimisation de Reflect.construct
- Issue 944062: JSCallReducer::ReduceArrayIndexOfIncludes ne parvient pas à insérer les vérifications Map
- CVE-2019-5831: Traitement incorrect des Maps dans V8
- Issue 944865: Représentation de valeur invalide dans V8
- CVE-2019-5841: Bogue dans l'heuristique d'inlining
- CVE-2019-5847: Les éléments scellés/gelés de V8 provoquent un crash
- CVE-2019-5853: Corruption de mémoire dans la vérification de longueur de regexp
- Issue 992914: La migration de Map ne respecte pas les types d'éléments, ce qui conduit à une confusion de type
- CVE-2020-6512: Confusion de type dans V8
- CVE-2020-16006: Corruption de mémoire due à une collision de hachage mal gérée dans DescriptorArray
- CVE-2021-37991: Condition de course lors de la compilation JIT concurrente
- Issue 1359937: La désérialisation de BigInts pourrait produire une valeur -0n invalide
- Issue 1377775: Vérification de type incorrecte lors de l'inlining de Array.prototype.at dans Turbofan
- Issue 2323: Pointeur valstack instable dans putprop
- Issue 2320: Dépassement du pointeur Memcmp dans la fonction intégrée de chaîne
- CVE-2020-13991: Libération incorrecte des arguments spread
- Issue 3784: Corruption de mémoire due à une énumération incorrecte des propriétés
- CVE-2020-13623: Dépassement de pile via les clés de propriété pour les objets Proxy
- CVE-2020-13649 (1): Corruption de mémoire due à la gestion des erreurs en cas de OOM
- CVE-2020-13649 (2): Corruption de mémoire due à la gestion des erreurs en cas de OOM
- CVE-2020-13622: Corruption de mémoire due à un traitement incorrect des clés de propriété pour les objets Proxy
- CVE-2020-14163: Corruption de mémoire due à une condition de course déclenchée par le ramasse-miettes lors de l'ajout de paires clé/valeur
- Issue 3813: Gestion incorrecte des erreurs dans la fonction SerializeJSONProperty
- Issue 3814: Objet Proxy inattendu dans l'assertion ecma_op_function_has_instance
- Issue 3836: Corruption de mémoire due à une initialisation incorrecte de TypedArray
- Issue 3837: Corruption de mémoire due à une gestion incorrecte de la mémoire dans getOwnPropertyDescriptor
- CVE-2020-1912: Corruption de mémoire lors de l'exécution de fonctions génératrices internes compilées paresseusement
- CVE-2020-1914: Corruption de bytecode lors du traitement de l'instruction SaveGeneratorLong
Avertissement
Ce n'est pas un produit officiellement supporté par Google.