
Технический анализ и эксплойт proof-of-concept для CVE-2023-28252 — уязвимости повышения привилегий в драйвере Windows Common Log File System (CLFS), использовавшейся в атаках программы-вымогателя Nokoyawa.
С февраля 2022 года сообщалось о новом вымогателе, который, по-видимому, использует 0-дневную уязвимость Windows, согласно исследованию, проведённому Trend Micro.
Более подробную информацию об этом вымогателе можно найти по этой
ссылке.
Согласно анализу «Лаборатории Касперского», группа вымогателей Nokoyawa использовала другие эксплойты, нацеленные на драйвер Common Log File System (CLFS), с июня 2022 года, со схожими, но отличными характеристиками, все связанные с одним разработчиком эксплойтов.
В апреле 2023 года, когда Microsoft выпустила патч, был присвоен
CVE-2023-28252.
Ранее, в 2022 году, аналогичная ошибка в том же компоненте была исследована
нами и задокументирована в этом
блог-посте
Для анализа необходимо знать формат файла .blf, который обрабатывается уязвимым драйвером Common Log File System с именем CLFS.sys, находящимся в папке драйверов внутри system32.
Более подробную информацию об этом типе файлов можно найти по ссылкам ниже:
https://github.com/ionescu007/clfs-docs/blob/main/README.md
https://www.coresecurity.com/core-labs/articles/understanding-cve-2022-37969-windows-clfs-lpe
Этот анализ выполнен для Windows 11 21H2, clfs.sys версии 10.0.22000.1574, хотя он также работает на Windows 10 21H2, Windows 10 22H2, Windows 11 22H2 и Windows Server 2022.
В предыдущих версиях Windows необходимо скорректировать некоторые значения, иначе мы получим BSOD.
Microsoft Patch Tuesday апрель 2023.
Вы можете проверить версию драйвера, как показано
Когда уязвимость была опубликована, в апреле 2023 года я вместе с Эстебаном Казимировым начал реверсинг драйвера CLFS.sys, хотя в данном случае одного анализа патча было недостаточно, чтобы понять, где находится ошибка и как её вызвать, поскольку эксплуатация очень сложна.
Позже вышел блог-пост, автор которого на примере образца вредоносного ПО показал некоторые части декопилированного кода HexRays и некоторую информацию, которая указала направление, в котором нужно было вести эксплуатацию.
Очевидно, предоставленная информация была неполной, но без этой помощи вряд ли удалось бы создать PoC, а затем и функциональный эксплойт.
Чтобы было проще понять, мы сначала объясним, как построить PoC, а затем проведём анализ уязвимости.
Этот блог-пост содержит два раздела:
Построение PoC:
1-Получение необходимых адресов ядра для эксплуатации
2-Подготовка пути для создания файлов .blf:
3-Создание файла "trigger blf" с помощью функции CreateLogFile()
4-Формирование файла "trigger blf"
5-Получение адреса BASE BLOCK файла trigger blf в ядре
6-Вызов AddLogContainer с дескриптором trigger blf
7-Подготовка файлов spray blf
8-Подготовка памяти для выполнения spray
9-Запуск ошибки
Отладка:
1-Проверка memory spray
2-Просмотр RecordOffset[12] файла trigger blf
3-Просмотр значения iFlushBlock в файле spray blf
4-Почему он читает из BLOCK 1 SHADOW, а не из BLOCK 0 CONTROL?
5-Почему контрольная сумма равна нулю в файлах blf spray?
6-Завершение эксплуатации.
7-Настоящий патч
Я создам функцию с именем InitEnvironment для получения некоторых необходимых адресов ядра.
Получаю адрес EPROCESS моего процесса и сохраняю его в переменной g_EProcessAddress, затем адрес EPROCESS процесса SYSTEM, и сохраняю его в system_EPROCESS, затем EHTREAD адрес главного потока моего процесса, и сохраняю его в g_EThreadAddress и, наконец, адрес PREVIOUS MODE, который в этой версии PoC не будет использоваться.

Этот метод хорошо известен, функция GetObjectKernelAddress, вызывает NtQuerySystemInformation дважды с первым аргументом SystemExtendedHandleInformation, первый вызов передаётся с неверным размером и возвращает ошибку, но также возвращает правильный размер, который используется во втором вызове и получает информацию обо всех дескрипторах, затем в цикле перебирает информацию каждого дескриптора и в поле Object правильного handleinfo получает искомый адрес в ядре.

