03月13, 2020

微软SMBv3客户端/服务端远程代码执行漏洞(CVE-2020-0796)技术分析

微软安全中心在北京时间3月12日23时发布了影响Windows 10 等系统用户的SMBv3远程代码执行漏洞补丁。我们建议受影响的用户尽快按微软更新信息指南安装该补丁:https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2020-0796。 同时,360Vulcan Team对该漏洞进行了快速分析,该漏洞属于高危的零接触远程代码执行漏洞,技术分析如下。

SMB漏洞分析

根本原因

漏洞发生在srv2.sys中,由于SMB没有正确处理压缩的数据包,在解压数据包的时候使用客户端传过来的长度进行解压时,并没有检查长度是否合法.最终导致整数溢出。

详细分析

SMB v3中支持数据压缩,如果SMB Header中的ProtocolId为0x424D53FC也就是0xFC, 'S', 'M', 'B'.那么就说明数据是压缩的,这时smb会调用压缩解压处理的函数.

首先SMB会调用srv2!Srv2ReceiveHandler函数接收smb数据包,并根据ProtocoIId设置对应的处理函数。

__int64 __fastcall Srv2ReceiveHandler(__int64 a1, void *Src, __int64 a3, unsigned int a4, unsigned int *a5, char *Srca, struct _SLIST_ENTRY *a7, _QWORD *a8)
{
    ...

    //
    // 这里判断头部ProtocolId
    //

     if ( **((_DWORD **)&v20[15].Next[1].Next + 1) == 'BMS\xFC' )
      {
        if ( KeGetCurrentIrql() > 1u )
        {
          v20[14].Next = (_SLIST_ENTRY *)v11;
          v20[2].Next = (_SLIST_ENTRY *)Srv2DecompressMessageAsync;
          v43 = HIDWORD(v20->Next) == 5;
          *((_DWORD *)&v20[3].Next + 2) = 0;
          if ( v43 )
          {
            LOBYTE(v71) = 1;
            LOBYTE(v35) = 1;
            SRV2_PERF_ENTER_EX(&v20[32].Next + 1, v35, 307i64, "Srv2PostToThreadPool", (_DWORD)v71);
          }
          v44 = *((_QWORD *)&v20[3].Next[8].Next + 1);
          v45 = *(_QWORD *)(v44 + 8i64 * KeGetCurrentNodeNumber() + 8);
          if ( !ExpInterlockedPushEntrySList((PSLIST_HEADER)(v45 + 16), v20 + 1) && *(_WORD *)(v45 + 66) )
            RfspThreadPoolNodeWakeIdleWorker(v45);
          goto LABEL_168;

            }
        }
}

从代码中我们可以看出如果是压缩的数据则调用Srv2DecompressMessageAsync函数进行解压缩. srv2!Srv2DecompressMessageAsync会调用Srv2DecompressData函数.

结构如下:

typedef struct _COMPRESSION_TRANSFORM_HEADER
{
  ULONG ProtocolId;
  ULONG OriginalCompressedSegmentSize;
  USHORT CompressionAlgorithm;
  USHORT Flags;
  ULONG Length;
}COMPRESSION_TRANSFORM_HEADER, *PCOMPRESSION_TRANSFORM_HEADER;

产生整数溢出漏洞的代码如下:

__int64 __fastcall Srv2DecompressData(__int64 pData)
{
  __int64 v2; // rax
  COMPRESSION_TRANSFORM_HEADER Header; // xmm0 MAPDST
  __m128i v4; // xmm0
  unsigned int CompressionAlgorithm; // ebp
  __int64 UnComparessBuffer; // rax MAPDST
  int v9; // eax
  int v11; // [rsp+60h] [rbp+8h]

  v11 = 0;
  v2 = *(_QWORD *)(pData + 0xF0);
  if ( *(_DWORD *)(v2 + 0x24) < 0x10u )         // 这里判断数据包长度的最小值
    return 0xC000090Bi64;
  Header = *(COMPRESSION_TRANSFORM_HEADER *)*(_QWORD *)(v2 + 0x18);// [v2+0x18]中为客户端传进来的Buffer
                                                // [v2+0x24]为数据包长度
  v4 = _mm_srli_si128((__m128i)Header, 8);
  CompressionAlgorithm = *(_DWORD *)(*(_QWORD *)(*(_QWORD *)(pData + 0x50) + 0x1F0i64) + 0x8Ci64);
  if ( CompressionAlgorithm != v4.m128i_u16[0] )
    return 0xC00000BBi64;                      
  UnCompressBuffer = SrvNetAllocateBuffer((unsigned int)(Header.OriginalCompressedSegmentSize + v4.m128i_i32[1]), 0i64);// OriginalCompressedSegmentSize + CompressedSegmentSize,这里没有检查相加的值,导致整数溢出,分配一个较小的UnCompressBuffer
  if ( !UnComparessBuffer )
    return 0xC000009Ai64;
  if ( (int)SmbCompressionDecompress(
              CompressionAlgorithm,             // CompressionAlgorithm
              *(_QWORD *)(*(_QWORD *)(pData + 0xF0) + 0x18i64) + (unsigned int)Header.Length + 0x10i64,// CompressedBuffer
              (unsigned int)(*(_DWORD *)(*(_QWORD *)(pData + 0xF0) + 0x24i64) - Header.Length - 0x10),// CompressedBufferSize
              (unsigned int)Header.Length + *(_QWORD *)(UnComparessBuffer + 0x18),// UncompressedBuffer,会传入SmbCompressionDecompress函数进行Decompress处理。
              Header.OriginalCompressedSegmentSize,
              &v11) < 0
    || (v9 = v11, v11 != Header.OriginalCompressedSegmentSize) )
  {
    SrvNetFreeBuffer(UnComparessBuffer);
    return 0xC000090Bi64;
  }
  if ( Header.Length )
  {
    memmove(
      *(void **)(UnComparessBuffer + 24),
      (const void *)(*(_QWORD *)(*(_QWORD *)(pData + 240) + 24i64) + 16i64),
      (unsigned int)Header.Length);
    v9 = v11;
  }
  *(_DWORD *)(UnComparessBuffer + 36) = Header.Length + v9;
  Srv2ReplaceReceiveBuffer(pData, UnComparessBuffer);
  return 0i64;
}

通过上述代码我们可以看到,这个Header中的两个长度都没有进行检查. 下面是MS文档中对这两个长度的描述:

  1. OriginalCompressedSegmentSize (4 bytes) The size, in bytes, of the uncompressed data segment.
  2. Offset/Length (4 bytes) If SMB2_COMPRESSION_FLAG_CHAINED is set in Flags field, this field MUST be interpreted as Length. The length, in bytes, of the compressed payload. Otherwise, this field MUST be interpreted as Offset. The offset, in bytes, from the end of this structure to the start of compressed data segment.

随后在srv2!Srv2DecompressData函数中,会调用SmbCompressionDecompress,进而最终调用nt!RtlDecompressBufferXpressLz进行数据解压,调用路径如下:

ffff9480`20e2ad98  nt!RtlDecompressBufferXpressLz+0x2d0
ffff9480`20e2adb0  nt!RtlDecompressBufferEx2+0x66
ffff9480`20e2ae00  srvnet!SmbCompressionDecompress+0xd8
ffff9480`20e2ae70  srv2!Srv2DecompressData+0xdc

nt!RtlDecompressBufferXpressLz会在smb compress协议数据包里解析字段,其中包含需要Decompress buffer的大小,并和之前通过SrvNetAllocateBuffer分配的buffer的OriginalCompressedSegmentSize进行比较,确认其大小不大于OriginalCompressedSegmentSize,随后进行memcpy。

signed __int64 __fastcall RtlDecompressBufferXpressLz(_BYTE *a1, unsigned int a2, _BYTE *a3, unsigned int a4, __int64 a5, _DWORD *a6)
{
        v9 = &a1[a2];
        ....
        if ( &a1[v21] > v9 )
          return 0xC0000242i64;
        ...
        v33 = a1;
        a1 += v21;
        qmemcpy(v33, v23, v21);
}

如上伪代码所示,a1指向SrvNetAllocateBuffer分配的UncompressBuffer,a2的值为OriginalCompressedSegmentSize,v21的值为从smb数据包中解析的解压缩数据的大小,该值可由攻击者控制,若该大小大于OriginalCompressedSegmentSize,则会返回0xC0000242错误,由于之前对长度没有检查,如果我们传入一个很大的OriginalCompressedSegmentSize触发整数溢出,同时v21就可以设置一个极大值,而依然可以通过对decompress size的判断,最终调用qmemcpy拷贝一个极大的size导致缓冲区溢出。

补丁

微软的补丁就是在Srv2DecompressData里面对上述两个Length做了检查.

__int64 __fastcall Srv2DecompressData(__int64 a1)
{

      //
      //添加了对Header中Length的检查
      //

      if ( RtlULongAdd(Header.OriginalCompressedSegmentSize, Header2.Length, &pulResult) < 0 )
      {

          ...
      }

      if ( pulResult > (unsigned __int64)(unsigned int)(*(_DWORD *)(v9 + 0x24) + 0x100) + 0x34 )
     {

         ...
     }

      if ( RtlULongSub(pulResult, Header.Length, &pulResult) < 0 )
     {

         ...
     }
}

协议文件见: SMB2 COMPRESSION_TRANSFORM_HEADER

本文链接:http://blogs.360.cn/post/CVE-2020-0796.html

-- EOF --

Comments