在SMBGhost漏洞(CVE-2020-0796)中,我提到了一种通过利用整数溢出漏洞修改Alloc.Userbuffer指针指向我们想要的地址并写入任意数据的write-what-where原语技术。类似于SMBGhost,此漏洞也存在于srv2.sys中的Srv2DecompressData函数中。让我们再次回顾由Zecops简化的与SMBGhost漏洞(CVE-2020-0796)相关的Srv2DecompressData函数。``` c 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;
}
Hàm Srv2DecompressData nhận vào một message được nén do client gửi đến và tiến hành cấp phát một vùng nhớ cần thiết, giải nén message vào đó. Sau đó, nếu trường Offset khác không, nó sẽ copy data (RawData) ở trước data được nén vào phần đầu vùng nhớ được cấp phát.

Lỗi SMBGhost nằm ở việc hàm không kiểm tra integer overflow, dẫn đến cấp phát sai kích thước gây ra buffer overflow. Sau 3 tháng kể từ ngày Microsoft vá SMBGhost, lỗ hỏng CVE-2020-1206 (SMBleed - theo cách gọi của [Zecops Blog](https://blog.zecops.com/)) được tìm thấy. Lỗ hỏng này cho phép chúng ta leak được địa chỉ của máy khác, và nếu kết hợp với SMBGhost, ta có thể có được RCE. Để có cái nhìn đơn giản hơn về hàm Srv2DecompressData, ta sẽ dùng lại hàm này khi chưa được vá lỗi SMBGhost và giả sử rằng nó đã được vá.
# Giả mạo OriginalCompressedSegmentSize
Như SMBGhost, lần này ta vẫn sẽ giả mạo OriginalCompressedSegmentSize với một số lớn hơn một chút so với dữ liệu giải nén mà ta gửi. Ví dụ ta nén một data có kích thước x byte, thay vì đặt vào trường OriginalCompressedSegmentSize x, ta sẽ đặt thành x + 0x1000, xem hình sau sẽ rõ hơn:

Uninitialized kernel data sẽ được coi như một phần của message.
Như ở bài phân tích [CVE-2020-0796](https://github.com/datntsec/CVE-2020-0796) tôi có nói, Srv2DecompressData vẫn sẽ bỏ qua giai đoạn kiểm tra sau hàm SmbCompressionDecompress nếu việc decompress diễn ra thành công:``` c
if (Status < 0 || FinalCompressedSize != Header->OriginalCompressedSegmentSize) {
SrvNetFreeBuffer(Alloc);
return STATUS_BAD_DATA;
}
Mặc dù trường OriginalCompressedSegmentSize được đặt thành x + 0x1000 thay vì x, nhưng sau khi giải nén thành công, biến FinalCompressedSize không chứa giá trị x, mà sẽ chứa giá trị x + 0x1000:```c 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 的值(对应于传入 SmbCompressionDecompress 函数的 OriginalCompressedSegmentSize)。随后的更新和检查几乎是不必要的,可能导致一些意外错误。
# 基础利用
Zecops 用于演示漏洞的消息结构是 [SMB2 WRITE message](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/e7046961-3318-4350-be2a-a8d69bb59ce8)。该结构包含可写入字节数、标志等字段,后面跟着一个任意长度的缓冲区。这对于利用漏洞来说非常完美,因为我们可以创建一个消息并指定头部,而缓冲区中包含未初始化的数据。
基于 [Zecops](https://blog.zecops.com/) 在微软 WindowsProtocolTestSuites 仓库中的 POC,为了更清楚地了解这一点,我们将为压缩函数添加这个小的补充:``` c
// HACK: fake size
if (((Smb2SinglePacket)packet).Header.Command == Smb2Command.WRITE)
{
((Smb2WriteRequestPacket)packet).PayLoad.Length += 0x1000;
compressedPacket.Header.OriginalCompressedSegmentSize += 0x1000;
}
请注意,此POC需要认证信息和写权限,这在很多情况下都是可用的。然而,返回的错误将适用于所有消息(包括有或没有认证信息的消息),因此有可能我们可以在无需认证的情况下进行利用。另外,我们将泄漏的内存来自先前在NonPagedPoolNx中的分配,并且由于我们可以控制分配大小,我们可以在一定程度上控制我们将泄漏的数据。
那么,如果没有认证信息,是否可以泄漏内核地址呢?为了回答这个问题,让我们进一步深入分析SMB。
在进行认证时,客户端会发送以下消息:
SMB2 NEGOTIATE → SMB2 SESSION_SETUP → SMB2 SESSION_SETUP
如果认证信息错误,连接会在第二个SMB2 SESSION_SETUP数据包之后被终止:

假设我们没有认证信息,我们将检查是否存在一个可以在无需认证的情况下发送的命令。通过搜索,我们注意到:
SMB2 NEGOTIATE,它也是整个会话中唯一的SMB2 NEGOTIATE命令。SMB2 SESSION_SETUP。其中,SMB2 NEGOTIATE消息不会被压缩。漏洞位于解压函数中,因此我们不会考虑它,只考虑SMB2 SESSION_SETUP消息。
如上所述,一个正常会话会发送两个SMB2 SESSION_SETUP命令。返回的数据包不包含任何我们可利用的数据,我们也没有办法影响返回的数据包。然而,第二个返回数据包将拥有空主体,并在数据包头中带有状态0xC000006D(STATUS_LOGON_FAILURE)。注意到,第一个SMB2 SESSION_SETUP数据包包含NTLM Negotiate消息请求,第二个数据包包含NTLM Authenticate消息。NTLM Negotiate消息相当简单,可能没什么有趣的,因此我们将深入探讨NTLM Authenticate消息。
在研究NTLM Authenticate消息后,我们注意到该消息中最复杂且最适合利用的部分是NTLM2 V2 Response结构。这个结构是一个大小不固定的字节数组,主要包含NTLMv2_CLIENT_CHALLENGE结构。我们看到,如果这个结构没有通过初始检查,将返回0xC000000D(STATUS_INVALID_PARAMETER)而不是0xC000006D(STATUS_LOGON_FAILURE)。其中一个初始检查是检查AvPairs字段。
AvPairs字段是一个大小不固定的字节数组,包含AV_PAIR结构。每个AV_PAIR定义了一个属性/值对,属性由AvId字段定义,AvLen字段定义值的字节长度,Value字段是一个大小不固定的字节数组,包含其自身的值。一个带有MsvAvEOL属性和零长度的项标记数组的结束。

Authenticate消息由msv1_0.dll模块中的SsprHandleAuthenticateMessage函数处理。在初始检查中,该函数确保AvPairs数组包含以下属性:0x0001(MsvAvNbComputerName)、0x0002(MsvAvNbDomainName)。然而,它们的值不被检查,它只通过遍历数组来检查所需属性是否存在以及它的长度是否在结构内。如果长度过大,传输将停止。因此,实际上,MsvAvEOL是否有效并未被检查。
此时,我们已发现我们可以构造一个请求,帮助我们回答以下问题:对于偏移量x处的两个字节,类型为uint16,其值是否大于y?x和y由我们控制。请看以下数据包:

value 0x0001(MsvAvNbComputerName)的内容并不重要,因此我们可以用它来调整第二个值的偏移量。对于第二个值,我们只将属性设置为0x0002(MsvAvNbDomainName),而不初始化len和value。同时,设置整个数据包的大小,使得根据length字段有y个字节。根据第二个值的未初始化length字段的值,可能有两种结果: