
Fuzzer del kernel Windows basato su snapshot e guidato dalla copertura
Rewind è un fuzzer basato su snapshot e guidato dalla copertura che ha come bersaglio i componenti del kernel Windows.
L'idea è partire da uno snapshot di un sistema vivo in esecuzione. Questo snapshot è composto dalle pagine di memoria fisica insieme allo stato della CPU.
Questo stato viene usato per impostare lo stato iniziale di una CPU virtuale. Sfruttando il paging on-demand, solo le pagine necessarie all'esecuzione della funzione bersaglio vengono lette dallo snapshot.
Poiché usiamo una macchina virtuale dedicata con solo le pagine di memoria fisica utili all'esecuzione della funzione bersaglio, il ripristino di uno snapshot è veloce.
Al momento sono disponibili 2 backend:
WHVP sfrutta l'API WHVP (Windows Hypervisor Platform) per
fornire l'accesso a una partizione Hyper-V. Vedi
https://docs.microsoft.com/en-us/virtualization/api/hypervisor-platform/hypervisor-platform
per maggiori dettagli.Bochs sfrutta l'emulatore Bochs
(https://bochs.sourceforge.io/)Un backend KVM è in fase di sviluppo e dovrebbe essere disponibile a breve.
Rewind fornisce 2 funzionalità principali:
Fornisce inoltre una TUI (Terminal User Interface) di base per riportare informazioni utili riguardo al fuzzing.
È stato testato su Windows e Linux (per ora solo backend bochs su Linux).
Ho sempre apprezzato fare ricerca sulle vulnerabilità del kernel, specialmente nel kernel Windows. Il processo prevede sempre un mix di analisi statica e dinamica. Fare analisi dinamica può diventare rapidamente noioso. Il ciclo debug / crash / reboot / reset di tutti i breakpoint è lento e doloroso. Quando si vuole fare del fuzzing, spesso richiede di configurare una o più macchine virtuali più un kernel debugger e creare alcuni script improvvisati per gestire il rilevamento dei crash...
Fare snapshot con le macchine virtuali aiuta, ma è lento.
Durante il 2018, Microsoft ha introdotto un nuovo set di API chiamato Windows Hypervisor Platform (WHVP). Queste API permettono di configurare una partizione (VM nel linguaggio di Hyper-V) con alcuni processori virtuali e di avere il controllo sulle uscite VM (VM exits) che si verificano nella macchina virtuale. È quasi come avere il proprio gestore di VM-exit in userland. Piuttosto comodo per fare cose utili, ad esempio Simpleator o applepie.
Così ho iniziato a giocare con WHVP e ho creato un primo PoC che mi permetteva di eseguire del shellcode in una partizione Hyper-V. Era scritto in Python ed era piuttosto lento. Questo primo PoC si è evoluto abbastanza rapidamente in una sorta di tracer basato su snapshot. Volevo avere qualcosa per avviare la CPU virtuale e abbastanza facile da configurare. Quindi, dato che stavo già usando un kernel debugger per giocare con il mio bersaglio, ho deciso di usare i kernel dump creati con WinDbg come mio snapshot. Con questo mi bastava configurare una partizione con una CPU virtuale. Il contesto della CPU virtuale viene impostato con il contesto preso dal dump. Ogni volta che la CPU virtuale ha bisogno di una pagina fisica, uso quelle del dump.
Con questo sono stato in grado di fare il fork dello stato del dump in una partizione e poi riprendere l'esecuzione. Mi ha permesso di tracciare facilmente l'esecuzione della mia funzione bersaglio. Modificando gli argomenti e ripristinando lo stato di memoria della partizione era anche molto facile fuzzare il bersaglio.
Questo lavoro è stato presentato alla conferenza SSTIC nel 2020 e rilasciato su github.
Lo strumento implementa 2 possibilità per ottenere la copertura. La prima sfrutta il classico TF (Trap Flag) per avere interruzioni INT1 su ogni istruzione. Richiede di modificare il bersaglio ed è lenta. Avrei preferito usare il MONITOR trap flag. Ma WHVP non offre questa possibilità.
Per avere prestazioni adeguate (richieste per il fuzzing), ho deciso di ridurre la precisione della copertura e aggiungere una modalità in cui si sa solo quando un'istruzione viene eseguita per la prima volta.
Per fare questo, patcho le pagine recuperate dallo snapshot con byte 0xcc (solo per le pagine eseguibili). Quando la CPU eseguirà queste istruzioni patchate, l'hypervisor intercetterà l'eccezione e riscriverà le istruzioni con il codice originale.
È come avere un singolo software breakpoint impostato su ogni istruzione. Funziona il 95% delle volte ma in particolari porzioni di codice (ad esempio quelle con jump table) fallirà perché i dati verranno sostituiti.
Per superare questo problema, un'opzione sarebbe disassemblare il codice prima di mapparlo e patchare solo ciò che è necessario (forse la prossima volta).
Durante i miei esperimenti ho incontrato diverse limitazioni usando WHVP. È lento, davvero lento. Il codice sorgente di VirtualBox ha alcuni commenti interessanti :)
Quindi per avere prestazioni adeguate devi davvero limitare le uscite VM ed è incompatibile se vuoi usare Hyper-V come hypervisor di tracing (poiché richiede un sacco di uscite VM).
Nello stesso periodo ho iniziato a usare bochs (in particolare la parte di instrumentazione) per verificare se le tracce ottenute dallo strumento fossero corrette. Bochs era una sorta di oracolo per vedere se avevo tracce divergenti.
Bochs è più veloce di WHVP quando si fa un trace completo e hai anche il vantaggio di avere gli accessi alla memoria più altre cose utili.
Ho deciso di aggiungere bochs come altro backend. whvp non era più un nome appropriato e mi sono stabilito su rewind.

rewind è stato progettato attorno al mio flusso di lavoro personale quando conduco
valutazioni della sicurezza per driver kernel sulla piattaforma Windows.
Il primo passo è installare il software bersaglio all'interno di una macchina virtuale. Dato che uso un mix di analisi statica e dinamica, configurerò anche un kernel debugger.
Dopo aver aperto alcuni driver casuali in IDA, inizierò rapidamente a
prendere di mira alcune funzioni. Per fare questo di solito metto alcuni breakpoint con
windbg e, combinato con ret-sync,
posso iniziare a giocare.
È qui che entra in gioco rewind. Invece di modificare buffer casuali
in memoria, fare singlestep e annotare l'IDB per avere un'idea
approssimativa di cosa sta succedendo, farò uno snapshot con windbg e userò
rewind al suo posto.
Questo semplificherà molto il processo. Avere uno snapshot offre molti vantaggi. Tutto è deterministico. Puoi riprodurre all'infinito una chiamata di funzione. Puoi lanciare un fuzzer se la funzione bersaglio sembra interessante. Puoi persino chiudere la VM poiché non serve più.
Ovviamente hai bisogno di Rust (installazione testata su Windows e Linux con Rust 1.50). CMake è anche richiesto da alcune dipendenze.
Per prima cosa clona il repository:
$ git clone [email protected]:quarkslab/rewind.git
Continua con l'installazione del backend bochs
Clona il repository bochscpu (https://github.com/yrp604/bochscpu) nella directory vendor:
$ cd vendor
$ git clone https://github.com/yrp604/bochscpu
Scarica gli artifact bochs precompilati da bochscpu-build (https://github.com/yrp604/bochscpu-build)
$ curl.exe -L --output bochs-x64-win.zip [artifact_url]
Estrai le cartelle lib e bochs nel checkout di bochscpu.
$ Expand-Archive -Path .\bochs-x64-win.zip -DestinationPath .\
$ copy -Recurse .\bochs-x64-win\msvc\* .\bochscpu\
Su Windows anche WHVP verrà compilato come backend.
In una sessione PowerShell elevata, usa il seguente comando per verificare se WHVP è abilitato:
Get-WindowsOptionalFeature -FeatureName HypervisorPlatform -Online
FeatureName : HypervisorPlatform
DisplayName : Windows Hypervisor Platform
Description : Enables virtualization software to run on the Windows hypervisor
RestartRequired : Possible
State : Enabled
CustomProperties :
Se non è abilitato, puoi usare il cmdlet Set-WindowsOptionalFeature per abilitarlo. Dovrai anche abilitare Hyper-V.
Devi anche avere installato un Windows SDK (10.0.19041.0). Puoi scaricarlo da https://developer.microsoft.com/fr-fr/windows/downloads/windows-10-sdk/.
Devi installare LLVM e impostare la variabile d'ambiente LIBCLANG_PATH (richiesta da bindgen).
Vedi https://rust-lang.github.io/rust-bindgen/requirements.html per una spiegazione dettagliata.
$ $env:LIBCLANG_PATH="C:\Program Files\LLVM\bin"
Da lì dovresti essere in grado di compilare rewind (richiesto nightly a causa di unwind_attributes nella crate bochscpu):
$ cd rewind_cli
$ cargo +nightly build --release
Il binario rewind sarà disponibile nella directory target/release.
Potresti anche usare cargo per installarlo localmente:
$ cd rewind_cli
$ cargo +nightly install --path .
> error: failed to run custom build command for `zydis v3.1.1`
whvp-sys non verrà compilatoUn tutorial di base che sfrutta CVE-2020-17087 è disponibile nella directory examples
Vedi TODO.md
hit, il tracer si comporterà male su alcune funzioni (è il caso di alcune switch table). La ragione è che ogni byte viene sostituito da software breakpoint (inclusi i dati se presenti in una pagina eseguibile). Un modo migliore per farlo sarebbe ottenere l'elenco di tutti i basic block da un disassembler, per esempio.Questo strumento è attualmente sviluppato e sponsorizzato da Quarkslab sotto licenza Apache 2.0.
Un saluto a @yrp604, @0verclk0, Alexandre Gazet per il loro aiuto, feedback e pensieri. Grazie anche a tutti i miei colleghi di Quarkslab!