"Patch Tuesday, Exploit Wednesday" è un vecchio detto degli hacker che si riferisce alla strumentalizzazione delle vulnerabilità il giorno successivo alla pubblicazione delle patch di sicurezza mensili. Con il miglioramento della sicurezza e il progressivo perfezionamento delle misure di mitigazione degli exploit, è aumentata la quantità di ricerca e sviluppo necessaria per utilizzare un exploit. Questo è particolarmente rilevante per le vulnerabilità di corruzione della memoria.
Figura 1: Cronologia dello sfruttamento
Tuttavia, con l'aggiunta di nuove caratteristiche (e codice C non sicuro per la memoria) nel kernel di Windows 11, possono essere introdotte nuove superfici di attacco. Concentrandoci su questo codice appena introdotto, dimostriamo che le vulnerabilità che possono essere sfruttate in modo banale si verificano ancora frequentemente. In questo post sul blog, analizziamo e sfruttiamo una vulnerabilità nel driver di funzione accessoria di Windows per Winsock, afd.sys, per l'escalation dei privilegi locali (LPE) su Windows 11. Sebbene nessuno di noi avesse alcuna esperienza precedente con questo modulo del kernel, siamo stati in grado di diagnosticare, riprodurre e armare la vulnerabilità in circa un giorno. Puoi trovare il codice da utilizzare qui.
Basandoci sui dettagli di CVE-2023-21768 pubblicati dal Microsoft Security Response Center (MSRC), la vulnerabilità si trova nel driver di funzione accessoria (AFD), il cui file binario è afd.sys. Il modulo AFD è il punto di ingresso del kernel per l'API Winsock. Utilizzando queste informazioni, abbiamo analizzato la versione del driver del dicembre 2022 e l'abbiamo confrontata con la versione appena rilasciata nel gennaio 2023. Questi campioni possono essere ottenuti individualmente da Winbindex senza il processo lungo di estrazione delle modifiche dalle patch Microsoft. Le due versioni analizzate sono mostrate di seguito.
Ghidra è stato utilizzato per creare esportazioni binarie per entrambi questi file in modo che potessero essere confrontati in BinDiff. Di seguito è mostrata una panoramica delle funzioni abbinate.
Figura 2: Confronto binario di AFD.sys
Sembra che solo una funzione sia stata modificata,
Prima della patch,
Figura 3: afd!AfdNotifyRemoveIoCompletion prima della patch
Dopo la patch, afd.sys versione 10.0.22621.1105.
Figura 4: afd!AfdNotifyRemoveIoCompletion dopo la patch
Questa modifica mostrata sopra è l'unico aggiornamento della funzione identificata. Un'analisi rapida ha mostrato che viene effettuato un controllo basato su è zero (indicando che la chiamata proviene dal kernel), un valore viene scritto su un puntatore specificato da un campo in una struttura sconosciuta. Se, d'altra parte, non è zero, allora viene chiamato ProbeForWrite per assicurarsi che il puntatore indicato nel campo sia un indirizzo valido che risieda in modalità utente.
Questo controllo manca nella versione pre-patch del driver. Poiché la funzione contiene un'istruzione switch specifica per , l'ipotesi è che lo sviluppatore avesse intenzione di aggiungere questo controllo, ma se ne sia dimenticato (a tutti noi manca il caffè, a volte ☕!).
Da questo aggiornamento, possiamo dedurre che un aggressore può raggiungere questo percorso di codice con un valore controllato infield_0x18
Il prototipo della funzione stessa contiene sia il valore che un puntatore alla struttura sconosciuta, rispettivamente come primo e terzo argomento.
Figura 5: Prototipo della funzione afd!AfdNotifyRemoveIoCompletion
Ora conosciamo la posizione della vulnerabilità, ma non come attivare l'esecuzione del percorso di codice vulnerabile. Faremo un po' di reverse engineering prima di iniziare a lavorare su una Proof-of-Concept (PoC).
Innanzitutto, la funzione vulnerabile è stata confrontata per capire dove e come veniva utilizzata.
Figura 6: Riferimenti incrociati afd!AfdNotifyRemoveIoCompletion
Una singola chiamata alla funzione vulnerabile viene effettuata in
Ripetiamo il processo, cercando riferimenti incrociati a
Figura 7: afd!AfdIrpCallDispatch
Questa tabella contiene le routine di dispatch per il driver AFD. Le routine di dispatch vengono utilizzate per gestire le richieste provenienti dalle applicazioni Win32 chiamando DeviceIoControl. Il codice di controllo per ogni funzione si trova in
Tuttavia, il puntatore sopra non rientra nella tabella
Figura 8: afd!AfdIoctlTable
Vale la pena notare che si tratta dell'ultimo codice di controllo input/output (IOCTL) nella tabella, il che indica che AfdNotifySock è probabilmente una nuova funzione di dispatch aggiunta di recente al driver AFD.
A questo punto avevamo un paio di opzioni. Potevamo effettuare il reverse engineering della corrispondente API Winsock in uno spazio utente per comprendere meglio come è stata chiamata la funzione kernel sottostante, oppure effettuare il reverse engineering del codice kernel e richiamarlo direttamente. In realtà non sapevamo a quale funzione Winsock corrispondesse
Ci siamo imbattuti in un codice pubblicato da x86matthew che esegue operazioni di socket chiamando direttamente il driver AFD, senza usare la libreria Winsock. Questo è interessante dal punto di vista stealth, ma per i nostri scopi è un buon modello per creare un handle per un socket TCP per fare richieste IOCTL al driver AFD. Da lì, siamo riusciti a raggiungere la funzione target, come dimostrato dal raggiungimento di un breakpoint impostato in WinDbg durante il debug del kernel.
Figura 9: Breakpoint afd!AfdNotifySock
Ora, torniamo al prototipo della funzione
A questo punto, non sappiamo come popolare i dati in lpInBuffer, che chiameremo
Esaminiamo ciascuno dei controlli.
Il primo controllo che incontriamo è all'inizio di
Figura 10: Controllo delle dimensioni di afd!AfdNotifySock
Questo controllo ci dice che la dimensione di
Il controllo successivo convalida i valori in vari campi della nostra struttura:
Figura 11: Validazione della struttura afd!AfdNotifySock
Al momento non sapevamo a cosa corrispondessero nessuno dei campi, quindi abbiamo passato un array di
Il controllo successivo che incontriamo avviene dopo una chiamata a ObReferenceObjectByHandle. Questa funzione prende come primo argomento il primo campo della nostra struttura di input.
Figura 12: afd!AfdNotifySock chiama nt!ObReferenceObjectByHandle
La chiamata deve avere esito positivo per procedere al percorso di esecuzione del codice corretto, il che significa che dobbiamo passare un handle valido a un
Dopodiché, raggiungiamo un ciclo il cui contatore era uno dei valori della nostra struttura:
Figura 13: Ciclo afd!AfdNotifySock
Questo ciclo controlla un campo della nostra struttura per verificare che contenga un puntatore valido in modalità utente e vi copia i dati. Il puntatore viene incrementato dopo ogni iterazione del ciclo. Abbiamo inserito i puntatori con indirizzi validi e impostato il contatore a 1. Da qui, siamo finalmente riusciti a raggiungere la funzione vulnerabile
Figura 14: Chiamata di afd!AfdNotifyRemoveIoCompletion
Una volta entrati in
Figura 15: Controllo del campo afd! Afd!AfdNotifyRemoveIoCompletion
Infine, l'ultimo controllo da superare prima di raggiungere il codice target è una chiamata a che deve restituire 0 ().
Questa funzione si bloccherà finché:
IoCompletionObject
.Controlliamo il valore di timeout tramite la nostra struttura, ma semplicemente impostare un timeout pari a 0 non è sufficiente affinché la funzione restituisca un risultato positivo. Affinché questa funzione venga restituita senza errori, deve essere disponibile almeno un record di completamento. Dopo alcune ricerche, abbiamo trovato la funzione non documentata NtSetIoCompletion, che incrementa manualmente il contatore delle I/O pendenti su un . Chiamare questa funzione sul creato in precedenza assicura che la chiamata a restituisca .
Figura 16: Controllo in afd!AfdNotifyRemoveIoCompletion e ritorno a nt!IoRemoveIoCompletion
Ora che possiamo raggiungere il codice vulnerabile, possiamo riempire il campo appropriato nella nostra struttura con un indirizzo arbitrario a cui scrivere. Il valore che scriviamo all'indirizzo proviene da un intero il cui puntatore viene passato alla chiamata a
Figura 17: Valore restituito da nt!KeRemoveQueueEx
Figura 18: Uso del valore restituito da nt!KeRemoveQueueEx
Nel nostro proof of concept, questo valore di scrittura è sempre uguale a . Abbiamo ipotizzato che il valore di ritorno di rappresenti il numero di elementi rimossi dalla coda, ma non abbiamo approfondito. A questo punto avevamo la primitiva necessaria e abbiamo proseguito con il completamento della catena di exploit. Successivamente abbiamo confermato che questa ipotesi era corretta e il valore di scrittura può essere incrementato arbitrariamente con ulteriori chiamate a sul .
Avendo la possibilità di scrivere un valore fisso (0x1) a un indirizzo arbitrario del kernel, abbiamo trasformato questa condizione in una completa capacità di lettura/scrittura arbitraria nel kernel. Poiché questa vulnerabilità interessa le ultime versioni di Windows 11 (22H2), abbiamo scelto di utilizzare una corruzione dell'oggetto Windows I/O ring per creare la nostra primitiva. Yarden Shafir ha scritto diversi eccellenti articoli sui Windows I/O ring e ha inoltre sviluppato e divulgato la primitiva che abbiamo utilizzato nella nostra catena di exploit. Per quanto ne sappiamo, questa è la prima volta che questa primitiva viene impiegata in un exploit pubblico.
Quando un I/O ring viene inizializzato da un utente, vengono create due strutture separate, una nello spazio utente e una nello spazio kernel. Queste strutture sono mostrate di seguito.
L'oggetto nel kernel corrisponde a
Figura 19: Inizializzazione nt!_IORING_OBJECT
Si noti che l'oggetto nel kernel ha due campi,
Dal lato dello spazio utente, quando si chiama kernelbase!CreateIoRing si ottiene un handle I/O ring in caso di successo. Questo handle è un puntatore a una struttura non documentata (HIORING). La nostra definizione di questa struttura è stata ottenuta dalla ricerca condotta da Yarden Shafir.
typedef struct _HIORING {
HANDLE handle;
NT_IORING_INFO Info;
ULONG IoRingKernelAcceptedVersion;
PVOID RegBufferArray;
ULONG BufferArraySize;
PVOID Unknown;
ULONG FileHandlesCount;
ULONG SubQueueHead;
ULONG SubQueueTail;
};
Se una vulnerabilità, come quella trattata in questo post sul blog, consente di aggiornare i campi
Come abbiamo visto in precedenza, siamo in grado di utilizzare la vulnerabilità per scrivere 0x1 in qualsiasi indirizzo del kernel che desideriamo. Per impostare la primitiva dell'I/O ring possiamo semplicemente attivare la vulnerabilità due volte.
Nel primo trigger impostiamo
Figura 20: nt!_IORING_OBJECT attiva per la prima volta il bug
E nel secondo trigger impostiamo RegBuffers a un indirizzo che possiamo allocare nello spazio utente (come 0x0000000100000000).
Figura 21: nt! _IORING_OBJECT attiva per la seconda volta il bug
Non resta che accodare le operazioni di I/O scrivendo puntatori a strutture
Figura 22: Configurazione dello spazio utente per la primitiva di lettura/scrittura (R/W) del kernel tramite I/O ring
Uno di queste strutture
Figura 23: Esempio di operazione contraffatta su I/O ring
Per eseguire una scrittura arbitraria, un'operazione di I/O è incaricata di leggere i dati da un handle file e scriverli in un indirizzo del kernel.
Figura 24: Scrittura arbitraria tramite I/O ring
Al contrario, per eseguire una lettura arbitraria, un'operazione di I/O viene incaricata di leggere dati da un indirizzo del kernel e scrivere tali dati in un handle di file.
Figura 25: Lettura arbitraria tramite I/O ring
Con la primitiva configurata, non resta che utilizzare alcune tecniche standard di post-sfruttamento del kernel per ottenere il token di un processo elevato come System (PID 4) e sovrascrivere il token di un altro processo.
Dopo il rilascio pubblico del nostro codice exploit, Xiaoliang Liu (@flame36987044) di 360 Icesword Lab ha annunciato pubblicamente per la prima volta di aver scoperto un campione che sfrutta questa vulnerabilità in natura (ITW) all'inizio di quest'anno. La tecnica utilizzata dal campione ITW era diversa dalla nostra. L'aggressore attiva la vulnerabilità usando la corrispondente funzione API di Winsock, , invece di chiamare direttamente il driver , come nel nostro exploit.
La dichiarazione ufficiale di 360 Icesword Lab è la seguente:
"360 IceSword Lab si concentra sul rilevamento e sulla difesa APT. Basandoci sul nostro sistema radar per vulnerabilità zero-day, abbiamo scoperto un campione di exploit di CVE-2023-21768 in natura a gennaio di quest'anno, che differisce dagli exploit annunciati da @chompie1337 e @FuzzySec in quanto sfrutta meccanismi di sistema e caratteristiche della vulnerabilità. L'exploit è correlato a e , rileva il numero di volte in cui viene chiamato, e noi sfruttiamo questo dato per modificare il conteggio dei privilegi."
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.
Potresti notare che in alcune parti del reverse engineering la nostra analisi è superficiale. A volte è utile osservare solo alcuni cambiamenti di stato rilevanti e trattare alcune parti del programma come una black box, per evitare di perdersi in un tunnel irrilevante. Questo ci ha permesso di utilizzare rapidamente un exploit, anche se massimizzare la velocità di completamento non era il nostro obiettivo.
Inoltre, abbiamo condotto una revisione delle differenze delle patch su tutte le vulnerabilità segnalate inafd.sys
La mancanza di supporto per Supervisor Mode Access Protection (SMAP) nel kernel di Windows ci lascia numerose possibilità per costruire nuove primitive di exploit solo dati. Queste primitive non sono realizzabili in altri sistemi operativi che supportano SMAP. Ad esempio, consideriamo la CVE-2021-41073, una vulnerabilità nell'implementazione dei buffer pre-registrati di I/O ring in Linux (la stessa funzionalità che sfruttiamo in Windows per una primitiva di lettura/scrittura). Questa vulnerabilità può permettere di sovrascrivere un puntatore kernel per un buffer registrato, ma non può essere usata per costruire una primitiva R/W arbitraria perché se il puntatore viene sostituito con un puntatore utente e il kernel cerca di leggerlo o scrivervi, il sistema si bloccherà.
Nonostante i migliori sforzi di Microsoft per eliminare le amate primitive di exploit, è inevitabile che ne vengano scoperte di nuove che ne prendano il posto. Siamo riusciti a utilizzare l'ultima versione di Windows 11 22H2 senza incontrare alcuna mitigazione o vincolo dovuto a caratteristiche di sicurezza basate sulla virtualizzazione come HVCI.