
Windows SMBv3의 CVE-2020-1206(SMBleed) 커널 정보 공개 취약점에 대한 기술 분석으로, 인증되지 않은 메모리 누수 오라클 및 SMBGhost와 결합한 RCE 익스플로잇 기술을 포함합니다.
SMBGhost 취약점(CVE-2020-0796)에서 저는 integer overflow 버그를 이용해 Alloc.Userbuffer 포인터를 우리가 원하는 주소로 변경하고 그곳에 임의의 데이터를 쓰는 write-what-where primitive 기법에 대해 이야기했습니다. SMB Ghost와 유사하게, 이 취약점도 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;
}
Srv2DecompressData 함수는 클라이언트가 보낸 압축된 메시지를 입력받아 필요한 메모리 영역을 할당하고 그곳에 메시지를 압축 해제합니다. 그 후 Offset 필드가 0이 아니면 압축된 데이터 앞의 데이터(RawData)를 할당된 메모리 영역의 앞부분에 복사합니다.

SMBGhost 취약점은 함수가 integer overflow를 검사하지 않아 잘못된 크기를 할당하여 buffer overflow가 발생하는 데 있습니다. Microsoft가 SMBGhost를 패치한 지 3개월 후, CVE-2020-1206(SMBleed - [Zecops Blog](https://blog.zecops.com/)에서 명명) 취약점이 발견되었습니다. 이 취약점을 통해 다른 컴퓨터의 주소를 누출(leak)할 수 있으며, SMBGhost와 결합하면 RCE를 얻을 수 있습니다. Srv2DecompressData 함수를 더 간단히 살펴보기 위해, SMBGhost가 패치되기 전의 이 함수를 사용하면서 패치되었다고 가정하겠습니다.
# OriginalCompressedSegmentSize 위조
SMBGhost와 마찬가지로, 이번에도 OriginalCompressedSegmentSize를 우리가 보내는 압축 해제 데이터보다 약간 큰 값으로 위조합니다. 예를 들어 x바이트 크기의 데이터를 압축한 경우 OriginalCompressedSegmentSize 필드에 x를 넣는 대신 x + 0x1000으로 설정합니다. 다음 그림을 보면 더 명확합니다:

초기화되지 않은 커널 데이터는 메시지의 일부로 간주됩니다.
제가 [CVE-2020-0796](https://github.com/datntsec/CVE-2020-0796) 분석 글에서 말했듯이, Srv2DecompressData는 압축 해제가 성공하면 SmbCompressionDecompress 함수 이후의 검사 단계를 여전히 건너뜁니다:``` c
if (Status < 0 || FinalCompressedSize != Header->OriginalCompressedSegmentSize) {
SrvNetFreeBuffer(Alloc);
return STATUS_BAD_DATA;
}
OriginalCompressedSegmentSize 필드는 x 대신 x + 0x1000으로 설정되어 있지만, 압축 해제에 성공한 후 FinalCompressedSize 변수에는 x 값이 포함되지 않고 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;
}
Bởi vì sau khi giải nén thành công, FinalCompressedSize được cập nhật để giữ giá trị CompressedBufferSize (tương ứng với OriginalCompressedSegmentSize được truyền vào hàm SmbCompressionDecompress). Việc cập nhật và kiểm tra sau đó là gần như không cần thiết, có thể dẫn đến một số lỗi không mong muốn.
Wait, I accidentally output source? Need output Korean. Let's correct.
Because after successful decompression... Actually source is Vietnamese, need Korean. Let's produce proper translation.
# 기본 수준의 익스플로잇
Zecops가 취약점을 증명하기 위해 사용하는 메시지 구조는 [SMB2 WRITE 메시지](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/e7046961-3318-4350-be2a-a8d69bb59ce8)입니다. 이 구조에는 쓸 수 있는 바이트 수, 플래그, ... 등의 필드가 포함되며, 그 뒤에 임의 길이의 버퍼가 이어집니다. 이는 취약점을 악용하기에 상당히 적합한데, 헤더를 지정하고 초기화되지 않은 데이터를 포함하는 버퍼를 가진 메시지를 생성할 수 있기 때문입니다.
Microsoft의 WindowsProtocolTestSuites 저장소에 있는 [Zecops](https://blog.zecops.com/)의 POC를 기반으로, 이에 대해 더 명확히 살펴보기 위해 compression 함수에 다음과 같은 작은 추가를 하겠습니다:``` c
// HACK: fake size
if (((Smb2SinglePacket)packet).Header.Command == Smb2Command.WRITE)
{
((Smb2WriteRequestPacket)packet).PayLoad.Length += 0x1000;
compressedPacket.Header.OriginalCompressedSegmentSize += 0x1000;
}
Lưu ý rằng POC này yêu cầu thông tin xác thực và chia sẻ quyền write, thường có sẵn trong nhiều trường hợp. Tuy nhiên, lỗi trả về sẽ áp dụng cho mọi message (bao gồm cả message có hoặc không có thông tin xác thực), nên có khả năng ta sẽ có thể khai thác mà không cần phải xác thực. Một điều nữa, là bộ nhớ mà chúng ta sẽ leak là từ các lần phân bổ trước trong NonPagedPoolNx và vì chúng ta có thể kiểm soát kích thước phân bổ, chúng ta sẽ kiểm soát được dữ liệu mà chúng ta sẽ leak ở một mức độ nào đó.
Vậy nếu không có thông tin xác thực thì có thể leak được địa chỉ kernel không? Để trả lời cho câu hỏi này, ta hãy phân tích SMB sâu hơn nữa.
클라이언트는 인증 시 다음 메시지를 보냅니다:
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 명령이 전송됩니다. 반환되는 패킷에는 악용에 필요한 데이터가 포함되어 있지 않으며, 반환 패킷에 영향을 줄 방법도 없습니다. 그러나 두 번째 응답 패킷은 패킷 헤더에 status 0xC000006D(STATUS_LOGON_FAILURE)를 가지며 빈 본문을 갖습니다.
첫 번째 SMB2 SESSION_SETUP 패킷에는 NTLM Negotiate message 요청이 포함되고, 두 번째 패킷에는 NTLM Authenticate message가 포함됩니다. NTLM Negotiate 메시지는 비교적 단순하고 흥미로운 부분이 없을 수 있으므로 NTLM Authenticate 메시지를 자세히 살펴보겠습니다.
NTLM Authenticate 메시지를 연구한 결과, 이 메시지에서 가장 복잡하고 악용에 가장 적합한 부분은 NTLM2 V2 Response 구조입니다. 이 구조는 고정 크기가 아닌 바이트 배열로, 주로 NTLMv2_CLIENT_CHALLENGE 구조를 포함합니다. 이 구조가 초기 검사를 통과하지 못하면 0xC000006D(STATUS_LOGON_FAILURE) 대신 0xC000000D(STATUS_INVALID_PARAMETER) 값이 반환된다는 것을 알 수 있습니다. 초기 검사 중 하나는 AvPairs 필드 검사입니다.
AvPairs 필드는 AV_PAIR 구조를 포함하는 고정 크기가 아닌 바이트 배열입니다. 각 AV_PAIR는 attribute/value 쌍을 정의하며, attribute는 AvId 필드로 정의되고, AvLen 필드는 value의 바이트 길이를 정의하며, Value 필드는 value 자체를 담는 고정 크기가 아닌 바이트 배열입니다. MsvAvEOL attribute와 길이가 0인 항목이 배열의 끝을 표시합니다.