
Analisi tecnica e exploit proof-of-concept per CVE-2023-28252, una vulnerabilità di escalation dei privilegi nel driver Windows Common Log File System (CLFS) utilizzata negli attacchi ransomware Nokoyawa.
Da febbraio 2022 è stato segnalato un nuovo ransomware che sembra sfruttare una vulnerabilità 0-day di Windows, secondo la ricerca condotta da Trend Micro.
Maggiori informazioni su questo ransomware si trovano a questo
link.
Secondo l'analisi di Kaspersky, il gruppo ransomware Nokoyawa ha utilizzato altri exploit che colpiscono il driver Common Log File System (CLFS) da giugno 2022, con caratteristiche simili ma distinte, tutti collegati a un singolo sviluppatore di exploit.
Nell'aprile 2023, quando Microsoft ha rilasciato la patch, è stato assegnato
CVE-2023-28252.
In precedenza, nel 2022, un bug simile nello stesso componente era stato studiato da noi e documentato in questo
blogpost
Per affrontare l'analisi, è necessario conoscere il formato del file .blf, gestito dal driver vulnerabile Common Log File System chiamato CLFS.sys e che si trova nella cartella dei driver all'interno di system32.
Maggiori informazioni su questo tipo di file si trovano nei link seguenti:
https://github.com/ionescu007/clfs-docs/blob/main/README.md
https://www.coresecurity.com/core-labs/articles/understanding-cve-2022-37969-windows-clfs-lpe
Questa analisi è fatta per Windows 11 21H2, clfs.sys versione 10.0.22000.1574 sebbene funzioni anche su Windows 10 21H2, Windows 10 22H2, Windows 11 22H2 e Windows server 2022.
Nelle versioni precedenti di Windows, è necessario regolare alcuni valori, altrimenti si produrrebbe un BSOD.
Microsoft Patch Tuesday aprile 2023.
È possibile controllare la versione del driver come mostrato
Quando la vulnerabilità è stata pubblicata, nell'aprile 2023 ho iniziato con Esteban Kazimirow a eseguire il reverse del driver CLFS.sys, sebbene in questo caso, analizzare solo la patch fosse molto difficile per dedurre dove fosse il bug e come innescarlo, poiché lo sfruttamento è molto complesso.
Successivamente, è uscito un blogpost il cui autore, a partire da un campione di malware, ha mostrato alcune parti del codice decompilato da HexRays e alcune informazioni che hanno guidato su dove doveva essere affrontato lo sfruttamento.
Ovviamente le informazioni fornite non erano complete, ma senza questo aiuto sarebbe stato improbabile riuscire a costruire il PoC e poi un exploit funzionante.
Per facilitare la comprensione, spiegheremo prima come costruire il PoC e poi faremo l'analisi della vulnerabilità.
Questo blogpost contiene due sezioni:
Costruzione del PoC:
1-Ottenere gli indirizzi kernel necessari per lo sfruttamento
2-Preparare il percorso per creare i file .blf:
3-Creare il file "trigger blf" usando la funzione CreateLogFile()
4-Costruire il file "trigger blf"
5-Ottenere l'indirizzo kernel del BASE BLOCK di trigger blf
6-Chiamare AddLogContainer con l'handle di trigger blf
7-Preparare i file spray blf
8-Preparare la memoria per eseguire lo spray
9-Innescare il bug
Debugging:
1-Controllare lo spray di memoria
2-Osservare RecordOffset[12] di trigger blf
3-Osservare il valore iFlushBlock nel file spray blf
4-Perché legge da BLOCK 1 SHADOW invece di BLOCK 0 CONTROL ?
5-Perché il checksum è uguale a zero nei file spray blf ?
6-Concludere lo sfruttamento.
7-La vera patch
Creerò una funzione chiamata InitEnvironment per ottenere alcuni indirizzi Kernel necessari.
Ottengo l'indirizzo EPROCESS del mio processo e lo memorizzo nella variabile g_EProcessAddress, poi l'indirizzo EPROCESS del processo SYSTEM e lo memorizzo in system_EPROCESS, poi l'indirizzo EHTREAD del thread principale del mio processo e lo memorizzo in g_EThreadAddress e infine l'indirizzo di PREVIOUS MODE che in questa versione del PoC non verrà utilizzato.