Мне также нужны адреса ядра следующих функций, экспортируемых CLFS.sys:
• ClfsEarlierLsn
• ClfsMgmtDeregisterManagedClient
И экспортируемые функции из NTOSKRNL.exe
• RtlClearBit/PoFxProcessorNotification
• SeSetAccessStateGenericMapping
Для получения этих адресов используется аналогичный метод, который используется для получения базы ядра обоих модулей, путём вызова NtQuerySystemInformation дважды, но в этом случае первый аргумент будет SYSTEM_INFORMATION_CLASS (в PoC я использую функцию FindKernelModulesBase для этой цели).
Затем он загружает CLFS.sys
и NTOSKRNL.exe как обычные модули в пользовательском режиме, вызывая
LoadLibrary, получает адреса в пользовательском режиме с помощью
GetProcAddress, а затем вычитает imagebase из каждого, что
даёт смещение функции, и, наконец, добавляет каждое смещение к
соответствующим базам ядра, тем самым получая адреса ядра всех
необходимых функций.

Я создаю функцию с именем createInitialTriggerBlfFile, которая сгенерирует и запишет файл .blf.
Путь, используемый в качестве аргумента в CreateLogFile, отличается от обычного пути; например, чтобы открыть файл 1280.blf, расположенный в папке C:\Users\Public, мы должны указать путь LOG:C:\Users\Public\1280. Это будет сохранено в переменной stored_name_CreateLog.
Я делаю это с помощью wsprintfW(), так как stored_env хранит путь C:\Users\Public, ранее полученный из переменных среды. К этой строке я добавлю префикс LOG: и случайное имя в конце, без расширения .blf.

Это будет путь к моему исходному файлу, который я назову "trigger blf". Конечно, я также должен сохранить обычный путь к тому же файлу без LOG: в начале и с расширением BLF, чтобы открыть его и изменить с помощью CreateFile(), WriteFIle() как любой другой файл; этот путь будет, например: C:\Users\Public\1280.blf, и он будет сохранён в переменной stored_name_fopen.

Конечно, оба пути соответствуют одному и тому же файлу, и я должен использовать один или другой по мере необходимости.
Функция CreateLogFile выполняет функцию, довольно похожую на CreateFile() (создаёт новые файлы или открывает существующие файлы и получает их дескриптор), даже некоторые аргументы похожи, но CreateLogFile() работает только с файлами blf.
Кроме того, при открытии существующего файла она проверяет правильность формата, даже если каждый блок имеет контрольную сумму, и если она неверна, возвращается ошибка.
Я создам 2 вида BLF файлов:
Trigger blf
Spray blf
Оба являются файлами blf, но изменёнными разными способами.
Таким образом, PoC сначала создаёт
файл "trigger blf", используя CreateLogFile, с путём,
например: LOG:C:\Users\Public\1280, который я настроил ранее и
который был сохранён в переменной stored_name_CreateLog.
Пятый аргумент fCreateDisposition, как и в CreateFileA(), может принимать следующие значения:

В этом случае я буду использовать аргумент OPEN_ALWAYS, поэтому файл будет создан, если его не существует, и открыт, если он существует. Поскольку файл ещё не существует, он будет создан со случайным именем.
logFile = CreateLogFile(stored_name_CreateLog, GENERIC_READ | GENERIC_WRITE, 1, 0, 4, 0);
CreateLogFile() создаст наш файл "trigger blf" с его 6 блоками и соответствующими контрольными суммами и вернёт дескриптор, который будет сохранён в переменной logFile.

Каждый блок будет иметь, начиная со смещения, показанного в левом столбце, заголовок размером 0x70 байт.
Так, например, заголовок CONTROL BLOCK находится от смещения 0x0 до 0x70.

Все заголовки всех блоков имеют одинаковую структуру, называемую _CLFS_LOG_BLOCK_HEADER.
Вот структура заголовка:

В смещении 0xC заголовка я могу найти контрольную сумму, поэтому, поскольку CONTROL BLOCK начинается со смещения 0, контрольная сумма будет в смещении 0xC файла, и, таким образом, каждый блок будет иметь свою контрольную сумму в смещении 0xC от начала своего блока.

Чтобы изменить файл trigger blf, я должен открыть его как обычный файл либо с помощью CreateFileA, либо с помощью fopen, а затем изменить его с помощью WriteFile или fwrite соответственно; я выполняю это в начале функции fun_prepare PoC.
Помните, что обычный путь хранится в переменной stored_name_fopen, поэтому я использую его для открытия файла с помощью wfopen_s (что является вариантом fopen, поддерживающим строки Unicode).
Файл изменяется в функции craftTriggerBlfFile, вызываемой из fun_prepare.

Затем я вызываю fseek, чтобы указать на изменяемое смещение, а затем с помощью fwrite файл изменяется.
Изменения, которые необходимо внести в
файл "trigger blf", следующие:
После внесения этих изменений вызывается FixCRCFile для вычисления новой контрольной суммы и исправления контрольных сумм первых 4 блоков. Следующие два блока не имеют никаких изменений, поэтому нет необходимости пересчитывать их контрольные суммы.

Драйвер CLFS.sys считывает шесть блоков файла и для хранения их содержимого выделяет память в пуле ядра.

Существует очень важная структура размером 0x90, которая в предыдущем блог-посте о CVE-2022-37969, благодаря реверсингу, я нашёл некоторые поля и назвал её pool_0x90. После ещё более тщательного реверсинга теперь я знаю, что её настоящее имя — m_rgBlocks, и по мере того, как контроллер выделяет память для копирования из файла содержимого каждого блока, он сохраняет размер каждого блока, начальное смещение и адрес ядра, где он был сохранён.

Он содержит шесть CLFS_METADATA_BLOCK, соответствующих каждому блоку по его номеру.
Каждая структура CLFS_METADATA_BLOCK имеет длину 0x18 байт. (0x18*6=0x90)
В смещении 0 находится
объединение, но, по крайней мере, в этом эксплойте используется только
поле pbImage, поэтому, упрощая, это будет:
Выделение этой структуры может происходить из двух разных мест драйвера CLFS.sys, в зависимости от создания нового файла или открытия существующего. В случае создания нового файла драйвер выделяет 0x90 байт из CClfsBaseFilePersisted::CreateImage+28A, а в случае существующего файла — из CClfsBaseFilePersisted: ReadImage+6E.
После этого я получу начальный адрес блока 2, который соответствует файлу trigger blf, называемому BASE BLOCK, который начинается со смещения 0x800 и имеет длину 0x7a00.

Внутри функции fun_prepare ниже этот адрес будет найден в ядре с помощью этого фрагмента кода.

Сначала функция getBigPoolInfo находит все выделения в пуле, которые имеют тег "Clfs" и размер 0x7a00, затем сохраняет их в массиве.
После этого он снова открывает ранее изменённый файл trigger blf, используя CreateLogFile с аргументом OPEN_EXISTING, таким образом открывается существующий файл, что приведёт к выделению его BASE BLOCK.
Когда getBigPoolInfo вызывается снова, появится один новый пул "Clfs" размером 0x7a00, и его адрес извлекается путём двукратного вызова NtQuerySystemInformation.
Адрес BASE BLOCK файла trigger blf сохраняется в переменной CLFS_kernelAddrArray.

Обратите внимание, что если изменённый файл trigger blf не имеет правильной контрольной суммы, функция CreateLogFile() завершится ошибкой.
Последняя часть функции fun_prepare вызывает API AddLogContainer, используя дескриптор файла trigger blf.

В последней функции PoC с именем to_trigger будет создан второй тип файла blf.
Я назову его spray blf.
Этот тип файла будет использоваться для заполнения пространства памяти
(spray), необходимо 10 копий этого типа, но изначально создаётся только
один. 
Будут созданы три массива для хранения случайных имён этих файлов:
stored_log_arrays: хранит десять новых случайных имён файлов .blf, которые будут использоваться с CreateLogFile.
stored_container_arrays: хранит случайные имена для создания десяти новых файлов контейнеров.
stored_fopen_arrays: хранит имена файлов журналов из первого массива (переменная stored_log_arrays), но с их обычным путём (без строки "LOG:") и с расширением .blf.

На каждой итерации файл blf копируется с помощью CopyFileW, присваиваются имена, хранящиеся в массивах.
Функция fun_trigger вызывает craftSprayBlfFile, где вносятся изменения в каждый файл, а FixCRCFile исправит CRC.

Таким образом, я создал 10 похожих файлов (spray blf) со случайными именами со следующими изменениями:

Последнее изменение — копирование всего блока 0 (CONTROL BLOCK) в блок 1 (CONTROL BLOCK SHADOW)

Эффект этих изменений, а также изменений, внесённых в файл trigger blf, будет объяснён позже, в главе об отладке.
Некоторые из этих изменений вызывают уязвимость, в то время как другие необходимы только для обхода проверок драйвера.
На данный момент файлы уже созданы и изменены, готовы к выполнению spray; затем, когда они будут открыты с помощью CreateLogFile, они будут размещены в нужной нам области памяти, как будет показано позже.

В функции to_trigger создаётся массив из 12 элементов, содержащий адрес BASE BLOCK файла trigger blf плюс 0x30.
Затем в функции fun_pipeSpray память заполняется spray из каналов; внутри неё есть цикл, который вызывает CreatePipe и создаёт количество каналов, переданное в качестве первого аргумента; второй аргумент — массив, который будет хранить дескрипторы всех созданных каналов.

Внутри цикла он вызывает CreatePipe,
создавая каналы чтения-записи.
Таким образом, сначала будет создано 0x5000 каналов,
а затем ещё 0x4000 каналов.
Затем использует WriteFile для записи в первые 5000 каналов недавно созданный массив с адресами BASE BLOCK + 0x30 файла trigger blf.

Теперь в памяти уже создан компактный блок; он освободит 0x667 каналов, начиная с номера 0x2000 и до 0x2667, поскольку в памяти каналы расположены не в том же порядке, в котором были созданы; это приведёт к появлению свободных мест в этом блоке памяти.
Обратите внимание,
что выделения каналов имеют пользовательский размер 0x90 байт, поэтому
при освобождении у нас будет
Он освобождает пространства памяти размером 0x90 между памятью, заполненной
каналами.
Затем он в цикле вызывает CreateLogFile с 10 файлами spray blf.
Когда CreateLogFile вызывается для открытия существующих файлов,
выполняется выделение 0x90 байт для m_rgBlocks по одному для каждого
файла spray blf; таким образом, эти выделения займут пробелы,
оставшиеся после освобождения каналов, поскольку они имеют одинаковый
размер.

Затем повторяется процесс записи в финальные 0x4000 каналов массива, содержащего адрес BASE BLOCK +0x30 файла trigger blf.
Все эти манипуляции создают контролируемое пространство памяти; я покажу, как оно выглядит при отладке, но идея в том, что m_rgBlocks каждого файла spray blf занимают пробелы размером 0x90 байт, которые были освобождены.
Затем, уже в финальной части, ошибка запускается внутри цикла while( 1 )
с помощью вызова AddLogContainer к файлам spray blf.
Внутри этого цикла срабатывает уязвимость:

Этот цикл завершится, когда найдет токен системы, используя функцию NtFsControlFile, которая читает атрибуты каналов.

Затем с помощью CreateLogFile снова перезаписывает токен нашего процесса недавно найденным системным токеном, и таким образом мы достигаем повышения привилегий.

Затем восстанавливаются некоторые значения, закрываются дескрипторы каналов и файлов blf, и запускается Notepad от имени System, чтобы убедиться, что повышение выполнено корректно.

Обратите внимание на файлы blf, созданные в папке PUBLIC. Помните, что если вы хотите повторить попытку, сначала необходимо удалить созданные файлы. Некоторые будут заблокированы, и их нельзя будет удалить, но PoC все равно будет работать.

Прежде чем я начну с эффекта изменений в trigger blf и spray blf файлах для выполнения эксплуатации, я должен проверить, что m_rgBlocks файлов spray blf расположены в дырах, которые возникают в распределении памяти, после выполнения разброса каналов (pipe spray) и последующего освобождения определенного количества каналов.
Когда эта процедура завершается, канал должен располагаться под 0x90 байтами m_rgBlocks, поэтому при использовании m_rgBlocks произойдет ВЫХОД ЗА ГРАНИЦЫ (OUT OF BOUNDS), и он прочитает из этого канала, который находится ниже.
В PoC есть идеальная точка для установки точки останова:

На этом этапе открытие файлов spray blf завершено, а функция AddLogContainer еще не вызывается.
Для отладки в пользовательском режиме я буду использовать x64dbg, а для режима ядра — IDA с плагином Windbg.

На этом этапе память уже должна быть подготовлена, и я могу увидеть распределение.
Я приостановлю IDA, чтобы найти интересную точку для установки точки останова.
Я установлю точку останова на CClfsBaseFilePersisted::AddContainer, которая вызывается из AddLogContainer, и в начале у нее регистр RCX указывает на структуру CClfsBaseFilePersisted, а по смещению 0x30 находится указатель на m_rgBlocks.

Когда точка останова срабатывает, я проверяю в стеке вызовов, что AddLogContainer вызывается из моего PoC.

Регистр RCX указывает на:

Первое поле — это указатель на vtable (CLFS! CClfsBaseFilePersisted::'vftable'), а по смещению 0x30 находится указатель на m_rgBlocks.

Блоки 0, 1, 4 и 5 еще не сохранили pbImage, в то время как блоки 2 (БАЗОВЫЙ БЛОК) и 3 (ТЕНЕВОЙ БЛОК) сохранили.
Каждый блок в таблице m_rgBlocks имеет свой cbOffset — смещение, с которого блок начинается в файле, cbImage — размер блока, и eBlockType — тип блока.
Если разброс выполнен правильно, под m_rgBlocks должен быть канал, а внутри — указатели на БАЗОВЫЙ БЛОК + 0x30 файла trigger blf.

Команда "!pool" в windbg отображает распределение памяти:

Каждый m_rgBlocks имеет тег "Clfs" и размер 0xa0, потому что это пользовательский размер 0x90 плюс заголовок 0x10, а ниже находится канал с тегом "NpFr", который имеет такой же пользовательский размер 0x90 + заголовок 0x10.
Поскольку распределение не является точной наукой, некоторые "Clfs" были размещены непрерывно, что нежелательно, но тот, с которым я работаю, расположен правильно, за ним следует канал.
Одним из первых изменений, которые влияют, является изменение в файле trigger blf по смещению 0x858, где хранится значение 0x369.

БАЗОВЫЙ БЛОК начинается со смещения 0x800 в файле.

Внутри _CLFS_LOG_BLOCK_HEADER по смещению 0x800+0x58 (0x58 от начала заголовка БАЗОВОГО БЛОКА).

По смещению 0x28 начинается массив RecordOffsets (DWORD).
Перемещаясь на 0x30 байт вперед, по смещению 0x58 (0x828+0x30=0x858 от начала), находится поле 12 массива RecordOffsets.

Я запускаю PoC для CreateLogFile, как показано на изображении ниже:


Перед тем как войти в CreateLogFile, я собираюсь установить точку останова в месте, где значение 0x369 еще не использовалось.
В случае, когда CreateLogFile открывает существующий файл, структура m_rgBlocks выделяется здесь:
CClfsBaseFilePersisted::ReadImage+6E
Итак, я установлю точку останова в IDA прямо здесь:

Когда точка останова срабатывает:

В m_rgBlocks все еще есть мусор, так как он еще не инициализирован, но как только pbImage блока 2 будет выделен, адрес сохранится по смещению 0x30 от начала, так как первое поле внутри каждого CLFS_METADATA_BLOCK — это pbImage.


Теперь я устанавливаю аппаратную точку останова на запись: ba w1 ffffd003'7f5bea30
После инициализации нулем, она останавливается, когда сохраняется pbImage.

Анализ показывает, что это соответствует блоку 0, потому что не учитывается константа r14*8, которая затем равна 0x30, в результате фактически записывается pbImage блока 2.

Обратите внимание, что CClfsBaseFilePersisted::ReadMetadataBlock используется для выделения любого из блоков, используя размер, переданный в качестве аргумента.

Теперь установите точку останова чтение/запись по адресу 0x58 от базового блока, чтобы увидеть, когда используется значение 0x369.
ba r1 FFFF978A'16ECF000+0x58

Когда точка останова срабатывает, считывается значение 0x369, расположенное по адресу RecordOffset[12], добавляется к необычному указателю в r14 и увеличивается содержимое RAX+r14.
Несколько строк выше в коде, ESI имеет значение 0x13 и умножается на 0x18, что является размером каждого блока в m_rgBlocks.
WINDBG>? 0x18*0x13
Evaluate expression: 456 = 00000000'000001c8
Если я добавлю значение r8= 0x1c8, которое больше 0x90, к начальному адресу m_rgBlocks, это будет чтение ЗА ГРАНИЦАМИ (OUT OF BOUNDS).


Под m_rgBlocks находится канал с указателем на БАЗОВЫЙ БЛОК + 0x30, он считывает этот указатель, который был стратегически размещен внутри канала.
Текущая позиция в коде
была вызвана из оператора while(1) главного модуля.
Внутри файла spray blf я стратегически разместил значение 0x13 по смещению 0x48a (iFlushBlock).

По смещению 0x8a файла spray blf находится iFlushBlock БЛОКА 0, значение которого равно 4, в то время как смещение 0x48a принадлежит iFlushBlock БЛОКА 1, и его значение равно 0x13

Теперь мне нужно выяснить, почему считывается iFlushBlock = 0x13 из БЛОКА 1 вместо iFlushBlock = 4 из БЛОКА 0.
Если я оглянусь назад, чтобы выяснить, откуда взялось 0x13, я вижу в стеке вызовов, что WriteMetadataBlock вызывается из CClfsBaseFilePersisted::ExtendMetadataBlock+416, там второй аргумент iFlushBlock — EDX=0x13, который приходит из r9w.


За несколько строк до этого,
CClfsBaseFile::GetControlRecord был вызван для получения адреса
БЛОКА 0, возможно, проблема здесь, поэтому я перезагружусь и поставлю точку останова на нем.
GetControlRecord вызывает CClfsBaseFile::AcquireMetadataBlock, который должен заполнить таблицу m_rgBlocks адресом блока 0, когда я прохожу эту функцию, она получает адрес блока 1, следовательно, проблема возникает внутри CClfsBaseFile::AcquireMetadataBlock.
Добавив 0x8A к полученному адресу, я могу подтвердить, что значение 0x13, принадлежащее БЛОКУ 1, присутствует.

Я перезагружусь и установлю точку останова там:

CClfsBaseFile::GetControlRecord+27 вызов CClfsBaseFile::AcquireMetadataBlock

Второй аргумент, переданный в AcquireMetadataBlock, равен нулю, он
соответствует блоку 0, он собирается скопировать из файла и сохранить его адрес
в m_rgBlocks
.
В перечислении типа блока _CLFS_METADATA_BLOCK_TYPE они имеют другие имена, чем я использовал, но это те же 6 блоков.

После проверки, что тип блока меньше максимального m_cBlocks=6, сохраняется значение reference, чтобы избежать чтения одного и того же блока дважды.

Вызывается ReadMetadataBlock, проблема чтения блока 1 вместо блока 0 будет внутри этой функции.

Если все в порядке, выделяется память с использованием cbImage в качестве размера, и адрес сохраняется в поле блок 0-> pbImage в m_rgBlocks.

Команда !pool отображает тег и размер выделенной памяти.

Итак, у меня уже есть адрес
pbImage блока 0, сохраненный в m_rgBlocks, поэтому мне нужно
увидеть, почему он копирует байты блока 1 туда вместо байтов блока 0.
Я добираюсь до вызова CClfsContainer::ReadSector, куда передается указатель на переменную, содержащую pbImage, для записи байтов.

Обратите внимание на изменения содержимого pbimage при прохождении через ReadSector.

Добавив 0x8a к pbImage, я могу найти значение 4, которое является правильным значением, вместо 0x13, следовательно, проблема должна возникнуть позже.
После вызова ClfsDecodeBlock возвращается ошибка 0x0C01A000A.
CClfsBaseFilePersisted::ReadMetadataBlock+153 вызывает ClfsDecodeBlock
После этой ошибки к типу добавляется 1 и снова вызывается CClfsBaseFilePersisted::ReadMetadataBlock, но с типом 1 для чтения блока 1.

В CClfsBaseFilePersisted::ReadMetadataBlock выделяется и сохраняется новый pbImage в m_rgBlocks для блока 1.

Блоки 0 и 1 имеют разные адреса, теперь если я добавлю 0x8a к адресу блока 1, его значение равно 0x13.
Возможно, поскольку блок 0 вернул ошибку, используется блок 1 и возвращается в GetControlRecord как Контрольный блок.
Как показано ранее, при использовании значения 0x13 вместо 4 происходит выход за границы m_rgBlocks и считываются значения из разброса каналов, контролируемые мной.
Затем освобождается pbImage из блока 0
и копируется указатель из блока 1 в блок 0.

Необходимо было бы найти значение, которое вызывает ошибку 0x0C01A000A внутри ClfsDecodeBlock.
Внутри ClfsDecodeBlock контрольная сумма первого блока равна нулю, это ошибка 0xC01A000A.

Перед вызовом AddLogContainer, при открытии любого файла spray blf в шестнадцатеричном редакторе контрольная сумма была изменена на ноль.

она должна была быть изменена ранее, когда файл открывался с помощью CreateLogFile.

По какой-то причине файлы spray blf после выхода из CreateLogFile имеют контрольную сумму блока 0 равную 0 и возвращают действительный дескриптор, давайте посмотрим, почему это происходит.
Я останавливаюсь на CreateLogFile перед открытием какого-либо файла spray blf.

Обратите внимание, что перед вызовом CreateLogFile файлы spray имеют правильную контрольную сумму в блоке 0, а после завершения функции значение контрольной суммы изменяется на ноль.

Итак, я устанавливаю точку останова на CClfsBaseFile::GetControlRecord, чтобы заглянуть внутрь.
После прохождения
CClfsContainer::ReadSector контрольная сумма не равна нулю.
Перед входом в вычисление CRC32 он обнуляет поле контрольной суммы в памяти для вычисления CRC, и результат корректен.


Затем он проверяет значение eExtendState =2 и переходит к WriteMetadataBlock.

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

Он проверяет некоторые значения, которые созданы в файле blf spray, чтобы достичь CClfsBaseFilePersisted::ExtendMetadataBlock.

После цикла для чтения блоков, которые еще не были прочитаны, блок 0 продолжает иметь контрольную сумму = 0.

Прибытие в WriteMetadataBlock.

Поскольку я работаю до того, как он заменит блок 0 на 1, значение iFlushBlock файла blf spray все еще равно 4 — правильное значение.

Теперь он работает с блоком 4 и запишет блок 4 в файл, здесь пока нет проблемы.
Затем он переходит к CClfsBaseFilePersisted::FlushControlRecord

Внутри он достигает WriteMetadataBlock, но с аргументом 0, чтобы записать блок 0 в файл.

Затем ClfsEncodeBlock возвращает ошибку 0xC01A000A, хотя он запишет файл с поврежденным блоком 0 в CClfsContainer::WriteSector, чуть ниже.

Переменная var_54 хранит значение ошибки 0xC01A000A и будет проверена перед выходом из функции.

Но после вызова CClfsContainer::WriteSector, который не возвращает ошибку, содержимое var_54 перезаписывается нулем.
Таким образом, функция возвращает ноль без ошибки, и она продолжает работу, поскольку CreateLogFile вернет дескриптор вместо значения ошибки.

Значение 0x13 в iFlushBlock вызывает выход за границы и приводит к чтению указателя, находящегося в каналах, который указывает на Базовый блок +30 файла trigger blf.
Затем он добавляет 0x28 к этому указателю ( 0x58 от начала базового блока trigger blf), который имеет значение 0x369.


Инструкция INC увеличит значение 0x14 на 1 и повторяется 4 раза, так что 0x14 становится 0x18.
WINDBG>db r14+369
ffffcb82'091e7397 14 00 00 00
После этого вызывается CreateLogFile, и считывается значение 0x1858.

GetSymbol проверяет, имеет ли поддельный блок, ранее созданный в trigger blf и указываемый смещением 0x1858, правильные значения.

Если бы указатель не был увеличен несколько раз, он бы имел исходное значение 0x1458 и указывал бы на правильный блок.
После выхода из GetSymbol он использует этот поддельный блок здесь.

Затем он считывает значение смещения 0x18 поддельного блока, куда я поместил 0x05000000, и переходит к содержимому того, что там находится.
WINDBG>dps 0x5000000
00000000'05000000 00000000'05001000

Он считывает содержимое 0x05000000 и его 0x05001000, а там находится ClfsEarlierLsn.

Эта функция используется для возврата значения 0xFFFFFFFF в RDX, хотя в первый раз это значение не используется.
Второй вызов происходит здесь: он вызывает PoFxProcessorNotification, который находился по адресу 0x501000 +8

WINDBG>dps 00000000**'05001000**
00000000'05001000 fffff805'7ab13220 CLFS! ClfsEarlierLsn
00000000'05001008 fffff805'769dc3b0 nt! PoFxProcessorNotification

В этой функции RCX = 0x05000000, она проверяет, что через 0x40 байт должно быть ненулевое значение.
WINDBG>dps rcx+40
00000000'05000040 00000000'05000000
Адрес для перехода будет через 0x68 байт.
WINDBG>dps rcx+68
00000000'05000068 fffff805'7ab2bfb0 CLFS! ClfsMgmtDeregisterManagedClient
А аргумент будет через 0x48 байт.
WINDBG>dps rcx+48
00000000'05000048 00000000'05000400
ClfsMgmtDeregisterManagedClient — это удобная функция, потому что я могу управлять аргументом, а также у меня есть два перехода на функции, которыми я управляю.

Первый вызов снова к ClfsEarlierLsn, который вернул RDX=0xFFFFFFFF.


Он возьмёт источник для записи из содержимого RDX=0xFFFFFFFF.
WINDBG>dps rdx
00000000'ffffffff ffff8005'3a4ee000
По адресу 0xFFFFFFFF я сохранил system_EPROCESS & 0xfffffffffffff000.

Целевой адрес — это указатель, находящийся по адресу 0x5000400 +0x48
*(UINT64*)0x5000448 = para_PipeAttributeobjInkernel + 0x18;

Указатель PipeAttribute в ядре, который указывает на буфер, заполненный символами «A», будет перезаписан старшей частью указателя SYSTEM EPROCESS.
Этот указатель был создан, когда я ранее вызывал _NtFsControlFile с буфером, полным «A».

Содержимое этого атрибута можно прочитать с помощью NtFsControlFile.

Теперь атрибут канала больше не указывает на буфер с «A», а на system_EPROCESS & 0xffffffffffffff000.

Этот код будет повторяться, пока не будет получен системный токен.


В Windows 11 системный токен находится по смещению 0x4b8 от структуры EPROCESS, только что прочитанной.

Мне нужно только записать этот системный токен в мой процесс, вызвав CreateLogFile.

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

При двойном вызове сначала вызывается ClfsEarlierLsn для возврата 0xFFFFFFFF в RDX, а затем вызывается nt_SeSetAccessStateGenericMapping.

Я проверяю, что значение, на которое указывает RDX, является системным токеном.

Токен моего процесса:

Он будет записан туда.
WINDBG>dps rax+8
ffff9b8b'fc446578 ffffc402'f601c06c
WINDBG>dps rax+8
ffff9b8b'fc446578 ffffc402'ef841919
Теперь мой процесс — System. Я могу запустить Notepad, чтобы проверить.



BINDIFF показывает много изменённых функций

Уязвимая функция находится здесь:


Первичная — это пропатченная версия, вторичная — уязвимая.
Патч проверяет возвращаемое значение CflsEncodeBlock, которое равно 0xC01A000A, сохраняет его в переменную var_54, и, поскольку оно отрицательное, проверяет его и предотвращает выполнение WriteSector.
Патч, помимо того, что не записывает файл, корректно возвращает 0xc01a000a, вследствие чего CreateLogFile не возвращает никакого дескриптора, и эксплуатация не может продолжаться.


Только если ClfsDecodeBlock не отрицателен, он переходит к WriteSector, но при этом возвращает отрицательное значение 0xC01A000A.
Это фактический патч, который действительно предотвращает эксплуатацию с помощью PoC, который я только что приложил.
На этом мы объяснили, как была эксплуатирована уязвимость, что приводит к контролю над функциями, позволяющими нам прочитать системный токен и записать его в наш собственный процесс для достижения локального повышения привилегий. Рабочий PoC можно найти на GitHub Fortra.
Надеемся, это было полезно. Если у вас есть вопросы, можете связаться с нами: