Il mese scorso, Microsoft ha corretto una vulnerabilità nel Microsoft Kernel Streaming Server, un componente del kernel Windows utilizzato nella virtualizzazione e condivisione dei dispositivi fotocamera. La vulnerabilità, CVE-2023-36802, consente a un aggressore locale di aumentare i privilegi a SYSTEM.
Questo post sul blog descrive il mio processo di esplorazione di una nuova superficie di attacco nel kernel di Windows, trovare una vulnerabilità 0-day, esplorare una classe di bug interessante e costruire un exploit stabile. Questo post non richiede alcuna conoscenza specialistica del kernel di Windows per essere seguito, anche se una comprensione di base della corruzione della memoria e dei concetti di sistema operativo è utile. Spiegherò anche le basi dell'analisi iniziale su un driver kernel sconosciuto e semplificherò il processo di ricerca di un nuovo target.
Microsoft Kernel Streaming Server (mskssrv.sys) è un componente di un servizio di Windows Multimedia Framework, Frame Server. Il servizio virtualizza il dispositivo della fotocamera e consente la condivisione del dispositivo tra più applicazioni.
Ho iniziato a esplorare questa superficie di attacco dopo aver notato CVE-2023-29360, che inizialmente era stata elencata come una vulnerabilità del driver TPM. Il bug si trova in realtà nel Microsoft Kernel Streaming Server. Anche se all'epoca non conoscevo MS KS Server, il nome di questo driver era sufficiente a catturare il mio interesse. Nonostante non sapessi nulla dello scopo o della funzionalità, ho pensato che un server di streaming nel kernel potesse essere un luogo fruttuoso per cercare le vulnerabilità. Andando alla cieca, ho cercato di rispondere alle seguenti domande:
Per rispondere alla prima domanda, ho iniziato analizzando il binario in un disassembler. Ho individuato rapidamente la vulnerabilità di cui sopra, un bug logico semplice ed elegante. Il problema sembrava semplice da attivare e da utilizzare appieno, quindi ho cercato di sviluppare un rapido proof-of-concept per capire meglio il funzionamento interno del driver mskssrv.sys.
Newsletter di settore
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.
L'abbonamento sarà fornito in lingua inglese. Troverai un link per annullare l'iscrizione in tutte le newsletter. Puoi gestire i tuoi abbonamenti o annullarli qui. Per ulteriori informazioni, consulta l'Informativa sulla privacy IBM.
Innanzitutto, dobbiamo essere in grado di raggiungere il driver da un'applicazione in spazio utente. La funzione vulnerabile è raggiungibile dalla routine DispatchDeviceControl del driver, il che significa che può essere raggiunta emettendo un IOCTL al driver. Per farlo, è necessario ottenere un handle verso il driver tramite una chiamata a CreateFile utilizzando il percorso del driver. Tipicamente, trovare il nome/percorso del dispositivo è semplice da identificare: trovare una chiamata a IoCreateDevice nel driver ed esaminare il terzo parametro che contiene il nome del dispositivo.
Funzione all'interno di mskssrv.sys che chiama IoCreateDevice con un puntatore NULL per il nome del dispositivo
In questo caso, il parametro per il nome del dispositivo è NULL. Il nome della funzione chiamante suggerisce che mskssrv è un driver PnP, e la chiamata a IoAttachDeviceToDeviceStack indica che l'oggetto dispositivo creato fa parte di uno stack di dispositivi. In pratica ciò significa che vengono chiamati più driver quando viene inviata una richiesta di I/O a un dispositivo. Per i dispositivi PnP, è necessario il percorso dell'interfaccia del dispositivo per accedere al dispositivo.
Usando il kernel debugger WinDbg possiamo vedere quali dispositivi appartengono al driver mskssrv e allo stack di dispositivi:
Output dai comandi !drvobj e !devobj che mostrano i dispositivi superiori e inferiori
Qui sopra vediamo che il dispositivo di mskssrv è collegato all'oggetto dispositivo inferiore appartenente al driver swenum.sys e ha un dispositivo superiore collegato a ksthunk.sys.
Da Device Manager possiamo trovare l'ID dell'istanza del dispositivo di destinazione:
La gestione dispositivi mostra l'ID dell'istanza del dispositivo e il GUID dell'interfaccia.
Ora abbiamo informazioni sufficienti per ottenere il percorso dell'interfaccia del dispositivo utilizzando il gestore di configurazione o le funzioni di SetupApi. Usando il percorso dell'interfaccia del dispositivo recuperato, possiamo aprire un handle sul dispositivo.
Infine, ora possiamo attivare l'esecuzione del codice all'interno di mskssrv.sys. Quando il dispositivo viene creato, viene chiamata la funzione di creazione di dispatch PnP del driver. Per attivare un'ulteriore esecuzione del codice, possiamo inviare IOCTL per comunicare con il dispositivo che verrà eseguito nella funzione di controllo del dispositivo dispatch del driver.
Quando si esegue un'analisi binaria, è buona prassi utilizzare una combinazione di strumenti statici (disassembler, decompiler) e dinamici (debugger). WinDbg può essere utilizzato per eseguire il debug del kernel del driver di destinazione. Impostando alcuni punti di interruzione nei punti in cui ci si aspetta che avvenga l'esecuzione del codice (creazione di dispatch, controllo del dispositivo di dispatch).
All'inizio ho avuto qualche difficoltà: nessuno dei breakpoint che ho impostato all'interno del driver veniva raggiunto. Ho avuto il dubbio di aver aperto il dispositivo giusto o di aver fatto qualcosa di sbagliato. In seguito mi sono reso conto che i miei breakpoint venivano annullati perché il driver veniva scaricato. Ho cercato delle risposte su internet, ma non ho trovato molti risultati cercando mskssrv, nonostante sia caricato e accessibile per impostazione predefinita su Windows. Tra i pochi risultati che ho trovato, c'era un thread su OSR, dove qualcun altro ha incontrato un problema simile.
A quanto pare, i driver dei filtri PnP possono essere scaricati se non sono stati usati da un po' di tempo, e ricaricati su richiesta quando necessario.
Ho risolto i problemi impostando i punti di interruzione dopo che un handle del dispositivo era stato aperto, ma prima di chiamare DeviceIoControl, per assicurarmi che il driver fosse stato caricato di recente.
Il driver mskssrv è un file binario di soli 72 KB e supporta i codici di controllo Device IO che richiamano le seguenti funzioni:
Osservando i nomi di questi simboli possiamo dedurre alcune funzionalità del driver, qualcosa che riguarda la trasmissione e la ricezione di flussi. A questo punto ho approfondito la funzionalità prevista per il driver. Ho trovato questa presentazione di Michael Maltsev sul framework multimediale di Windows, dove ho capito che il driver fa parte di un meccanismo inter-processo per condividere flussi di telecamera.
Poiché il driver non è molto grande e non ci sono molte IOCTL, potrei esaminare ogni funzione per avere un'idea degli interni del driver. Ogni funzione IOCTL opera su un oggetto di registrazione del contesto o su un oggetto di registrazione del flusso, che viene allocato e inizializzato tramite i corrispondenti IOCTL "Initialize". Il puntatore all'oggetto è memorizzato Irp->CurrentStackLocation->FileObject->FsContext2. FileObject punta all'oggetto file del dispositivo creato per ogni file aperto, e FsContext2 è un campo pensato per memorizzare i metadati per ogni oggetto file.
Ho notato questo bug mentre cercavo di capire come comunicare direttamente con il driver, rinunciando prima all'analisi dei componenti della modalità utente, fsclient.dll e frameserver.dll. Ho quasi perso di vista il bug, perché ho dato per scontato che gli sviluppatori avessero creato un semplice controllo che poi è stato ignorato. Diamo un'occhiata alla funzione PublishRx IOCTL:
Snippet di decompilazione FSRendezvousServer::PublishRx
Dopo che l'oggetto stream viene recuperato da FsContext2, viene chiamata la funzione FSRendezvousServer::FindObject, per verificare che il puntatore corrisponda a un oggetto trovato in due elenchi memorizzare dal server globale FSRendezvousServer. Inizialmente, pensavo che questa funzione avesse un modo per verificare il tipo di oggetto richiesto. Tuttavia, la funzione restituisce TRUE se il puntatore viene trovato nell' elenco degli oggetti del contesto o nell'elenco degli oggetti del flusso. Si noti che a FindObject non viene passata alcuna informazione sul tipo di oggetto. Ciò significa che un oggetto contesto può essere passato come oggetto flusso. Si tratta di una vulnerabilità di confusione del tipo di oggetto! Si verifica in ogni funzione IOCTL che opera sugli oggetti di flusso. Per correggere la vulnerabilità, Microsoft ha sostituito FSRendezvousServer::FindObject con FSRendezvousServer::FindStreamObject, che prima verifica che l'oggetto sia un oggetto stream controllando un campo di tipo.
Poiché gli oggetti di registrazione del contesto sono più piccoli (0x78 byte) degli oggetti di registrazione del flusso (0x1D8 byte), le operazioni sugli oggetti del flusso possono essere eseguite su memoria fuori dai limiti:
Illustrazione della vulnerabilità per confusione del tipo di oggetto
Per utilizzare la primitiva di vulnerabilità, dobbiamo avere la possibilità di controllare la memoria fuori dai limiti a cui si accede. Questo può essere fatto innescando l'allocazione di molti oggetti nella stessa area di memoria dell'oggetto vulnerabile. Questa tecnica si chiama spraying heap o pool. L'oggetto vulnerabile è allocato in un pool di heap Non-Paged a bassa frammentazione. Possiamo usare la tecnica classica di Alex Ionescu per spruzzare buffer che danno il controllo totale dei contenuti della memoria sotto un 0x30 byte DATA_QUEUE_ENTRY header. Spruzzando con questa tecnica, possiamo ottenere la disposizione della memoria mostrata nel diagramma:
Utilizzando il metodo scelto per la spruzzatura in piscina, i campi negli offset degli oggetti all'interno degli intervalli 0xC0-0x10F e 0x150-0x19F possono essere controllati. Ho rivisitato ancora una volta le funzioni IOCTL per gli oggetti di flusso per cercare primitive di exploit. Ho cercato luoghi in cui i campi degli oggetti controllabili sono accessibili e manipolati.
Ho trovato un buon primitivo write-where costante nell'IOCTL di PublishRx. Questa primitiva può essere utilizzata per scrivere un valore costante a un indirizzo di memoria arbitrario. Diamo un'occhiata a un frammento della funzione FSStreamReg: :PublishRx:
Snippet di decompilazione FSStreamReg::PublishRx
L'oggetto stream contiene una testa di elenco a offset 0x188 che descrive una lista di oggetti FSFrameMdl. Nel frammento di decompilazione sopra, questa lista viene iterata e, se il valore del tag nell'oggetto FSFrameMdl corrisponde al tag nel buffer di sistema passato dall'applicazione, viene chiamata la funzione FSFrameMdl::UnmapPages.
Utilizzando la primitiva di exploit sopra citata, è possibile controllare completamente la FSFrameMdlList e quindi l'oggetto FsFrameMdl a cui punta pFrameMdl. Vediamo ora UnmapPages:
Decompilazione FSFrameMdl:UnmapPages
Nell'ultima riga della funzione decompilata sopra, il valore costante 2 viene scritto in un valore offset di questo oggetto (FSFrameMdl) che è controllabile. Questa scrittura costante può essere utilizzata insieme alla tecnica dell'anello I/O per ottenere una lettura, scrittura e escalation di privilegi arbitraria del kernel. Puoi saperne di più su come funziona questa tecnica qui e qui.
Anche se ho scelto di utilizzare la primitiva di scrittura costante, un'altra primitiva exploit utile appare anche in questa funzione. Sia gli argomenti BaseAddress che MemoryDescriptorList alla chiamata a MmUnmapLockedPages sono controllabili. Questo potrebbe essere usato per demappare una mappatura a un indirizzo virtuale arbitrario e costruire un primitivo simile a use-after-free .
A questo punto, sono state identificate diverse primitive di utilizzare che forniscono lettura-scrittura arbitraria del kernel. Potresti aver notato che ci sono diversi controlli sul contenuto dell'oggetto stream che devono essere superati per attivare il percorso di codice desiderato. Per la maggior parte, lo stato corretto dell'oggetto può essere ottenuto tramite la pool spraying. Tuttavia, ho riscontrato un problema che ha causato qualche difficoltà. Di seguito viene mostrato un frammento di codice di FSStreamReg::PublishRx dopo aver eseguito il ciclo di FSFrameMdllist:
Snippet di decompilazione FSStreamReg::PublishRx
Nella decompilazione precedente, bPagesUnmapped è una variabile booleana che viene impostata se viene chiamato FSFrameMdl::UnmapPages. Se è così, allora viene recuperata 0x1a8 offset dell'oggetto stream e, se non è nullo, viene chiamato KeSetEvent .
Questo offset corrisponde alla memoria fuori limite e punta all'interno di un POOL_HEADER, la struttura dati che separa le allocazioni di buffer nel pool. In particolare, punta al campo ProcessBilled, che viene utilizzato per memorizzare un puntatore all'oggetto _EPROCESS per il processo che viene "addebitato" con l'allocazione. Viene utilizzato per tenere conto del numero di allocazioni di pool che un particolare processo può avere. Non tutte le allocazioni di pool vengono "addebitate" a un processo e quelle che non lo sono hanno il campo ProcessBilled impostato su NULL in POOL_HEADER. Inoltre, il puntatore EPROCESS memorizzato in ProcessBilled è in realtà sottoposto a XOR con un cookie casuale, quindi ProcessBilled non contiene un puntatore valido.
Ciò rappresenta una difficoltà, poiché i buffer NpFr vengono addebitati al processo chiamante e pertanto viene impostato ProcessBilled . Quando si attiva il primitivo da utilizzare necessario, bPagesUnmapped sarà impostato su TRUE. Se un puntatore non valido viene passato a KeSetEvent, il sistema si blocca. Pertanto, è necessario assicurarsi che la POOL_HEADER sia per un'allocazione non addebitata. A questo punto, ho notato che l'oggetto di registrazione del contesto (Creg) non viene caricato. Tuttavia, questo oggetto non consente il controllo sui contenuti di memoria sull'offset FSFrameMdl . Quindi, sia gli oggetti NpFr che Creg devono essere spruzzati e devono anche essere sequenziati correttamente.
A differenza delle allocazioni big pool, non puoi far trapelare gli indirizzi delle allocazioni LFH tramite NtQuerySystemInformation. Inoltre, l'ordine di allocazione è casuale. Pertanto, non c'è modo di sapere se i buffer adiacenti all'oggetto vulnerabile siano nell'ordine giusto per attivare sia l'exploit sia evitare il crash del sistema. Fortunatamente, la vulnerabilità può essere utilizzata per innescare una perdita di pool dei buffer adiacenti. Diamo un'occhiata alla funzione IOCTL per ConsumeTx:
Snippet di decompilazione FSRendezvousServer::ConsumeTx
Sopra, la funzione FSStreamReg::GetStats è chiamata:
Decompilazione FSStreamReg::GetStats
In questo caso, il contenuto della memoria fuori limite dell'oggetto stream vulnerabile viene copiato nel SystemBuffer che viene restituito all'applicazione user space chiamante. Questa primitiva di fuga di informazioni del pool può essere utilizzata per eseguire un controllo della firma sui buffer adiacenti all'oggetto vulnerabile. È possibile eseguire una scansione di molti oggetti vulnerabili fino a quando non viene individuato l'oggetto all'interno del layout di memoria desiderato. Una volta localizzato l'oggetto desiderato, la disposizione della memoria è la seguente:
CVE-2023-36802 Layout del toelettatore per piscina a bassa frammentazione
Ora, avendo individuato l'oggetto vulnerabile target nella posizione corretta della memoria, la primitiva sopra menzionata sull'oggetto target può essere utilizzata senza mandare in crash il sistema.
Dopo aver segnalato il problema al MSRC, è stato scoperto lo sfruttamento selvaggio della vulnerabilità.
I metodi di sfruttamento presentati in questo post sul blog sono solo alcuni dei numerosi approcci che potrebbero essere adottati. Al momento, non ci sono informazioni pubbliche su come gli aggressori abbiano sfruttato questa vulnerabilità. Può trovare il codice di sfruttamento qui.
L'analisi retroattiva della patch ha rivelato che un'ampia porzione di nuovo codice è stata aggiunta a mskssrv.sys nella build 1809 di Windows 10. Monitorare nuove aggiunte di codice è spesso fruttuoso per individuare vulnerabilità.
Un'altra lezione stanca ma classica da imparare da questa analisi: non fare supposizioni sui controlli eseguiti. Un amico e collega ha suggerito che la confusione di tipo usando FsContext2 potrebbe essere una "classe di bug comune ma poco studiata". Credo che sia necessaria un'analisi più approfondita delle varianti per questa classe di bug, in particolare nei driver che si occupano della comunicazione tra processi.
La scoperta di questa vulnerabilità è avvenuta semplicemente cercando di interagire con una superficie di attacco sconosciuta. Avere una "conoscenza molto vicina allo zero" di un sistema può anche significare avere la mente fresca per romperlo.