
Технический анализ уязвимости CVE-2020-1206 (SMBleed) — раскрытия информации в ядре Windows SMBv3, включая неаутентифицированный оракул утечки памяти и техники эксплуатации в сочетании с SMBGhost для удалённого выполнения кода (RCE).
В уязвимости SMBGhost (CVE-2020-0796) я рассказывал о технике write-what-where primitive через использование целочисленного переполнения (integer overflow) для изменения указателя Alloc.Userbuffer таким образом, чтобы он указывал на нужный нам адрес, и записи туда произвольных данных. Подобно SMBGhost, эта уязвимость также присутствует в функции Srv2DecompressData в srv2.sys. Давайте ещё раз посмотрим на функцию Srv2DecompressData, связанную с уязвимостью SMBGhost (CVE-2020-0796), упрощённую Zecops.``` 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 не равно нулю, она копирует данные (RawData), расположенные перед сжатыми данными, в начало выделенной области памяти.

Ошибка SMBGhost заключается в том, что функция не проверяет целочисленное переполнение, что приводит к выделению неправильного размера и вызывает переполнение буфера. Через 3 месяца после того, как Microsoft исправила SMBGhost, была найдена уязвимость CVE-2020-1206 (SMBleed, как её называет [Zecops Blog](https://blog.zecops.com/)). Эта уязвимость позволяет нам утечь адрес другой машины, а в сочетании с 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 + 0x1000 вместо x, после успешной распаковки переменная 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;
}
Поскольку после успешной распаковки FinalCompressedSize обновляется, чтобы содержать значение CompressedBufferSize (соответствующее OriginalCompressedSegmentSize, переданному в функцию SmbCompressionDecompress). Последующее обновление и проверка практически не нужны и могут привести к некоторым непредвиденным ошибкам.
# Базовая эксплуатация
Структура сообщения, которую Zecops использует для демонстрации уязвимости, — это [сообщение SMB2 WRITE](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/e7046961-3318-4350-be2a-a8d69bb59ce8). Эта структура содержит такие поля, как количество записываемых байтов, флаги и т. д., за которыми следует буфер произвольной длины. Это довольно удобно для эксплуатации, поскольку можно создать сообщение и указать заголовок с буфером, содержащим неинициализированные данные.
Основываясь на POC от [Zecops](https://blog.zecops.com/) в репозитории Microsoft WindowsProtocolTestSuites, чтобы лучше понять это, мы добавим небольшое дополнение к функции сжатия:``` 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.