Questo metodo è ben noto, la funzione GetObjectKernelAddress chiama NtQuerySystemInformation due volte con il primo argomento SystemExtendedHandleInformation, la prima chiamata passa una dimensione errata e restituisce errore, ma restituisce anche la dimensione corretta che viene utilizzata nella seconda chiamata e ottiene le informazioni di tutti gli handle, quindi scorrendo in un ciclo le informazioni di ogni handle e nel campo Object del handleinfo corretto si ottiene l'indirizzo cercato nel kernel.

Ho anche bisogno degli indirizzi kernel delle seguenti funzioni esportate da CLFS.sys:
• ClfsEarlierLsn
• ClfsMgmtDeregisterManagedClient
E le funzioni esportate da NTOSKRNL.exe
• RtlClearBit/PoFxProcessorNotification
• SeSetAccessStateGenericMapping
Per ottenere questi indirizzi si usa un metodo simile a quello usato per ottenere la base kernel di entrambi i moduli, chiamando NtQuerySystemInformation due volte, ma in questo caso il primo argomento sarà SYSTEM_INFORMATION_CLASS (nel PoC usiamo la funzione FindKernelModulesBase per questo scopo).
Successivamente carica CLFS.sys e NTOSKRNL.exe come modelli normali in modalità utente chiamando LoadLibrary, ottiene gli indirizzi in modalità utente con GetProcAddress e poi sottrae l'imagebase da ciascuno, ottenendo così l'offset della funzione e infine aggiunge ciascun offset alle corrispondenti basi kernel e in questo modo ottiene gli indirizzi kernel di tutte le funzioni necessarie.

Creo una funzione chiamata createInitialTriggerBlfFile che genererà e scriverà un file .blf.
Il percorso utilizzato come argomento in CreateLogFile è diverso da un percorso normale; per esempio, per aprire il file 1280.blf situato nella cartella C:\Users\Public, dobbiamo impostare il percorso LOG:C:\Users\Public\1280. Questo verrà salvato nella variabile stored_name_CreateLog.
Faccio questo usando wsprintfW() poiché stored_env memorizza il percorso C:\Users\Public, precedentemente ottenuto dalle variabili d'ambiente. A questa stringa anteporrò la stringa LOG: e un nome casuale alla fine, senza l'estensione .blf.

Questo sarà il percorso del mio file iniziale che chiamerò "trigger blf". Naturalmente, devo anche salvare il percorso normale dello stesso file senza il LOG: davanti e con l'estensione BLF per aprirlo e modificarlo con CreateFile(), WriteFile() come qualsiasi altro file; questo percorso sarà, per esempio: C:\Users\Public\1280.blf, e sarà memorizzato nella variabile stored_name_fopen.**

Naturalmente, entrambi i percorsi corrispondono allo stesso file, e devo usarli uno o l'altro a seconda del caso.
La funzione CreateLogFile svolge una funzione abbastanza simile a CreateFile() (crea nuovi file o apre file esistenti e ottiene il loro handle), anche alcuni argomenti sono simili, ma CreateLogFile() funziona solo con file blf.
Inoltre, quando apre un file esistente, verifica che il formato sia corretto, persino che ogni blocco abbia un checksum e se questo non è corretto restituirà un errore.
Creerò 2 tipi di file BLF:
Il Trigger blf
Lo Spray blf
Entrambi sono file blf ma modificati in modo diverso.
In questo modo il PoC crea prima il file "trigger blf", usando CreateLogFile, con il percorso per esempio: LOG:C:\Users\Public\1280 che ho impostato in precedenza, ed era memorizzato nella variabile stored_name_CreateLog.
Il quinto argomento fCreateDisposition, come in CreateFileA(), può assumere i seguenti valori:

In questo caso userò l'argomento OPEN_ALWAYS, quindi il file verrà creato se non esiste e se esiste verrà aperto. Poiché il file non esiste ancora, verrà creato con un nome casuale.
logFile = CreateLogFile(stored_name_CreateLog, GENERIC_READ | GENERIC_WRITE, 1, 0, 4, 0);
CreateLogFile() creerà il nostro file "trigger blf" con i suoi 6 blocchi e i relativi checksum e restituirà l'handle che sarà memorizzato nella variabile logFile.

Ogni blocco avrà dall'offset mostrato nella colonna sinistra un'intestazione la cui dimensione è 0x70 byte.
Quindi, per esempio, l'intestazione del CONTROL BLOCK va dall'offset 0x0 a 0x70.

Tutte le intestazioni di tutti i blocchi hanno la stessa struttura chiamata _CLFS_LOG_BLOCK_HEADER.
Questa è la struttura dell'intestazione:

All'offset 0xC dell'intestazione trovo il checksum, quindi poiché il CONTROL BLOCK inizia all'offset 0, il checksum sarà all'offset 0xC del file e così ogni blocco avrà il suo checksum a 0xC dall'inizio del suo blocco.

Per modificare il file trigger blf, devo aprirlo come un file normale usando CreateFileA o fopen e poi modificarlo con WriteFile o fwrite rispettivamente; eseguo questa operazione all'inizio della funzione fun_prepare del PoC.
Ricorda che il percorso normale è memorizzato nella variabile stored_name_fopen, quindi lo uso per aprire il file con wfopen_s (che è una variante di fopen che supporta stringhe Unicode).
Il file viene modificato nella funzione craftTriggerBlfFile chiamata da fun_prepare.

Poi chiamo fseek per puntare all'offset da modificare e poi con fwrite il file viene modificato.
Le modifiche da apportare al file "trigger blf" sono le seguenti:
Dopo aver apportato queste modifiche, viene chiamata FixCRCFile per calcolare il nuovo checksum e correggere i checksum dei primi 4 blocchi. I due blocchi successivi non hanno modifiche, quindi non è necessario ricalcolare i loro checksum.

Il driver CLFS.sys legge i sei blocchi del file e per memorizzare il loro contenuto effettua un'allocazione nel pool del Kernel.

C'è una struttura molto importante di dimensione 0x90 che nel precedente blogpost su CVE-2022-37969, attraverso il reverse ho trovato alcuni campi e l'ho chiamata pool_0x90. Dopo molto più reverse, ora so che il suo vero nome è m_rgBlocks e mentre il controller alloca memoria per copiare dal file il contenuto di ogni blocco, lì salva la dimensione di ogni blocco, l'offset di inizio e l'indirizzo kernel dove è stato memorizzato.

Ha sei strutture CLFS_METADATA_BLOCK che corrispondono a ciascun blocco per numero.
Ogni struttura CLFS_METADATA_BLOCK è lunga 0x18 byte. (0x18*6=0x90)
All'offset 0 c'è una union, ma almeno in questo exploit viene utilizzato solo il campo pbImage, quindi semplificando sarebbe:
L'allocazione di questa struttura può avvenire da due punti diversi del driver CLFS.sys, a seconda della creazione di un nuovo file o dell'apertura di uno esistente. Nel caso in cui un nuovo file viene creato, il driver alloca i 0x90 byte da CClfsBaseFilePersisted::CreateImage+28A, mentre nel caso di un file esistente li alloca da CClfsBaseFilePersisted::ReadImage+6E.
Successivamente, ottengo l'indirizzo di inizio del blocco 2 che corrisponde al file trigger blf, chiamato BASE BLOCK che inizia all'offset 0x800 e la sua lunghezza è 0x7a00.

All'interno della funzione fun_prepare di seguito, questo indirizzo verrà trovato nel kernel usando questo pezzo di codice.

Prima, la funzione getBigPoolInfo trova tutte le allocazioni nel pool che hanno il tag "Clfs" e una dimensione di 0x7a00, poi le memorizza in un array.
Successivamente riapre il file trigger blf precedentemente modificato usando CreateLogFile con l'argomento OPEN_EXISTING, quindi apre un file esistente; questo eseguirà l'allocazione del suo BASE BLOCK.
Quando getBigPoolInfo viene chiamato di nuovo, ci sarà una nuova pool "Clfs" di dimensione 0x7a00, e il suo indirizzo viene recuperato chiamando NtQuerySystemInformation due volte.
L'indirizzo del BASE BLOCK del file trigger blf è memorizzato nella variabile CLFS_kernelAddrArray.

Nota che se il file trigger blf modificato non ha il checksum corretto, la funzione CreateLogFile() fallirà.
L'ultima parte della funzione fun_prepare chiama l'API AddLogContainer usando l'handle del file trigger blf.

Nell'ultima funzione del PoC chiamata to_trigger verrà creato un secondo tipo di file blf,
Lo chiamerò spray blf.
Questo tipo di file verrà utilizzato per riempire uno spazio di memoria (spray); sono necessari 10 file uguali di questo tipo, ma inizialmente ne viene creato solo uno. 
Verranno creati tre array per memorizzare i nomi casuali di questi file:
stored_log_arrays: memorizza dieci nuovi nomi casuali di file .blf che verranno utilizzati con CreateLogFile.
stored_container_arrays: memorizza nomi casuali per creare dieci nuovi file contenitore.
stored_fopen_arrays: memorizza i nomi dei file di log del primo array (variabile stored_log_arrays), ma con il loro percorso normale (senza la stringa "LOG:") e con l'estensione .blf.

Ad ogni iterazione il file blf viene copiato usando CopyFileW, vengono assegnati i nomi memorizzati negli array.
La funzione fun_trigger chiama craftSprayBlfFile dove vengono apportate modifiche a ciascun file e FixCRCFile correggerà i CRC.

Riassumendo, ho creato 10 file simili (spray blf) con nomi casuali con le seguenti modifiche:

L'ultima modifica è copiare l'intero blocco 0 (CONTROL BLOCK) nel blocco 1 (CONTROL BLOCK SHADOW)

L'effetto di queste modifiche, insieme a quelle apportate al file trigger blf, verrà spiegato più avanti nel capitolo di debug.
Alcune di queste modifiche sono quelle che producono la vulnerabilità, mentre altre sono necessarie solo per bypassare i controlli del driver.
A questo punto i file sono già creati e modificati, pronti per eseguire lo spray; poi quando vengono aperti con CreateLogFile, verranno posizionati nell'area di memoria che desideriamo, come mostreremo in seguito.

Nella funzione to_trigger viene creato un array di 12 elementi, contenente l'indirizzo del BASE BLOCK del file trigger blf più 0x30.
Poi, nella funzione fun_pipeSpray, la memoria viene riempita con uno spray di pipe; al suo interno c'è un ciclo che chiama CreatePipe e crea il numero di pipe passato come primo argomento; il secondo argomento è un array che memorizzerà gli handle di tutte le pipe create.

Dentro un ciclo, chiama CreatePipe creando pipe di lettura-scrittura.
In questo modo vengono prima create 0x5000 pipe e poi chiamate di nuovo per creare altre 0x4000 pipe.
Poi usa WriteFile per scrivere nelle prime 5000 pipe l'array appena creato con gli indirizzi di BASE BLOCK + 0x30 del file trigger blf.

Ora abbiamo già un blocco compatto creato in memoria; verranno rilasciate 0x667 pipe dal numero 0x2000 fino a 0x2667, poiché in memoria le pipe non sono nello stesso ordine in cui sono state create; ciò che accadrà è che ci saranno spazi liberi in questo blocco di memoria.
Nota che le allocazioni delle pipe hanno dimensione utente di 0x90 byte, quindi quando vengono rilasciate avremo
Libera gli spazi di memoria di dimensione 0x90 tra la memoria piena di pipe.
Poi itera per chiamare CreateLogFile con i 10 file spray blf.
Quando CreateLogFile viene chiamato per aprire file esistenti, viene eseguita l'allocazione di 0x90 byte per m_rgBlocks, una per ogni file spray blf, quindi queste allocazioni occuperanno i vuoti lasciati quando sono state rilasciate le pipe, poiché hanno la stessa dimensione.

Poi ripete il processo di scrittura nelle ultime 0x4000 pipe dell'array che contiene l'indirizzo di BASE BLOCK +0x30 di trigger blf.
Tutte queste manipolazioni creano uno spazio di memoria controllato; mostrerò come appare quando viene eseguito il debug, ma l'idea è che m_rgBlocks di ogni file spray blf occupa i vuoti di 0x90 byte che sono stati rilasciati.
Poi, già nella parte finale, il bug viene innescato all'interno di un while( 1 ) usando una chiamata a AddLogContainer per i file spray blf.
All'interno di questo while il bug viene attivato:

Questo while uscirà quando trova il token di Sistema, utilizzando la funzione NtFsControlFile che leggerà gli attributi delle pipe.

Quindi, usando CreateLogFile, sovrascrive nuovamente il token del nostro processo con il token di Sistema appena trovato e in questo modo otteniamo l'elevazione dei privilegi.

Poi ripristina alcuni valori, chiude gli handle delle pipe e dei file blf, ed esegue un Blocco note come Sistema per verificare che abbiamo effettuato l'elevazione correttamente.

Nota i file blf creati nella cartella PUBBLICA. Ricorda che se vuoi riprovare, devi prima eliminare i file creati. alcuni saranno bloccati e non potranno essere cancellati, ma il PoC funzionerà comunque.

