
Tecniche basate su named pipe per lo sfruttamento di overflow del pool mirando alle versioni più recenti (e più vecchie) di Windows, dimostrate su CVE-2020-17087 e un overflow off-by-one
In questo documento forniamo una serie di tecniche che possono essere utilizzate per sfruttare gli overflow nel pool non paginato su Windows. Le tecniche (ab)usano le funzionalità fornite dal file system named pipe (npfs) per trasformare l'overflow in lettura/scrittura arbitraria e aumentare i privilegi.
La tabella seguente mostra la copertura di sfruttabilità fornita dal presente documento su diverse categorie di overflow, basata sul livello di controllo su:
memcpy(vulnerable_chunk, user_controlled_data, overflow_size) vs memset(vulnerable_chunk, 0, overflow_size)memcpy(vulnerable_chunk, input_buffer, user_controlled_size) vs memcpy(vulnerable_chunk, input_buffer, random_size)| Controllo della dimensione dell'overflow | Nessun controllo della dimensione dell'overflow | |
|---|---|---|
| Controllo dei dati dell'overflow | ✔ | ✔ |
| Nessun controllo dei dati dell'overflow | ✔ | ✓ |
Le tecniche precedentemente documentate sull'argomento ricadevano principalmente nella categoria "Controllo dei dati dell'overflow && Controllo della dimensione dell'overflow" e l'obiettivo di questa ricerca era espandere tale copertura. Questa ricerca è stata innescata dopo aver visto l'analisi di Project Zero su CVE-2020-17087 che menzionava l'uso di named pipe per stabilire una scrittura arbitraria, una primitiva che non era documentata (all'epoca).
Per ulteriori discussioni sulla tabella sopra, vedere il capitolo "Approccio a diverse categorie di overflow del pool". Ora entreremo nei concetti relativi alle named pipe che ci permetteranno di costruire le primitive di sfruttamento.
Le named pipe sono un meccanismo di comunicazione inter-processo che consente a due processi potenzialmente appartenenti a computer diversi di condividere dati. Una breve descrizione del suo funzionamento (per maggiori informazioni vedere [1]): una connessione named pipe ha l'estremità server, che crea la pipe, e l'estremità client, che si connette a tale pipe. Quando viene stabilita una connessione named pipe, il driver sottostante crea due code, una per ciascuna estremità, all'interno del Context Control Block (CCB). Il CCB, nel contesto di npfs, è una struttura non documentata utilizzata per contenere informazioni su una particolare connessione server/client. Quelle code trovate all'interno del CCB memorizzano voci principalmente relative ai dati scritti dall'"altra" estremità o alle operazioni di lettura in sospeso dall'estremità corrente. La struttura utilizzata per le voci della coda è la seguente:```c struct DATA_QUEUE_ENTRY { LIST_ENTRY NextEntry; _IRP* Irp; _SECURITY_CLIENT_CONTEXT* SecurityContext; uint32_t EntryType; uint32_t QuotaInEntry; uint32_t DataSize; uint32_t x; char Data[]; }
Note: questa è una struttura non documentata, alcune informazioni sono state ottenute tramite [ReactOS](https://reactos.org/)
Una panoramica dei campi sopra e di alcuni meccanismi implementati da npfs:
**NextEntry**: usato per creare una lista doppiamente collegata con tutte le voci di dati in coda. Le voci sono principalmente relative a operazioni di lettura e scrittura. Un modo per creare voci di operazioni di scrittura è attraverso la chiamata API WriteFile e queste voci vengono rimosse dalla lista quando tutti i loro dati vengono letti da un client (ad esempio usando la ReadFile). La lista include un nodo sentinella, memorizzato all'interno del CCB del named pipe.
<p align="center">
<img src="https://raw.githubusercontent.com/vp777/windows-non-paged-pool-overflow-exploitation/HEAD/images/linkedlist.svg" />
</p>
**SecurityContext**:```
nt!_SECURITY_CLIENT_CONTEXT
+0x000 SecurityQos : _SECURITY_QUALITY_OF_SERVICE
+0x010 ClientToken : Ptr64 Void
+0x018 DirectlyAccessClientToken : UChar
+0x019 DirectAccessEffectiveOnly : UChar
+0x01a ServerIsRemote : UChar
+0x01c ClientTokenControl : _TOKEN_CONTROL
Questo campo permette all'estremità server di una named pipe di impersonificare il contesto di sicurezza di un client. Una panoramica di come funziona:
Il server può quindi chiamare ImpersonateNamedPipeClient, che tenterà di impersonare il contesto di sicurezza memorizzato nel CCB dopo il passo (4).
Si noti che npfs espone due operazioni del file system relative all'impersonificazione.
EntryType: Le voci di dati possono avere diversi tipi che cambiano il modo in cui i dati nella struttura vengono trattati. Due tipi importanti sono le voci bufferizzate e non bufferizzate.
Voci bufferizzate:
Il DATA_QUEUE_ENTRY allocato è abbastanza grande da contenere i dati effettivi della richiesta. Le voci bufferizzate sono soggette al meccanismo di gestione delle quote, che vedremo più avanti, e possono essere create tramite la normale chiamata API WriteFile.
Voci non bufferizzate:
Il DATA_QUEUE_ENTRY allocato è abbastanza grande da contenere l'intestazione senza i dati. L'Irp associato alla richiesta è collegato alla voce e fa riferimento ai dati effettivi della richiesta. Un modo per creare voci non bufferizzate è chiamare NpInternalWrite (fsctl code: 0x119FF8).
Irp: l'IRP associato al DATA_QUEUE_ENTRY. Due dei casi in cui questo campo viene popolato sono:
a) Quando abbiamo voci non bufferizzate
b) Quando una voce bufferizzata viene creata con una dimensione che supera la quota disponibile della pipe.
QuotaInEntry: Questo è un campo utilizzato per indicare la quota consumata dalla particolare voce. Per le voci non bufferizzate è 0. Nelle voci bufferizzate, inizia con DataSize e diminuisce ad ogni lettura fino a quando il suo valore diventa 0.
DataSize: Questa è la lunghezza dei dati utente associati al corrente DATA_QUEUE_ENTRY
x: questo campo non è inizializzato nella creazione della voce, probabilmente usato per il padding
Meccanismo di gestione delle quote: permette all'estremità server del canale di comunicazione di specificare la dimensione massima dei dati che le code possono contenere. Quando questo limite viene superato:
In passato, Alex Ionescu ha documentato in un blogpost[2] l'uso di voci bufferizzate per spargere il pool non paginato. Un altro modo semplice per spargere il pool non paginato è attraverso l'uso di voci non bufferizzate. Come abbiamo visto in precedenza, le voci non bufferizzate permettono l'allocazione di memoria con controllo completo sia sulla dimensione che sui dati (ad esempio, nessuna intestazione DATA_QUEUE_ENTRY). Il fatto di avere il controllo completo sui dati rende le voci non bufferizzate più adatte per alcuni casi poiché:
Il seguente codice può essere utilizzato per creare voci non bufferizzate:```c //create the pipe/file in FILE_FLAG_OVERLAPPED mode (blocking mode) NtFsControlFile(pipe_handle, 0, 0, 0, &isb, 0x119FF8, buf, sz, 0, 0);
Si noti che le voci non bufferizzate vengono create principalmente attraverso le funzioni `NpInternal*` e non è certo se tali funzionalità siano destinate ad essere esposte al codice userspace. Ad esempio, `NpInternalTransceive` non consente chiamate dirette dai programmi userspace.
## Divulgazione di Memoria/Lettura Arbitraria
### Controllo completo sui dati di overflow
1. Stabilire una lettura arbitraria utilizzando l'overflow per riscrivere le intestazioni di `DATA_QUEUE_ENTRY` e falsificare una voce non bufferizzata. Questa tecnica è stata documentata per la prima volta da Corentin Bayet e Paul Fariello in [3]. Si noti che questa è stata anche la prima ricerca a documentare l'uso di named pipe per stabilire una primitiva di lettura e sfruttare un pool overflow.
La voce falsificata sarebbe simile a questa: ```
DATA_QUEUE_ENTRY:
NextEntry=whatever;
Irp=Forged IRP Address;
SecurityContext=ideally 0;
EntryType=1;
QuotaInEntry=ideally 0;
DataSize=arbitrary read size;
x=whatever;
IRP->SystemBuffer = arbitrary read address
Per comodità, possiamo impostare l'Irp a un indirizzo di spazio utente (e in assenza di SMAP), ma non è la nostra unica opzione.
Divulgare la memoria adiacente al chunk overflowato usando l'overflow per riscrivere le intestazioni DATA_QUEUE_ENTRY e forgiare una voce bufferizzata con DataSize maggiore del valore originale. Questa tecnica sembra essere stata documentata per la prima volta da @scwuaptx tramite una sfida HITCON CTF [4].
Questa tecnica può essere utilizzata per divulgare puntatori/metadati dell'heap e altri dati interessanti che potrebbero essere trovati/posizionati dopo la nostra DATA_QUEUE_ENTRY.
Per far funzionare il tutto, la DATA_QUEUE_ENTRY forgiata dovrebbe apparire così: ``` DATA_QUEUE_ENTRY: NextEntry=whatever; Irp=ideally 0; SecurityContext=ideally 0; EntryType=0; QuotaInEntry=ideally 0; //mostly irrelevent in case we use the peek operation DataSize=something bigger than the original size; x=whatever;
Ci sono alcuni casi in cui potremmo avere a disposizione un set limitato di caratteri per l'overflow della memoria (ad es. RtlZeroMemory(buffer, bufferlen+1)). In questi casi, possiamo fare overflow del Flink di un DATA_QUEUE_ENTRY e farlo puntare a una posizione dove abbiamo pieno controllo sui dati. Possiamo quindi utilizzare le tecniche precedentemente descritte per stabilire le letture di memoria. Nella maggior parte delle architetture a 64 bit supportate, dobbiamo fare attenzione a creare indirizzi canonici. Quando questo viene preso in considerazione e assumendo un'architettura little endian, un modo semplice per reindirizzare il Flink verso una posizione controllata è sovrascrivere i primi due byte, poiché ciò farà sì che il DATA_QUEUE_ENTRY punti a una posizione di memoria vicina all'entry corrente. Quindi, con un opportuno heap grooming, facciamo sì che quella posizione contenga il DATA_QUEUE_ENTRY contraffatto per le letture di memoria relative/arbitrarie.
Questa tecnica è illustrata di seguito:
In questo diagramma, vediamo che l'entry vittima originariamente puntava all'entry copertura. Dopo l'overflow, il suo Flink è stato reindirizzato e ora punta al DATA_QUEUE_ENTRY segreto, composto da dati controllati dall'utente. Utilizziamo quindi la tecnica di divulgazione della memoria descritta in precedenza per far trapelare i dati di "chunk 2". È da notare che ci sono casi in cui l'entry babushka potrebbe finire per essere la stessa dell'entry copertura, come ad esempio nel poc fornito per vuln_driver_al20c
Dopo aver ottenuto il layout sopra, ciò che resta è leggere DataSize+DataSize1-sizeof(DATA_QUEUE_ENTRY)+n dopo di che saremo in grado di leggere n byte da "chunk 2". DataSize2 dovrebbe essere almeno DataSize1-sizeof(DATA_QUEUE_ENTRY)+n
In pratica, c'è un'ulteriore sfida prima di utilizzare questa tecnica. Dopo Windows 7, Microsoft ha implementato lo sgancio sicuro (safe-unlinking) nei membri LIST_ENTRY. In base a ciò, dopo aver letto i byte DataSize, il DATA_QUEUE_ENTRY overflowato verrà rimosso dalla coda e Flink/Blink verranno convalidati, cosa che nel nostro caso attiverà un bug check (entry->Flink->Blink!=entry). Fortunatamente, possiamo eseguire un'operazione "sola lettura" sulla coda della pipe utilizzando PeekNamedPipe e aggirare questo problema.
Quindi un approccio pratico a quanto discusso qui è:
size=DataSize+DataSize2+n per far trapelare n byte dall'indirizzo impostato nel SystenBuffer dell'IRP.L'approccio discusso qui è illustrato di seguito:
Analogamente alla lettura arbitraria, stabilire qualsiasi tipo di primitiva di scrittura utilizzando named pipe è diventato più difficile con le operazioni LIST_ENTRY rafforzate. Su Windows 7, ad esempio, è possibile scrivere un indirizzo del kernel (nodo sentinella della coda in Ccb) in una posizione arbitraria. Avremmo potuto farlo contraffacendo un DATA_QUEUE_ENTRY, con il suo Flink impostato sull'indirizzo di destinazione e poi leggendo l'intera entry dati. Ciò avrebbe causato lo scollegamento dell'entry dati dalla lista, che a sua volta avrebbe causato l'esecuzione di dqe->Flink->Blink=dqe->Blink. Come indirizzo di destinazione avremmo potuto potenzialmente utilizzare il campo size di un oggetto gdi adatto.
Dopo Windows 7, dobbiamo seguire una strategia diversa. Qui assumiamo di aver già stabilito la primitiva di lettura relativa/arbitraria suggerita nel capitolo "Controllo limitato sui dati di overflow". Quindi il piano è abusare del meccanismo di gestione delle quote discusso in precedenza per contraffare un DATA_QUEUE_ENTRY che simuli una scrittura bloccata, attraverso la quale contraffacciamo un IRP che stabilirebbe la scrittura arbitraria al suo completamento.
Ora la sfida più grande è contraffare un IRP valido che ci permetta di stabilire la scrittura arbitraria al completamento. Poiché l'IRP è una struttura complicata e viene elaborata legittimamente dal kernel (cioè IofCompleteRequest) e non da npfs come nel caso della tecnica di Lettura Arbitraria, dobbiamo essere precisi. Il modo più semplice che ho trovato per raggiungere questo obiettivo è stato creare un'entry dati che contenga un IRP, utilizzare la lettura arbitraria per leggere quell'IRP, modificare l'IRP in modo che esegua la scrittura arbitraria al completamento e creare un'entry unbuffered* per contenere quell'IRP contraffatto. Infine, con l'IRP contraffatto in posizione, facciamo un po' di spazio nella coda leggendo alcuni dati e dovremmo essere in grado di causare il completamento del nostro IRP contraffatto e quindi stabilire la scrittura arbitraria.
*: È importante utilizzare un'entry unbuffered per contenere l'IRP contraffatto poiché molto probabilmente verrà deallocata alla fine della chiamata a IofCompleteRequest.
Per riferimento, il codice relativo alla raccolta degli IRP in procinto di essere completati può essere trovato alla fine di NpReadDataQueue nella versione inline di NpCompleteStalledWrites.
Il DATA_QUEUE_ENTRY bloccato simulato e l'IRP contraffatto potrebbero assomigliare a questo: ``` DATA_QUEUE_ENTRY: NextEntry.Flink=accessible address; Irp=Forged IRP Address; SecurityContext=ideally 0; EntryType=0; QuotaInEntry=DataSize-1; DataSize=arbitrary write size; x=whatever;
Forged IRP: Flags=Flags&~IRP_DEALLOCATE_BUFFER|IRP_BUFFERED_IO|IRP_INPUT_OPERATION; AssociatedIrp=Source Address; UserBuffer=Destination Address; ThreadListEntry.Flink->Blink==ThreadListEntry.Blink->Flink==&ForgedIRPAddr->ThreadListEntry;
Per riassumere:
1. Spruzzare la memoria con voci della coda dati
2. Utilizzare i passaggi descritti nella sezione "Controllo limitato sui dati di overflow" per stabilire la lettura relativa/arbitraria
3. Dopo il passo (1), è probabile che un chunk adiacente raggiungibile tramite la nostra lettura relativa contenga una voce dati. Identificare quel chunk e il suo handle (ad esempio, identificatore univoco nei dati utente o brute force) e trovare il suo indirizzo (dqe->Flink->Blink).
In alcuni casi, invece di identificare l'handle del chunk "successivo", potrebbe essere più semplice identificare l'indirizzo della pipe vittima. Ad esempio, troviamo l'indirizzo del chunk successivo (dqe->Flink->Blink) e poi calcoliamo gli indirizzi dei chunk precedenti/successivi e proviamo a identificare la voce vittima. (ad esempio nel PoC per [CVE-2020-17087](https://github.com/vp777/Windows-Non-Paged-Pool-Overflow-Exploitation/tree/master/exploits) sappiamo che victim_entry->Flink%0x10000==0x0020)
4. Creare una voce dati sull'handle identificato che avrà un IRP. Ho testato questo con una voce bufferizzata mentre si superava la quota della pipe, ma dovrebbe funzionare anche per voci non bufferizzate.
5. La nuova voce dovrebbe essere aggiunta alla coda dati accanto alla voce leakata. Utilizzare la lettura arbitraria per trovare l'indirizzo della voce appena creata (leaked_entry->Flink), il suo indirizzo IRP e infine i dati dell'IRP.
6. Modificare l'IRP per abilitare la scrittura arbitraria come mostrato sopra. Ad esempio, nei [PoC](https://github.com/vp777/Windows-Non-Paged-Pool-Overflow-Exploitation/tree/master/exploits), l'indirizzo sorgente è impostato al token del processo di sistema e l'indirizzo destinazione è impostato al token del processo corrente. Si noti che possiamo facilmente identificare gli indirizzi sopra menzionati tramite l'IRP trovato al passo (5) e le informazioni sul thread associato.
7. Leggere 1 byte per attivare la scrittura arbitraria. Nota: dovrebbe essere possibile impostare QuotaInEntry su DataSize e attivare il completamento dell'IRP con lunghezza di lettura zero per un'operazione FSCTL_PIPE_INTERNAL_READ_OVFLOW sulla pipe.
## Rilascio Arbitrario di Oggetti SECURITY_CLIENT_CONTEXT
Questo potrebbe essere un'alternativa alla scrittura arbitraria per l'escalation dei privilegi. Come abbiamo già visto, dopo ogni operazione di lettura su una voce dati, ci sarà un tentativo di determinare se l'attuale SecurityContext debba essere memorizzato o meno nel Ccb corrente. Ciò che è interessante per il nostro scopo è il fatto che, nel caso in cui il campo SecurityContext della DATA_QUEUE_ENTRY sia popolato, verrà chiamata NpFreeClientSecurityContext con un argomento tra i due seguenti:
1. il SecurityContext memorizzato nella DATA_QUEUE_ENTRY nel caso in cui l'impersonificazione del client sia disabilitata, come descritto nell'introduzione.
2. il SecurityContext memorizzato nel Ccb nel caso in cui l'impersonificazione sia abilitata. Essenzialmente, si pulisce il vecchio contesto prima di sostituirlo con quello nuovo.
Per riferimento, il segmento di codice all'interno di NpReadDataQueue dove questa funzionalità è implementata è mostrato di seguito:
<p align="center">
<img src="https://assets.kitploit.com/production/public/readmes/34121/f7b8de5a0d2a5ca92717a401975f394e4ee3411c720a07d9ecc869979caf5c11.png" width="330px">
</p>
L'opzione (1) sembra essere più diretta poiché rilascia il contesto di sicurezza trovato nella voce corrente invece di quello precedente, ma entrambe dovrebbero essere utilizzabili.
Quindi una panoramica ad alto livello di come questo potrebbe essere sfruttato è quella di forgiare una struttura SECURITY_CLIENT_CONTEXT che sia impersonabile dal server, detenga privilegi elevati ma non richieda permessi speciali per essere impersonata (ad esempio, vedere le [osservazioni](https://docs.microsoft.com/en-us/windows/win32/api/namedpipeapi/nf-namedpipeapi-impersonatenamedpipeclient)).
Passaggi:
1. I passaggi iniziali dovrebbero essere simili al processo di scrittura arbitraria. Per prima cosa, stabiliamo la lettura relativa/arbitraria, otteniamo i dati dell'IRP, troviamo il thread/processo corrente e potenzialmente altri token elevati che ci permetterebbero di costruire quel token speciale che è impersonabile senza permessi.
2. Trovare l'handle della pipe e l'indirizzo di una voce diversa da quella utilizzata per stabilire la primitiva di lettura/rilascio. Chiamiamola pipe_handle_client/pipe_handle_server.
3. Creare n voci scrivendo nella pipe_handle_client
4. Partire dall'ultima voce e leggere il suo SecurityContext utilizzando la lettura arbitraria
5. Attivare il rilascio arbitrario sull'indirizzo acquisito al passo (4)
6. Spruzzare voci non bufferizzate con il SECURITY_CLIENT_CONTEXT forgiato creato al passo (1)
7. Utilizzare la lettura arbitraria per verificare se siamo riusciti a sostituire la memoria puntata dal SecurityContext memorizzato in (4) con il SECURITY_CLIENT_CONTEXT forgiato
8. Se fallisce, andare alla voce dati precedente (Blink) e ripetere il passo (4). Le voci per cui non siamo riusciti ad allocare il nostro SCC forgiato dovrebbero essere considerate corrotte e un tentativo di leggerle molto probabilmente causerà un BSOD. Ecco perché partiamo dalla fine della lista e ci muoviamo all'indietro; abbiamo n tentativi per allocare la struttura forgiata.
9. Leggere tutte le voci in pipe_handle_server fino a quando almeno un byte viene letto dal SecurityContext sovrascritto (non più del suo DataSize). A quel punto, il ClientContext con i dati forgiati dovrebbe già essere copiato nel Ccb della pipe.
10. Chiamare ImpersonateNamedPipeClient su pipe_handle_server
Nel tempo limitato speso per testare questo, sono riuscito ad attaccare un token forgiato a un thread, ma la struttura _TOKEN forgiata aveva alcune incoerenze che necessitavano di correzione (ad esempio, controlli di integrità e campi che puntano a indirizzi assoluti all'interno del token stesso). Tuttavia, con un po' di sforzo dovrebbe essere possibile eseguire l'escalation utilizzando questa tecnica.
## Approcciare Diverse Categorie di Overflow del Pool
Ora daremo una panoramica di come le tecniche discusse potrebbero essere utilizzate in diversi scenari di overflow. Rivediamo la tabella vista nell'introduzione:
| | Controllo Dimensione Overflow | Nessun Controllo Dimensione Overflow |
|-----------------|:--------------:|:-------------:|
| **Controllo Dati Overflow** | ✔ | ✔ |
| **Nessun Controllo Dati Overflow** | ✔ | ✓ |
1. *Controllo Dati && Controllo Dimensione*
Tutte le tecniche discusse qui dovrebbero essere applicabili.
2. *Controllo Dati && Nessun Controllo Dimensione*
Lo sfruttamento degli overflow in questa categoria dovrebbe essere simile agli overflow trovati in "Nessun Controllo Dati && Nessun Controllo Dimensione" che è descritto di seguito. L'unica differenza è che abbiamo il controllo sui dati di overflow e quindi possiamo evitare il problema delle pipe corrotte. Ad esempio, come dati di overflow possiamo usare ripetutamente un indirizzo sotto il nostro controllo (ad esempio, indirizzo virtuale dello spazio utente) che contiene una voce dati forgiata. (ad esempio, `overflow_data=struct.pack("<Q", userspace_address)*overflow_size/8+victim_entry_flink_bytes`). L'obiettivo è far sì che le voci dati della "memoria di riempimento" appaiano così: ```
DATA_QUEUE_ENTRY:
NextEntry=userspace_address;
Irp=userspace_address;
SecurityContext=userspace_address;
EntryType=userspace_address;
QuotaInEntry=userspace_address;
DataSize=userspace_address;
x=userspace_address;
Basandosi sull'implementazione della funzione NpReadDataQueueEntry, che viene utilizzata per le operazioni di lettura, le voci di dati con valori EntryType maggiori di uno vengono saltate in modo sicuro (cioè viene usato NextEntry) quando viene eseguita un'operazione di peek. Quindi possiamo usare l'operazione di peek per identificare il victim_entry, poiché le voci di "memoria di padding" utilizzerebbero la voce di dati contraffatta nel userspace_address in contrasto con la voce vittima che utilizzerebbe la voce contraffatta specificata nel Flink reindirizzato.
Nessun Controllo sui Dati && Controllo della Dimensione
Qui dovremmo essere in grado di utilizzare le tecniche relative all'overflow del Flink in "Controllo limitato sui dati di overflow".
Nessun Controllo sui Dati && Nessun Controllo della Dimensione
Questa dovrebbe essere la categoria di overflow più difficile da sfruttare. La sua sfruttabilità dipenderà fortemente dalle specifiche del caso sottostante. Diciamo che abbiamo un overflow causato da qualcosa come: memset(vulnerable_chunk, 0, overflow_size)
Il diagramma seguente illustra il nostro stato iniziale:
Dal momento che non abbiamo alcun controllo sui dati di overflow, possiamo provare a integrare la tecnica descritta in "Controllo limitato sui dati di overflow". L'obiettivo ora è posizionare un DATA_QUEUE_ENTRY vicino alla fine dell'area soggetta a overflow e tentare di far sì che il suo Flink venga parzialmente sovrascritto (idealmente 1-2 byte).
Questo approccio è illustrato di seguito:
Come possiamo vedere nel diagramma, potrebbe essere necessario avere una memoria di padding tra il chunk vulnerabile e la voce vittima per allineare correttamente la voce vittima per l'overflow.
La dimensione della memoria di padding necessaria dipende realmente dalla dimensione del vulnerable_chunk e dalla overflow_size. Sulla base di questi, abbiamo due possibilità:
i. Non è necessaria alcuna memoria di padding. In questo caso possiamo procedere normalmente con il resto dei passaggi per stabilire le primitive di lettura/scrittura. Un esempio di questo caso è fornito nel vulnerable_driver, dove essenzialmente abbiamo a che fare con un overflow off-by-one.
ii. È necessaria memoria di padding. Questo è normalmente il caso quando overflow_size-vulnerable_chunk_size>usable_overflow_size+userlying_pool_header_size
Per capire meglio quando questa situazione potrebbe verificarsi, esaminiamo brevemente CVE-2020-17087, poiché è un caso in cui è richiesta memoria di padding.
I parametri dell'overflow sono i seguenti:```c vulnerable_chunk_size = (user_controlled_size6)%65536; vulnerable_chunk = AllocateMemory(vulnerable_chunk_size); memset(vulnerable_chunk, 0x30, user_controlled_size6); //not the same, but mostly equivalent
In questo caso possiamo avere i seguenti parametri di overflow:```c
user_controlled_size = 0x2ae3;
vulnerable_chunk_size = (0x2ae3*6)%65536 = 0x152;
vulnerable_chunk = AllocateMemory(0x152); //it falls into the 0x170 LFH bucket
memset(vulnerable_chunk, 0x30, 0x10152);
Per sfruttare questo problema con la tecnica di overflow del Flink, è necessario il seguente layout di memoria:
Quindi, abbiamo un usable_overflow_size=1-4, che è il numero di byte necessari per utilizzare la nostra tecnica e fare overflow del Flink, ma l'overflow è ben oltre: 0x10152-0x170 byte. I byte oltre quelli usati per l'overflow del Flink rappresentano la memoria di padding.
Ora, per far funzionare le cose, dobbiamo avere il controllo sulle allocazioni nella memoria di padding prima dell'overflow. Questo perché non vogliamo che vengano eseguite operazioni all'interno di quella memoria dopo l'overflow, poiché tutto verrà sovrascritto (es. metadati corrotti dell'allocatore pool, strutture dati, ecc.). Alcune opzioni per gestire la memoria di padding:
a. Nel caso ci troviamo a integrità media, se possibile, spruzzare la memoria con oggetti di cui possiamo divulgare l'indirizzo (es. NtQuerySystemInformation) e assicurarci di avere il layout del pool appropriato prima di innescare l'overflow.
b. A bassa integrità, usiamo voci di dati per riempire quella memoria. La sfida più grande qui è l'identificazione della voce vittima dopo l'overflow. Dopo l'overflow, lo stato in cui ci troviamo include un mucchio di voci di dati corrotte (le voci che riempiono la memoria di padding) e solo una voce valida (voce vittima). In questa situazione, abbiamo un problema che deriva dal fatto che l'ordine di allocazione dei chunk del pool non sempre corrisponde all'ordine in cui i chunk sono posizionati in memoria (es. chunkB viene allocato dopo chunkA, ma potrebbe essere posizionato prima di chunkA in memoria). Ad esempio, questo è il comportamento atteso quando LFH serve la dimensione del chunk vittima. Inoltre, le operazioni eseguite sulle voci corrotte dovrebbero portare a un BSOD.
Considerato quanto sopra, non possiamo sempre sapere/calcolare dove si trova l'handle della voce vittima. Purtroppo, non sono riuscito a identificare una soluzione solida a questo problema. Tuttavia, poiché questa capacità ci permetterebbe di avere un insieme universale di tecniche che funzionerebbero su praticamente qualsiasi situazione di overflow del pool non paginato, ho dedicato il capitolo "Identifying Corrupted Pipes" a discutere l'argomento in modo più approfondito.
Ora, nel caso in cui la dimensione del chunk vittima non sia servita da LFH o possiamo in qualche modo garantire l'ordine di creazione => ordine di allocazione di memoria, allora un modo per identificare il chunk vittima sarebbe attraversare le voci vittima in ordine inverso di creazione fino a quando non identifichiamo la voce vittima. Si noti che questa è stata la strategia utilizzata nel CVE-2020-17087 poc dove la dimensione vittima è stata scelta in modo da essere servita dall'allocatore Variable Size (VS).
In alcuni casi, è utile avere la capacità di identificare pipe con voci di dati corrotte. Ad esempio, quando l'overflow è causato da un integer overflow e la voce vittima cade nell'intervallo dell'Low Fragmentation Heap.
Quindi ora siamo nello stato mostrato nel diagramma, abbiamo la voce vittima i cui header sono stati riscritti per facilitare la primitiva di lettura/scrittura, ma diverse voci di dati sono state corrotte nel processo. Il problema qui è che normalmente non sappiamo quale handle di pipe corrisponda alla voce vittima valida. Un modo per trovarlo è iterare su tutti gli handle di pipe ed eseguire un'operazione che verifichi che stiamo trattando con la voce vittima (es. operazione di lettura che divulga i dati del chunk successivo). Nel nostro caso, questo non è un buon approccio poiché la maggior parte delle operazioni sulle voci corrotte (es. lettura) molto probabilmente causerà un cambiamento momentaneo nell'immagine di sfondo (cioè causerà BSOD). Quindi vogliamo saltarle.
Due approcci per ottenere ciò potrebbero essere:
2. Trovare una funzionalità in npfs che possa funzionare attraverso una voce di dati corrotta, e il cui flusso di controllo/risposte dipenda dalle intestazioni DATA_QUEUE_ENTRY. Ad esempio, chiamando l'operazione che corrisponde al codice `0x116000` (FSCTL_PIPE_INTERNAL_READ_OVFLOW) con lunghezza di lettura uguale a 0, NpReadDataQueue seguirà percorsi di codice diversi in base al valore di EntryType. Se EntryType è maggiore di 1, allora `isb.Status` sarà uguale a 0, altrimenti sarà 0x80000005 (nota, esiste anche un canale temporale semi-affidabile che permette di determinare quale percorso è stato intrapreso):```c
NtFsControlFile(pipe_handle, 0, 0, 0, &isb, 0x116000, buf, 0, buf, 0);
//isb.Status==0?"corrupted":"good" (assuming the overflow written something different to 0,1)
D'altro canto, c'è una limitazione con gli esempi forniti sopra: funzionano solo per pipe create con il flag PIPE_TYPE_MESSAGE. Questo non è ideale poiché in pratica non possiamo usare l'operazione Peek per andare oltre la prima voce di dati e utilizzare il Flink appositamente costruito per attivare le nostre voci di dati contraffatte (cioè l'approccio usato in "Limited control over the overflow data").
Questo comportamento dell'operazione peek è un po' controintuitivo (forse un bug?) poiché la modalità di lettura dell'operazione è normalmente basata sulla modalità di lettura della pipe e non sulla sua modalità di tipo. Questo è effettivamente vero per ReadFile (cioè usa la modalità di lettura) ma non per l'operazione peek (usa la modalità di tipo). Nella documentazione di PeekNamedPipe si vede un tentativo di spiegare questo comportamento (cioè "The data is read in the mode specified with CreateNamedPipe. For example, create a pipe with PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE. If you change the mode to PIPE_READMODE_BYTE with SetNamedPipeHandleState, ReadFile will read in byte mode, but PeekNamedPipe will continue to read in message mode"). Il problema è che questo comportamento permane anche quando la pipe è aperta con "PIPE_TYPE_MESSAGE | PIPE_READMODE_BYTE", il che sembra non essere conforme alla documentazione.
Oltre allo spraying e alla falsificazione delle strutture dati, le voci unbuffered possono anche essere utilizzate per divulgare i dati overflowati. Questo è vero, poiché i loro chunk in memoria sono composti al 100% dai dati utente, quindi non c'è rischio di corruzione dopo l'overflow (l'header del pool, se esiste, sarebbe comunque corrotto). Quindi dopo l'overflow, la voce unbuffered verrà riempita con i dati overflowati che dovremmo essere in grado di leggere successivamente.
Casi d'uso potenziali:
x oggetti di una dimensione target e assumiamo anche che i subsegmenti siano allocati sequenzialmente. Quindi allochiamo 2*x voci unbuffered e 1 buffered. Induciamo ripetutamente l'overflow (prerequisito è un modo affidabile per indurre la vulnerabilità) finché l'overflow non colpisce una delle voci buffered. Quindi procediamo sequenzialmente con l'ordine di allocazione attraverso le nostre pipe, leggiamo i loro contenuti e troviamo l'ultima voce unbuffered overflowata (overflown_unbuffered_entry_index). La voce buffered allocata nell'intervallo di: overflown_unbuffered_entry_index-x a overflown_unbuffered_entry_index+x dovrebbe essere la victim_entry