Skip to content
KitploitKITPLOIT
StrumentiBlog
Invia
StrumentiBlog
Invia

Strumenti di Hacking, PenTest e Cybersecurity per il tuo Arsenale di Sicurezza!

Kitploit è una directory di strumenti di hacking, cybersecurity e pentesting. Scopri gli ultimi aggiornamenti dei progetti per trovare vulnerabilità, analizzare sistemi, automatizzare i test e rafforzare la tua sicurezza.

··Feed·Contatto·Privacy·© 2026 Kitploit

Directory degli strumenti

Categorie

Vedi tutte le categorie
Loading categories
CVE-2020-1206 — Analisi tecnica della vulnerabilità di divulgazione di informazioni del kernel CVE-2020-1206 (SMBleed) in Windows SMBv3, inclusi l'oracolo di perdita di memoria non autenticato e le tecniche di sfruttamento in combinazione con SMBGhost per RCE. | Kitploit
Strumenti/GitHubGitHub/datntsec/cve-2020-1206
Memory ForensicsAnalisi delle VulnerabilitàExploitRaccolta InformazioniPenetration TestingBinary Exploitation
GitHubdatntsec/cve-2020-1206

CVE-2020-1206

Analisi tecnica della vulnerabilità di divulgazione di informazioni del kernel CVE-2020-1206 (SMBleed) in Windows SMBv3, inclusi l'oracolo di perdita di memoria non autenticato e le tecniche di sfruttamento in combinazione con SMBGhost per RCE.

Vedi Repository
5 anni faNon ancora revisionato

Più Popolari

Vedi tutti →

Scopri gli strumenti più utilizzati dalla nostra community.

Esplora tutti gli strumenti

Sfoglia la nostra collezione di strumenti

Vedi tutti gli strumenti →
Condividi

Nella vulnerabilità SMBGhost (CVE-2020-0796) ho parlato di una tecnica di primitiva write-what-where attraverso l'uso di un bug di overflow integer per modificare il puntatore Alloc.Userbuffer in modo che punti a un indirizzo desiderato e scrivere dati arbitrari al suo interno. Similmente a SMB Ghost, questa vulnerabilità esiste anche nella funzione Srv2DecompressData in srv2.sys. Rivediamo la funzione Srv2DecompressData relativa alla vulnerabilità SMBGhost (CVE-2020-0796) semplificata da Zecops``` c typedef struct _COMPRESSION_TRANSFORM_HEADER { ULONG ProtocolId; ULONG OriginalCompressedSegmentSize; USHORT CompressionAlgorithm; USHORT Flags; ULONG Offset; } COMPRESSION_TRANSFORM_HEADER, *PCOMPRESSION_TRANSFORM_HEADER;

typedef struct _ALLOCATION_HEADER { // ... PVOID UserBuffer; // ... } ALLOCATION_HEADER, *PALLOCATION_HEADER;

NTSTATUS Srv2DecompressData(PCOMPRESSION_TRANSFORM_HEADER Header, SIZE_T TotalSize) { PALLOCATION_HEADER Alloc = SrvNetAllocateBuffer( (ULONG)(Header->OriginalCompressedSegmentSize + Header->Offset), NULL); If (!Alloc) { return STATUS_INSUFFICIENT_RESOURCES; }

root@kitploit:~
ULONG FinalCompressedSize = 0;


NTSTATUS Status = SmbCompressionDecompress(
    Header->CompressionAlgorithm,
    (PUCHAR)Header + sizeof(COMPRESSION_TRANSFORM_HEADER) + Header->Offset,
    (ULONG)(TotalSize - sizeof(COMPRESSION_TRANSFORM_HEADER) - Header->Offset),
    (PUCHAR)Alloc->UserBuffer + Header->Offset,
    Header->OriginalCompressedSegmentSize,
    &FinalCompressedSize);
if (Status < 0 || FinalCompressedSize != Header->OriginalCompressedSegmentSize) {
    SrvNetFreeBuffer(Alloc);
    return STATUS_BAD_DATA;
}


if (Header->Offset > 0) {
    memcpy(
        Alloc->UserBuffer,
        (PUCHAR)Header + sizeof(COMPRESSION_TRANSFORM_HEADER),
        Header->Offset);
}


Srv2ReplaceReceiveBuffer(some_session_handle, Alloc);
return STATUS_SUCCESS;

}

root@kitploit:~
La funzione Srv2DecompressData riceve un messaggio compresso inviato dal client e procede ad allocare la memoria necessaria, decomprimendo il messaggio al suo interno. Successivamente, se il campo Offset è diverso da zero, copia i dati (RawData) che precedono i dati compressi all'inizio della memoria allocata.

![](https://assets.kitploit.com/production/public/readmes/24502/a5bf8b5059336fb3677162343bace0d0b45085f2eb89e42d85f81e032fe233dc.png)

Il bug SMBGhost risiede nel fatto che la funzione non verifica un integer overflow, portando ad un'allocazione di dimensione errata che causa un buffer overflow. Tre mesi dopo che Microsoft ha corretto SMBGhost, è stata scoperta la vulnerabilità CVE-2020-1206 (SMBleed - secondo la denominazione di [Zecops Blog](https://blog.zecops.com/)). Questa falla ci permette di leakare l'indirizzo di un'altra macchina e, se combinata con SMBGhost, possiamo ottenere RCE. Per avere una visione più semplice della funzione Srv2DecompressData, riutilizzeremo questa funzione quando non era ancora stata corretta la vulnerabilità SMBGhost e supporremo che sia stata corretta.

# Falsificazione di OriginalCompressedSegmentSize
Come per SMBGhost, anche questa volta falsificheremo OriginalCompressedSegmentSize con un numero leggermente maggiore rispetto ai dati decompressi che inviamo. Ad esempio, se comprimiamo dati di dimensione x byte, invece di impostare il campo OriginalCompressedSegmentSize a x, lo imposteremo a x + 0x1000, come mostrato nella figura seguente:

![](https://assets.kitploit.com/production/public/readmes/24502/687d3bdc67e4b2f5bb3cecbe41ea52be98a5c904a8688b325a69acb0a854ca75.png)

I dati del kernel non inizializzati verranno considerati come parte del messaggio.

Come ho detto nell'analisi di [CVE-2020-0796](https://github.com/datntsec/CVE-2020-0796), Srv2DecompressData salterà comunque la fase di controllo dopo la funzione SmbCompressionDecompress se la decompressione avviene con successo:``` c
if (Status < 0 || FinalCompressedSize != Header->OriginalCompressedSegmentSize) {
    SrvNetFreeBuffer(Alloc);
    return STATUS_BAD_DATA;
}

Sebbene il campo OriginalCompressedSegmentSize sia impostato a x + 0x1000 invece di x, dopo una decompressione riuscita, la variabile FinalCompressedSize non contiene il valore x, ma conterrà il valore x + 0x1000:```c NTSTATUS SmbCompressionDecompress( USHORT CompressionAlgorithm, PUCHAR UncompressedBuffer, ULONG UncompressedBufferSize, PUCHAR CompressedBuffer, ULONG CompressedBufferSize, PULONG FinalCompressedSize) { // ...

root@kitploit:~
NTSTATUS Status = RtlDecompressBufferEx2(
    ...,
    FinalUncompressedSize,
    ...);
if (status >= 0) {
    *FinalCompressedSize = CompressedBufferSize;
}

// ...

return Status;

}

root@kitploit:~
Bởi vì sau khi giải nén thành công, FinalCompressedSize được cập nhật để giữ giá trị CompressedBufferSize (tương ứng với OriginalCompressedSegmentSize được truyền vào hàm SmbCompressionDecompress). Việc cập nhật và kiểm tra sau đó là gần như không cần thiết, có thể dẫn đến một số lỗi không mong muốn.

# Sfruttamento di base
La struttura del messaggio utilizzata da Zecops per dimostrare la vulnerabilità è il [SMB2 WRITE message](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/e7046961-3318-4350-be2a-a8d69bb59ce8). Questa struttura contiene campi come il numero di byte scrivibili, flag, ..., seguiti da un buffer di lunghezza arbitraria. Ciò è abbastanza perfetto per sfruttare il bug, poiché possiamo creare un messaggio e specificare l'intestazione, con un buffer contenente dati non inizializzati.

Sulla base del POC di [Zecops](https://blog.zecops.com/) nel repository WindowsProtocolTestSuites di Microsoft, per avere un quadro più chiaro di ciò, aggiungeremo questa piccola integrazione alla funzione di compressione:``` c
// HACK: fake size
if (((Smb2SinglePacket)packet).Header.Command == Smb2Command.WRITE)
{
    ((Smb2WriteRequestPacket)packet).PayLoad.Length += 0x1000;
    compressedPacket.Header.OriginalCompressedSegmentSize += 0x1000;
}

Notare che questo POC richiede credenziali e l'autorizzazione di scrittura condivisa, spesso disponibile in molti casi. Tuttavia, l'errore restituito si applica a ogni messaggio (inclusi messaggi con o senza credenziali), quindi è possibile sfruttare la vulnerabilità senza autenticazione. Un'altra cosa: la memoria che analizzeremo proviene da allocazioni precedenti in NonPagedPoolNx e, poiché possiamo controllare la dimensione dell'allocazione, avremo un certo controllo sui dati che verranno esposti.

Sorgente del POC di SMBleed

Quindi, senza credenziali, è possibile esporre un indirizzo del kernel? Per rispondere a questa domanda, analizziamo SMB più a fondo.

Approfondiamo SMB

Quando si autenticano le informazioni, il client invia i seguenti messaggi:

SMB2 NEGOTIATE → SMB2 SESSION_SETUP → SMB2 SESSION_SETUP

Se le credenziali sono errate, la connessione viene terminata dopo il secondo pacchetto SMB2 SESSION_SETUP:

Supponendo di non avere credenziali, controlliamo se esiste un comando che può essere inviato senza autenticazione. Cercando, notiamo:

  • Il primo comando da inviare è SMB2 NEGOTIATE, ed è anche l'unico comando SMB2 NEGOTIATE durante l'intera sessione.
  • I comandi successivi, fino all'autenticazione riuscita, devono essere SMB2 SESSION_SETUP.

Il messaggio SMB2 NEGOTIATE non viene compresso. Il bug si trova nella funzione di decompressione, quindi non lo esamineremo, ma considereremo solo i messaggi SMB2 SESSION_SETUP.

SMB2 SESSION_SETUP

Come detto sopra, una sessione normale invia due comandi SMB2 SESSION_SETUP. I pacchetti di risposta non contengono dati necessari per lo sfruttamento e non abbiamo modo di influenzare il pacchetto di risposta. Tuttavia, il secondo pacchetto di risposta avrà un corpo vuoto con stato 0xC000006D (STATUS_LOGON_FAILURE) nell'header del pacchetto. Notiamo che il primo pacchetto SMB2 SESSION_SETUP contiene una richiesta NTLM Negotiate message e il secondo contiene un NTLM Authenticate message. Il messaggio NTLM Negotiate è piuttosto semplice e potrebbe non essere interessante, quindi approfondiremo il NTLM Authenticate message.

NTLM Authenticate message

Dopo aver studiato il NTLM Authenticate message, notiamo che la parte più complessa di questo messaggio, la più adatta allo sfruttamento, è la struttura NTLM2 V2 Response. Questa struttura è un array di byte di dimensione variabile, che contiene principalmente la struttura NTLMv2_CLIENT_CHALLENGE. Vediamo che se questa struttura non supera i controlli iniziali, viene restituito il valore 0xC000000D (STATUS_INVALID_PARAMETER) invece di 0xC000006D (STATUS_LOGON_FAILURE). Uno dei controlli iniziali riguarda il campo AvPairs.

Il campo AvPairs è un array di byte di dimensione variabile che contiene strutture AV_PAIR. Ogni AV_PAIR definisce una coppia attributo/valore; l'attributo è definito dal campo AvId, il campo AvLen definisce la lunghezza in byte del valore, e il campo Value è un array di byte di dimensione variabile che contiene il valore stesso. Un elemento con attributo MsvAvEOL e lunghezza zero segna la fine dell'array.

Il messaggio Authenticate viene elaborato dalla funzione SsprHandleAuthenticateMessage nel modulo msv1_0.dll. Durante i controlli iniziali, questa funzione garantisce che l'array AvPairs contenga i seguenti attributi: 0x0001 (MsvAvNbComputerName), 0x0002 (MsvAvNbDomainName). Tuttavia, il loro valore non viene controllato; viene solo verificato, scorrendo l'array, se l'attributo richiesto esiste e se la sua lunghezza rientra nella struttura. Se la lunghezza è troppo grande, la trasmissione viene interrotta. Quindi, in pratica, MsvAvEOL non viene verificato per validità.

A questo punto, abbiamo scoperto che possiamo creare una richiesta che ci aiuti a rispondere alla seguente domanda: Dati due byte all'offset x, di tipo uint16, il valore è maggiore di y? x e y sono controllati da noi. Consideriamo il seguente pacchetto:

Il contenuto del valore 0x0001 (MsvAvNbComputerName) non è importante, quindi possiamo usarlo per regolare l'offset del secondo valore. Per il secondo valore, impostiamo solo l'attributo a 0x0002 (MsvAvNbDomainName), senza inizializzare len e value. Contemporaneamente, impostiamo la dimensione totale del pacchetto in modo che abbia y byte secondo il campo length. Ci sono due possibili risultati a seconda del valore non inizializzato del campo length del secondo valore:

  • length <= y: In questo caso, il controllo viene superato perché il valore 0x0002 (MsvAvNbDomainName) viene trovato come valido. Il server restituisce 0xC000006D (STATUS_LOGON_FAILURE) perché le credenziali non sono corrette.
  • length > y: In questo caso, il controllo fallisce perché il secondo valore ha una lunghezza non valida e viene scartato. Il server restituisce 0xC000000D (STATUS_INVALID_PARAMETER) in questo caso.

In base alla risposta del server, possiamo sempre dedurre la risposta alla domanda precedente.

Tuttavia, il NTLM Authenticate message è limitato a 0xB48 byte e viene scartato se è più grande. Il controllo viene eseguito dalla funzione SspContextGetMessage (SpLsaModeInitialize-->NtLmFunctionTable.SpAcceptLsaModeContext-->SsprHandleAuthenticateMessage-->SspContextGetMessage) nel modulo msv1_0.dll. Quindi, supponendo di scrivere solo 1 byte di lunghezza, e l'altro byte conterrà un valore non inizializzato, possiamo bypassare questo controllo? Sfortunatamente no, perché il valore uint16 è codificato in little endian. Quindi, non possiamo ottenere ciò che vogliamo in una singola sessione SMB, esamineremo altri fattori.

Osservazione #1: Elenchi Lookaside

Come accennato nella ricerca precedente (CVE-2020-0796), i moduli kernel che gestiscono SMB (srv2.sys e srvnet.sys) utilizzano una funzione di allocazione personalizzata - SrvNetAllocateBuffer - esportata da srvnet.sys. Questa funzione utilizza elenchi lookaside per allocazioni piccole per ottimizzare. Gli elenchi lookaside vengono utilizzati per memorizzare in modo efficiente un insieme di buffer di dimensione fissa, riutilizzabili per il driver.

Gli elenchi lookaside vengono creati all'inizializzazione; l'elenco per ogni dimensione e processore logico è descritto nella tabella seguente:

Ogni cella con il simbolo "📝" è un elenco lookaside separato. Per semplificare l'analisi, supponiamo che il nostro target abbia un solo processore logico. In questo caso, finché viene allocata la stessa quantità di byte, viene utilizzato lo stesso elenco lookaside e quindi lo stesso buffer viene riutilizzato più volte. Possiamo usare questo per avere un certo controllo sui dati non inizializzati.

Osservazione #2: Fallimento della decompressione

Rivediamo cosa succede quando un pacchetto compresso viene decompresso (fare riferimento al writeup CVE-2020-0796 per maggiori dettagli e pseudo-codice):

Nel caso in cui CompressedData non sia valido, la fase di decompressione fallisce, la fase di copia non viene eseguita e la connessione viene interrotta. Ma la decompressione può fallire solo dopo aver decompresso una parte di CompressedData valida. Questo ci permette di creare una richiesta in cui i dati da noi scelti vengono scritti a un offset da noi scelto, come mostrato nella figura seguente:

Ritorno al NTLM Authenticate message

Possiamo utilizzare le osservazioni precedenti per far funzionare la nostra tecnica usando due passaggi:

  1. Inviare un messaggio con dati compressi non validi in modo che venga decompresso un singolo byte 0. Quel byte sarà il primo byte del campo length del secondo valore nell'array AvPairs.
  2. Inviare un messaggio come prima, ma assicurandosi che lo stesso elenco lookaside venga utilizzato per l'allocazione, in modo che il byte 0 sia lì.

Questa volta, questa tecnica può rispondere alla seguente domanda: Dato un byte all'offset x, il valore è maggiore di y? Come prima, x e y sono controllati da noi.

Poiché possiamo riutilizzare il buffer più volte assicurandoci di usare lo stesso elenco lookaside, possiamo ripetere i passaggi più volte cambiando y e infine dedurre il valore del byte a un certo offset.

Tuttavia, questa tecnica ha una limitazione: l'offset del byte che possiamo leggere è limitato al byte 0xADB dall'inizio del buffer del pacchetto. Questo perché l'offset del NTLM Authenticate message (AUTHENTICATE_MESSAGE) è limitato a 0x40 byte dopo la fine degli header SMB2 SESSION_SETUP (eseguito dalla funzione Smb2ValidateSessionSetup in srv2.sys) e la dimensione del NTLM Authenticate message (AUTHENTICATE_MESSAGE) è limitata a 0xB48 byte. Cercheremo un modo per risolvere questo problema.

Supponiamo di voler leggere un byte all'offset 0x1100. Non possiamo farlo direttamente con la tecnica sopra, ma possiamo comunque usare un'altra tecnica: poiché i buffer vengono riutilizzati dagli elenchi lookaside, possiamo "sollevare" il byte target attraverso la funzione di decompressione impostando il campo Offset per superare quel byte. Dobbiamo solo assicurarci che i dati in quella posizione possano essere interpretati come dati compressi validi, altrimenti la copia non avverrà.

Il buffer del pacchetto contiene i dati inviati dal client, che includono 16 byte di header che non vengono copiati durante la decompressione. Di conseguenza, i dati copiati e decompressi, incluso il byte target, vengono spostati di circa 16 byte più vicini all'inizio del buffer allocato. Possiamo ripetere questo processo alcune volte, finché l'offset del byte target non è sufficientemente basso.

POC per l'esposizione dell'indirizzo

Puoi trovare uno script che dimostra la tecnica precedente qui. Ricorda che abbiamo supposto che il server abbia un solo processore logico, quindi dovrai configurare correttamente la tua macchina virtuale affinché lo script funzioni. Se tutto funziona, lo script leggerà ed esporrà l'indirizzo del pool NonPagedPoolNx. In realtà, sarà l'indirizzo di uno dei buffer che si trovano nello stesso elenco lookaside.

Poiché questa tecnica ha parecchie limitazioni, non la analizzerò ulteriormente. Tuttavia, puoi comunque leggere lo script sopra e analizzarlo da solo.

Un approccio diverso – decompressione

Durante la ricerca, Zecops ha notato che il pacchetto SMB decompresso non è l'unica struttura complessa che può essere non valida in vari modi. Anche prima di elaborare tutte le strutture relative a SMB, il buffer compresso stesso può essere non valido. Se la decompressione fallisce, la connessione al server viene interrotta.

Microsoft fornisce tre algoritmi di compressione tra cui scegliere per l'implementazione di SMB: LZNT1, Plain LZ77 e LZ77 + Huffman. Considereremo solo LZNT1 perché è abbastanza semplice - circa 80 righe di Python per una funzione di decompressione. Descriverò brevemente il processo di decompressione: i dati compressi consistono in una sequenza di blocchi compressi, ogni blocco inizia con un uint16 che segna la lunghezza del blocco. Quando si incontra una lunghezza pari a zero, la decompressione è completa. Useremo questo per scrivere una sequenza di byte 0 che rappresentano dati compressi validi. Lo scopo è rispondere alla domanda precedente: Dato un byte all'offset x, il valore è maggiore di y? Ovviamente, x e y saranno ancora controllati da noi.

Ecco un esempio di dati compressi che invieremo:

Ci sono due possibili risultati a seconda del valore non inizializzato del primo byte del campo length:

  • length <= y: In questo caso, il primo blocco sarà tutto byte 0, il che è perfettamente valido, e la lunghezza del blocco successivo sarà 0, completando la decompressione. Il server restituirà una risposta.
  • length > y: In questo caso, il primo o il secondo blocco compresso conterranno byte 0xFF, e questo blocco non verrà decompresso. Il server interromperà la connessione a causa di dati compressi non validi.

Proprio come nella tecnica precedente, possiamo usare le Osservazioni 1 e 2 per creare un messaggio con un byte non inizializzato in mezzo al messaggio usando due passaggi:

  1. Inviare un messaggio con dati compressi non validi in modo che solo una parte dei dati venga decompressa, simile alla figura sopra.
  2. Inviare un secondo messaggio e assicurarsi che lo stesso elenco lookaside venga utilizzato per il primo messaggio, in modo che i byte del primo passaggio siano lì.

Nota che il valore Offset nell'header del pacchetto SMB punterà ai dati compressi, che potrebbero essere validi o meno a seconda del valore del byte non inizializzato.

Il vantaggio più notevole di questa tecnica rispetto alla precedente è che non c'è più un limite di offset.

Quindi, riassumendo, abbiamo due tecniche per leggere una regione di memoria non inizializzata da un buffer del pool allocato dalla funzione SrvNetAllocateBuffer del modulo srvnet.sys. La prima tecnica crea un pacchetto SMB speciale, quindi deduce le informazioni attraverso la risposta del server. La seconda tecnica, con meno limitazioni, crea dati compressi speciali e li invia, quindi deduce le informazioni in base al fatto che il server interrompa o meno la connessione.

Pertanto, possiamo usare una delle due tecniche per lo sfruttamento. E come ho detto, la prima tecnica ha molte limitazioni, quindi approfondiremo solo la seconda tecnica.

Questa tecnica ci aiuterà a sfruttare il write-what-where primitive che Zecops ha dimostrato in precedenza nella ricerca precedente sulla possibilità di ottenere un escalamento dei privilegi locale. Useremo questa tecnica per esporre un indirizzo nel layout di memoria per poter usare il write-what-where primitive. Sfortunatamente, la memoria allocata da SrvNetAllocateBuffer è principalmente usata per dati di rete come i pacchetti SMB e non contiene alcun puntatore di sistema. E poiché abbiamo bisogno di ottenere RCE, esporre regioni di memoria non inizializzate da allocazioni precedenti di SrvNetAllocateBuffer è inutile perché non siamo sicuri della posizione del puntatore desiderato. Dobbiamo trovare qualcosa di più utile.

SrvNetAllocateBuffer e il layout del buffer allocato

Come ho detto nella ricerca sull'escalation dei privilegi locale (CVE-2020-0796), la funzione SrvNetAllocateBuffer non restituisce solo un buffer con la dimensione richiesta. Invece, restituisce un puntatore a una regione situata immediatamente sotto il buffer utente del blocco di memoria allocato dal pool, che contiene informazioni sul buffer allocato. Il layout del blocco di memoria allocato dal pool è il seguente:

Sebbene la nostra tecnica di lettura possa leggere solo byte dalla regione "User Buffer", possiamo comunque usare un'altra tecnica per copiare parti della struttura SRVNET_BUFFER_HDR nel "User Buffer" di un altro buffer per poterla leggere. Impostando il campo Offset in modo che punti alla struttura SRVNET_BUFFER_HDR al di fuori dei dati che vogliamo leggere. Dobbiamo solo assicurarci che i dati in quella posizione possano essere interpretati come dati compressi validi, altrimenti la copia non avverrà.

Caccia ai puntatori

Consideriamo i campi della struttura SRVNET_BUFFER_HDR e vediamo se c'è qualche contenuto degno di essere letto:``` c #pragma pack(push, 1) struct SRVNET_BUFFER_HDR { /00/ LIST_ENTRY ConnectionBufferList; /10/ WORD BufferFlags; // 0x01 - no transport header, 0x02 - part of a lookaside list /12/ WORD LookasideListIndex; // 0 to 8 /14/ WORD LookasideListLogicalProcessor; /16/ WORD TracingDataCount; // 0, 1 or 2, for TracingPtr1/2, TracingUnknown1/2 /18/ PBYTE UserBufferPtr; /20/ DWORD UserBufferSizeAllocated; /24/ DWORD UserBufferSizeUsed; /28/ DWORD PoolAllocationSize; /2C/ BYTE unknown1[4]; /30/ PBYTE PoolAllocationPtr; /38/ PMDL pMdl1; /40/ DWORD BytesProcessed; /44/ BYTE unknown2[4]; /48/ SIZE_T BytesReceived; /50/ PMDL pMdl2; /58/ PVOID pSrvNetWskStruct; /60/ DWORD SmbFlags; /64/ PVOID TracingPtr1; /6C/ SIZE_T TracingUnknown1; /74/ PVOID TracingPtr2; /7C/ SIZE_T TracingUnknown2; /84/ BYTE unknown3[12]; }; #pragma pack(pop)

root@kitploit:~
I puntatori `UserBufferPtr`, `PoolAllocationPtr`, `pMdl1`, `pMdl2` sono puntatori che puntano all'interno del blocco di memoria allocato dal pool, con offset che possono essere calcolati in anticipo, quindi ci basta leggerne uno. Avere un puntatore che punti al blocco di memoria allocato dal pool ci sarà sicuramente utile nell'exploit. Inoltre, i seguenti puntatori sono anch'essi molto importanti:

- **ConnectionBufferList**: Una lista collegata di tutti i buffer ricevuti ma non ancora elaborati di una connessione. L'inizio di questa lista è un oggetto connessione creato dalla funzione `SrvNetAllocateConnection` in srvnet.sys. Un buffer viene aggiunto alla lista dalla funzione `SrvNetWskReceiveComplete`. Nel nostro caso, ci sarà un solo buffer nella lista, quindi entrambi i puntatori (Flink e Blink della struttura `LIST_ENTRY`) punteranno all'inizio della lista all'interno dell'oggetto connessione.
- **pSrvNetWskStruct**: Inizialmente, un puntatore che punta all'oggetto connessione menzionato sopra. Il puntatore viene impostato dalla funzione `SrvNetWskReceiveEvent`, ma viene sovrascritto dalla funzione `SrvNetWskReceiveComplete` con un puntatore alla struttura `SRVNET_BUFFER_HDR`. Quindi, leggerlo non è più utile che leggere uno dei quattro puntatori già menzionati. Tra l'altro, se cercate "pSrvNetWskStruct", vedrete che ha un ruolo nell'exploit di EternalBlue.
- **TracingPtr1/2**: Questi puntatori vengono usati solo quando la funzionalità di tracing è attivata.

![](https://assets.kitploit.com/production/public/readmes/24502/916c7b643fdf7c8967b2b80189a686858d4cceaf35ddb1f38b83a7ae92f7a14f.png)

Come potete vedere, l'unico altro puntatore utile da leggere è un puntatore nella struttura `ConnectionBufferList`. Entrambi i puntatori (Blink e Flink nella struttura `LIST_ENTRY`) puntano all'oggetto connessione. Questo oggetto è stato chiamato `SRVNET_RECV` dal ricercatore di EternalBlue, quindi useremo anche noi questo nome.

## Ottenere un indirizzo base di un modulo

Ora che sappiamo come ottenere due puntatori – uno che punta al blocco di memoria allocato dal pool e uno che punta alla struttura `SRVNET_RECV` – possiamo modificare liberamente i due buffer usando la primitive write-what-where. Potrebbero esserci molti modi per ottenere RCE, ma ottenere un indirizzo base di un modulo è la scelta più semplice perché ci sono molte cose che possiamo modificare nella sezione dati di un modulo. Come abbiamo visto, non c'è alcun puntatore nel blocco di memoria allocato da `SrvNetAllocateBuffer` che punti a un modulo. Tuttavia, ci sono comunque alcuni puntatori che puntano a moduli:

![](https://assets.kitploit.com/production/public/readmes/24502/403108b6e79960d84369827376e208b606ba05304ae052ec4ae251cd289399a7.png)

La tecnica di lettura che abbiamo ci permette solo di leggere dati nella zona "User Buffer", mentre questi puntatori sono piuttosto distanti e puntati da molti altri puntatori. Abbiamo bisogno di un codice che possa fare quanto segue per copiare il valore del puntatore nella zona "User Buffer":``` c
ptr1 = *(pSrvNetRecv + offset1)
value = *ptr1
ptr2 = *(pSrvNetRecv + offset2)
*ptr2 = value

Se possiamo trovare un tale frammento di codice, lo attiveremo per copiare il primo puntatore (es. HandlerFunctions) nell'area "User Buffer", leggerlo, quindi copiare il secondo puntatore (es. il puntatore alla funzione Srv2ConnectHandler) nell'"User Buffer" e leggerlo, deducendo l'indirizzo base del modulo da esso. Il team di Zecops ha cercato un frammento di codice simile per molto tempo, ma non ne ha trovato uno adatto. Alla fine, hanno utilizzato un'altra alternativa legata alla funzione SrvNetFreeBuffer (semplificata come sotto) che ha una funzionalità quasi desiderata:``` c void SrvNetFreeBuffer(PSRVNET_BUFFER_HDR Buffer) { PMDL pMdl1 = Buffer->pMdl1; PMDL pMdl2 = Buffer->pMdl2;

root@kitploit:~
if (pMdl2->MdlFlags & 0x0020) {
    // MDL_PARTIAL_HAS_BEEN_MAPPED flag is set.
    MmUnmapLockedPages(pMdl2->MappedSystemVa, pMdl2);
}

if (Buffer->BufferFlags & 0x02) {
    if (Buffer->BufferFlags & 0x01) {
        pMdl1->MappedSystemVa = (BYTE*)pMdl1->MappedSystemVa + 0x50;
        pMdl1->ByteCount -= 0x50;
        pMdl1->ByteOffset += 0x50;
        pMdl1->MdlFlags |= 0x1000; // MDL_NETWORK_HEADER

        pMdl2->StartVa = (PVOID)((ULONG_PTR)pMdl1->MappedSystemVa & ~0xFFF);
        pMdl2->ByteCount = pMdl1->ByteCount;
        pMdl2->ByteOffset = pMdl1->MappedSystemVa & 0xFFF;
        pMdl2->Size = /* some calculation */;
        pMdl2->MdlFlags = 0x0004; // MDL_SOURCE_IS_NONPAGED_POOL
    }

    Buffer->BufferFlags = 0;

    // ...

    pMdl1->Next = NULL;
    pMdl2->Next = NULL;

    // Return the buffer to the lookaside list.
} else {
    SrvNetUpdateMemStatistics(NonPagedPoolNx, Buffer->PoolAllocationSize, FALSE);
    ExFreePoolWithTag(Buffer->PoolAllocationPtr, '00SL');
}

}

root@kitploit:~
Quando si rilascia il buffer, se i flag del buffer sono 0x02 (il che significa che il buffer fa parte di una lookaside list) e 0x01 (il che significa che il buffer non ha un transport header) sono impostati, vengono eseguite alcune operazioni su due oggetti MDL per aggiungere il transport header prima di reimpostare i flag a 0 e restituire il buffer alla lookaside list. Se esaminiamo attentamente le operazioni sugli oggetti MDL, possiamo notare che il codice esegue una doppia dereference di lettura seguita da una doppia dereference di scrittura con due variabili che controlliamo (due puntatori MDL), che è ciò che stiamo cercando. Lo svantaggio è che anche il contenuto che vogliamo leggere viene modificato, un effetto collaterale che speriamo di evitare.

Con quanto sopra, ecco come riusciamo a leggere il puntatore AcceptSocket:
1. Preparare il buffer A da una lookaside list in modo che la regione “User buffer” sia riempita con zeri. La regione user buffer di questo buffer conterrà il puntatore che leggeremo.
2. Preparare il buffer B da un'altra lookaside list in modo che:
   - Il puntatore pMdl1 punti all'indirizzo del puntatore AcceptSocket meno 0x18, (poiché l'offset di MappedSystemVa è 0x18 nella struttura MDL).
   - Il puntatore pMdl2 punti alla regione “User buffer” del Buffer A.
   - Il campo Flags sia impostato a 0x03.

Possiamo sovrascrivere i campi della struttura SRVNET_BUFFER_HDR decomprimendoli da un buffer più grande tramite la tecnica descritta nella sezione [Observation #2](https://github.com/datntsec/CVE-2020-1206#observation-2-failing-the-decompression) sopra.

3. Quando il Buffer B viene rilasciato, si verificano le seguenti operazioni:
   - I flag MDL vengono letti dal secondo MDL nel buffer A. Se il flag MDL_PARTIAL_HAS_BEEN_MAPPED è impostato, MmUnmapLockedPages verrà chiamato e il sistema potrebbe crashare. Questo è il motivo per cui dobbiamo riempire il buffer con zeri al passo 1.
   - Il puntatore AcceptSocket e la memoria circostante vengono modificati come descritto qui:```
+00 |  00 00 00 00 00 00 00 00
+08 |  __ __ __|10 __ __ __ __
+10 |  __ __ __ __ __ __ __ __
+18 |  [+50..................]  <--  AcceptSocket
+20 |  __ __ __ __ __ __ __ __
+28 |  [-50......] [+50......]
  • Il puntatore AcceptSocket e la memoria circostante verranno letti come descritto qui:``` +00 | __ __ __ __ __ __ __ __ +08 | __ __ __ __ __ __ __ __ +10 | __ __ __ __ __ __ __ __ +18 | ab cd ef gh ij kl mn op <-- AcceptSocket +20 | __ __ __ __ __ __ __ __ +28 | qr st uv wx __ __ __ __
root@kitploit:~
- Vùng “User buffer” của buffer A sẽ được sửa đổi như được mô tả ở đây: (Các byte màu cam chứa con trỏ mà chúng ta muốn đọc, chúng ta chỉ cần sắp xếp chúng đúng cách)```
+00 |  00 00 00 00 00 00 00 00
+08 |  ?? ?? 04 00 __ __ __ __
+10 |  __ __ __ __ __ __ __ __
+18 |  __ __ __ __ __ __ __ __
+20 |  00 c0 ef gh ij kl mn op
+28 |  qr st uv wx ab 0d 00 00

  1. Leggere il puntatore AcceptSocket dalla zona "User buffer" del buffer A.

La buona notizia è che abbiamo letto il puntatore. La cattiva notizia è che abbiamo danneggiato alcuni dati nella struttura SRVNET_RECV. Per nostra fortuna, l'errore non influisce sul sistema finché non accade nulla alla connessione interessata. Quando accade qualcosa, ad esempio la chiusura della connessione, il sistema si bloccherà. Non è un problema perché presto avremo RCE e potremo correggere l'errore se lo desideriamo.

Dopo aver letto il puntatore AcceptSocket, continuiamo a utilizzare la stessa tecnica per leggere il puntatore srvnet!SrvNetWskConnDispatch. Il motivo per cui leggiamo il puntatore AcceptSocket e non il puntatore HandlerFunctions è che l'array di HandlerFunctions è condiviso tra tutte le connessioni, mentre il buffer puntato da AcceptSocket non è condiviso con altre connessioni. Quindi, se danneggiamo parti di AcceptSocket, influirà solo sulla stabilità di una singola connessione.

Se abbiamo una copia del file srvnet.sys utilizzato sul computer target, possiamo facilmente dedurre l'indirizzo base del modulo srvnet.sys sottraendo l'offset del puntatore SrvNetWskConnDispatch che abbiamo leakato.

Implementing arbitrary read

Supponiamo di avere l'indirizzo base del modulo srvnet.sys, possiamo chiamare qualsiasi funzione del modulo. Ma per quanto riguarda gli argomenti della funzione? La funzione srv2!Srv2ReceiveHandler viene chiamata da SrvNetCommonReceiveHandler e la chiamata ha la seguente forma:``` c HandlerFunctions = *(pSrvNetRecv + 0x118); Arg1 = *(ULONG_PTR)(pSrvNetRecv + 0x128); Arg2 = *(ULONG_PTR)(pSrvNetRecv + 0x130); (HandlerFunctions[1])(Arg1, Arg2, Arg3, Arg4, Arg5, Arg6, Arg7, Arg8);

root@kitploit:~
I primi due argomenti vengono letti dalla struttura SRVNET_RECV, quindi possiamo controllarli, ma non possiamo controllare gli argomenti rimanenti. La convenzione di chiamata x86-64 specifica che il chiamante è responsabile di allocare e liberare lo spazio dello stack per gli argomenti, quindi, sebbene sia prevista una funzione a 8 argomenti, possiamo sostituire il puntatore con una funzione che ne accetta un numero qualsiasi.

![](https://assets.kitploit.com/production/public/readmes/24502/223827f49f26993604c20b873d75ea5b11d306a85ff5210ceb2583f0a0b9452c.png)

Di seguito sono riportati i passaggi che utilizzeremo per attivare la chiamata di funzione:
1. Invia un messaggio appositamente creato in modo che il puntatore alla struttura SRVNET_RECV della connessione venga copiato in un buffer che possiamo leggere.
2. Invia un altro messaggio valido, che riutilizzerà la stessa struttura SRVNET_RECV, ma senza chiudere la connessione. Nota che quando la connessione viene chiusa, la struttura SRVNET_RECV non viene liberata. La funzione `SrvNetPrepareConnectionForReuse` viene chiamata per resettare la struttura in modo che possa essere riutilizzata per la connessione successiva.
3. Leggi il puntatore alla struttura SRVNET_RECV che abbiamo copiato al passo 1.
4. Sostituisci il puntatore `HandlerFunctions` e gli argomenti utilizzando la tecnica write-what-where primitive.
5. Invia un messaggio aggiuntivo attraverso la connessione del passo 2 per far sì che venga chiamata la funzione sostitutiva di `srv2!Srv2ReceiveHandler`.

Ora tutto ciò che dobbiamo fare è trovare una funzione per copiare la memoria da una posizione all'altra, in modo da poter copiare arbitrariamente la memoria in un buffer del pool che possiamo leggere. `memcpy` è un'opzione e `srvnet.sys` ha una tale funzione (più precisamente `memmove`), ma questa funzione richiede un terzo argomento, quello che specifica il numero di byte da copiare, che non possiamo controllare. Tuttavia, non siamo limitati alle funzioni implementate in `srvnet.sys`; possiamo anche chiamare funzioni dalla tabella di import di `srvnet` e `RtlCopyUnicodeString` è una scelta perfetta per quello che vogliamo fare.

La funzione `RtlCopyUnicodeString` accetta due puntatori a `UNICODE_STRING` come argomenti e copia il contenuto della stringa sorgente in quella di destinazione. A differenza delle stringhe C terminate da null, le stringhe nel kernel sono definite dalla struttura `UNICODE_STRING` che contiene un puntatore alla stringa e la lunghezza della stringa in byte. Il buffer della stringa può contenere qualsiasi dato binario. Se guardi il codice della funzione `RtlCopyUnicodeString`, puoi vedere che la copia viene effettuata con `memmove`, cioè pura copia di dati binari. Tutto ciò che dobbiamo fare è preparare due strutture `UNICODE_STRING` e chiamare `RtlCopyUnicodeString`, quindi leggere i dati copiati:

![](https://assets.kitploit.com/production/public/readmes/24502/8a450b86ecf834ec4905a2bb72dab3781d1dd2ad56ee197a003e282180ce51bc.png)

## Esecuzione di shellcode
Dopo aver ottenuto una comoda primitiva di lettura arbitraria, passiamo alla prossima sfida verso l'obiettivo di Remote Code Execute eseguendo uno shellcode. Utilizzeremo la tecnica che Morten Schenk ha presentato nel suo talk al [Black Hat USA 2017](https://www.blackhat.com/docs/us-17/wednesday/us-17-Schenk-Taking-Windows-10-Kernel-Exploitation-To-The-Next-Level%E2%80%93Leveraging-Write-What-Where-Vulnerabilities-In-Creators-Update.pdf) (pagine 47-51).

L'idea è di scrivere uno shellcode al di sotto della struttura `KUSER_SHARED_DATA`, che ha un indirizzo costante, l'unico indirizzo non randomizzato nel layout della memoria del kernel nelle versioni recenti di Windows. Quindi, modificare la voce della tabella delle pagine (PTE) associata, rendendo la pagina eseguibile. L'indirizzo base delle voci della tabella delle pagine nel kernel è randomizzato, ma può essere ottenuto dalla funzione `MiGetPteAddress` in `ntoskrnl.exe`. Di seguito sono riportati i passaggi che utilizzeremo per eseguire il nostro shellcode:
1. Usa la primitiva di lettura arbitraria per ottenere l'indirizzo base di `ntoskrnl.exe` dalla tabella di import di `srvnet`.
2. Leggi l'indirizzo base della voce della tabella delle pagine dalla funzione `MiGetPteAddress`, come descritto nelle slide di Morten.
3. Scrivi lo shellcode all'indirizzo `KUSER_SHARED_DATA + 0x800` (0xFFFFF78000000800). Nota che possiamo anche usare uno dei buffer del pool per memorizzare lo shellcode; l'uso di `KUSER_SHARED_DATA` è solo per semplificare le cose.
4. Calcola l'indirizzo della voce della tabella delle pagine associata e cancella il bit NX per consentire l'esecuzione, come descritto nelle slide di Morten.
5. Chiama lo shellcode utilizzando la tecnica descritta sopra per chiamare una funzione arbitraria.

Lo shellcode utilizzato da Zecops per la reverse shell è [sleepya's shellcode](https://github.com/worawit/MS17-010/tree/master/shellcode) scritto per lo sfruttamento di EternalBlue. Hanno modificato lo shellcode per renderlo eseguibile sulle versioni recenti di Windows.

# Debug

![](https://assets.kitploit.com/production/public/readmes/24502/916c7b643fdf7c8967b2b80189a686858d4cceaf35ddb1f38b83a7ae92f7a14f.png)

Le informazioni di un pacchetto SMB hanno una struttura simile a quella sopra. Supponiamo di aver bisogno di leak dell'indirizzo del User Buffer; dovremo leggere il puntatore `UserBufferPtr`. Per leggere questo puntatore, sfruttiamo la [tecnica](https://github.com/datntsec/CVE-2020-1206#srvnetallocatebuffer-and-the-allocated-buffer-layout) di posizionare il campo offset oltre il puntatore in modo che venga copiato nell'area user buffer di un altro buffer.

Esempio di un pacchetto SMB inviato dal client con il seguente contenuto:```c
Header:
-   Id = 0x424d53fc
-   OriginalCompressedSegmentSize = 0x0
-   CompressionAlgorithm = 1
-   Flag = 0
-   Offset = 0x2116
Data = ‘A’ * 0x1101.

Questo pacchetto, quando arriva al server, viene memorizzato in un buffer creato dalla funzione SrvNetAllocateBuffer. Poiché l'intero pacchetto ha una dimensione compresa tra 0x1100 e 0x2100, questa funzione restituisce un alloc con un buffer utente di dimensione 0x2100 (lo chiameremo Alloc A), quindi memorizza le informazioni inviate dal client come mostrato nell'immagine sottostante:

Possiamo vedere che i dati dall'indirizzo 0xffffd38439044050 a 0xffffd38439045160 sono dati inviati dal client, i dati da 0xffffd38439045160 a 0xffffd38439046150 sono dati non ancora inizializzati lato server, e i dati da 0xffffd38439046150 a 0xffffd38439046240 sono i dati di SRVNET_BUFFER_HDR di Alloc A. Quindi il puntatore che vogliamo leggere si trova a 0xffffd38439046150 + 0x18 = 0xffffd38439046168.

Per leggere questo puntatore, ho utilizzato la tecnica che ho menzionato sopra, impostando il campo offset oltre il puntatore da leggere. Per questo motivo, anche se il pacchetto ha una dimensione inferiore a 0x2100, l'offset è stato impostato a 0x2116.

Successivamente, il server SMB chiamerà la funzione SrvNetAllocateBuffer per allocare un'area di memoria basata sulla somma di OriginalCompressedSegmentSize e Offset (0x2116). Di conseguenza, alloca un alloc con un buffer utente di dimensione 0x4100 (lo chiameremo Alloc B). I dati allocati avranno la forma seguente:

Per evitare errori imprevisti, in precedenza ho creato e ricreato più volte buffer nella stessa lookaside list di Alloc B, riempiendoli di byte 0x00.

Successivamente, il server SMB procederà a decomprimere i dati compressi e a copiare i dati non compressi inviati dal client nel buffer utente di Alloc B:

Come possiamo vedere, non ci sono dati compressi poiché OriginalCompressedSegmentSize = 0; il programma copierà i dati da 0xffffd38439044060 a 0xffffd38439044060 + 0x2116 = 0xffffd38439046176 di Alloc A nel buffer utente di Alloc B. Pertanto, una parte delle informazioni di SRVNET_BUFFER_HDR di Alloc A viene copiata nel buffer utente di Alloc B.

Ora useremo la tecnica menzionata in precedenza per fare il leak dell'indirizzo dell'allocazione pool (indirizzo del User Buffer).

Supponiamo di voler sapere se un byte all'indirizzo 0xffffd3843636f15e è maggiore di 0x7f o meno. Creeremo un pacchetto SMB con le seguenti informazioni:``` c Header:

  • Id = 0x424d53fc
  • OriginalCompressedSegmentSize = 0x1ff2
  • CompressionAlgorithm = 1
  • Flag = 0
  • Offset = 0x210e Data = ‘B’ * 0x210e + compress(‘\xb0’ + ‘\x00’(0x7f+3) + ‘\xff’(0xff - 0x7f)) + ‘\xff’*0x1fe9
root@kitploit:~
Perché è necessario creare un SMB di questo tipo, lo analizzeremo passo dopo passo. Innanzitutto, la somma di OriginalCompressedSegmentSize e Offset è 0x4100, quindi verrà riutilizzato un alloc con un user buffer simile, cioè l'Alloc B già utilizzato in precedenza. Poiché vogliamo indovinare se un byte all'indirizzo 0xffffd3843636f15e è maggiore di 0x7f, e questo indirizzo si trova a 0x210e dall'indirizzo del user buffer, i dati non compressi saranno di 0x210e byte (‘B’ * 0x210e). Seguirà una regione di dati compressi validi (compressi dalla funzione compress()), seguita da dati compressi non validi (‘\xff’* 0x1fe9). In modo che durante la decompressione, solo la parte di dati compressi validi venga decompressa in un altro alloc, poi la connessione verrà interrotta a causa dei dati compressi non validi successivi, la regione di dati non compressi non verrà copiata in quell'alloc, quindi la parte di dati che abbiamo copiato in precedenza rimarrà invariata.

![](https://assets.kitploit.com/production/public/readmes/24502/3e1ffd768b120645917e7f1757987f80c79514b52f3bc18e46fdfb0a2d2ca05f.png)

Sopra è mostrato un Alloc contenente le informazioni di cui abbiamo parlato, creato dal server SMB. Successivamente, il server SMB chiamerà la funzione SrvNetAllocateBuffer per creare un Alloc corrispondente. Poiché la somma di OriginalCompressedSegmentSize e Offset è 0x4100, l'Alloc B verrà riutilizzato:

![](https://assets.kitploit.com/production/public/readmes/24502/5b218cbd142d58da175ee6a58bfc505646a560467db6db28ab70e0db34ee9ce3.png)

Quindi il server SMB procede a decomprimere le informazioni inviate dal client nell'user buffer di Alloc B corrispondente all'offset.

![](https://assets.kitploit.com/production/public/readmes/24502/f3b545366e8f3bcff6a69d184cc383bfcf70643c23ff0ac01ece1a95cc0fd88d.png)

I dati in rosso sono i dati decompressi, il resto rimarrà invariato. Come mostrato nell'immagine sopra, possiamo vedere che il byte che ci interessa rimarrà invariato, immediatamente seguito dai dati appena decompressi.

Per sapere se questo byte è maggiore di 0x7f, procediamo come segue:

Continuiamo a creare un pacchetto SMB con il seguente contenuto:``` c
Header:
-   Id = 0x424d53fc
-   OriginalCompressedSegmentSize = 0x2004
-   CompressionAlgorithm = 1
-   Flag = 0
-   Offset = 0x20fd
Data = ‘B’ * 0x20f1

Sebbene la somma di OriginalCompressedSegmentSize e Offset sia maggiore di 0x4100, quando si alloca una regione per memorizzare il pacchetto inviato dal client (con dimensione totale inferiore a 0x4100), il server SMB alloca comunque un Alloc con un buffer utente di 0x4100 byte, come mostrato nell'immagine seguente:

I dati evidenziati in verde sono quelli dell'Alloc B allocato in precedenza, che, appartenendo alla stessa lookaside list, vengono riutilizzati.

Successivamente, il server SMB chiama la funzione SrvNetAllocateBuffer per allocare un Alloc che conterrà i dati dopo la decompressione:

Tramite la decompressione, il server SMB recupera i dati da User buffer address + Offset = 0xffffd3843636d060 + 20fd = 0xffffd3843636f15d. I dati estratti hanno la seguente forma:

Con l'algoritmo di decompressione già descritto, vengono prelevati i primi 2 byte utilizzati come lunghezza del blocco; in base a tale lunghezza, viene estratta la parte successiva e decompressa. Nell'esempio sopra, la lunghezza è 0xB0D3, ma secondo l'algoritmo la lunghezza reale segue la formula: length = length & 0xFFF + 1 → la lunghezza sarà 0xD4. Vengono quindi estratti i successivi D4 byte e la decompressione procede normalmente fino a incontrare un byte FF (poiché i D4 byte includono tutti i byte 00 e una parte dei byte FF). A questo punto i dati compressi sono considerati non validi, la decompressione si interrompe e la connessione viene chiusa.

Basandosi sulla chiusura della connessione da parte del server, possiamo dedurre che il byte che dobbiamo individuare sia maggiore di 0x7f.

E se il byte da indovinare fosse invece più piccolo? Continuiamo l'analisi di cui sopra, ma questa volta utilizziamo come byte di confronto D7, quindi D3 è minore di D7. Vediamo cosa accade:

Per prima cosa, inviamo al server SMB il seguente pacchetto:``` c Header:

  • Id = 0x424d53fc
  • OriginalCompressedSegmentSize = 0x1ff2
  • CompressionAlgorithm = 1
  • Flag = 0
  • Offset = 0x210e Data = ‘B’ * 0x210e + compress(‘\xb0’ + ‘\x00’(0xd7+3) + ‘\xff’(0xff - 0xd7)) + ‘\xff’*0x1fe9
root@kitploit:~
Il server SMB creerà un alloc di memorizzazione come segue:

![](https://assets.kitploit.com/production/public/readmes/24502/3df2c6c8e5de90714455663bbb922efec7ae6888a970a3d65e389a28aea16837.png)

Successivamente chiamerà la funzione SrvNetAllocateBuffer per allocare un alloc come segue:

![](https://assets.kitploit.com/production/public/readmes/24502/51affc73361727907ada6fb3f439831d0e95cee531a65106fa79ce4519ceaab8.png)

Ovviamente questo alloc viene riutilizzato dall'alloc con la stessa lookasidelist (Alloc B). Successivamente il programma procede con la decompressione normale per dati compressi validi, e disconnette per dati compressi non validi:

![](https://assets.kitploit.com/production/public/readmes/24502/01a9cc43acbc6b7cc283f40a0dbe348608a3566bfc34ce24aa2cc694315e67a3.png)

Nel passaggio successivo, invieremo altri dati simili a quelli precedenti e il server SMB allocherà un alloc corrispondente:

![](https://assets.kitploit.com/production/public/readmes/24502/ed438d4d0049b2188573e0fb3171cadf23701c7b0cef2de43a52434c07381341.png)

I dati evidenziati in verde come sopra sono i dati di Alloc B allocati in precedenza, poiché appartengono alla stessa lookaside list vengono riutilizzati.

Successivamente il server SMB chiamerà la funzione SrvNetAllocateBuffer per allocare un Alloc contenente i dati dopo la decompressione:

![](https://assets.kitploit.com/production/public/readmes/24502/7ed3dca4594bd1772c44689190d4df3b597d43daf7c87edb281b3e82a834884e.png)

Attraverso la decompressione, il server SMB recupererà i dati da User buffer address + Offset = 0xffffd3843636d060 + 20fd = 0xffffd3843636f15d. I dati estratti avranno la seguente forma:

![](https://assets.kitploit.com/production/public/readmes/24502/68f677093ef93e02c5df9d70dce560e4d06e74446970f1f14f1ad37f08ce898e.png)

Similmente a prima, la lunghezza iniziale sarà 0xB0D3, che dopo il calcolo diventerà 0xD4. E estrarrà i successivi D4 byte e procederà con la decompressione normale. Tuttavia, poiché D4 < D7, i dati compressi estratti per la decompressione conterranno solo byte 0. Decomprimerà normalmente fino alla fine di quel blocco. Successivamente, estrarrà la lunghezza del blocco successivo tramite la lunghezza del blocco precedente, i due byte successivi sono D5 e D6 che sono 0x0, quindi la sua lunghezza è 0x0, quindi la lunghezza estratta sarà 0x0000 → fine del processo di decompressione → decompressione riuscita → il server SMB restituisce una risposta → sappiamo che il byte cercato è minore o uguale a 0xD7.

Allo stesso modo procederemo fino a quando non avremo leakato tutti i 6 byte di un indirizzo. Otterremo l'indirizzo dell'allocation pool.

Una volta ottenuto l'indirizzo dell'allocation pool, procederemo a trovare l'indirizzo base di srvnet recuperando il puntatore alla struttura SRVNET_RECV in modo simile al leak dell'indirizzo dell'allocation pool.

Dopo aver ottenuto i due indirizzi: allocation pool e SRVNET_RECV rispettivamente con valori: `0xffffd38439044000` e `0xffffd3843654ddd8`, procediamo a leakare l'indirizzo base di srvnet.

![](https://assets.kitploit.com/production/public/readmes/24502/403108b6e79960d84369827376e208b606ba05304ae052ec4ae251cd289399a7.png)

![](https://assets.kitploit.com/production/public/readmes/24502/239c0c59c0e492097bdaf2eac907812124a89ef8bb31841287202ae80f4d13d0.png)

Per leggere il puntatore AcceptSocket, dobbiamo procedere come segue:
1. Preparare Alloc A da una lookaside list in modo che l'area “User buffer” sia riempita con zeri. Questo buffer conterrà poi il puntatore che leggeremo. Qui Alloc A sarà utilizzato dall'Alloc corrispondente all'indirizzo dell'allocation pool che abbiamo leakato. Pertanto, l'area User buffer di Alloc A inizierà all'indirizzo 0xffffd38439044050 attraverso l'uso della stessa lookaside list.
2. Preparare Alloc B da un'altra lookaside list in modo che:
- Il puntatore pMdl1 punti all'indirizzo del puntatore AcceptSocket meno 0x18, (poiché l'offset di MappedSystemVa è 0x18 nella struttura MDL).
- Il puntatore pMdl2 punti all'area “User buffer” di Buffer A.
- Il campo Flags sia impostato a 0x03.

Quindi gli indirizzi dei due puntatori Mdl sono rispettivamente: mdl1_ptr: `0xffffd3843654de68`, mdl2_ptr: `0xffffd38439045250`.

Possiamo sovrascrivere i campi della struttura SRVNET_BUFFER_HDR decomprimendoli da un buffer più grande tramite la tecnica descritta nella sezione [Observation #2](https://github.com/datntsec/CVE-2020-1206#observation-2-failing-the-decompression).

Spiegherò meglio questo passaggio subito dopo il punto 4.

3. Quando Buffer B viene rilasciato, si verificheranno le seguenti operazioni:
- I flag MDL verranno letti dal secondo MDL nel buffer A. Se il flag MDL_PARTIAL_HAS_BEEN_MAPPED è impostato, verrà chiamata MmUnmapLockedPages e il sistema potrebbe crashare. Ecco perché dobbiamo riempire il buffer di zeri nel passaggio 1.
- L'area “User buffer” di Alloc A verrà modificata e conterrà le informazioni che dobbiamo leggere.
4. Leggere il puntatore AcceptSocket dall'area “User buffer” di buffer A.
- Utilizzare la tecnica di leak dell'indirizzo già usata sopra per leggere il puntatore AcceptSocket.

Di seguito descriverò meglio questi passaggi:

Nel passaggio 1 è piuttosto semplice e simile a quanto sopra, quindi non ne parlerò ulteriormente.

Nel passaggio 2, per prima cosa creeremo un pacchetto da inviare al server SMB con il seguente contenuto:``` c
Header:
-   Id = 0x424d53fc
-   OriginalCompressedSegmentSize = -0x38
-   CompressionAlgorithm = 1
-   Flag = 0
-   Offset = 0x10138
Data = ‘A’ * 0x10138 + compress(mdl1_ptr  + ‘\x00’*0x10 + mdl2_ptr) + ‘\xff’*0x10

Come possiamo vedere, OriginalCompressedSegmentSize contiene un valore negativo e la somma OriginalCompressedSegmentSize + Offset = 0x10100. Tuttavia, la dimensione del pacchetto inviato dal client al server è maggiore di 0x10100. Quindi l'allocazione iniziale creata dal server prima della decompressione sarà più grande dell'allocazione che contiene i dati dopo la decompressione. Il valore OriginalCompressedSegmentSize viene impostato negativo qui per fare in modo che la somma di OriginalCompressedSegmentSize e Offset sia esattamente 0x10100, senza influenzare la posizione dei dati compressi, poiché essa dipende da Offset. Inoltre, 0x38 è l'offset del puntatore Mdl1 nella struttura SRVNET_BUFFER_HDR.

Così il server creerà un'allocazione contenente i dati del client come segue:

Successivamente chiamerà la funzione SrvNetAllocateBuffer per allocare un buffer con dimensione della zona User buffer pari a 0x10100, cioè Alloc B secondo i passaggi precedenti:

Procede con la decompressione, naturalmente solo una parte dei dati viene decompressa correttamente:

Sulla base delle figure sopra, si può vedere che l'area dei due puntatori Mdl in SRVNET_BUFFER_HDR di Alloc B è stata modificata al valore desiderato.

Similmente a quanto sopra, questa volta impostiamo il flag a 3 regolando l'offset del pacchetto inviato come segue:``` c Header:

  • Id = 0x424d53fc
  • OriginalCompressedSegmentSize = -0x10
  • CompressionAlgorithm = 1
  • Flag = 0
  • Offset = 0x10110 Data = ‘A’ * 0x10110 + compress(‘\x00\x03’) + ‘\xff’*0x10
root@kitploit:~
Infine avrà la forma:

![](https://assets.kitploit.com/production/public/readmes/24502/467ea07dff7cfaca7062ed50d1605e40586c412f40bcadaf01aa37d862efd3bd.png)

Quando Alloc B viene liberato, verranno eseguiti i seguenti frammenti di codice:``` c
pMdl1->MappedSystemVa = (BYTE*)pMdl1->MappedSystemVa + 0x50;
pMdl1->ByteCount -= 0x50;
pMdl1->ByteOffset += 0x50;
pMdl1->MdlFlags |= 0x1000; // MDL_NETWORK_HEADER

pMdl2->StartVa = (PVOID)((ULONG_PTR)pMdl1->MappedSystemVa & ~0xFFF);
pMdl2->ByteCount = pMdl1->ByteCount;
pMdl2->ByteOffset = pMdl1->MappedSystemVa & 0xFFF;
pMdl2->Size = /* some calculation */;
pMdl2->MdlFlags = 0x0004; // MDL_SOURCE_IS_NONPAGED_POOL

Come sopra, pMdl1->MappedSystemVa (offset 0x18) conterrà il valore di pMdl1->MappedSystemVa + 0x50 = 0xffffd3843654de68 + 0x18 + 0x50 = 0xffffd3843654ded0.

Prima di liberare Alloc B, SRVNET_RECV sarà:

Dopo aver eseguito le prime 4 righe del codice sopra:

Prima di liberare Alloc A:

Dopo aver eseguito tutto il codice sopra:

E i byte che dobbiamo leggere in Alloc A sono i seguenti byte blu:

Quindi, basta utilizzare la tecnica di leak byte per byte sopra per ottenere l'indirizzo di AcceptSocket + 0x50. In questa parte, sarà 0xffffd3843ea02418 → AcceptSocket: 0xffffd3843ea023c8

Analogamente, faremo per leak l'indirizzo AcceptSocket→ srvnet!SrvNetWskConnDispatch

Dobbiamo preparare tutto come segue:

Dopo che Alloc B viene liberato, tutto cambierà come segue:

I byte che ci servono per ottenere l'indirizzo di AcceptSocket-> srvnet!SrvNetWskConnDispatch + 50 saranno in Alloc A, quei byte sono quelli colorati in blu nell'immagine sottostante:

Quindi AcceptSocket-> srvnet!SrvNetWskConnDispatch sarà 0xfffff80060e9d170, supponendo di conoscere il suo offset nel modulo srvnet.sys, possiamo calcolare l'indirizzo base di srvnet.

In questa parte, srvnet base è: 0xFFFFF80060E70000 con offset di srvnet!SrvNetWskConnDispatch pari a 0x2d170.

Successivamente, utilizzeremo la tecnica Write-what-where primitive di CVE-2020-0796 per scrivere arbitrariamente in un'area di memoria.

Per prima cosa, cercheremo di leak l'indirizzo base di ntoskrnl, attraverso il leak dell'indirizzo della funzione IoSizeofWorkItem importata da srvnet. Per fare ciò, creeremo 2 strutture UNICODESTRING come segue:``` c // Destination unicode string desLength = 6; desMaximumLength = 6; desBuffer = allocation_pool_object_ptr + 0x1650 + 0x20 + 2;

// Source unicode string srcLength = 6; srcMaximumLength = 6; srcBuffer = srvnet_base_ptr + OFFSETS['srvnet!imp_IoSizeofWorkItem'];

root@kitploit:~
Con `allocation_pool_object_ptr` come indirizzo dell'allocation pool che è stato leakato e `OFFSETS['srvnet!imp_IoSizeofWorkItem']` come offset della funzione IoSizeofWorkItem importata da srvnet.

Queste 2 strutture UNICODE_STRING saranno salvate a `allocation_pool_object_ptr + 0x1650` attraverso la tecnica Write-what-where trovata in CVE-2020-0796.
Per prima cosa salveremo la stringa unicode Destination a `allocation_pool_object_ptr + 0x1650`, procediamo a creare il pacchetto SMB come segue:``` c
Header:
-   Id = 0x424d53fc
-   OriginalCompressedSegmentSize = 0xffffffff
-   CompressionAlgorithm = 1
-   Flag = 0
-   Offset = 0x22
Data:
sentinel = os.urandom(2)  // 16 bits for verification
data = struct.pack('<HHIQ', desLength, desMaximumLength, 0, desBuffer)  // dest unicode string
data += struct.pack('<HHIQ', srcLength, srcMaximumLength, 0, srcBuffer) // src unicode string
data += sentinel
data_to_compress = os.urandom(0x1100 - len(data))
// 0x18 null bytes that override the struct.
data_to_compress += b'\x00'*0x18
// Target address.
data_to_compress += struct.pack('<Q', allocation_pool_object_ptr + 0x1650)
data = data + compress(data_to_compress)

Sopra, i dati contengono un sentinel generato dalla funzione os.urandom(2), che avrà una lunghezza di 2 byte, e questi 2 byte ci aiuteranno a sapere se l'indirizzo che abbiamo leakato è effettivamente l'indirizzo che dobbiamo leakare confrontandolo dopo che il processo di leak è riuscito.

Se la dimensione totale del pacchetto inviato dal client è maggiore di 0x1100 (questo dipenderà dai dati randomizzati prima della compressione), allora sicuramente allocation_pool_object_ptr verrà utilizzato per contenerlo sul server SMB:

Successivamente, il server SMB chiamerà la funzione SrvNetAllocateBuffer per allocare un'area di memoria per la decompressione. Ma poiché la somma di OriginalCompressedSegmentSize e Offset è 0x21, alloca solo un'area con un user buffer di dimensione 0x1100:

Si verifica l'errore di heap overflow (descritto in CVE-2020-0796) e dopo che il server SMB ha decompresso (prima che avvenga la copia dei dati non compressi) si avrà:

Quindi il puntatore UserBufferPtr punta all'inizio di allocation_pool_object_ptr + 0x1650 e quando avviene la copia, allocation_pool_object_ptr:

Allo stesso modo, inseriremo un altro sentinel sotto inviando il seguente pacchetto:``` c Header:

  • Id = 0x424d53fc
  • OriginalCompressedSegmentSize = 0xffffffff
  • CompressionAlgorithm = 1
  • Flag = 0
  • Offset = 0x2 Data: data = sentinel data_to_compress = os.urandom(0x1100 - len(data)) // 0x18 null bytes that override the struct. data_to_compress += b'\x00'*0x18 // Target address. data_to_compress += struct.pack('<Q', allocation_pool_object_ptr + 0x1650 + 0x28) data = data + compress(data_to_compress)
root@kitploit:~
Pertanto, quando il server SMB riceve il pacchetto, alloca la memoria corrispondente. A questo punto, la memoria allocata è `allocation_pool_object_ptr` e conterrà i dati come segue:

![](https://assets.kitploit.com/production/public/readmes/24502/24cc306a702cdb5f575bd1d612ea6b35db87aaf875fd45fa64977e50c61e85ed.png)

Dopo il processo di decompressione, i dati saranno:

![](https://assets.kitploit.com/production/public/readmes/24502/d6442e5a936e1c46ebb9e3a4651e8859dca56c1aa09e9e79ed9155ea105dede2.png)

Abbiamo quindi creato due stringhe Unicode e due sentinelle per verificare i dati che abbiamo leakato.

Successivamente, chiameremo la funzione `RtlCopyUnicodeString` e passeremo le due stringhe Unicode create sopra.

Per chiamare la funzione `RtlCopyUnicodeString`, sovrascriveremo prima il puntatore HandlerFunctions con l'indirizzo della funzione `RtlCopyUnicodeString`. Questa funzione è importata dal modulo srvnet e ha un offset (nel mio modulo) di 0x32288.

Pertanto, con la tecnica write-what-where, scriveremo l'indirizzo 0xFFFFF80060E70000 + 0x32288 - 0x8 in HandlerFunctions.

Per prima cosa, leakiamo un puntatore SRVNET_RECV (0xffffe00f0b593dd8).

![](https://assets.kitploit.com/production/public/readmes/24502/f7c5bde869eb5ba21a0b9dd4e2034417487150592178aa1eb1aef523e246a5c2.png)

Salveremo la connessione per continuare a inviare i pacchetti successivi.

Successivamente, utilizzeremo la tecnica write-what-where per scrivere sul puntatore RtlCopyUnicodeString - 0x8 (il motivo di - 0x8 è per far sì che la funzione RtlCopyUnicodeString sostituisca la funzione Srv2ReceiveHandler in HandlerFunctions).

![](https://assets.kitploit.com/production/public/readmes/24502/a88b44518f26ed6f0958772bdd4bf124c7c2403a525944b060a37cf17309a092.png)

Poi scriveremo rispettivamente i due puntatori delle due stringhe Unicode create sopra come due argomenti di HandlerFunction.

![](https://assets.kitploit.com/production/public/readmes/24502/691bdb0ce599face8b1f35393db9cc37e1f85a075c4d259a7fb96adee9fb66d2.png)

A questo punto, sulla stessa connessione, la funzione Srv2ReceiveHandler è stata sostituita da RtlCopyUnicodeString. Quindi, quando inviamo un pacchetto, la funzione RtlCopyUnicodeString verrà chiamata e copierà la stringa Unicode.

![](https://assets.kitploit.com/production/public/readmes/24502/2410149b163fd4ebbfb13fa5d2a127cafcfdc82146c33544b0a6cc6905c13d2b.png)

La prossima cosa da fare è leakare 10 byte di indirizzo da 0xffffd38439045670 a 0xffffd3843904567a (includendo le due sentinelle alle due estremità dell'indirizzo da leakare). Successivamente, verificare se i due byte all'inizio e alla fine dell'indirizzo leakato sono sentinelle; se lo sono, abbiamo leakato correttamente (0xfffff8068152c380).

Dopo aver leakato l'indirizzo di nt!IoSizeofWorkItem (0xfffff8068152c380), sottrarremo il suo offset (0x12C380) per ottenere l'indirizzo base di ntoskrnl (0xfffff80681400000).

Nota che ogni offset di ogni file modulo su diverse versioni di Windows è diverso, quindi assicurati di avere il file modulo corretto sulla macchina target.

Allo stesso modo, dopo aver ottenuto l'indirizzo base di ntoskrnl, otterremo MiGetPteAddress (0xBA968) e l'indirizzo base del PTE (MiGetPteAddress + 0x13):

![](https://assets.kitploit.com/production/public/readmes/24502/5c92df61b331c038bd90b2ccb878b7027bdbf6338602aecdda1b49a867eb42b1.png)

Il passo successivo è scrivere lo shellcode in 0xFFFFF78000000800 usando la tecnica write-what-where. Poi ricalcolare l'indirizzo dello shellcode nel PTE tramite la formula sottostante e cancellare il bit NX per rendere lo shellcode eseguibile:``` c
shellcode_addr >>= 9
shellcode_addr &= 0x7FFFFFFFF8
shellcode_addr += pte_base

Infine, scriveremo l'indirizzo dello shellcode in allocation_pool_object_ptr + 0x50 + 0x1600 e procederemo a chiamare lo shellcode sostituendo quell'indirizzo con HandlerFunctions e passando l'indirizzo nt_base_ptr allo shellcode.

Godetevi la RCE :))

Riferimenti

  • SMBleedingGhost Writeup: Chaining SMBleed (CVE-2020-1206) with SMBGhost
  • SMBleedingGhost Writeup Part II: Unauthenticated Memory Read – Preparing the Ground for an RCE
  • SMBleedingGhost Writeup Part III: From Remote Read (SMBleed) to RCE
  • Exploiting SMBGhost (CVE-2020-0796) for a Local Privilege Escalation: Writeup + POC
  • lznt1.py
  • TAKING WINDOWS 10 KERNEL EXPLOITATION TO THE NEXT LEVEL – LEVERAING WRITEWHAT-WHERE VULNERABILITIES IN CREATORS UPDATE
  • VERGILIUS_MDL
  • Exploit Development: Leveraging Page Table Entries for Windows Kernel Exploitation
  • RtlUnicodeStringCopy function
  • UNICODE_STRING structure

DatntSec. Viettel Cyber Security.

Scarica lo strumento
→ Dimensione allocazione ↓Processore logico0x11000x21000x41000x81000x101000x201000x401000x801000x100100
Processore 1📝📝📝📝📝📝📝📝📝
Processore 2📝📝📝📝📝📝📝📝📝
...
Processore n📝📝📝📝📝📝📝📝📝