Prima di iniziare con l'effetto delle modifiche per innescare blf e spruzzare blf file per eseguire lo sfruttamento, devo verificare che m_rgBlocks di spray blf file si trovino nei buchi che si verificano nella distribuzione della memoria, dopo aver eseguito lo spray di pipe e il successivo rilascio di un numero fisso di pipe.
Al termine di questa procedura, una pipe dovrebbe trovarsi sotto i 0x90 byte di m_rgBlocks, quindi quando m_rgBlocks viene utilizzato, si verificherà un OUT OF BOUNDS e leggerà da quella pipe che si trova sotto.
Il PoC ha un punto ideale per posizionare un breakpoint:

A questo punto, l'apertura dei file spray blf è completa e la funzione AddLogContainer non è ancora stata chiamata.
Per eseguire il debug in modalità utente, userò x64dbg e per la modalità kernel, IDA con il plugin Windbg.

A questo punto la memoria dovrebbe essere già preparata, e posso vedere la distribuzione.
Metterò in pausa IDA per trovare un punto interessante dove posizionare un breakpoint.
Imposterò un breakpoint su CClfsBaseFilePersisted::AddContainer, che viene chiamato da AddLogContainer e all'inizio, ha il registro RCX che punta alla struttura CClfsBaseFilePersisted e all'offset 0x30 c'è un puntatore a m_rgBlocks.

Quando il breakpoint viene raggiunto, controllo nello stack delle chiamate che AddLogContainer viene chiamato dal mio PoC.

il registro RCX punta a:

Il primo campo è il puntatore a una vtable (CLFS! CClfsBaseFilePersisted::'vftable') e all'offset 0x30 c'è il puntatore a m_rgBlocks.

I blocchi 0, 1, 4 e 5 non hanno ancora salvato pbImage, mentre i blocchi 2 (BASE BLOCK) e 3 (SHADOW BLOCK) lo hanno.
Ogni blocco nella tabella m_rgBlocks ha il suo cbOffset che è l'offset dove il blocco inizia nel file, cbImage è la dimensione del blocco, e eBlockType è il tipo di blocco.
Se lo spray è corretto, sotto m_rgBlocks dovrebbe esserci una pipe e al suo interno, i puntatori a BASE BLOCK + 0x30 di trigger blf.

Il comando "!pool" su windbg mostra la distribuzione della memoria:

Ogni m_rgBlocks ha un tag "Clfs" e la sua dimensione è 0xa0 perché è la dimensione utente 0x90 più 0x10 di header e sotto c'è una pipe con il tag "NpFr" che ha la stessa dimensione utente 0x90
Poiché la distribuzione non è una scienza esatta, alcuni "Clfs" sono stati posizionati in modo continuo, il che è indesiderabile, ma quello con cui sto lavorando è posizionato correttamente seguito da una pipe.
Una delle prime modifiche che influiscono è quella fatta nel file trigger blf all'offset 0x858, dove viene memorizzato il valore 0x369.

Il BASE BLOCK inizia all'offset 0x800 nel file.

All'interno del _CLFS_LOG_BLOCK_HEADER all'offset 0x800+0x58 (0x58 dall'inizio dell'header di BASE BLOCK).

All'offset 0x28 inizia l'array RecordOffsets (DWORD).
Spostandosi in avanti di 0x30 byte, all'offset 0x58 (0x828+0x30=0x858 dall'inizio), si trova il campo 12 di RecordOffsets.

Eseguo il PoC fino a CreateLogFile come mostrato nell'immagine qui sotto:


Prima di entrare in CreateLogFile imposterò un breakpoint in un punto in cui il valore 0x369 non è ancora stato utilizzato.
Nel caso in cui CreateLogFile apra un file esistente, la struttura m_rgBlocks viene allocata qui:
CClfsBaseFilePersisted::ReadImage+6E
Quindi, imposterò un breakpoint su IDA proprio qui:

Quando il breakpoint viene attivato**:**

In m_rgBlocks c'è ancora della spazzatura perché non è ancora inizializzato, ma non appena pbImage del blocco 2 viene allocato, l'indirizzo verrà salvato all'offset 0x30 dall'inizio, poiché il primo campo all'interno di ogni CLFS_METADATA_BLOCK è pbImage.


Ora imposto un breakpoint hardware in scrittura: ba w1 ffffd003'7f5bea30
Dopo l'inizializzazione a zero, si ferma quando salva pbImage.

L'analisi dice che corrisponde a blocco0, perché non considera la costante r14*8 che è 0x30 successivamente, di conseguenza sta realmente scrivendo il pbImage del blocco 2.

Nota che CClfsBaseFilePersisted::ReadMetadataBlock viene utilizzato per allocare uno qualsiasi dei blocchi, utilizzando la dimensione passata come argomento.

Ora imposta un breakpoint lettura/scrittura a 0x58 dal blocco base, per vedere quando usa il valore 0x369.
ba r1 FFFF978A'16ECF000+0x58

Quando il breakpoint viene raggiunto, legge il valore 0x369 situato nel RecordOffset[12], lo aggiunge a un puntatore strano su r14 e incrementa il contenuto di RAX+r14.
Poche righe sopra nel codice, ESI ha il valore 0x13 e moltiplica per 0x18, che è la dimensione di ogni blocco in m_rgBlocks.
WINDBG>? 0x18*0x13
Espressione valutata: 456 = 00000000'000001c8
Se aggiungo il valore di r8= 0x1c8 che è maggiore di 0x90, all'indirizzo iniziale di m_rgBlocks, leggerà OUT OF BOUNDS.


Sotto m_rgBlocks, c'è la pipe con il puntatore a BASE BLOCK + 0x30, legge questo puntatore che è stato posizionato strategicamente all'interno della pipe.
La posizione corrente nel
codice è stata chiamata dall'istruzione while(1) del modulo principale.
All'interno del file spray blf ho posizionato strategicamente il valore 0x13 all'offset 0x48a (iFlushBlock).

All'offset 0x8a del file spray blf, si trova iFlushBlock di BLOCK 0, il cui valore è 4, mentre l'offset 0x48a appartiene a iFlushBlock di BLOCK 1, e il suo valore è 0x13

Ora devo scoprire perché legge iFlushBlock = 0x13 da BLOCK 1 invece di iFlushBlock = 4 da BLOCK 0.
Se guardo indietro per scoprire da dove viene 0x13, vedo sullo stack delle chiamate che WriteMetadataBlock viene chiamato da CClfsBaseFilePersisted::ExtendMetadataBlock+416, lì il secondo argomento iFlushBlock è EDX=0x13, che proviene da r9w.


Poche righe prima,
CClfsBaseFile::GetControlRecord è stata chiamata per recuperare l'indirizzo
di BLOCK 0, forse il problema è qui, quindi riavvierò e imposterò un breakpoint su di essa.
GetControlRecord chiama CClfsBaseFile::AcquireMetadataBlock che dovrebbe riempire la tabella m_rgBlocks con l'indirizzo di block 0, quando passo oltre questa funzione ottiene l'indirizzo di block 1, quindi, il problema si verifica all'interno di CClfsBaseFile::AcquireMetadataBlock.
Aggiungendo 0x8A all'indirizzo recuperato, posso confermare che il valore 0x13 che appartiene a BLOCK 1 è presente.

Riavvierò e imposterò un breakpoint lì:

CClfsBaseFile::GetControlRecord+27 chiama CClfsBaseFile::AcquireMetadataBlock

Il secondo argomento passato a AcquireMetadataBlock è zero,
corrisponde a block 0, sta per copiare dal file e memorizzare il suo indirizzo
in m_rgBlocks
.
Nell'enumerazione del tipo di blocco _CLFS_METADATA_BLOCK_TYPE hanno nomi diversi da quelli che ho usato, ma sono gli stessi 6 blocchi.

Dopo aver verificato che il tipo di blocco è inferiore al massimo m_cBlocks=6, salva un valore di riferimento per evitare di leggere lo stesso blocco due volte.

ReadMetadataBlock viene chiamato, il problema di leggere block 1 invece di block 0 sarebbe all'interno di questa funzione.

Se tutto è a posto, alloca usando cbImage come dimensione e memorizza l'indirizzo nel campo block 0-> pbImage in m_rgBlocks.

Il comando !pool mostra il tag e la dimensione allocata.

Quindi, ho già l'indirizzo di
pbImage di block 0 memorizzato in m_rgBlocks, quindi
devo vedere perché copia i byte di block 1 lì invece dei byte di
block 0.
Arrivo a una chiamata a CClfsContainer::ReadSector dove viene passato un puntatore a una variabile contenente pbImage, per scrivere i byte.

Nota le modifiche apportate al contenuto di pbImage quando si passa oltre ReadSector.

Aggiungendo 0x8a a pbImage posso trovare il valore 4 che è il valore corretto, invece di 0x13, quindi il problema deve verificarsi più tardi.
Dopo aver chiamato ClfsDecodeBlock restituisce un errore 0x0C01A000A.
CClfsBaseFilePersisted::ReadMetadataBlock+153 chiama ClfsDecodeBlock
Dopo questo errore, aggiunge 1 al tipo e chiama CClfsBaseFilePersisted::ReadMetadataBlock di nuovo ma con tipo 1 per leggere il blocco 1.

