Skip to content
KitploitKITPLOIT
ИнструментыБлог
Отправить
ИнструментыБлог
Отправить

Инструменты для хакинга, пентеста и кибербезопасности — ваш арсенал защиты!

Kitploit — это каталог инструментов для хакинга, кибербезопасности и пентестинга. Находите последние обновления проектов для поиска уязвимостей, анализа систем, автоматизации тестирования и усиления вашей безопасности.

··Ленты·Контакты·Конфиденциальность·© 2026 Kitploit

Каталог инструментов

Категории

Все категории
Loading categories
CVE-2020-1206 | Kitploit
Инструменты/GitHubGitHub/datntsec/cve-2020-1206
Криминалистика памятиАнализ уязвимостейЭксплуатацияСбор информацииТестирование на ПроникновениеЭксплуатация Бинарных Файлов
GitHubdatntsec/cve-2020-1206

CVE-2020-1206

Репозиторий
5 лет назадЕщё не проверено

Популярное

Смотреть все →

Откройте для себя самые используемые инструменты нашего сообщества.

Изучить все инструменты

Просмотрите нашу коллекцию инструментов

Смотреть все инструменты →
Поделиться

В уязвимости 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; }

root@kitploit:~
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;

}

root@kitploit:~
Функция Srv2DecompressData принимает сжатое сообщение, отправленное клиентом, и выделяет необходимую область памяти, распаковывая в неё сообщение. Затем, если поле Offset не равно нулю, она копирует данные (RawData), расположенные перед сжатыми данными, в начало выделенной области памяти.

![](https://assets.kitploit.com/production/public/readmes/24502/a5bf8b5059336fb3677162343bace0d0b45085f2eb89e42d85f81e032fe233dc.png)

Ошибка 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 — см. рисунок ниже:

![](https://assets.kitploit.com/production/public/readmes/24502/687d3bdc67e4b2f5bb3cecbe41ea52be98a5c904a8688b325a69acb0a854ca75.png)

Неинициализированные данные ядра будут считаться частью сообщения.

Как я говорил в анализе [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) { // ...

root@kitploit:~
NTSTATUS Status = RtlDecompressBufferEx2(
    ...,
    FinalUncompressedSize,
    ...);
if (status >= 0) {
    *FinalCompressedSize = CompressedBufferSize;
}

// ...

return Status;

}

root@kitploit:~
Поскольку после успешной распаковки 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, и поскольку мы можем контролировать размер выделения, мы будем в некоторой степени контролировать данные, которые утечём.

Исходный код POC SMBleed

Итак, можно ли без учетных данных утечь адрес ядра? Чтобы ответить на этот вопрос, давайте проанализируем SMB глубже.

Углубляемся в 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

Как уже было сказано выше, в обычной сессии отправляются две команды SMB2 SESSION_SETUP. Ответные пакеты не содержат данных, которые могли бы быть полезны для эксплуатации, и у нас нет способа повлиять на ответный пакет. Однако второй ответный пакет имеет пустое тело со статусом 0xC000006D (STATUS_LOGON_FAILURE) в заголовке пакета. Заметим, что первый пакет SMB2 SESSION_SETUP содержит запрос NTLM Negotiate message, а второй пакет содержит NTLM Authenticate message. Сообщение NTLM Negotiate довольно простое и, возможно, не содержит ничего интересного, поэтому мы углубимся в NTLM Authenticate message.

NTLM Authenticate message

Изучив NTLM Authenticate message, мы замечаем, что самая сложная часть этого сообщения, наиболее подходящая для эксплуатации, — это структура 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 обрабатывается функцией SsprHandleAuthenticateMessage в модуле msv1_0.dll. В ходе начальных проверок эта функция гарантирует, что массив AvPairs содержит следующие атрибуты: 0x0001 (MsvAvNbComputerName), 0x0002 (MsvAvNbDomainName). Однако их значения не проверяются; проверка выполняется путём прохода по массиву и выяснения, существует ли требуемый атрибут и укладывается ли его длина в структуру. Если длина слишком велика, проход останавливается. Поэтому на практике корректность MsvAvEOL не проверяется.

На данный момент мы выяснили, что можем создать запрос, который поможет ответить на следующий вопрос: для двух байтов на смещении x, имеющих тип uint16, больше ли значение, чем y? x и y контролируются нами. Рассмотрим следующий пакет:

Содержимое значения 0x0001 (MsvAvNbComputerName) неважно, поэтому мы можем использовать его для регулировки смещения второго значения. Для второго значения мы задаём только атрибут 0x0002 (MsvAvNbDomainName), не инициализируя len и value. Одновременно мы задаём размер всего пакета так, чтобы по полю length было y байт. Возможны два исхода в зависимости от неинициализированного значения поля length второго значения:

  • length <= y: В этом случае проверка проходит, поскольку найдено допустимое значение 0x0002 (MsvAvNbDomainName). Сервер возвращает 0xC000006D (STATUS_LOGON_FAILURE), так как учетные данные неверны.
  • length > y: В этом случае проверка не удаётся, поскольку второе значение имеет недопустимую длину и отбрасывается. Сервер возвращает 0xC000000D (STATUS_INVALID_PARAMETER) в этом случае.

По ответу сервера мы всегда можем вывести ответ на приведённый выше вопрос.

Однако NTLM Authenticate message ограничен 0xB48 байт и будет отклонён, если превысит этот размер. Проверка выполняется функцией SspContextGetMessage в модуле msv1_0.dll. Тогда предположим, что мы записываем только 1 байт len, а второй байт будет содержать неинициализированное значение; можно ли обойти это? К сожалению, нет, поскольку значение uint16 кодируется в little endian. Таким образом, мы не можем добиться желаемого в рамках одной SMB-сессии; рассмотрим другие факторы.

Наблюдение №1: Lookaside lists

Как упоминалось в предыдущем исследовании (CVE-2020-0796), модули ядра, обрабатывающие SMB (srv2.sys и srvnet.sys), используют пользовательскую функцию выделения — SrvNetAllocateBuffer, экспортируемую srvnet.sys. Эта функция использует lookaside lists для небольших выделений с целью оптимизации. Lookaside lists используются для эффективного хранения набора буферов фиксированного размера, которые могут повторно использоваться драйвером.

Lookaside lists создаются при инициализации; списки для каждого размера и логического процессора описаны в следующей таблице:

Каждая ячейка с символом "📝" — это отдельный lookaside list. Чтобы упростить анализ, предположим, что у целевой системы только один логический процессор. В этом случае, пока выделяется одинаковое количество байт и используется один и тот же lookaside list, один и тот же буфер будет использоваться повторно много раз. Мы можем использовать это, чтобы получить некоторый контроль над неинициализированными данными.

Наблюдение №2: Сбой декомпрессии

Давайте ещё раз посмотрим, что происходит, когда сжатый пакет декомпрессируется (обратитесь к writeup CVE-2020-0796 за дополнительными деталями и псевдокодом):

В случае недопустимых CompressedData этап декомпрессии завершается неудачей, этап копирования не выполняется и соединение разрывается. Но декомпрессия может завершиться неудачей лишь после распаковки части допустимых CompressedData. Это позволяет нам создать запрос, при котором выбранные нами данные будут записаны по выбранному нами смещению, как показано на рисунке ниже:

Возвращаемся к сообщению NTLM Authenticate

Мы можем использовать приведённые выше наблюдения, чтобы заставить нашу технику работать, выполнив два шага:

  1. Отправить сообщение с недопустимыми сжатыми данными, чтобы был распакован только один единственный байт 0. Этот байт станет первым байтом поля length второго Value в массиве AvPairs.
  2. Отправить сообщение, как и раньше, но гарантировать, что для выделения используется тот же lookaside list, чтобы байт 0 оказался там.

На этот раз данная техника может ответить на следующий вопрос: для одного байта на смещении x, больше ли значение, чем y? Как и раньше, x и y контролируются нами.

Поскольку мы можем многократно переиспользовать буфер, гарантируя использование одного и того же lookaside list, мы можем повторять шаги много раз, меняя y, и в итоге вывести значение байта на определённом смещении.

Однако у этой техники есть ограничение: смещение байта, который мы можем прочитать, ограничено байтом 0xADB от начала буфера пакета. Это связано с тем, что смещение NTLM Authenticate message (AUTHENTICATE_MESSAGE) ограничено 0x40 байтами после завершения заголовков SMB2 SESSION_SETUP (что обеспечивается функцией Smb2ValidateSessionSetup в srv2.sys), а размер NTLM Authenticate message (AUTHENTICATE_MESSAGE) ограничен 0xB48 байт. Мы найдём способ обойти это.

Предположим, мы хотим прочитать байт на смещении 0x1100. Мы не можем сделать это напрямую с помощью описанной выше техники, однако можем использовать дополнительный приём: поскольку буферы переиспользуются из lookaside lists, мы можем «поднять» целевой байт с помощью функции декомпрессии, установив поле Offset так, чтобы перепрыгнуть через этот байт. Нужно лишь гарантировать, что данные там можно интерпретировать как допустимые сжатые данные; в противном случае копирование не произойдёт.

Буфер пакета, содержащий данные, отправленные клиентом, содержит дополнительные 16 байт заголовков, которые не копируются при декомпрессии. В результате скопированные и распакованные данные, включая целевой байт, копируются на позицию, которая на 16 байт ближе к началу выделенного буфера. Мы можем повторить это несколько раз, пока смещение целевого байта не станет достаточно низким.

POC утечки адреса

Скрипт, демонстрирующий описанную выше технику, можно найти здесь. Помните, что мы предположили, что сервер имеет только один логический процессор, поэтому вам придётся правильно настроить виртуальную машину, чтобы скрипт работал. Если всё пойдёт гладко, скрипт прочитает и утечёт адрес пула NonPagedPoolNx. На самом деле это будет адрес одного из буферов, находящихся в одном и том же lookaside list.

Поскольку у этой техники довольно много ограничений, я не буду анализировать её глубже. Однако вы можете прочитать скрипт выше и проанализировать его самостоятельно.

Другой подход – декомпрессия

В ходе исследования Zecops обнаружили, что распакованный SMB-пакет — не единственная сложная структура, которая может быть недопустимой разными способами. Ещё до обработки всех структур, связанных с SMB, сжатый буфер также может оказаться недопустимым. Если декомпрессия не удаётся, соединение с сервером разрывается.

Microsoft предоставляет три алгоритма сжатия на выбор при реализации SMB: LZNT1, Plain LZ77 и LZ77 + Huffman. Мы рассмотрим только LZNT1, поскольку он довольно прост — примерно 80 строк Python для функции декомпрессии. Я кратко опишу процесс декомпрессии: сжатые данные состоят из последовательности сжатых блоков, каждый из которых начинается с переменной uint16, обозначающей длину этого блока. Когда встречается длина, равная 0, декомпрессия завершается. Мы воспользуемся этим, чтобы записать последовательность нулевых байтов, представляющую собой допустимые сжатые данные. Цель — ответить на вопрос выше: для одного байта на смещении x, больше ли значение, чем y? Разумеется, x и y по-прежнему контролируются нами.

Ниже приведён пример сжатых данных, которые мы отправим:

Возможны два исхода в зависимости от неинициализированного значения первого байта поля length:

  • length <= y: В этом случае первый блок будет полностью состоять из нулевых байтов, что совершенно допустимо, длина следующего блока будет равна 0, и декомпрессия завершится. Сервер вернёт ответ.
  • length > y: В этом случае первый или второй сжатый блок будет содержать байты 0xFF, и такой блок не удастся распаковать. Сервер разорвёт соединение из-за недопустимых сжатых данных.

Как и в предыдущей технике, мы можем использовать наблюдения №1 и №2, чтобы создать сообщение с одним неинициализированным байтом в середине сообщения, выполнив два шага:

  1. Отправить сообщение с недопустимыми сжатыми данными, чтобы распаковалась только часть данных, как на рисунке выше.
  2. Отправить второе сообщение и гарантировать, что используется тот же lookaside list, что и в первом сообщении, чтобы байты из шага 1 оказались на месте.

Обратите внимание, что значение Offset в заголовке SMB-пакета указывает на сжатые данные, которые могут быть допустимыми или нет в зависимости от значения неинициализированного байта.

Самое заметное преимущество этой техники по сравнению с предыдущей — отсутствие ограничения на смещение.

Итак, подведём итог: у нас есть две техники чтения неинициализированной памяти из pool-буфера, выделяемого функцией SrvNetAllocateBuffer модуля srvnet.sys. Первая техника создаёт специальный SMB-пакет, а затем извлекает информацию по ответу сервера. Вторая техника, менее ограниченная, заключается в создании особых сжатых данных и их отправке, после чего информация извлекается по тому, разрывает ли сервер соединение.

Таким образом, мы можем использовать одну из двух техник для эксплуатации. И, как я уже говорил, первая техника имеет много ограничений, поэтому мы сосредоточимся только на второй.

Эта техника позволит нам эксплуатировать примитив write-what-where, который Zecops ранее продемонстрировал в предыдущем исследовании о возможности достижения локального повышения привилегий. Мы будем использовать эту технику для утечки адресов в раскладке памяти, чтобы затем применить примитив write-what-where. К сожалению, память, выделяемая функцией SrvNetAllocateBuffer, в основном используется для сетевых данных, таких как SMB-пакеты, и не содержит каких-либо системных указателей. А поскольку нам нужно достичь RCE, утечка неинициализированных областей памяти от предыдущих выделений SrvNetAllocateBuffer бесполезна из-за неопределённости расположения искомого указателя. Нам нужно найти что-то более полезное.

SrvNetAllocateBuffer и структура выделенного буфера

Как я уже говорил в исследовании о локальном повышении привилегий (CVE-2020-0796), функция SrvNetAllocateBuffer не просто возвращает буфер запрошенного размера. Вместо этого она возвращает указатель на область, расположенную непосредственно под user buffer структуры pool-allocated memory block, содержащую информацию о выделенном буфере. Расположение pool-allocated memory block выглядит следующим образом:

Хотя наша техника чтения позволяет читать только байты из области «User Buffer», мы всё же можем использовать другой приём, чтобы скопировать части структуры SRVNET_BUFFER_HDR в «User Buffer» другого буфера и затем прочитать их. Для этого нужно установить поле Offset так, чтобы оно указывало на структуру SRVNET_BUFFER_HDR за пределами данных, которые мы хотим прочитать. Нужно лишь гарантировать, что данные там можно интерпретировать как допустимые сжатые данные; в противном случае копирование не произойдёт.

Охота за указателями

Давайте рассмотрим поля структуры SRVNET_BUFFER_HDR и посмотрим, есть ли там что-то, что стоит прочитать:``` c #pragma pack(push, 1) struct SRVNET_BUFFER_HDR { /00/ LIST_ENTRY ConnectionBufferList; /10/ WORD BufferFlags; // 0x01 - no transport header, 0x02 - part of a lookaside list /12/ WORD LookasideListIndex; // 0 to 8 /14/ WORD LookasideListLogicalProcessor; /16/ WORD TracingDataCount; // 0, 1 or 2, for TracingPtr1/2, TracingUnknown1/2 /18/ PBYTE UserBufferPtr; /20/ DWORD UserBufferSizeAllocated; /24/ DWORD UserBufferSizeUsed; /28/ DWORD PoolAllocationSize; /2C/ BYTE unknown1[4]; /30/ PBYTE PoolAllocationPtr; /38/ PMDL pMdl1; /40/ DWORD BytesProcessed; /44/ BYTE unknown2[4]; /48/ SIZE_T BytesReceived; /50/ PMDL pMdl2; /58/ PVOID pSrvNetWskStruct; /60/ DWORD SmbFlags; /64/ PVOID TracingPtr1; /6C/ SIZE_T TracingUnknown1; /74/ PVOID TracingPtr2; /7C/ SIZE_T TracingUnknown2; /84/ BYTE unknown3[12]; }; #pragma pack(pop)

root@kitploit:~
Указатели `UserBufferPtr`, `PoolAllocationPtr`, `pMdl1`, `pMdl2` указывают внутрь блока памяти, выделенного из пула, и их смещения могут быть вычислены заранее, так что нам достаточно прочитать один из них. Наличие указателя на блок памяти из пула наверняка поможет нам при эксплуатации. Кроме того, следующие указатели тоже очень важны:
- **ConnectionBufferList**: Связный список всех полученных, но ещё не обработанных буферов соединения. Головой этого списка является объект соединения, создаваемый функцией SrvNetAllocateConnection в srvnet.sys. Буфер добавляется в список функцией SrvNetWskReceiveComplete. В нашем случае в списке будет только один буфер, поэтому оба указателя (Flink и Blink структуры LIST_ENTRY) будут указывать на голову списка внутри объекта соединения.
- **pSrvNetWskStruct**: Изначально это указатель на объект соединения, упомянутый выше. Указатель устанавливается функцией SrvNetWskReceiveEvent, но перезаписывается функцией SrvNetWskReceiveComplete указателем на структуру SRVNET_BUFFER_HDR. Поэтому чтение его не полезнее, чем чтение одного из четырёх указателей, описанных выше. Кстати, если поискать «pSrvNetWskStruct», можно обнаружить, что он играет роль в эксплуатации EternalBlue.
- **TracingPtr1/2**: Эти указатели используются только тогда, когда включена функция трассировки.

![](https://assets.kitploit.com/production/public/readmes/24502/916c7b643fdf7c8967b2b80189a686858d4cceaf35ddb1f38b83a7ae92f7a14f.png)

Как видите, единственный другой полезный для чтения указатель — это указатель в структуре ConnectionBufferList. Оба указателя (Blink и Flink в структуре LIST_ENTRY) указывают на объект соединения. Этот объект был назван SRVNET_RECV исследователем EternalBlue, поэтому мы тоже будем использовать это имя.

## Получение базового адреса модуля
Теперь, когда мы знаем, как получить два указателя — один на блок памяти из пула и один на структуру SRVNET_RECV, — мы можем свободно модифицировать оба буфера с помощью примитива write-what-where. Возможно, существует множество способов достичь RCE, но получение базового адреса модуля — самый простой вариант, потому что в секции данных модуля есть много всего, что мы можем изменить. Как мы уже видели, ни один указатель в блоке памяти, выделенном SrvNetAllocateBuffer, не указывает на модуль. Тем не менее, всё же есть несколько указателей, которые указывают на модули:

![](https://assets.kitploit.com/production/public/readmes/24502/403108b6e79960d84369827376e208b606ba05304ae052ec4ae251cd289399a7.png)

Имеющаяся у нас техника чтения позволяет читать только данные в области «User Buffer», тогда как эти указатели находятся довольно далеко и на них указывает множество других указателей. Нам нужен фрагмент кода, который сможет скопировать значение указателя в область «User Buffer»:``` c
ptr1 = *(pSrvNetRecv + offset1)
value = *ptr1
ptr2 = *(pSrvNetRecv + offset2)
*ptr2 = value

Если бы мы могли найти такой фрагмент кода, мы бы активировали его для копирования первого указателя (например, HandlerFunctions) в область «User Buffer», прочитали его, затем скопировали второй указатель (например, указатель на функцию Srv2ConnectHandler) в «User Buffer» и прочитали его, вычислив из него базовый адрес модуля. Группа Zecops долгое время искала такой фрагмент кода, но не нашла подходящего. В конце концов они использовали другой вариант, связанный с функцией SrvNetFreeBuffer (упрощённой, как показано ниже), которая выполняет почти желаемую функцию:``` c void SrvNetFreeBuffer(PSRVNET_BUFFER_HDR Buffer) { PMDL pMdl1 = Buffer->pMdl1; PMDL pMdl2 = Buffer->pMdl2;

root@kitploit:~
if (pMdl2->MdlFlags & 0x0020) {
    // MDL_PARTIAL_HAS_BEEN_MAPPED flag is set.
    MmUnmapLockedPages(pMdl2->MappedSystemVa, pMdl2);
}

if (Buffer->BufferFlags & 0x02) {
    if (Buffer->BufferFlags & 0x01) {
        pMdl1->MappedSystemVa = (BYTE*)pMdl1->MappedSystemVa + 0x50;
        pMdl1->ByteCount -= 0x50;
        pMdl1->ByteOffset += 0x50;
        pMdl1->MdlFlags |= 0x1000; // MDL_NETWORK_HEADER

        pMdl2->StartVa = (PVOID)((ULONG_PTR)pMdl1->MappedSystemVa & ~0xFFF);
        pMdl2->ByteCount = pMdl1->ByteCount;
        pMdl2->ByteOffset = pMdl1->MappedSystemVa & 0xFFF;
        pMdl2->Size = /* some calculation */;
        pMdl2->MdlFlags = 0x0004; // MDL_SOURCE_IS_NONPAGED_POOL
    }

    Buffer->BufferFlags = 0;

    // ...

    pMdl1->Next = NULL;
    pMdl2->Next = NULL;

    // Return the buffer to the lookaside list.
} else {
    SrvNetUpdateMemStatistics(NonPagedPoolNx, Buffer->PoolAllocationSize, FALSE);
    ExFreePoolWithTag(Buffer->PoolAllocationPtr, '00SL');
}

}

root@kitploit:~
При освобождении буфера, если установлены флаги буфера 0x02 (означает, что буфер является частью lookaside-списка) и 0x01 (означает, что у буфера нет транспортного заголовка), выполняются некоторые операции над двумя объектами MDL для добавления транспортного заголовка перед сбросом флагов в 0 и возвратом буфера в lookaside-список. Если присмотреться к операциям над объектами MDL, можно заметить, что код выполняет чтение с двойным разыменованием (double-dereference-read), за которым следует запись с двойным разыменованием (double-dereference-write), с использованием двух контролируемых нами переменных (двух указателей MDL) — именно то, что мы ищем. Недостаток в том, что содержимое, которое мы хотим прочитать, также изменяется — побочный эффект, которого мы надеемся избежать.

С учётом вышеизложенного, вот как нам удаётся прочитать указатель AcceptSocket:
1. Подготавливаем буфер A из lookaside-списка так, чтобы область «User buffer» была заполнена нулями. Область user buffer этого буфера будет содержать указатель, который мы собираемся прочитать. 
2. Подготавливаем буфер B из другого lookaside-списка, чтобы:
- Указатель pMdl1 указывает на адрес указателя AcceptSocket минус 0x18 (поскольку смещение MappedSystemVa равно 0x18 в структуре MDL).
- Указатель pMdl2 указывает на область «User buffer» буфера A.
- Поле Flags установлено в 0x03.

Мы можем перезаписать поля структуры SRVNET_BUFFER_HDR, распаковав их из буфера большего размера с помощью техники, описанной в разделе [Observation #2](https://github.com/datntsec/CVE-2020-1206#observation-2-failing-the-decompression) выше.

3. Когда буфер B освобождается, происходят следующие операции:
- Флаги MDL будут считаны из второго MDL в буфере A. Если установлен флаг MDL_PARTIAL_HAS_BEEN_MAPPED, будет вызвана MmUnmapLockedPages, и система может упасть. Именно поэтому на шаге 1 мы должны заполнить буфер нулями.
- Указатель AcceptSocket и окружающая его память будут изменены, как описано здесь:```
+00 |  00 00 00 00 00 00 00 00
+08 |  __ __ __|10 __ __ __ __
+10 |  __ __ __ __ __ __ __ __
+18 |  [+50..................]  <--  AcceptSocket
+20 |  __ __ __ __ __ __ __ __
+28 |  [-50......] [+50......]
  • Указатель AcceptSocket и память вокруг него будут прочитаны, как описано здесь:``` +00 | __ __ __ __ __ __ __ __ +08 | __ __ __ __ __ __ __ __ +10 | __ __ __ __ __ __ __ __ +18 | ab cd ef gh ij kl mn op <-- AcceptSocket +20 | __ __ __ __ __ __ __ __ +28 | qr st uv wx __ __ __ __
root@kitploit:~
- Область «User buffer» буфера A будет изменена, как описано здесь: (Оранжевые байты содержат указатель, который мы хотим прочитать, нам просто нужно правильно их расположить)```
+00 |  00 00 00 00 00 00 00 00
+08 |  ?? ?? 04 00 __ __ __ __
+10 |  __ __ __ __ __ __ __ __
+18 |  __ __ __ __ __ __ __ __
+20 |  00 c0 ef gh ij kl mn op
+28 |  qr st uv wx ab 0d 00 00

  1. Считываем указатель AcceptSocket из области «User buffer» буфера A.

Хорошая новость: мы считали указатель. Плохая новость: мы повредили некоторые данные в структуре SRVNET_RECV. К счастью, ошибка не влияет на систему, пока ничего не происходит с соответствующим соединением. Когда что-то происходит, например, закрытие соединения, система падает. Это не проблема, потому что скоро у нас будет RCE, и мы сможем исправить ошибку, если захотим.

После чтения указателя AcceptSocket мы продолжаем использовать ту же технику для чтения указателя srvnet!SrvNetWskConnDispatch. Причина, по которой мы читаем указатель AcceptSocket, а не указатель HandlerFunctions, заключается в том, что массив HandlerFunctions является общим для всех соединений, тогда как буфер, на который указывает AcceptSocket, не используется совместно с другими соединениями. Поэтому, если мы повредим части AcceptSocket, это повлияет только на стабильность одного соединения.

Если у нас есть копия файла srvnet.sys, используемого на целевой машине, мы можем легко вычислить базовый адрес модуля srvnet.sys, вычтя смещение указателя SrvNetWskConnDispatch, который мы слили.

Реализация произвольного чтения

Предположим, что у нас есть базовый адрес модуля srvnet.sys, тогда мы можем вызвать любую функцию модуля. Но как насчёт аргументов функции? Функция srv2!Srv2ReceiveHandler вызывается через SrvNetCommonReceiveHandler, и вызов выглядит следующим образом:``` c HandlerFunctions = *(pSrvNetRecv + 0x118); Arg1 = *(ULONG_PTR)(pSrvNetRecv + 0x128); Arg2 = *(ULONG_PTR)(pSrvNetRecv + 0x130); (HandlerFunctions[1])(Arg1, Arg2, Arg3, Arg4, Arg5, Arg6, Arg7, Arg8);

root@kitploit:~
Первые два аргумента читаются из структуры SRVNET_RECV, поэтому мы можем контролировать их, однако остальные аргументы мы контролировать не можем. Соглашение о вызовах x86-64 определяет, что вызывающий отвечает за выделение и освобождение пространства стека для аргументов, поэтому, хотя предполагается вызов функции с 8 аргументами, мы можем подменить указатель функцией, ожидающей любое другое количество аргументов.

![](https://assets.kitploit.com/production/public/readmes/24502/223827f49f26993604c20b873d75ea5b11d306a85ff5210ceb2583f0a0b9452c.png)

Ниже приведены шаги, которые мы будем использовать для запуска вызова функции:
1. Отправить специально сформированное сообщение, чтобы указатель на структуру SRVNET_RECV соединения был скопирован в буфер, который мы можем прочитать.
2. Отправить другое допустимое сообщение, которое будет повторно использовать ту же структуру SRVNET_RECV, но не закрывать соединение. Обратите внимание, что при закрытии соединения структура SRVNET_RECV не освобождается. Вызывается функция SrvNetPrepareConnectionForReuse, чтобы сбросить структуру для повторного использования следующим соединением.
3. Прочитать указатель на структуру SRVNET_RECV, скопированный на шаге 1.
4. Заменить указатель HandlerFunctions и аргументы, используя примитив write-what-where.
5. Отправить дополнительное сообщение через соединение с шага 2, чтобы была вызвана функция, заменяющая srv2!Srv2ReceiveHandler.

Теперь всё, что нам нужно сделать, — найти функцию для копирования памяти из одного места в другое, чтобы можно было скопировать произвольную память в пул-буфер, из которого мы сможем её прочитать. memcpy — один из вариантов, и в srvnet.sys есть такая функция (точнее, memmove), но ей требуется третий аргумент, определяющий количество копируемых байт, который мы не контролируем. Однако мы не ограничены функциями, реализованными в srvnet.sys, — мы также можем вызывать функции из таблицы импорта srvnet, и функция RtlCopyUnicodeString идеально подходит для того, что нам нужно.

Функция RtlCopyUnicodeString принимает два указателя на UNICODE_STRING в качестве аргументов и копирует содержимое исходной строки в строку назначения. В отличие от C-строк, завершающихся нулём, строки в ядре определяются структурой UNICODE_STRING, которая содержит указатель на строку и длину строки в байтах. Буфер строки может содержать любые двоичные данные. Если посмотреть на код функции RtlCopyUnicodeString, можно увидеть, что копирование выполняется с помощью memmove, то есть это чистое копирование двоичных данных. Всё, что нам нужно сделать, — подготовить две структуры UNICODE_STRING и вызвать RtlCopyUnicodeString, а затем прочитать скопированные данные:

![](https://assets.kitploit.com/production/public/readmes/24502/8a450b86ecf834ec4905a2bb72dab3781d1dd2ad56ee197a003e282180ce51bc.png)

## Выполнение shellcode
После получения удобного примитива произвольного чтения мы переходим к следующей задаче на пути к цели удалённого выполнения кода (Remote Code Execute) через запуск shellcode. Мы будем использовать технику, представленную Мортеном Шенком в его выступлении на [Black Hat USA 2017](https://www.blackhat.com/docs/us-17/wednesday/us-17-Schenk-Taking-Windows-10-Kernel-Exploitation-To-The-Next-Level%E2%80%93Leveraging-Write-What-Where-Vulnerabilities-In-Creators-Update.pdf) (страницы 47–51).

Идея состоит в том, чтобы записать код shellcode ниже структуры KUSER_SHARED_DATA, адрес которой постоянен и является единственным нерандомизированным адресом в раскладке памяти ядра в последних версиях Windows. Затем модифицировать соответствующий page table entry, сделав страницу исполняемой. Базовый адрес записей таблицы страниц в ядре рандомизирован, но его можно получить из функции MiGetPteAddress в ntoskrnl.exe. Ниже приведены шаги, которые мы будем использовать для выполнения нашего shellcode:
1. Использовать примитив произвольного чтения, чтобы получить базовый адрес ntoskrnl.exe из таблицы импорта srvnet.
2. Прочитать базовый адрес page table entry из функции MiGetPteAddress, как описано в слайдах Мортена.
3. Записать shellcode по адресу KUSER_SHARED_DATA + 0x800 (0xFFFFF78000000800). Обратите внимание, что можно также использовать один из пул-буферов для хранения shellcode; использование KUSER_SHARED_DATA нужно, чтобы всё было проще.
4. Вычислить адрес соответствующего page table entry и снять бит NX, чтобы разрешить исполнение, как описано в слайдах Мортена.
5. Вызвать shellcode, используя описанную выше технику вызова произвольной функции.

Shellcode, который Zecops использует для reverse shell, — это [shellcode от sleepya](https://github.com/worawit/MS17-010/tree/master/shellcode), написанный для эксплуатации EternalBlue. Они модифицировали этот shellcode, чтобы он работал на последних версиях Windows.

# Отладка

![](https://assets.kitploit.com/production/public/readmes/24502/916c7b643fdf7c8967b2b80189a686858d4cceaf35ddb1f38b83a7ae92f7a14f.png)

Информация SMB-пакета имеет структуру, примерно как показано выше. Предположим, нам нужно сливать адрес User Buffer; для этого нужно прочитать указатель UserBufferPtr. Чтобы прочитать этот указатель, мы воспользуемся [техникой](https://github.com/datntsec/CVE-2020-1206#srvnetallocatebuffer-and-the-allocated-buffer-layout), которая задаёт поле offset так, чтобы оно выходило за пределы указателя, и таким образом указатель копируется в область user buffer другого буфера.

Пример SMB-пакета, отправляемого клиентом, выглядит следующим образом:```c
Header:
-   Id = 0x424d53fc
-   OriginalCompressedSegmentSize = 0x0
-   CompressionAlgorithm = 1
-   Flag = 0
-   Offset = 0x2116
Data = ‘A’ * 0x1101.

При поступлении этого пакета на сервер он сохраняется в буфере, созданном функцией SrvNetAllocateBuffer. Поскольку размер всего пакета лежит в диапазоне от 0x1100 до 0x2100, эта функция возвращает alloc с областью user buffer размером 0x2100 (назовём его Alloc A), после чего сохраняет отправленную клиентом информацию, как показано на рисунке ниже:

Мы видим, что часть от адреса 0xffffd38439044050 до 0xffffd38439045160 — это данные, отправленные клиентом, часть от 0xffffd38439045160 до 0xffffd38439046150 — неинициализированные данные на стороне сервера, часть от 0xffffd38439046150 до 0xffffd38439046240 — данные SRVNET_BUFFER_HDR структуры Alloc A. Таким образом, указатель, который мы хотим прочитать, находится по адресу 0xffffd38439046150 + 0x18 = 0xffffd38439046168.

Чтобы прочитать этот указатель, я использовал методику, о которой я уже говорил выше, установив поле offset за пределы считываемого указателя. Именно поэтому в приведённом выше пакете, хотя его размер меньше 0x2100, offset был задан как 0x2116.

Далее SMB-сервер вызовет функцию SrvNetAllocateBuffer для выделения области памяти на основе суммы OriginalCompressedSegmentSize и Offset (0x2116). Затем он выделит alloc с областью user buffer размером 0x4100 (назовём его Alloc B). Выделенные данные будут выглядеть следующим образом:

Чтобы избежать непредвиденных ошибок, заранее я многократно создавал буферы из того же lookaside list, что и Alloc B, и заполнял их байтами 0x0.

Далее SMB-сервер приступит к распаковке сжатых данных и скопирует несжатые данные, отправленные клиентом, в область user buffer для Alloc B:

Как мы видим, сжатые данные отсутствуют, поскольку OriginalCompressedSegmentSize = 0; программа скопирует данные из Alloc A с адреса 0xffffd38439044060 до 0xffffd38439044060 + 0x2116 = 0xffffd38439046176 в область user buffer для Alloc B. Таким образом, часть информации SRVNET_BUFFER_HDR из Alloc A была скопирована в область user buffer для Alloc B.

Теперь воспользуемся методикой, о которой мы говорили ранее, чтобы раскрыть адрес allocation pool (адрес User Buffer).

Предположим, мы хотим узнать, больше ли байт по адресу 0xffffd3843636f15e, чем 0x7f? Мы создадим SMB-пакет со следующей информацией:``` c Header:

  • Id = 0x424d53fc
  • OriginalCompressedSegmentSize = 0x1ff2
  • CompressionAlgorithm = 1
  • Flag = 0
  • Offset = 0x210e Data = ‘B’ * 0x210e + compress(‘\xb0’ + ‘\x00’(0x7f+3) + ‘\xff’(0xff - 0x7f)) + ‘\xff’*0x1fe9
root@kitploit:~
Зачем нужно создавать такой SMB, разберём по частям. Сначала сумма OriginalCompressedSegmentSize и Offset равна 0x4100, поэтому будет повторно использован Alloc с аналогичным user buffer, а именно Alloc B, использованный ранее. Поскольку мы хотим определить, больше ли байт по адресу 0xffffd3843636f15e значения 0x7f, а этот адрес находится на расстоянии 0x210e от адреса user buffer, несжатые данные будут иметь размер 0x210e байт (‘B’ * 0x210e). Далее следует область допустимых сжатых данных (сжатых функцией compress()), за которой идут недопустимые сжатые данные (‘\xff’ * 0x1fe9). Чтобы при распаковке только допустимые сжатые данные были распакованы в другую область Alloc, после чего соединение будет разорвано из-за недопустимых сжатых данных, идущих после, а несжатые данные не будут скопированы в этот Alloc, в результате чего ранее скопированные нами данные останутся нетронутыми.

![](https://assets.kitploit.com/production/public/readmes/24502/3e1ffd768b120645917e7f1757987f80c79514b52f3bc18e46fdfb0a2d2ca05f.png)

Выше показан Alloc, содержащий описанную выше информацию, созданный SMB-сервером. Затем SMB-сервер вызовет функцию SrvNetAllocateBuffer для создания соответствующего Alloc. Поскольку сумма его OriginalCompressedSegmentSize и Offset равна 0x4100, Alloc B будет переиспользован:

![](https://assets.kitploit.com/production/public/readmes/24502/5b218cbd142d58da175ee6a58bfc505646a560467db6db28ab70e0db34ee9ce3.png)

После этого SMB-сервер распаковывает информацию, отправленную клиентом, в user buffer Alloc B с соответствующим смещением.

![](https://assets.kitploit.com/production/public/readmes/24502/f3b545366e8f3bcff6a69d184cc383bfcf70643c23ff0ac01ece1a95cc0fd88d.png)

Данные, выделенные красным, — это распакованные данные, остальная часть остаётся без изменений. Как видно на рисунке выше, нужный нам байт сохраняется, а сразу после него находятся только что распакованные данные.

Чтобы узнать, больше ли этот байт 0x7f, мы поступаем следующим образом:

Продолжаем создание SMB-пакета со следующим содержимым:``` c
Header:
-   Id = 0x424d53fc
-   OriginalCompressedSegmentSize = 0x2004
-   CompressionAlgorithm = 1
-   Flag = 0
-   Offset = 0x20fd
Data = ‘B’ * 0x20f1

Хотя сумма OriginalCompressedSegmentSize и Offset больше 0x4100, при выделении области alloc для хранения пакета, отправленного клиентом (с общим размером менее 0x4100), SMB server по-прежнему выделяет только один Alloc с областью user buffer размером 0x4100 байт, как показано ниже:

Данные, выделенные зелёным цветом, как показано выше, — это данные Alloc B, выделенного ранее; из-за того же lookaside list они были переиспользованы.

Затем SMB Server вызовет функцию SrvNetAllocateBuffer для выделения Alloc, содержащего данные после распаковки:

В ходе распаковки SMB server берёт данные из User buffer address + Offset = 0xffffd3843636d060 + 20fd = 0xffffd3843636f15d. Извлечённые данные будут иметь следующий вид:

Согласно описанному выше алгоритму распаковки, он берёт первые 2 байта и использует их как length блока; на основе этой length он берёт следующую за length часть и распаковывает её.

Как указано выше, length будет равна 0xB0D3, однако по алгоритму её фактическое значение вычисляется по формуле: length = length & 0xFFF + 1 → length будет равна 0xD4. Затем берутся следующие D4 байта и выполняется обычная распаковка до тех пор, пока не встретится байт FF (поскольку в 0xD4 байтах содержатся все байты 00 и часть байтов FF); в этот момент сжатые данные считаются недействительными, распаковка прекращается и соединение разрывается.

Основываясь на разрыве соединения сервером, можно предположить, что искомый байт больше 0x7f.

А что если искомый байт меньше? Продолжим анализ, но в этот раз используем для сравнения байт D7, так что D3 будет меньше D7. Посмотрим, что произойдёт:

Сначала отправим SMB server пакет следующего вида:``` c Header:

  • Id = 0x424d53fc
  • OriginalCompressedSegmentSize = 0x1ff2
  • CompressionAlgorithm = 1
  • Flag = 0
  • Offset = 0x210e Data = ‘B’ * 0x210e + compress(‘\xb0’ + ‘\x00’(0xd7+3) + ‘\xff’(0xff - 0xd7)) + ‘\xff’*0x1fe9
root@kitploit:~
Phía SMB server sẽ tạo một alloc lưu trữ như sau:

![](https://assets.kitploit.com/production/public/readmes/24502/3df2c6c8e5de90714455663bbb922efec7ae6888a970a3d65e389a28aea16837.png)

Tiếp theo nó sẽ gọi hàm SrvNetAllocateBuffer để cấp phát một alloc như sau:

![](https://assets.kitploit.com/production/public/readmes/24502/51affc73361727907ada6fb3f439831d0e95cee531a65106fa79ce4519ceaab8.png)

Tất nhiên alloc này được tái sử dụng lại từ alloc có cùng lookasidelist (Alloc B). Sau đó chương trình tiến hành giải nén bình thường với dữ liệu nén hợp lệ, và ngắt kết nối với dữ liệu nén không hợp lệ:

![](https://assets.kitploit.com/production/public/readmes/24502/01a9cc43acbc6b7cc283f40a0dbe348608a3566bfc34ce24aa2cc694315e67a3.png)

Bước tiếp theo, ta sẽ gửi tiếp một dữ liệu tương tự như lần trước và SMB server sẽ cấp phát một alloc tương ứng:

![](https://assets.kitploit.com/production/public/readmes/24502/ed438d4d0049b2188573e0fb3171cadf23701c7b0cef2de43a52434c07381341.png)

Các dữ liệu được tô màu xanh lá như trên là những dữ liệu của Alloc B được cấp phát lần trước, do cùng lookaside list nên được tái sử dụng.

Tiếp theo SMB Server sẽ gọi hàm SrvNetAllocateBuffer để cấp phát một Alloc chứa dữ liệu sau khi giải nén:

![](https://assets.kitploit.com/production/public/readmes/24502/7ed3dca4594bd1772c44689190d4df3b597d43daf7c87edb281b3e82a834884e.png)

Qua việc giải nén, SMB server sẽ lấy dữ liệu từ User buffer address + Offset = 0xffffd3843636d060 + 20fd = 0xffffd3843636f15d. Dữ liệu được lấy ra sẽ có dạng như sau:

![](https://assets.kitploit.com/production/public/readmes/24502/68f677093ef93e02c5df9d70dce560e4d06e74446970f1f14f1ad37f08ce898e.png)

Tương tự như lần trước thì length ban đầu sẽ là 0xB0D3, qua tính toán sẽ thành 0xD4. Và nó sẽ lấy ra D4 byte tiếp theo và tiến hành giải nén bình thường. Tuy nhiên do D4 < D7 nên dữ liệu nén được lấy ra để giải nén lúc này chỉ chứa toàn byte 0. Nó sẽ giải nén bình thường cho đến hết block đó. Tiếp theo nó sẽ lấy ra độ dài của block tiếp theo thông qua độ dài của block trước đó, hai byte tiếp theo là D5 và D6 là 0x0, nên độ dài của nó lúc này là 0x0, do đó length lấy ra sẽ là  0x0000 → kết thúc quá trình giải nén → giải nén thành công  → SMB server trả lại một response → ta biết được byte cần biết nhỏ hơn hoặc bằng 0xD7. 

Tương tự như vậy ta sẽ làm cho đến khi leak được toàn bộ 6 byte của một address. Ta sẽ có được allocation pool address.

Khi có được allocation pool address, ta sẽ tiến hành tìm địa chỉ srvnet base address thông qua việc lấy con trỏ trỏ tới cấu trúc SRVNET_RECV bằng cách tương tự như leak allocation pool address. 

Sau khi có được 2 địa chỉ: allocation pool và SRVNET_RECV lần lượt có giá trị: `0xffffd38439044000` và `0xffffd3843654ddd8`, ta tiến hành leak srvnet base address. 

![](https://assets.kitploit.com/production/public/readmes/24502/403108b6e79960d84369827376e208b606ba05304ae052ec4ae251cd289399a7.png)

![](https://assets.kitploit.com/production/public/readmes/24502/239c0c59c0e492097bdaf2eac907812124a89ef8bb31841287202ae80f4d13d0.png)

Để đọc con trỏ AcceptSocket, ta cần làm như sau:
1. Chuẩn bị Alloc A từ một lookaside list sao cho vùng “User buffer” được lấp đầy bởi các số 0. Buffer này sau đó sẽ chứa con trỏ mà chúng ta sẽ đọc. Ở đây Alloc A sẽ được sử dụng từ Alloc ứng với allocation pool address mà ta leak được. Do đó, vùng User buffer của Alloc A sẽ bắt đầu từ địa chỉ 0xffffd38439044050 thông qua việc sử dụng chung một lookaside list. 
2. Chuẩn bị Alloc B từ một lookaside list khác để:
- Con trỏ pMdl1 trỏ đến địa chỉ của con trỏ AcceptSocket trừ đi 0x18, (do offset của MappedSystemVa là 0x18 trong cấu trúc MDL). 
- Con trỏ pMdl2 trỏ đến vùng “User buffer” của Buffer A.
- Trường Flags được set thành 0x03.

Như vậy địa chỉ của 2 con trỏ Mdl lần lượt là: mdl1_ptr: `0xffffd3843654de68`, mdl2_ptr: `0xffffd38439045250`.

Ta có thể ghi đè các trường cấu trúc SRVNET_BUFFER_HDR bằng cách giải nén chúng từ buffer lớn hơn thông qua kỹ thuật được mô tả trong phần [Observation #2](https://github.com/datntsec/CVE-2020-1206#observation-2-failing-the-decompression).

Tôi sẽ nói rõ hơn bước này ngay sau bước 4.

3. Khi Buffer B được giải phóng, các hoạt động sau sẽ diễn ra:
- Các MDL flags sẽ được đọc từ MDL thứ hai tại buffer A. Nếu MDL_PARTIAL_HAS_BEEN_MAPPED flag được set, MmUnmapLockedPages sẽ được gọi và hệ thống có khả năng bị crash. Đó là lý do tại sao ta phải lấp đầy buffer bằng các số 0 ở bước 1.
- Vùng “User buffer” của Alloc A sẽ được sửa đổi và chứa các thông tin mà ta cần đọc.
4. Đọc con trỏ AcceptSocket từ vùng “User buffer” của buffer A.
- Sử dụng kĩ thuật leak địa chỉ đã dùng ở trên để đọc con trỏ AcceptSocket.

Sau đây tôi sẽ mô tả rõ hơn về các bước trên:

Ở bước 1 khá đơn giản  và tương tự như trên, nên tôi sẽ không nói đến nữa.

Ở bước 2, đầu tiên ta sẽ tạo một gói tin gửi đến SMB Server với nội dung như sau:``` c
Header:
-   Id = 0x424d53fc
-   OriginalCompressedSegmentSize = -0x38
-   CompressionAlgorithm = 1
-   Flag = 0
-   Offset = 0x10138
Data = ‘A’ * 0x10138 + compress(mdl1_ptr  + ‘\x00’*0x10 + mdl2_ptr) + ‘\xff’*0x10

Как мы можем видеть, OriginalCompressedSegmentSize содержит отрицательное значение, а сумма OriginalCompressedSegmentSize + Offset = 0x10100. Однако размер пакета, который клиент отправляет на сервер, больше 0x10100. Таким образом, изначальный Alloc, создаваемый сервером до распаковки, будет больше, чем Alloc, содержащий данные после распаковки. Значение OriginalCompressedSegmentSize здесь устанавливается отрицательным для того, чтобы сумма OriginalCompressedSegmentSize и Offset была точно равна 0x10100, не влияя при этом на расположение сжатых данных, поскольку оно зависит от Offset. А 0x38 — это смещение указателя Mdl1 в структуре SRVNET_BUFFER_HDR.

Таким образом, сервер создаст Alloc, содержащий данные клиента, следующим образом:

Затем он вызовет функцию SrvNetAllocateBuffer, чтобы выделить alloc с размером области User buffer равным 0x10100, то есть Alloc B, как описано в шагах выше:

Выполняется распаковка; естественно, распаковать удаётся только часть корректных данных:

На основе приведённых выше рисунков можно увидеть, что область двух указателей Mdl в SRVNET_BUFFER_HDR структуры Alloc B была изменена на желаемое значение.

Аналогично предыдущему, в этот раз мы установим flag равным 3 путём регулировки смещения отправляемого пакета следующим образом:``` c Header:

  • Id = 0x424d53fc
  • OriginalCompressedSegmentSize = -0x10
  • CompressionAlgorithm = 1
  • Flag = 0
  • Offset = 0x10110 Data = ‘A’ * 0x10110 + compress(‘\x00\x03’) + ‘\xff’*0x10
root@kitploit:~
В итоге это будет выглядеть так:

![](https://assets.kitploit.com/production/public/readmes/24502/467ea07dff7cfaca7062ed50d1605e40586c412f40bcadaf01aa37d862efd3bd.png)

Когда Alloc B освобождается, выполняются следующие фрагменты кода:``` c
pMdl1->MappedSystemVa = (BYTE*)pMdl1->MappedSystemVa + 0x50;
pMdl1->ByteCount -= 0x50;
pMdl1->ByteOffset += 0x50;
pMdl1->MdlFlags |= 0x1000; // MDL_NETWORK_HEADER

pMdl2->StartVa = (PVOID)((ULONG_PTR)pMdl1->MappedSystemVa & ~0xFFF);
pMdl2->ByteCount = pMdl1->ByteCount;
pMdl2->ByteOffset = pMdl1->MappedSystemVa & 0xFFF;
pMdl2->Size = /* some calculation */;
pMdl2->MdlFlags = 0x0004; // MDL_SOURCE_IS_NONPAGED_POOL

Как указано выше, pMdl1->MappedSystemVa (смещение 0x18) будет содержать значение pMdl1->MappedSystemVa + 0x50 = 0xffffd3843654de68 + 0x18 + 0x50 = 0xffffd3843654ded0.

Перед освобождением Alloc B функция SRVNET_RECV будет выглядеть так:

После выполнения первых 4 строк приведённого выше кода:

Перед освобождением Alloc a будет:

После выполнения всего приведённого выше кода:

А байты, которые нам нужно прочитать в Alloc A, — это синие байты ниже:

Таким образом, нам достаточно использовать описанную выше технику чтения отдельных байтов, чтобы получить адрес AcceptSocket + 0x50. В данном случае это 0xffffd3843ea02418 → AcceptSocket: 0xffffd3843ea023c8.

Аналогичным образом мы поступим, чтобы прочитать адрес AcceptSocket→ srvnet!SrvNetWskConnDispatch.

Нам нужно подготовить всё следующим образом:

После того как Alloc B освобождён, всё изменится так:

Байты, которые нам нужны, чтобы получить адрес AcceptSocket-> srvnet!SrvNetWskConnDispatch + 50, находятся в Alloc A; эти байты выделены синим цветом на рисунке ниже:

Таким образом, AcceptSocket-> srvnet!SrvNetWskConnDispatch будет равен 0xfffff80060e9d170. Предположим, что нам известно его смещение в модуле srvnet.sys, тогда мы сможем вычислить базовый адрес srvnet.

В данном случае базовый адрес srvnet: 0xFFFFF80060E70000, а смещение srvnet!SrvNetWskConnDispatch равно 0x2d170.

Далее мы используем технику Write-what-where primitive из CVE-2020-0796, чтобы произвольно записывать данные в область памяти.

Сначала мы найдём способ прочитать базовый адрес ntoskrnl, получив адрес функции IoSizeofWorkItem, которую импортирует srvnet. Для этого сначала создадим две структуры UNICODESTRING следующим образом:``` c // Destination unicode string desLength = 6; desMaximumLength = 6; desBuffer = allocation_pool_object_ptr + 0x1650 + 0x20 + 2;

// Source unicode string srcLength = 6; srcMaximumLength = 6; srcBuffer = srvnet_base_ptr + OFFSETS['srvnet!imp_IoSizeofWorkItem'];

root@kitploit:~
С `allocation_pool_object_ptr` — это адрес allocation pool, полученный в результате утечки, а `OFFSETS['srvnet!imp_IoSizeofWorkItem']` — это смещение функции IoSizeofWorkItem, импортируемой srvnet.

Эти 2 структуры UNICODE_STRING будут сохранены по адресу `allocation_pool_object_ptr + 0x1650` с помощью техники Write-what-where, найденной в CVE-2020-0796.  
Сначала мы сохраним Destination unicode string по адресу `allocation_pool_object_ptr + 0x1650`, затем создадим SMB-пакет следующим образом:``` c
Header:
-   Id = 0x424d53fc
-   OriginalCompressedSegmentSize = 0xffffffff
-   CompressionAlgorithm = 1
-   Flag = 0
-   Offset = 0x22
Data:
sentinel = os.urandom(2)  // 16 bits for verification
data = struct.pack('<HHIQ', desLength, desMaximumLength, 0, desBuffer)  // dest unicode string
data += struct.pack('<HHIQ', srcLength, srcMaximumLength, 0, srcBuffer) // src unicode string
data += sentinel
data_to_compress = os.urandom(0x1100 - len(data))
// 0x18 null bytes that override the struct.
data_to_compress += b'\x00'*0x18
// Target address.
data_to_compress += struct.pack('<Q', allocation_pool_object_ptr + 0x1650)
data = data + compress(data_to_compress)

Выше данные содержат sentinel, созданный с помощью функции os.urandom(2); его длина составляет 2 байта, и эти 2 байта позволят нам понять, является ли утёкший адрес именно тем адресом, который нам нужен, путём сравнения после успешного завершения процесса утечки.

Если общий размер пакета, отправленного клиентом, больше 0x1100 (это будет зависеть от данных, которые были случайным образом сгенерированы до сжатия), то allocation_pool_object_ptr обязательно будет использован для его хранения на SMB-сервере:

Затем SMB-сервер вызовет функцию SrvNetAllocateBuffer для выделения области памяти под декомпрессию. Но поскольку сумма OriginalCompressedSegmentSize и Offset равна 0x21, выделяется только область с пользовательским буфером размером 0x1100:

Возникает ошибка heap overflow (как описано в CVE-2020-0796), и после декомпрессии на SMB-сервере (до выполнения копирования несжатых данных):

Таким образом, указатель UserBufferPtr указывает на начало allocation_pool_object_ptr + 0x1650, и когда происходит копирование, allocation_pool_object_ptr будет:

Аналогичным образом добавим ещё один sentinel ниже, отправив следующий пакет:``` c Header:

  • Id = 0x424d53fc
  • OriginalCompressedSegmentSize = 0xffffffff
  • CompressionAlgorithm = 1
  • Flag = 0
  • Offset = 0x2 Data: data = sentinel data_to_compress = os.urandom(0x1100 - len(data)) // 0x18 null bytes that override the struct. data_to_compress += b'\x00'*0x18 // Target address. data_to_compress += struct.pack('<Q', allocation_pool_object_ptr + 0x1650 + 0x28) data = data + compress(data_to_compress)
root@kitploit:~
Таким образом, когда SMB-сервер получает пакет, он выделяет соответствующий участок памяти; в данном случае выделенный участок памяти — это `allocation_pool_object_ptr`, и он будет содержать следующие данные:

![](https://assets.kitploit.com/production/public/readmes/24502/24cc306a702cdb5f575bd1d612ea6b35db87aaf875fd45fa64977e50c61e85ed.png)

После процесса распаковки данные будут следующими:

![](https://assets.kitploit.com/production/public/readmes/24502/d6442e5a936e1c46ebb9e3a4651e8859dca56c1aa09e9e79ed9155ea105dede2.png)

Таким образом, мы создали две Unicode-строки и два sentinel для проверки данных, полученных в результате утечки.

Далее мы вызовем функцию `RtlCopyUnicodeString` и передадим в неё две созданные выше Unicode-строки.

Чтобы вызвать функцию `RtlCopyUnicodeString`, сначала мы перезапишем указатель HandlerFunctions на адрес функции `RtlCopyUnicodeString`; эта функция импортируется модулем srvnet и имеет смещение (в моём модуле) 0x32288.

Так, с помощью техники write-what-where мы запишем адрес 0xFFFFF80060E70000 + 0x32288 - 0x8 в HandlerFunctions.

Сначала мы сливаем указатель SRVNET_RECV (0xffffe00f0b593dd8).

![](https://assets.kitploit.com/production/public/readmes/24502/f7c5bde869eb5ba21a0b9dd4e2034417487150592178aa1eb1aef523e246a5c2.png)

Мы сохраним соединение, чтобы продолжить отправку пакетов ниже.

Далее мы с помощью техники write-what-where запишем в указатель RtlCopyUnicodeString - 0x8 (причина - 0x8 в том, чтобы функция RtlCopyUnicodeString заменила функцию Srv2ReceiveHandler в HandlerFunctions).

![](https://assets.kitploit.com/production/public/readmes/24502/a88b44518f26ed6f0958772bdd4bf124c7c2403a525944b060a37cf17309a092.png)

Затем мы поочерёдно запишем 2 указателя двух созданных выше Unicode-строк в 2 аргумента HandlerFunction.

![](https://assets.kitploit.com/production/public/readmes/24502/691bdb0ce599face8b1f35393db9cc37e1f85a075c4d259a7fb96adee9fb66d2.png)

В этот момент на том же соединении функция Srv2ReceiveHandler была заменена на RtlCopyUnicodeString, поэтому, когда мы отправляем пакет, функция RtlCopyUnicodeString будет вызвана и скопирует Unicode String.

![](https://assets.kitploit.com/production/public/readmes/24502/2410149b163fd4ebbfb13fa5d2a127cafcfdc82146c33544b0a6cc6905c13d2b.png)

Следующее, что нам нужно сделать, — это слить 10 байт адреса с 0xffffd38439045670 по 0xffffd3843904567a (включая 2 sentinel на обоих концах адреса, который нужно слить). Затем проверить, являются ли 2 байта в начале и в конце слитого адреса sentinel; если это sentinel, значит, мы слили правильно (0xfffff8068152c380).

После того как мы слили адрес nt!IoSizeofWorkItem (0xfffff8068152c380), мы вычтем его смещение (0x12C380), чтобы получить базовый адрес ntoskrnl (0xfffff80681400000).

Обратите внимание, что каждое смещение каждого файла модуля на разных версиях Windows различается, поэтому убедитесь, что у вас есть правильный файл модуля на целевой машине.

Аналогично, после получения базового адреса ntoskrnl мы получим MiGetPteAddress (0xBA968) и базовый адрес PTE (MiGetPteAddress + 0x13):

![](https://assets.kitploit.com/production/public/readmes/24502/5c92df61b331c038bd90b2ccb878b7027bdbf6338602aecdda1b49a867eb42b1.png)

Следующий шаг — мы запишем shellcode по адресу 0xFFFFF78000000800 с помощью техники write-what-where. Затем пересчитаем адрес shellcode в pte по формуле ниже и очистим бит NX, чтобы shellcode мог выполняться:``` c
shellcode_addr >>= 9
shellcode_addr &= 0x7FFFFFFFF8
shellcode_addr += pte_base

Наконец, мы запишем адрес шеллкода в allocation_pool_object_ptr + 0x50 + 0x1600 и вызовем шеллкод, подменив этот адрес с помощью HandlerFunctions и передав в качестве аргумента адрес nt_base_ptr.

Наслаждайтесь RCE :))

Ссылки

  • SMBleedingGhost Writeup: Chaining SMBleed (CVE-2020-1206) with SMBGhost
  • SMBleedingGhost Writeup Part II: Unauthenticated Memory Read – Preparing the Ground for an RCE
  • SMBleedingGhost Writeup Part III: From Remote Read (SMBleed) to RCE
  • Exploiting SMBGhost (CVE-2020-0796) for a Local Privilege Escalation: Writeup + POC
  • lznt1.py
  • TAKING WINDOWS 10 KERNEL EXPLOITATION TO THE NEXT LEVEL – LEVERAING WRITEWHAT-WHERE VULNERABILITIES IN CREATORS UPDATE
  • VERGILIUS_MDL
  • Exploit Development: Leveraging Page Table Entries for Windows Kernel Exploitation
  • RtlUnicodeStringCopy function
  • UNICODE_STRING structure

DatntSec. Viettel Cyber Security.

Скачать инструмент
→ Размер выделения ↓Логический процессор0x11000x21000x41000x81000x101000x201000x401000x801000x100100
Процессор 1📝📝📝📝📝📝📝📝📝
Процессор 2📝📝📝📝📝📝📝📝📝
...
Процессор n📝📝📝📝📝📝📝📝📝