简介
SMBv3.1.1压缩机制中的SMBGhost(CVE-2020-0796)漏洞大概三个月前已经修复。在之前的文章中,我们分析了该漏洞,并演示了一种利用该漏洞进行本地提权的方法。正如我们在研究中发现的,这并不是SMB解压功能中唯一的缺陷。SMBleed发生在与SMBGhost相同的函数中。该漏洞允许攻击者读取未初始化的内核内存,会在本文中详细介绍。
分析
这个漏洞发生在与SMBGhost相同的函数中,即srv2.sys SMB服务驱动程序中的Srv2DecompressData函数。下面是该函数的简化版,省略了无关的信息:
typedef struct _COMPRESSION_TRANSFORM_HEADER
{
ULONG ProtocolId;
ULONG OriginalCompressedSegmentSize;
USHORT CompressionAlgorithm;
USHORT Flags;
ULONG Offset;
} COMPRESSION_TRANSFORM_HEADER, *PCOMPRESSION_TRANSFORM_HEADER;
typedef struct _ALLOCATION_HEADER
{
// ...
PVOID UserBuffer;
// ...
} ALLOCATION_HEADER, *PALLOCATION_HEADER;
NTSTATUS Srv2DecompressData(PCOMPRESSION_TRANSFORM_HEADER Header, SIZE_T TotalSize)
{
PALLOCATION_HEADER Alloc = SrvNetAllocateBuffer(
(ULONG)(Header->OriginalCompressedSegmentSize + Header->Offset),
NULL);
If (!Alloc) {
return STATUS_INSUFFICIENT_RESOURCES;
}
ULONG FinalCompressedSize = 0;
NTSTATUS Status = SmbCompressionDecompress(
Header->CompressionAlgorithm,
(PUCHAR)Header + sizeof(COMPRESSION_TRANSFORM_HEADER) + Header->Offset,
(ULONG)(TotalSize - sizeof(COMPRESSION_TRANSFORM_HEADER) - Header->Offset),
(PUCHAR)Alloc->UserBuffer + Header->Offset,
Header->OriginalCompressedSegmentSize,
&FinalCompressedSize);
if (Status < 0 || FinalCompressedSize != Header->OriginalCompressedSegmentSize) {
SrvNetFreeBuffer(Alloc);
return STATUS_BAD_DATA;
}
if (Header->Offset > 0) {
memcpy(
Alloc->UserBuffer,
(PUCHAR)Header + sizeof(COMPRESSION_TRANSFORM_HEADER),
Header->Offset);
}
Srv2ReplaceReceiveBuffer(some_session_handle, Alloc);
return STATUS_SUCCESS;
}
Srv2DecompressData函数接收客户端发送的压缩消息,分配所需的内存,并解压缩数据。然后,如果Offset字段不为零,它会将放置在压缩数据之前的数据复制到已分配的缓冲区的起始位置。
SMBGhost漏洞是由于缺少整数溢出检查而发生的,微软已经修复了这个问题,即使进行了这些检查,仍然存在严重的漏洞。你能发现吗?
再次伪造OriginalCompressedSegmentSize
之前,我们通过将OriginalCompressedSegmentSize字段设置为一个较大的数字来利用SMBGhost,从而导致整数溢出,然后越界写入。如果我们将它设置为一个比我们发送的实际解压缩数据稍大一点的数字,会发生什么?例如,如果我们压缩的数据在解压缩后的大小是x,我们将OriginalCompressedSegmentSize设为x + 0x1000,我们将得到:
未初始化的内核数据将被视为我们消息的一部分。
如果你没有阅读我们之前的文章,你可能会认为Srv2DecompressData函数调用应该会失败,因为SmbCompressionDecompress调用之后的检查:
if (Status < 0 || FinalCompressedSize != Header->OriginalCompressedSegmentSize) {
SrvNetFreeBuffer(Alloc);
return STATUS_BAD_DATA;
}
具体来说,在我们的示例中,你可以假设,虽然OriginalCompressedSegmentSize字段的值是x+0x1000,但在这种情况下,FinalCompressedSize将设置为x。实际上,由于SmbCompressionDecompress函数的实现,FinalCompressedSize也将被设置为x + 0x1000:
NTSTATUS SmbCompressionDecompress(
USHORT CompressionAlgorithm,
PUCHAR UncompressedBuffer,
ULONG UncompressedBufferSize,
PUCHAR CompressedBuffer,
ULONG CompressedBufferSize,
PULONG FinalCompressedSize)
{
// ...
NTSTATUS Status = RtlDecompressBufferEx2(
...,
FinalUncompressedSize,
...);
if (status >= 0) {
*FinalCompressedSize = CompressedBufferSize;
}
// ...
return Status;
}
如果解压成功,则更新FinalCompressedSize以保存CompressedBufferSize的值,该值是缓冲区的大小。这看起来没有必要,更新FinalCompressedSize值使SMBGhost的利用更加容易,而且还存在SMBleed漏洞。
exploitation
我们用来演示该漏洞的SMB消息是SMB2 WRITE消息。消息结构包含要写入的字节数和flags等字段,然后是可变长的缓冲区。这非常适合利用这个漏洞,因为我们可以通过指定header来创建消息,但是缓冲区包含未初始化的数据。我们的POC基于Microsoft的WindowsProtocolTestSuites存储库(我们也将其用于第一次SMBGhost复制),为压缩函数引入了一个小的补充:
// HACK: fake size
if (((Smb2SinglePacket)packet).Header.Command == Smb2Command.WRITE)
{
((Smb2WriteRequestPacket)packet).PayLoad.Length += 0x1000;
compressedPacket.Header.OriginalCompressedSegmentSize += 0x1000;
}
请注意,我们的POC需要凭据和可共享写,这在许多场景中都可用,但是这个漏洞适用于每个消息,因此它可能在没有身份验证的情况下被利用。还要注意,泄漏的内存来自NonPagedPoolNx池中以前的分配,并且由于我们控制分配的大小,因此我们也许能够在某种程度上控制泄漏的数据。
受影响的Windows版本
Windows 10版本1903、1909和2004会受到影响。在测试期间,我们的POC使一台Windows 10 1903机器崩溃了。在分析了崩溃后,我们发现早期未修补版本的Windows 10 1903在处理有效的压缩SMB数据包时有一个空指针解引用漏洞,我们没有进一步分析是否有可能绕过空指针解引用漏洞。
在未修补的系统中,此处发生空指针解引用。
打补丁后的系统,添加了空指针检查。
这是受影响的Windows版本的摘要,其中安装了相关更新:
我们还没有尝试绕过空指针解引用,但是可以通过另一种方法来实现(例如,使用SMBGhost Write-What-Where原语)
SMBleedingGhost? SMBleed与SMBGhost 预授权RCE
在没有身份验证的情况下利用SMBleed漏洞不太容易,但是也可以。我们可以将它与SMBGhost漏洞一起使用,从而实现RCE。相关技术细节的报告将很快发表。目前,请参见下面演示利用漏洞的POC。此POC仅限用于学习和研究目的,以及安全评估。使用风险自负,ZecOps对POC不承担任何责任。
SMBGhost + SMBleed RCE POC Source Code
防范措施
- 更新Windows系统,将彻底解决问题(推荐)
- 禁用445端口
- 加强主机隔离
- 禁用SMB 3.1.1压缩(不推荐使用的解决方案)