In CClfsBaseFilePersisted::ReadMetadataBlock Alloca e memorizza un nuovo pbImage in m_rgBlocks per block 1.

Blocks 0 e 1 hanno indirizzi diversi, ora se aggiungo 0x8a all'indirizzo di block 1 il suo valore è 0x13.
Forse dal momento che block 0 ha restituito un errore, usa block 1 e lo restituisce a GetControlRecord come Control Block.
Come mostrato in precedenza, quando utilizza il valore 0x13 invece di 4, esce dai limiti di m_rgBlocks e legge i valori dello spray di pipe controllati da me.
Poi libera pbImage da block 0
e copia il puntatore da block 1 a block 0.

Sarebbe necessario trovare il valore che causa l'errore 0x0C01A000A all'interno di ClfsDecodeBlock.
All'interno di ClfsDecodeBlock il checksum del primo blocco è zero, questo è l'errore 0xC01A000A.

Prima di chiamare AddLogContainer, aprendo qualsiasi file spray blf con un editor esadecimale, il checksum è stato cambiato in zero.

dovrebbe essere stato cambiato prima quando è stato aperto con CreateLogFile.

Per qualche motivo i file spray blf terminano dopo l'uscita da CreateLogFile con checksum di block 0 uguale a 0 e restituiscono un handle valido, vediamo perché accade.
Mi fermo su CreateLogFile prima di aprire qualche file spray blf.

Nota che prima di chiamare CreateLogFile, spray files hanno il checksum corretto in block 0 e dopo aver completato la funzione, il valore del checksum cambia in zero.

Quindi, imposto un breakpoint su CClfsBaseFile::GetControlRecord, per guardare all'interno.
Dopo aver passato
CClfsContainer::ReadSector il checksum non è zero.
Prima di entrare per calcolare il CRC32, mette il campo checksum a zero in memoria per calcolare il CRC, e il risultato è corretto.


Poi controlla il valore di eExtendState =2 e va a WriteMetadataBlock.

Qui il checksum è ancora zero in memoria, devo solo vedere quando questo valore viene scritto nel file.

Controlla alcuni valori che sono realizzati nel file blf spray per raggiungere CClfsBaseFilePersisted::ExtendMetadataBlock.

Dopo un ciclo per leggere i blocchi che non sono stati ancora letti, block 0 continua con checksum = 0.

Arrivando a WriteMetadataBlock.

Poiché sto eseguendo prima che sostituisca block 0 con 1, il valore iFlushBlock del file blf spray è ancora 4 il valore corretto.

Ora sta lavorando con block 4, e scriverà block 4 nel file, qui non è ancora il problema.
Poi arriva a CClfsBaseFilePersisted::FlushControlRecord

All'interno raggiunge WriteMetadataBlock, ma con argomento 0, per scrivere block 0 nel file**.

Poi ClfsEncodeBlock restituisce l'errore 0xC01A000A, sebbene scriverà il file con il block 0 errato in CClfsContainer::WriteSector, appena sotto.

La variabile var_54 memorizza il valore di errore 0xC01A000A e verrà controllata prima di uscire dalla funzione.

Ma dopo aver chiamato CClfsContainer::WriteSector che non restituisce errori, il contenuto di var_54 viene sovrascritto con zero.
Quindi, la funzione restituisce zero senza errori e continua a funzionare poiché CreateLogFile restituirà un handle invece di un valore di errore.

Il valore 0x13 in iFlushBlock lo fa uscire dai limiti e leggerà il puntatore che si trova nelle pipe che punta al Base Block +30 di trigger blf.
Then it adds 0x28 to that pointer, ( 0x58 from the beginning of the
base block of the trigger blf) that has the value 0x369.


L'istruzione INC aumenterà il valore 0x14 di 1 e si ripete 4 volte, quindi 0x14 termina a 0x18.
WINDBG>db r14+369
ffffcb82'091e7397 14 00 00 00
Successivamente, viene chiamato CreateLogFile e legge il valore 0x1858.

GetSymbol controlla se il blocco falso
creato in precedenza in trigger blf, puntato dall'offset 0x1858,
ha i valori corretti.

se il puntatore non fosse stato incrementato più volte, avrebbe il valore originale 0x1458 e punterebbe al blocco giusto.
Dopo l'uscita da GetSymbol, utilizzerà quel blocco falso qui.

