Il piccolo bug che ce l'ha fatta: CVE-2024-30089 è una vulnerabilità sottile del kernel che ho usato per sfruttare una macchina Windows 11 completamente aggiornata (con tutta la Virtualization Based Security e le mitigazioni hardware attivate) e ho ottenuto la mia prima vittoria a Pwn2Own quest'anno.
In questo articolo descrivo il mio approccio diretto alla caccia ai bug: scegliere un punto di partenza e seguire intuitivamente un percorso finché qualcosa non cattura la mia attenzione. Questo bug è interessante perché può essere attivato in modo affidabile a causa di un errore logico. L'errore si verifica in uno stato specifico all'interno di un sistema di comunicazione inter-processo, che poi causa un use-after-free. Trovare il bug ha richiesto di confrontare i percorsi di codice del programma tra i vari stati possibili, un processo che descriverò nel dettaglio. Altrettanto intriganti sono l'origine del bug e l'approccio adottato da Microsoft per correggerlo. Anche questi argomenti sono trattati in questo post.
Una domanda comune che ricevo sulla ricerca sulla vulnerabilità è come si inizia. In effetti, scegliere un obiettivo e rispettarlo è una delle fasi più difficili del processo di ricerca. La vulnerabilità qui discussa si trova nel Microsoft Kernel Streaming Service (mskssrv.sys). Dai un'occhiata a questo post sul blog per avere una panoramica generale del sottosistema. In quel post ho evidenziato alcune caratteristiche del sottosistema MSKSSRV che potrebbero renderlo una buona superficie di attacco, in particolare il suo meccanismo di comunicazione tra processi (IPC).
La base di codice di MSKSSRV è piuttosto ridotta e anche l'ultima vulnerabilità in questo sottosistema che ho scoperto è stata sfruttata in modo indipendente come 0-day. Ho anche sentito parlare di ulteriori sforzi da parte di altri ricercatori e aziende per verificare questo driver. Per questo motivo, inizialmente sono caduto nella trappola comune di dare per scontato che non ci fossero più bug da trovare in questa superficie di attacco. Ma dato che l'avevo suggerito nel mio precedente post sul blog, ho scelto di fidarmi del mio istinto e continuare a cercare.
Un ottimo modo per ottenere nuove idee di ricerca è quello di tenersi informati sulle ricerche in corso. Ho letto un eccellente postsul blog di k0shl che mi ha ispirato a dare la caccia a un particolare tipo di bug. Nella vulnerabilità riscontrata da k0shl, il conteggio di riferimento di un oggetto viene inizializzato e incrementato senza un adeguato blocco, creando un uso dopo la finestra libera. Nonostante il bug di k0shl sia un bug di userland e non del kernel, lo stile di codifica della libreria vulnerabile mi ha ricordato quando in precedenza ho verificato il driver MSKSSRV.
L'MS KS Server (MSKSSRV) interagisce con un processo userland tramiteFSStreamReg. e FSContextReg derivano entrambi dalla classe di baseFSRegObject non implementa una funzione di blocco vtable, e l'implementazione della classe base () è semplicemente un'istruzione nop . Ciò significa che nessun meccanismo di blocco è effettivamente implementato per gli oggetti . Al contrario,FSStreamRegIl meccanismo di blocco viene utilizzato per accedere agli oggetti da pulire, nella funzione:
Blocco di codice 1: FSRendezvousServer::Chiudi, blocco e sblocco del percorso per FSRegObjects
Esistono due oggetti derivati dalla classe di base
Come ho menzionato nel mio ultimo post sul blog, l'aspetto di condivisione degli oggetti tra processi di MSKSSRV è una via interessante per le vulnerabilità, quindi ho deciso di concentrarmi ulteriormente su questo. Il meccanismo IPC del sottosistema è illustrato nel diagramma seguente:
Diagramma 1: comunicazione tra processi in MS KS Server
Aprire un handle file al dispositivo MSKSSRV, tramite CreateFile, crea un FILE_OBJECT che corrisponde a quell’handle. Utilizzando questo handle, un processo può inizializzare un nuovo flusso o un oggetto contestuale inviando al dispositivo un IOCTL tramite la funzione DeviceIoControl . Il processo di inizializzazione indica quale processo remoto può registrare l’oggetto specificando l’ID del processo tramite l’argomentolpInBufferFSRegObject è memorizzato in
Irp->CurrentStackLocation->FileObject->FsContext2.
Lo stesso puntatore all’oggetto FSRegObject viene memorizzato due volte in FsContext2, una volta nel usato dal processo di inizializzazione e una volta nel usato dal processo di registrazione. In questo modo, i riferimenti all’oggetto possono essere condivisi tra processi. Ad esempio, utilizzando un oggetto FSStreamReg, più processi possono avere accesso allo stream frame buffer, come mostrato nel diagramma 1. Il conteggio di riferimento di viene inizializzato a 1 e poi incrementato nuovamente dopo il completamento dell’inizializzazione. La registrazione del aumenta nuovamente il conteggio dei riferimenti, per un totale di tre riferimenti per oggetto.
Diagramma 2: inizializzazione e registrazione di oggetti FSContextReg
Ho deciso di dare un’altra occhiata a dove avevo effettuato l’audit in precedenza cercando di cercare le vulnerabilità di blocco. Ho notato la funzione
Blocco di codice 2: routine DispatchCleanup e DipatchCleanup per il driver del server MS KS
Una routine di dispatch gestisce uno o più tipi di IRP, che sono richieste I/O impacchettate. In Windows, quando tutti i riferimenti a un file sono stati chiusi, il driver corrispondente del file system riceve una richiesta
In MSKSSRV,
Blocco di codice 3: FSRendezvousServer::Close, Process checks on FSRegObjects
Le informazioni specifiche del processo sono memorizzate nel
Irp->CurrentStackLocation->FileObject->FsContext2
al momento dell’inizializzazione o della registrazione. Il driver determina quali processi le risorse specifiche (
oggetti, oggetti evento e altre cose) devono rilasciare controllando l’ID del processo del chiamante. Se il processo è quello di inizializzazione o registrazione, viene effettuata una pulizia aggiuntiva per quelle risorse specifiche di processo.
Questo mi ha colpito perché generalmente tutte le routine Dispatch si eseguono in un contesto di processo arbitrario, con alcune eccezioni. In altre parole, il sistema sceglie un thread per svolgere il lavoro di Dispatch, mentre il thread scelto è arbitrario. Ho scoperto che
Inoltre, come caratteristiche del sistema operativo Windows, gli handle possono essere condivisi con altri processi (tramite eredità di processi figli o utilizzando la funzione API DuplicateHandle). Attraverso il file handle condiviso, anche l’altro processo può interagire con lo stesso
Diagramma 3: condivisione dell'handle del dispositivo MS KS Server
Per questo motivo, è anche possibile che il contesto del processo durante
Diagramma 4: processo estraneo che chiude l'ultimo handle a un FILE_OBJECT
Ho notato che la funzione
Blocco di codice 4: FSRegObject::Release può essere chiamato due volte da FSRendezvousServer::Close
In generale, un numero di dereferenze superiore a quello dei riferimenti su un oggetto non è l'unico modo in cui può verificarsi un utilizzo dopo la liberazione. Tuttavia in questo caso, possiamo essere sicuri che se un è stato liberato, il suo conteggio dei riferimenti è sceso a zero. L'ultima volta che un valido viene consultato durante le richieste IRP è in una chiamata a . Quindi, se è possibile uno use-after-free, una chiamata a avverrà sempre dopo che l'oggetto è già stato liberato. Durante la chiamata, l'oggetto verrà nuovamente dereferenziato. Per questo motivo, contare il numero di dereferenziazioni è una buona euristica per trovare utilizzi successivi alla liberazione in questo caso particolare.
L'unica cosa rimasta da fare era tracciare i possibili stati del programma, prendendo nota di quando l'oggetto viene liberato e reso accessibile. L'ho fatto emulando mentalmente la logica del programma durante le richieste , ognuna delle quali inizia con le corrispondenti funzioni di Dispatch (blocco di codice 2), per ognuno dei casi possibili.
Di seguito sono riportati gli stati in base ai quali il processo chiude il riferimento finale a un handle. Ogni voce rappresenta il numero di dereferenziamenti dell'oggetto FSContextReg che avvengono se il processo corrispondente chiude l'handle finale. Nota: non c'è alcuna differenza funzionale tra HANDLE #1 (handle di inizializzazione) e HANDLE #2 (handle di registrazione), poiché il campo punta alla stessa memoria in entrambi gli s rappresentati dagli handle corrispondenti.
Dereferenze FSContextReg per ciascuno dei possibili stati IPC di MSKSSRV
Operazione riuscita. L'ultimo stato produce quattro dereferenze: due da parte del processo esterno e due da parte del processo di inizializzazione (e anche di registrazione), mentre inizialmente sono presenti solo tre riferimenti, il che significa che è possibile un utilizzo dopo la liberazione! Ho anche ripetuto lo stesso esercizio con oggetti
Mentre facevo l'esercizio cerebrale della macchina virtuale descritto sopra, ho trovato il problema. Se il processo è il processo di inizializzazione o il processo di registrazione, viene eseguita la pulizia appropriata e il puntatore viene memorizzato in
Blocco di codice 5: FSRegObject::CloseInitProcess imposta il puntatore FSContext su NULL
Ciò significa che
Tuttavia, se il processo chiamante è estraneo, non avviene alcuna pulizia e
Ora, se il secondo handle viene chiuso da un processo che ha sia inizializzato che registrato l'oggetto FSContextReg, l'oggetto ripulirà tutte le risorse di processo memorizzate, diventando vuoto. Questo fa sì che
Ciò equivale a un totale di quattro dereferenziazioni su un singolo oggetto
Diagramma 5: CVE-2024-30089 raffigurato
Il lettore che sta seguendo potrebbe chiedersi perché un processo estraneo non può essere l'ultimo a chiudere entrambi gli handle, dato che questo porterebbe apparentemente anche a quattro dereferenze. Prima che l'oggetto venga distrutto e liberato, viene scollegato da una lista memorizzata nell'obiettivo globale FSRendezvousServer , il puntatore in è selezionato per essere un membro valido della lista. In questo caso, l'oggetto viene liberato nella seconda chiamata a alla fine della funzione. Durante la quarta chiamata a , l'oggetto è già stato scollegato dall'elenco, rendendolo un oggetto non valido, quindi non può essere utilizzato. Per attivare un use-after-free, deve verificarsi dopo che l'oggetto è stato recuperato e convalidato in . Lo snippet di codice qui sotto mostra la primitiva use-after-free che può essere ottenuta dalla vulnerabilità:
Blocco di codice 6: percorso primitivo UAF
Nella guida all'aggiornamento della sicurezza per questa vulnerabilità, il punteggio CVSS indica che "Lo sfruttamento [è] più probabile" e la complessità dell'attacco per questa vulnerabilità è "Bassa". Sebbene Microsoft non fornisca spiegazioni dettagliate per il loro punteggio, ho notato alcuni modelli durante le patch che differenziavano altre vulnerabilità. La vulnerabilità ha probabilmente ricevuto questo punteggio perché deriva da un errore logico, il che la rende attivabile in modo affidabile. Seguendo i passaggi descritti nel Diagramma 5, un aggressore può innescare sistematicamente lo scenario use-after-free illustrato nel blocco di codice 6. Tuttavia, questo non significa che utilizzarlo nella pratica sia semplice. Nella prossima parte di questa serie verrà fornita una panoramica dettagliata delle fasi di sfruttamento.
Comprendere come si è verificato un bug è importante per coltivare un approccio proattivo alle pratiche di sviluppo sicure. Per individuare come è stata introdotta la vulnerabilità, ho analizzato le versioni precedenti del driver ottenute da Winbindex e ho cercato eventuali differenze logiche nella funzione
Nella sezione dedicata alla vulnerabilità, ho menzionato che la causa ultima di questo bug non è stata l'impostazione di
Irp->CurrentStackLocation->FileObject->FsContext2
a
se il processo di chiamata è un processo estero. Con mia sorpresa, ho visto esattamente questa riga di codice in una versione iniziale di mskssrv.sys:
Blocco di codice 7: Versione iniziale di FSRendezvousServer::Close, FsContext2 è impostato su NULL
Nel blocco di codice mostrato sopra,
Blocco di codice 8: FsContext impostato su NULL all'interno di un controllo del flag di funzionalità
Mostrato nel blocco di codice qui sopra è un controllo per le caratteristiche
Feature_Servicing_TeamsUsingMediaFoundationCrashing
.
I flag delle funzioni sono una componente di Windows che attiva varie funzionalità ed esperimenti, anche se non ci sono molte informazioni pubbliche a riguardo. In questo post sul blog, discutiamo di come i flag delle funzioni sono stati utilizzati per le patch di vulnerabilità. I flag delle funzioni sono talvolta utilizzati per testare una funzionalità prima che venga adottata ufficialmente. In questo caso, se la funzione
Feature_Servicing_TeamsUsingMediaFoundationCrashing
è abilitata, non è impostato su NULL, introducendo la vulnerabilità. Questa caratteristica è stata osservata come abilitata di default nelle installazioni di Windows 10. In Windows 11 e come mostrato nel blocco di codice 1, questa funzione condizionale non è presente e il puntatore non è impostato su , rendendolo anche vulnerabile.
A causa del nome della funzione, ho approfondito le funzionalità di Microsoft Teams, il software di videoconferenza. Ho confermato che l'applicazione può utilizzare la funzionalità MSKSSRV per condividere flussi multimediali tra processi. È possibile che la condivisione degli handle dello stream stesse causando il crash di Teams. Un argomento interessante per ulteriori ricerche potrebbe essere quello di esaminare come Teams condivide gli handle del dispositivo MSKSSRV tra i processi e perché l'esecuzione di una corretta pulizia del puntatore potrebbe causare il crash dell'applicazione.
Questa parte della serie è incentrata sulla vulnerabilità stessa, che include la relativa patch. Ero molto interessato a esaminare la patch per questo bug, dato che la correzione che avevo proposto sembrava causare crash in Microsoft Teams. È importante ricordare che, a questo punto, devo ancora esaminare come un'applicazione reale utilizzi in pratica il driver MSKSSRV. La mancanza di questo contesto introduce punti ciechi nella comprensione del motivo per cui un sistema è progettato in un certo modo. Una patch completa per questo bug richiederebbe una ristrutturazione del codice di base e potrebbe rivelare maggiori dettagli su come dovrebbe funzionare il sistema IPC. Speravo anche di ottenere qualche insight sulle pratiche di codifica sicura dagli sviluppatori Microsoft.
Con mia grande delusione, l'errore logico che ha causato la vulnerabilità, che è stato corretto negli aggiornamenti di sicurezza di giugno 2024, non è stato affrontato direttamente. Invece, è stato aggiunto un controllo del token di accesso prima dei percorsi di codice vulnerabili. Vedi il codice qui sotto per il contesto iniziale IOCTL, gestito dalla funzione
Blocco di codice 9: la funzione IOCTL inizia con un controllo delle funzioni e verifica se il processo chiamante è un server di frame
La funzione sopra inizia verificando se una funzione è abilitata. Probabilmente si tratta del flag delle funzioni corrispondente alla patch. Se la funzione è abilitata,
Diamo un'occhiata a
Blocco di codice 10: KsIsCurrentProcessFrameServer che esegue un controllo SID sul token di accesso del thread chiamante
Questa funzione controlla il token del thread chiamante rispetto a due specifici identificatori di sicurezza (SID). I SID corrispondono a un token nel gruppo . Se uno dei SID è abilitato nel token di accesso del thread chiamante, allora il codice della funzione vulnerabile può essere eseguito.
Dopo aver visto questo, ho sospettato che probabilmente era ancora presente un bug tra Amministratore e Kernel. Alla fine, i problemi di corruzione della memoria non sono stati affatto risolti. L'ho confermato apportando una leggera modifica al mio sfruttamento originale: un utente amministratore può avviare il servizio , aprire un handle sul servizio e creare il processo di sfruttamento usando l'handle. Sono riuscito a ottenere una primitiva R/W completa del kernel su un sistema completamente patchato.
Sebbene Microsoft non consideri Amministratore a Kernel un limite di sicurezza, bug simili sono stati usati dagli attori delle minacce per ottenere una primitiva R/W del kernel e utilizzarla per l'accecamento del Rilevamento e risposta degli endpoint (EDR) e le operazioni sui rootkit. Se ti interessano i tipi di cose che si possono fare con questa primitiva, dai un'occhiata al mio intervento a BlackHat insieme a FuzzySec.
Questo post si è concentrato sulla parte di ricerca sulle vulnerabilità del mio progetto Pwn2Own, che consisteva nel trovare una vulnerabilità kernel 0-day che può essere sfruttata per l'escalation dei privilegi. Questo post delinea il percorso: trarre ispirazione da altre ricerche, non riuscire a trovare un bug, scegliere una nuova prospettiva, trovare qualcosa di sospetto e infine individuare dove risiede la vulnerabilità. Ora che è stato identificato un bug ed è disponibile una primitiva use-after-free, il resto dovrebbe essere semplice, giusto? Microsoft sembra pensarla così: ha valutato questo bug come "sfruttamento più probabile" con complessità di attacco "bassa". Hanno ragione? Nella prossima parte parlerò della strategia di sfruttamento e svelerò il significato del titolo della serie!
Resta al passo con le tendenze più importanti e interessanti del settore relative ad AI, automazione, dati e oltre con la newsletter Think. Leggi l' Informativa sulla privacy IBM.
Andréa Piazza, per i diagrammi straordinari
Emma Kirkpatrick, per avermi spiegato pazientemente il modello di sicurezza di Windows