Un fuzzzer guidato dalla copertura per interpreti di linguaggi dinamici basato su un linguaggio intermedio personalizzato ("FuzzIL") che può essere mutato e tradotto in JavaScript.
Utilizzo
I passi fondamentali per utilizzare questo fuzzzer sono:
Scaricare il codice sorgente di uno dei motori JavaScript supportati. Consulta la directory Targets/ per l'elenco dei motori JavaScript supportati.
Applicare le patch corrispondenti dalla directory del target. Vedi anche il README.md in quella directory.
Compilare il motore con strumentazione di copertura (richiede clang >= 4.0) come descritto nel README.
Compilare il fuzzzer: swift build [-c release].
Eseguire il fuzzzer: swift run [-c release] FuzzilliCli --profile=<profilo> [altre opzioni CLI] /percorso/del/jsshell. Vedi anche swift run FuzzilliCli --help.
La compilazione e l'esecuzione di Fuzzilli e dei motori JavaScript supportati all'interno di Docker e su Google Compute Engine è anche supportata.
Hacking
Dai un'occhiata a main.swift per vedere un esempio di utilizzo della libreria Fuzzilli e sperimentare con le varie opzioni di configurazione. Successivamente, guarda per la logica di fuzzing di alto livello. Da lì, approfondisci qualsiasi parte che sembri interessante.
Sarebbe molto apprezzato se potessi inviare una breve nota (possibilmente includendo un numero CVE) a [email protected] o aprire una pull request per qualsiasi vulnerabilità trovata con l'aiuto di questo progetto, in modo che possa essere inclusa nella sezione vetrina dei bug. Per il resto, puoi ovviamente rivendicare qualsiasi bounty per bug, crediti CVE, ecc. per le vulnerabilità :)
Concetto
Quando si fuzzza per bug del core dell'interprete, ad esempio nei compilatori JIT, la correttezza semantica dei programmi generati diventa un problema. Questo è in contrasto con la maggior parte degli altri scenari, ad esempio il fuzzing delle API runtime, in cui la correttezza semantica può essere facilmente aggirata racchiudendo il codice generato in costrutti try-catch. Esistono diverse possibilità per ottenere un tasso accettabile di campioni semanticamente corretti, una delle quali è un approccio mutazionale in cui tutti i campioni nel corpus sono anche semanticamente validi. In tal caso, ogni mutazione ha solo una piccola probabilità di trasformare un campione valido in uno non valido.
Per implementare un fuzzzer JavaScript basato su mutazioni, è necessario definire mutazioni per il codice JavaScript. Invece di mutare l'AST o altri elementi sintattici di un programma, viene definito un linguaggio intermedio personalizzato (IL) sul quale le mutazioni al flusso di controllo e di dati di un programma possono essere eseguite più direttamente. Questo IL viene poi tradotto in JavaScript per l'esecuzione. Il linguaggio intermedio ha approssimativamente il seguente aspetto:
Un programma FuzzIL è semplicemente una lista di istruzioni.
Un'istruzione FuzzIL è un'operazione insieme a variabili di input e output e potenzialmente uno o più parametri (racchiusi tra virgolette singole nella notazione sopra).
Gli input per le istruzioni sono sempre variabili, non ci sono valori immediati.
Ogni output di un'istruzione è una nuova variabile, e le variabili esistenti possono essere riassegnate solo tramite operazioni dedicate come l'istruzione Reassign.
Ogni variabile è definita prima di essere utilizzata.
Su questi programmi possono essere eseguite diverse mutazioni:
InputMutator: sostituisce le variabili di input delle istruzioni con altre diverse per mutare il flusso di dati del programma.
CodeGenMutator: genera codice e lo inserisce da qualche parte nel programma mutato. Il codice viene generato eseguendo un generatore di codice o copiando alcune istruzioni da un altro programma nel corpus (splicing).
CombineMutator: inserisce un programma dal corpus in una posizione casuale nel programma mutato.
OperationMutator: muta i parametri delle operazioni, ad esempio sostituendo una costante intera con una diversa.
e altri...
Una discussione molto più approfondita su come funziona Fuzzilli può essere trovata qui.
Implementazione
Il fuzzzer è implementato in Swift, con alcune parti (ad esempio misurazioni della copertura, interazioni socket, ecc.) implementate in C.
Architettura
Un'istanza del fuzzzer (implementata in Fuzzer.swift) è composta dai seguenti componenti centrali:
MutationFuzzer: produce nuovi programmi a partire da quelli esistenti applicando mutazioni. Successivamente esegue i campioni prodotti e li valuta.
ScriptRunner: esegue programmi nel linguaggio target.
Corpus: memorizza campioni interessanti e li fornisce al fuzzzer centrale.
Environment: ha conoscenza dell'ambiente runtime, ad esempio i builtin disponibili, i nomi delle proprietà e i metodi.
Minimizer: minimizza i programmi che causano crash o sono interessanti.
Evaluator: valuta se un campione è interessante secondo una metrica, ad esempio la copertura del codice.
Lifter: traduce un programma FuzzIL nel linguaggio target (JavaScript).
Inoltre, sono opzionalmente disponibili diversi moduli:
Statistics: raccoglie varie informazioni statistiche.
ThreadSync: sincronizza più istanze all'interno dello stesso processo.
Storage: salva su disco i programmi che causano crash.
Il fuzzzer è guidato da eventi, con la maggior parte delle interazioni tra diverse classi che avvengono tramite eventi. Gli eventi vengono lanciati, ad esempio, a seguito di un crash o del ritrovamento di un programma interessante, dell'esecuzione di un nuovo programma, della generazione di un messaggio di log e così via. Vedi Events.swift per l'elenco completo degli eventi. Il meccanismo degli eventi disaccoppia efficacemente i vari componenti del fuzzzer e rende facile implementare moduli aggiuntivi.
Un programma FuzzIL può essere costruito utilizzando un'istanza di ProgramBuilder. Un ProgramBuilder fornisce metodi per creare e aggiungere nuove istruzioni, aggiungere istruzioni da un altro programma, recuperare variabili esistenti, interrogare il contesto di esecuzione nella posizione corrente (ad esempio se si è all'interno di un ciclo) e altro ancora.
Esecuzione
Fuzzilli utilizza una modalità di esecuzione personalizzata chiamata REPRL (read-eval-print-reset-loop). A tale scopo, il motore target viene modificato per accettare un input di script tramite pipe e/o memoria condivisa, eseguirlo, quindi resettare il suo stato interno e attendere il prossimo script. Questo elimina il sovraccarico derivante dalla creazione del processo e, in gran parte, dall'inizializzazione del motore.
Scalabilità
C'è un'istanza di Fuzzer per processo target. Ciò consente l'esecuzione sincrona dei programmi e semplifica quindi l'implementazione di vari algoritmi come mutazioni consecutive e minimizzazione. Inoltre, evita la necessità di implementare un accesso thread-safe allo stato interno, ad esempio al corpus. Ogni istanza del fuzzzer ha la propria DispatchQueue, corrispondente concettualmente a un singolo thread. Come regola generale, ogni interazione con un'istanza di Fuzzer deve avvenire sulla coda di dispatch di quell'istanza. Ciò garantisce la thread-safety poiché la coda è seriale. Per maggiori dettagli, consulta la documentazione.
Per scalare, le istanze del fuzzzer possono formare una gerarchia ad albero, in cui riportano i campioni interessanti e i crash appena trovati al loro nodo padre. A sua volta, un nodo padre sincronizza il suo corpus con i suoi nodi figli. La comunicazione tra nodi nell'albero può avvenire in diversi modi, ciascuno implementato come modulo:
Comunicazione tra thread: sincronizza le istanze nello stesso processo accodando compiti alla DispatchQueue dell'altro fuzzzer.
Questo design consente al fuzzzer di scalare su molti core in una singola macchina e su molte macchine diverse. Poiché un singolo nodo padre può diventare rapidamente sovraccarico se troppe istanze gli inviano programmi, è possibile configurare più livelli di istanze, ad esempio un'istanza radice, 16 nodi intermedi collegati alla radice e 256 "foglie" collegate ai nodi intermedi. Vedi la directory Cloud/ per maggiori informazioni sul fuzzing distribuito.
Risorse
Ulteriori risorse su questo fuzzzer:
Una presentazione su Fuzzilli tenuta a Offensive Con 2019.
La tesi magistrale per la quale è stata realizzata l'implementazione iniziale.
Un articolo di Sensepost sull'uso di Fuzzilli per trovare un bug in v8.
Un articolo di Doyensec sul fuzzing del motore JerryScript con Fuzzilli.
Un articolo scientifico del NDSS Symposium 2023 su Fuzzilli e come si confronta con altri fuzzer.
Vetrina dei bug
La seguente è una lista di alcuni dei bug trovati con l'aiuto di Fuzzilli. Dovrebbero essere inclusi in questa lista solo i bug con impatto sulla sicurezza che erano presenti almeno in una versione Beta del software interessato. Poiché Fuzzilli viene spesso utilizzato per test di fuzzing continui durante lo sviluppo, molti problemi trovati da esso non sono inclusi in questa lista, poiché vengono tipicamente scoperti prima che il codice vulnerabile raggiunga una versione Beta. Una lista di tutti i problemi recentemente trovati da Fuzzilli in V8 può comunque essere trovata qui.
Un ringraziamento speciale a tutti gli utenti di Fuzzilli che hanno segnalato bug trovati con esso!
WebKit/JavaScriptCore
Issue 185328: Il compilatore DFG utilizza un registro di output errato per l'operazione NumberIsInteger
CVE-2018-4299: performProxyCall perde un oggetto interno nello script
CVE-2018-4359: compileMathIC produce codice macchina errato
CVE-2019-8518: Accesso OOB in FTL JIT a causa di LICM che sposta l'accesso all'array prima del controllo dei limiti
CVE-2019-8558: UaF di CodeBlock a causa di Watchpoint pendenti
CVE-2019-8611: L'ottimizzazione AIR rimuove erroneamente l'assegnazione a un registro
CVE-2019-8623: Lo spostamento di codice invariante di ciclo (LICM) in DFG JIT lascia una variabile di stack non inizializzata
CVE-2019-8622: doesGC() di DFG è errato riguardo al comportamento dell'operazione HasIndexedProperty su StringObjects
CVE-2019-8671: DFG: Lo spostamento di codice invariante di ciclo (LICM) lascia l'accesso alla proprietà dell'oggetto senza guardia
CVE-2019-8672: Use-after-free di JSValue in ValueProfiles
CVE-2019-8678: JSC non riesce a eseguire haveABadTime() quando alcuni prototipi vengono modificati, portando a confusioni di tipo
CVE-2019-8685: JSPropertyNameEnumerator utilizza ID di struttura errati
CVE-2019-8765: Confusione di tipo GetterSetter durante la compilazione DFG
CVE-2019-8820: Confusione di tipo durante il bailout nella ricostruzione degli oggetti arguments
CVE-2019-8844: ObjectAllocationSinkingPhase non dovrebbe inserire hint per allocazioni che non sono più valide
CVE-2020-3901: Confusione di tipo GetterSetter nel codice FTL JIT (a causa di LICM non sempre sicuro)
CVE-2021-30851: Lock mancante durante la ricerca simultanea in HashTable
CVE-2021-30818: Confusione di tipo durante la ricostruzione degli argomenti all'uscita OSR di DFG
CVE-2022-46696: Fallimento dell'asserzione a causa di una mancata eccezione di controllo nel codice compilato JIT
CVE-2022-46699: Fallimento dell'asserzione a causa di una memorizzazione errata delle proprietà speciali negli IC
CVE-2022-46700: Intl.Locale.prototype.hourCycles perde un JSValue vuoto nello script
CVE-2025-43214: Corruzione della memoria durante JSToWasmEntry quando si itera sullo stack