Salta ai contenuti

Fuzzing Moderno nel 2026: Snapshot, Framework e Fuzzing di Quello Che Non Potevi Prima

· 13 min read · default
cybersecurityfuzzingvulnerability-researchreverse-engineeringtestingsecurity

Il fuzzing ha un problema di reputazione: molti ingegneri ancora lo immaginano come lanciare byte casuali a un programma fino a quando non crasha. Quella descrizione era all''incirca accurata nel 1990 ed è stata fuorviante per un decennio. Il fuzzing moderno è guidato da copertura — il fuzzer strumenta il target, osserva quali percorsi di codice ogni input raggiunge, e dirige la mutazione verso input che esplorano nuovo territorio. Questo ciclo di feedback è la differenza tra pungere casualmente un parser e camminare sistematicamente nello suo spazio di stato, ed è il motivo per cui il fuzzing ora trova migliaia di CVE reali l''anno in software che è stato revisionato da esperti.

Il panorama 2026 ha maturato bene oltre un singolo tool, però, e gli sviluppi interessanti riguardano il raggiungimento di target che il fuzzing convenzionale non poteva: kernel del sistema operativo, codice sepolto dietro setup costosa, e applicazioni il cui scopo di input sconfigge la mutazione di byte. Questa guida copre quel panorama — i classici guidati da copertura, syzkaller per kernel, Snapchange per snapshot fuzzing, e LibAFL per costruire il fuzzer di cui il tuo target ha realmente bisogno — più cargo-fuzz e honggfuzz per il lavoro di tutti i giorni.

Perché la guida da copertura ha cambiato tutto

Il meccanismo vale la pena di capire perché spiega cosa il fuzzing è bravo a fare e dove si blocca. Un fuzzer guidato da copertura compila il target con strumentazione che registra quali spigoli del grafico di flusso di controllo un''esecuzione ha toccato. Mantiene un corpus di input, e quando un input mutato raggiunge un spigolo che nessun input precedente ha raggiunto, quell''input è giudicato interessante e aggiunto al corpus per essere mutato ulteriormente. Nel corso di milioni di iterazioni, il corpus accumula input che collettivamente esercitano percorsi profondi e insoliti — inclusi percorsi che nessun umano ha scritto un test per.

Questo produce un comportamento che sembra quasi intelligente. Dato un corpus seminato con un PNG valido, un fuzzer scoprirà la struttura del chunk, poi i tipi di chunk validi, poi i rami di parsing per ogni tipo, progressivamente costruendo input che raggiungono più profondo nel decodificatore. Non "capisce" mai PNG; tiene solo quello che ha spostato la copertura.

Il corollario è dove il fuzzing si blocca: controlli difficili che non può indovinare oltre. Un confronto di costante magica, un checksum o una firma crittografica crea un muro — la mutazione casuale essenzialmente non produrrà mai gli 8 byte giusti, quindi tutto dietro quel controllo rimane inesplorato. Le risposte pratiche sono seminare il corpus con input validi, fornire un dizionario di valori magici, o patchare i checksum in una build di fuzz. Riconoscere un plateau di copertura come "ho colpito un muro" piuttosto che "non ci sono più bug" è uno degli istinti più utili in questo lavoro.

Fuzzing del kernel: syzkaller

I kernel del sistema operativo sono un target ostile per i fuzzer convenzionali. L''input non è un file ma una sequenza di syscall con argomenti interdipendenti — un descrittore di file da open deve fluire in read, e la maggior parte delle sequenze di syscall casuali falliranno immediatamente con EINVAL. I crash abbattono l''intera macchina piuttosto che un singolo processo, e la copertura deve essere raccolta dallo spazio kernel.

syzkaller risolve tutti e tre. Descrive le syscall in un linguaggio dichiarativo (syzlang) in modo che possa generare sequenze plausibili con argomenti correttamente tipizzati e interdipendenti; esegue i target dentro VM scartabili in modo che i crash siano sopravvivibili e raccolti automaticamente; e usa KCOV per il feedback di copertura del kernel più KASAN per catturare errori di memoria che altrimenti sarebbero corruzione silenziosa. Il syzbot continuo di Google viene eseguito contro Linux e ha segnalato migliaia di bug.

La lezione si generalizza oltre ai kernel: syzkaller funziona perché qualcuno ha codificato la conoscenza della struttura dell''input in descrizioni. Quando gli input di un target hanno una grammatica, insegnare al fuzzer quella grammatica batte la mutazione di byte grezzi di un ampio margine. Il costo corrispondente è reale — estendere syzlang per un sottosistema sotto-testato è lavoro genuino, ed è anche il contributo di valore più alto che la maggior parte delle persone può fare al fuzzing del kernel.

Snapshot fuzzing: andare oltre la setup

La seconda frontiera è i target dove il codice interessante siede dietro l''inizializzazione costosa. Considera il fuzzing del parser di query di un database: ogni iterazione avrebbe bisogno di avviare il server, inizializzare l''archiviazione, autenticarsi e stabilire una sessione prima che una singola query sia analizzata. Con forse dieci iterazioni al secondo, il fuzzing guidato da copertura è senza speranza — la tecnica ha bisogno di migliaia.

Snapshot fuzzing inverte questo. Esegui il target una volta esattamente al momento di interesse, scatta una memoria snapshot dello stato dell''intera macchina, e poi ripristina quello snapshot per ogni iterazione successiva. Tutto il costo di setup è pagato una volta. Snapchange (da AWS) implementa questo con KVM: catturi uno snapshot con QEMU, scrivi un piccolo harness Rust descrivendo dove iniettare l''input e quando un''iterazione termina, e riproduce da quello stato a tassi molto alti.

Questo sblocca categorie che erano precedentemente impraticabili: protocolli di rete stateful fuzzati a metà sessione, codice dietro autenticazione, codice dell''hypervisor e del kernel, e qualsiasi applicazione con startup pesante. Il compromesso è lo sforzo — scrivi un fuzzer Rust piuttosto che eseguire un comando, e devi comprendere il layout della memoria del target abbastanza bene da iniettare correttamente l''input. È una tecnica specializzata che paga esattamente quando l''alternativa è non fare fuzzing del target affatto.

Framework: costruisci il fuzzer di cui il target ha bisogno

Il terzo sviluppo è filosofico. AFL++ e libFuzzer sono eccellenti a quello per cui erano stati progettati e scomodi quando il tuo target non si adatta — un protocollo binario custom, un''immagine firmware emulata, un input che è un albero piuttosto che un buffer. Storicamente hai piegato il tool, solitamente male.

LibAFL, dal team AFL++, tratta un fuzzer come parti componibili: observer che registrano dati, feedback che giudicano l''interessantezza, mutatori che trasformano input, scheduler, stage ed executor. Assembli la combinazione di cui il tuo target ha bisogno, definisci il tuo tipo di input e mutatori se l''input è strutturato, scegli un executor (in-process, forkserver, emulazione QEMU, strumentazione Frida, snapshot), e ottieni scaling multi-core gratuitamente.

Il framing onesto è che questo è un impegno più grande che eseguire un tool, in modo che la sequenza importa: inizia con cargo-fuzz per Rust o honggfuzz/AFL++ per target nativi, e passa a LibAFL solo quando puoi articolare specificamente perché non si adattano. "L''input è una state machine di protocollo e la mutazione di byte non produce mai un secondo messaggio valido" è una tale ragione; "voglio che sia più veloce" solitamente non lo è.

Fuzzing di tutti i giorni che i team effettivamente mantengono

La maggior parte del valore, per la maggior parte dei team, viene dal fuzzing continuo non glamoroso di parser e gestori di input non affidabile. cargo-fuzz rende questo quasi senza attrito per Rust: scrivi una funzione che accetta &[u8] (o, meglio, un valore tipizzato via il crate arbitrary) ed esegui un comando. honggfuzz è similarmente facile per codice nativo e aggiunge copertura basata su hardware via Intel PT/BTS, che ti consente di fare fuzzing binari che non puoi ricompilare — prezioso per dipendenze closed-source.

Due pratiche separano i team che ricevono valore da team che abbandonano il fuzzing dopo una settimana. Primo, semina il corpus con veri input validi; un fuzzer che inizia da un corpus vuoto spende un tempo enorme a riscoprire la validità del formato di base che potevi avergli passato. Secondo, esegui continuamente e tratta i risultati come test: converti ogni crash minimizzato in un test di regressione in modo che rimanga corretto, e lascia il corpus persistere tra le esecuzioni in modo che il progresso si accumuli. Il fuzzing non è un audit di un pomeriggio; è un processo di background che continua a trovare cose mentre il codice cambia.

I sanitizer meritano una menzione perché moltiplicano l''efficacia. AddressSanitizer trasforma la corruzione silenziosa della memoria in un crash immediato e diagnosticabile, e UBSan cattura il comportamento non definito che potrebbe altrimenti apparire come una strana miscompilazione più tardi. Il fuzzing senza sanitizer trova solo i bug che per caso crashano da soli — tipicamente una frazione piccola di quello che è effettivamente lì.

Triage: il lavoro che inizia quando il crash arriva

Trovare un crash è l''inizio, e i team abitualmente sottovalutano lo sforzo tra "il fuzzer si è fermato" e "uno sviluppatore può correggere questo." Quattro passi fanno la differenza tra un report utile e uno ignorato.

Minimizza l''input. Un input che crasha da un fuzzer è tipicamente pieno di byte irrilevanti che hanno sopravvissuto solo perché nulla li ha rimossi. Ogni fuzzer serio spedisce un minimizzatore (cargo fuzz tmin, afl-tmin, syzkaller syz-repro), ed eseguirlo trasforma un blob da 4KB in un manciata di byte che isolano il trigger effettivo. Questo importa enormemente per lo sviluppatore che deve capirlo.

Deduplica. Un fuzzer che viene eseguito durante la notte riporterà lo stesso bug dozzine di volte attraverso input diversi. Raggruppare per ubicazione di crash e firma di stack trasforma 200 crash in sei bug distinti. Senza questo passo, il triage sembra impossibilmente costoso e le persone rinunciano.

Valuta l''exploitability, attentamente. Non ogni crash è una vulnerabilità. Un dereference di puntatore nullo in un parser è solitamente un denial of service; un heap buffer overflow con lunghezza controllata dall''attaccante è potenzialmente molto peggio. L''output del sanitizer aiuta enormemente qui — ASan ti dice il tipo di errore di memoria, le dimensioni, e entrambi gli stack di allocazione e accesso. Resisti alla tentazione di etichettare tutto come critico, perché un team che riceve severità gonfiate smette di fidarsi dei report.

Converti in un test di regressione. L''input minimizzato diventa un test di unità impegnato insieme alla fix. Questo è quello che mantiene il bug corretto e quello che rende il fuzzing composto nel tempo piuttosto che riscoprire gli stessi problemi dopo un refactor.

I team che ricevono valore sostenuto dal fuzzing sono quelli che costruiscono questo pipeline una volta, non quelli che trovano di più i crash.

Scegliere dove iniziare

La decisione segue il target. Per codice Rust, usa cargo-fuzz, e usa arbitrary per fuzz di API tipizzate piuttosto che solo parser di byte. Per binari userspace nativi con codice sorgente, honggfuzz o AFL++ con sanitizer. Per binari senza codice sorgente, feedback hardware di honggfuzz o modalità QEMU di AFL++. Per kernel del sistema operativo, syzkaller, e considera estendere syzlang per il sottosistema di cui ti importa. Per codice dietro setup costosa o sessioni stateful, Snapchange o una configurazione snapshot-capable di LibAFL. E per input con vera struttura — protocolli, AST, formati di file con grammatica — o un mutatore consapevole di grammatica o un fuzzer LibAFL custom, perché la mutazione di byte si plateauerà presto.

Su tutti questi, la stessa disciplina si applica: semina bene, esegui con sanitizer, esegui continuamente, minimizza i crash, e converti i risultati in test di regressione. Il tool importa meno di se il loop continua a girare.

La linea di fondo

Il fuzzing nel 2026 è una disciplina sistematica di ricerca di vulnerabilità costruita su feedback di copertura, e la sua frontiera è raggiungere target che erano precedentemente fuori portata. syzkaller fuzz i kernel codificando la struttura di syscall ed eseguendo in VM scartabili; Snapchange usa snapshot KVM per fuzz il codice sepolto dietro setup costosa; LibAFL ti consente di costruire un fuzzer corrispondente a un target insolito invece di piegare un tool monolitico; e cargo-fuzz e honggfuzz rendono il fuzzing continuo di tutti i giorni abbastanza economico da effettivamente sostenere. Inizia con il tool facile per il tuo linguaggio, semina il corpus con input veri, abilita sempre i sanitizer, tratta un plateau di copertura come un muro da ingegnerizzare piuttosto che un all-clear, e trasforma ogni crash in un test.

Riferimenti e Risorse

Tool

Background e analisi

Cheatsheet correlati di 1337skills