Poi leggerà il valore dell'offset 0x18
del blocco falso dove ho inserito 0x05000000 e salterà al contenuto di ciò che c'è.
WINDBG>dps 0x5000000
00000000'05000000 00000000'05001000

Legge il contenuto di 0x05000000 e il suo 0x05001000 e lì c'è ClfsEarlierLsn.

Questa funzione è utilizzata per restituire il valore 0xFFFFFFFF in RDX sebbene questa prima volta quel valore non venga utilizzato.
La seconda chiamata avviene qui, chiama PoFxProcessorNotification che era in 0x501000 +8

WINDBG>dps 00000000**'05001000**
00000000'05001000 fffff805'7ab13220 CLFS! ClfsEarlierLsn
00000000'05001008 fffff805'769dc3b0 nt! PoFxProcessorNotification

in questa funzione RCX = 0x05000000 , controlla che 0x40 byte dopo sia diverso da zero
WINDBG>dps rcx+40
00000000'05000040 00000000'05000000
L'indirizzo a cui saltare sarà 0x68 dopo.
WINDBG>dps rcx+68
00000000'05000068 fffff805'7ab2bfb0 CLFS! ClfsMgmtDeregisterManagedClient
E l'argomento sarà 0x48 byte dopo.
WINDBG>dps rcx+48
00000000'05000048 00000000'05000400
La funzione ClfsMgmtDeregisterManagedClient è comoda perché posso controllare l'argomento e ho anche due salti a funzioni controllate da me.

La prima chiamata è di nuovo a ClfsEarlierLsn che ha restituito RDX=0xFFFFFFFF.


prenderà la sorgente da scrivere dal contenuto di RDX=0xFFFFFFFF.
WINDBG>dps rdx
00000000'ffffffff ffff8005'3a4ee000
All'indirizzo 0xFFFFFFFF avevo memorizzato system_EPROCESS & 0xfffffffffffff000.

La destinazione è il puntatore situato in 0x5000400 +0x48
*(UINT64*)0x5000448 = para_PipeAttributeobjInkernel + 0x18;

Il puntatore PipeAttribute nel kernel che punta a un buffer riempito con "A" verrà sovrascritto con la parte alta del puntatore SYSTEM EPROCESS.
Questo puntatore è stato creato quando ho chiamato in precedenza _NtFsControlFile con un buffer pieno di “A” .

Il contenuto di quell'attributo può essere letto usando NtFsControlFile.

Ora l'attributo pipe non punta più al buffer con “A” ma a system_EPROCESS & 0xffffffffffffff000.

Questo codice verrà ripetuto fino a quando non viene recuperato il token di sistema.


Su Windows 11 il token di sistema si trova all'offset 0x4b8 della struttura EPROCESS letta di recente.

Devo solo scrivere quel token di sistema nel mio processo chiamando CreateLogFile.

Per fare questo lavoro, basta ripetere il passaggio usato per leggere il token di sistema.

Nella doppia chiamata, prima chiama ClfsEarlierLsn per restituire 0xFFFFFFFF in RDX e poi chiama nt_SeSetAccessStateGenericMapping.

Controllo che il valore puntato da RDX sia il Token di Sistema.

Il token del mio processo è:

Scriverà lì.
WINDBG>dps rax+8
ffff9b8b'fc446578 ffffc402'f601c06c
WINDBG>dps rax+8
ffff9b8b'fc446578 ffffc402'ef841919
Ora il mio processo è System posso eseguire un Blocco note per verificare.



BINDIFF mostra molte funzioni modificate

La funzione vulnerabile è qui:


La primaria è la versione patchata, la secondaria è la versione vulnerabile.
La patch testa il valore di ritorno di CflsEncodeBlock, che è 0xC01A000A, lo memorizza nella variabile var_54, e poiché è negativo, lo controlla ed evita la WriteSector.
La patch, oltre a non scrivere il file, la funzione restituisce correttamente 0xc01a000a, con cui CreateLogFile non restituisce alcun handle e lo sfruttamento non può continuare.


Solo se ClfsDecodeBlock non è negativo, va a WriteSector ma lascia restituire il valore negativo 0xC01A000A.
Questa è la vera patch che impedisce
lo sfruttamento utilizzando il PoC che ho appena allegato.
A questo punto abbiamo spiegato come è stato sfruttato il bug, porta a controllare le funzioni che ci permettono di leggere il token SYSTEM e scriverlo nel nostro processo per ottenere l'elevazione dei privilegi locali. Potete trovare il PoC funzionante su GitHub di Fortra.
Speriamo vi sia utile, se avete dubbi potete contattarci: