
Эксплойт Proof-of-concept для CVE-2022-37969 — локальное повышение привилегий в драйвере Windows Common Log File System. Демонстрирует heap spray, кражу токена и произвольную запись в ядро для получения привилегий SYSTEM.
авторы: Ricardo Narvaja и Daniel Kazimirow (Solid)
Только для демонстрационных целей. Полный эксплойт работает на уязвимых системах Windows 11 21H2.
Функциональный PoC на основе ранее опубликованной информации от Zscaler
Ознакомьтесь с подробным описанием Понимание уязвимости CVE-2022-37969 в драйвере Windows Common Log File System, локальное повышение привилегий.
Пошаговое руководство по эксплуатации:
В данном сценарии использовался Windows 11 21H2 (сборка ОС 22000.918) clfs.sys v10.0.22000.918
Первым шагом является создание файла с именем MyLog.blf в общей папке (%public%), с помощью функции CreateLogFile():



Затем он создает несколько файлов журнала со случайными именами с помощью цикла.
И внутри цикла он вызывает нашу функцию getBigPoolInfo():

Он вызывает NtQuerySystemInformation() с первым аргументом 0x42 (66 десятичное), что вернет в v5 информацию о выделениях в большом пуле (bigpool), структура которого имеет тип SYSTEM_BIGPOOL_INFORMATION.

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

v5 получит информацию о структуре SYSTEM_BIG_POOL_INFORMATION.

Количество выделений в bigpool хранится в первом поле с именем Count, во втором поле находится массив структур SYSTEM_BIGPOOL_ENTRY.

Затем мы будем искать по всем структурам тег «Clfs» и размер 0x7a00.

Он сохраняет в массиве с именем kernelAddrArray VirtualAddress, который является первым полем каждой структуры, имеющей тег CLFS и размер 0x7a00. С этого момента пулы, удовлетворяющие обоим условиям, будут называться «правильными пулами».

В дополнение к сохранению каждого правильного пула в массиве, он сохраняет последний найденный правильный пул в содержимом переменной a2, которая используется как аргумент функции.

Таким образом, a2 всегда указывает на последний созданный правильный пул с тегом CLFS и размером 0x7a00.
Переменная v26 всегда хранит предыдущий найденный правильный пул, так как она равна v24 (v26=v24) перед вызовом getBigPoolinfo(), но v24 обновляется при выходе из этого вызова последним найденным правильным пулом, а v26 остается с предыдущим найденным правильным пулом.

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

Таким образом, в v32 будет сохранена разница между VirtualAddress последних двух найденных правильных пулов.

Затем он делает нечто подобное, в данном случае v23 изначально равен нулю, поэтому в первый раз v23=v32.

В следующий раз в цикле v23 все еще имеет то же значение и не равно нулю, поэтому он прерывается и переходит сюда.

V32 содержит последнюю разницу, а v23 предыдущую, если они равны, то выходит и увеличивает на один, но сбрасывает счетчик в ноль.
Идея состоит в том, чтобы найти 6 последовательных сравнений тегов CLFS и размера 0x7a00, разницы которых равны, и эта разница будет 0x11000. Мы увидим при выполнении, что когда будет найдено 6 (начиная с нуля) последовательных с равными расстояниями, будет получено это значение разницы между ними.

Там мы видим, что он нашел 6 последовательных и вышел из цикла создания файлов журнала.
В папке «public» мы можем увидеть созданные файлы

Наша функция craftFile() открывает исходный файл (MyLog.blf) и изменяет его для срабатывания ошибки.

После изменения файла необходимо изменить CRC32, иначе возникнет ошибка поврежденного файла.
Это значение находится по смещению 0x80C файла.

Затем выполняется HeapSpray с помощью функции VirtualAlloc() для выделения памяти по произвольным адресам 0x10000 и 0x5000000 соответственно, и сохранения во втором выделении (0x10000) значения 0x5000000 через каждые 0x10 байт.

Он использует CreatePipe() для создания анонимного канала и вызова NtFsControlFile() с аргументом 0x11003c для добавления атрибута, позже вы можете вызвать эту же функцию с аргументом 0x110038 для его чтения.
Более подробную информацию об этом методе можно найти ЗДЕСЬ

Там мы видим входной буфер, который является добавляемым атрибутом, если мы снова вызовем NtFsControlFile() с аргументом 0x11038, на выходе он должен вернуть этот же атрибут.

Поиск в пуле тега созданного атрибута (NpAt)


И когда он его находит, он сохраняет VirtualAddress этого пула в v30.Pointer.
V30.pointer+24 указывает на AttributeValueSize в пуле ядра и сохраняет его в одном из ранее выполненных HeapSpray.

Идея заключается в записи по этому адресу ядра+8 для перезаписи AttributeValue.


Структура PipeAttribute имеет в качестве первого поля LIST_ENTRY размером 16 байт, затем указатель на имя атрибута размером 8 байт, и затем по смещению 0x18 (24 десятичное) поле AttributeValueSize, которое мы и сохраняем в HeapSpray.

После этого мы загружаем CLFS.sys и ntoskrnl в пользовательском режиме и с помощью GetProcAddress() находим адреса функций ClfsEarlierLsn() и SeSetAccessStateGenericMapping().

Затем мы вызываем функцию FindKernelModulesBase(), которая найдет базовый адрес ядра обоих модулей, используя NtquerySystemInformation() на этот раз с аргументом SystemModuleInformation для получения информации обо всех модулях.

Таким образом, мы можем вычислить смещение каждой функции, а затем получить их в ядре.
Функция pipeArbitraryWrite() вызывается дважды, есть флаг, который изначально равен нулю для первого вызова, а во втором вызове он равен 1, что изменит значения HeapSpray.

В первом вызове по адресу памяти 0x5000000 расположены следующие значения

Помните, что это значение, помимо выделения по этому адресу, также сохраняется в нашем HeapSpray.

Так выглядит память после первого вызова, как мы уже сказали, по адресу около 0x5000000

А в HeapSpray по адресу памяти 0x10000 будет храниться указатель на AttributeValueSize каждые 0x10 байт, помимо указателя на 0x5000000.

Эта последовательность вызовет ошибку:

CreateLogFile() снова вызывается для подготовленного файла и для другого со случайным именем.
AddLogContainer() затем вызывается с использованием дескрипторов этих файлов.

Вызывается NtSetinformationFile(), и дескрипторы закрываются, в результате чего указатель повреждается (это будет объяснено позже)

HeapSpray предотвращает возникновение BSOD в этой точке:

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


RAX принимает значение 0x5000000 и переходит сначала к функции, расположенной по адресу 0x5000000+18, а затем к 0x5000000+8.


Таким образом, сначала переход к fnClfsEarlierLsn(), а затем к fnSeSetAccessStateGenericMapping().

Мы трассируем от точки останова и видим, что достигаем CLFS!ClfsEarlierLsn().

Эта функция вызывается специально, потому что при возврате она устанавливает EDX в 0xFFFFFFFF

По адресу 0xFFFFFFFF мы сохранили результат SYSTEM _EPROCESS & 0xFFFFFFFFFFFFFFF000

Как мы уже упоминали, при возврате из CLFS!ClfsEarlierLsn() значение RDX равно 0x00000000FFFFFFFF

Мы переходим ко второй функции nt!SeSetAccessStateGenericMapping()


Эта функция полезна, поскольку RCX указывает на наш HeapSpray, а значение RDX равно 0xFFFFFFFF, содержимое которого мы контролируем



Содержимое RCX+0x48 содержит указатель на AttributeValueSize, который был сохранен в v30.Pointer+24.

Это значение указателя AttributeValueSize перемещается в RAX, затем читает содержимое адреса 0xFFFFFFFF, где мы сохранили адрес SYSTEM _EPROCESS & 0xFFFFFFFFFFFFFFF000.


Затем перезаписывает в RAX+8 следующее поле, которое является AttributeValue()

Конечно, AttributeValue обычно указывает в ядре на добавленный нами атрибут.
И теперь мы перезапишем его указателем на результат SYSTEM _EPROCESS & 0xFFFFFFFFFFFFFFF00.
Это будет означать, что когда мы снова вызовем функцию NtFsControlFile(), на этот раз с аргументом 0x110038 для чтения атрибута, вместо возврата символа «A», на который указывал указатель AttributeValue, он теперь будет читать из _EPRROCESS & 0xFFFFFFFFFFFFFFFFF000 запрошенное количество байт и возвращать его в выходной буфер, с помощью которого мы сможем при первом вызове получить значение SYSTEM TOKEN.

v9b — начальный адрес Output Buffer, куда было скопировано содержимое результата System EPROCESS & 0xFFFFFFFFFFFFFFF000.
К этому он добавляет v14, которые являются последними 3 байтами System EPROCESS, а затем добавляет 0x4b8, что является смещением Token для этой версии Windows 11, затем находит содержимое этого адреса, которое будет содержать сохраненное значение System Token.



Помните, что последние 4 бита были изменены, это несущественно, поэтому значение все равно совпадает.
Во втором вызове значение Flag равно 1, так как оно было увеличено в конце первого вызова.

Там мы видим порядок, в котором сохраняются значения

Адрес 0xFFFFFFFF со значением, которое мы только что нашли для System Process Token.


А в HeapSpray находится значение адреса Token моего процесса, из которого я вычитаю 8. Это значение плюс восемь будет использоваться как цель, помните, что вы записывали по адресу, указанному RAX+8.


В адресе памяти, начинающемся с 0x5000000

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

Затем ошибка срабатывает во второй раз так же, как и в первой попытке.

Он снова приходит к CLFS!ClfsEarlierLsn().

устанавливая RDX в 0xFFFFFFFF

Затем он приходит к nt!SeSetAccessStateGenericMapping()

Читает адрес Token моего процесса минус 8, куда будет производиться запись

Затем читает SYSTEM TOKEN

И записывает по адресу Token моего процесса (прибавляя 8) System Token

И таким образом мой процесс получает System Token

После записи токена мы запускаем процесс для проверки привилегий, в данном случае запускаем Notepad.exe



Помните, что этот PoC работает только в Windows 11, в Windows 10 он вызовет BSOD, поэтому для корректной работы необходимо внести некоторые изменения, которые не описаны в этом посте.
Анализ структур
Структуры и большая часть документации по формату файлов CLFS взяты из отличной работы IONESCU по внутреннему устройству CLFS.
Мы видим, что в функцию ClfsBaseFilePersisted::LoadContainerQ была добавлена проверка

Значения, которые выполняют сложение, принадлежат структуре _CLFS_BASE_RECORD_HEADER.

Обратите внимание, что Base Block начинается со смещения 0x800 файла и заканчивается смещением 0x71FF, при этом первые 0x70 байт соответствуют Log Block Header
В качестве хорошей практики мы можем добавить структуру _CLFS_LOG_BLOCK_HEADER в IDA
struct _CLFS_LOG_BLOCK_HEADER
{
UCHAR MajorVersion;
UCHAR MinorVersion;
UCHAR Usn;
char ClientId;
USHORT TotalSectorCount;
USHORT ValidSectorCount;
ULONG Padding;
ULONG Checksum;
ULONG Flags;
CLFS_LSN CurrentLsn;
CLFS_LSN NextLsn;
ULONG RecordOffsets[16];
ULONG SignaturesOffset;
};
Затем у нас есть Base Record Header (_CLFS_BASE_RECORD_HEADER), который начинается со смещения 0x870 от начала файла и имеет длину 0x1338 байт.

Если вы хотите импортировать его в IDA, предварительно необходимо добавить следующие типы и отсутствующие структуры
typedef GUID CLFS_LOG_ID;
typedef UCHAR CLFS_LOG_STATE;
struct _CLFS_METADATA_RECORD_HEADER
{
ULONGLONG ullDumpCount;
};
Теперь он готов к добавлению:
typedef struct _CLFS_BASE_RECORD_HEADER
{
CLFS_METADATA_RECORD_HEADER hdrBaseRecord;
CLFS_LOG_ID cidLog;
ULONGLONG rgClientSymTbl[0x0b];
ULONGLONG rgContainerSymTbl[0x0b];
ULONGLONG rgSecuritySymTbl[0x0b];
ULONG cNextContainer;
CLFS_CLIENT_ID cNextClient;
ULONG cFreeContainers;
ULONG cActiveContainers;
ULONG cbFreeContainers;
ULONG cbBusyContainers;
ULONG rgClients[0x7c];
ULONG rgContainers[0x400];
ULONG cbSymbolZone;
ULONG cbSector;
USHORT bUnused;
CLFS_LOG_STATE eLogState;
UCHAR cUsn;
UCHAR cClients;
} CLFS_BASE_RECORD_HEADER, *PCLFS_BASE_RECORD_HEADER;

После включения структур мы замечаем, что выполняется сложение между cbSymbolZone и адресом, где заканчивается _CLFS_BASE_RECORD_HEADER. (start + 1338h)
Помните, что cbSymbolZone был изменён в поддельном файле журнала с 0x000000F8 на 0x0001114B.
(смещение 0x1b98 файла)
0x800(смещение начала базового блока) + 0x70 (logBlockHeader) + 0x1328 (cbsymbolZone)
0x800+0x70+0x1328 = 0x1b98
Изменённый cbsymbolZone в файле MyLog.blf:


Поскольку исправление находится в функции CClfsBaseFilePersisted::LoadContainerQ, нам нужно взглянуть на объект CClfsBaseFilePersisted.
Установив точку останова в CLFS!CClfsBaseFilePersisted::LoadContainerQ, при вызове CreateLogFile с дескриптором поддельного файла он сработает.

Вызов функции CClfsBaseFile::GetBaseLogRecord для получения адреса Base Log Record (_CLFS_BASE_RECORD_HEADER)

RAX будет указывать на адрес _CLFS_BASE_RECORD_HEADER

Обратите внимание на структуру _CLFS_BASE_RECORD_HEADER в памяти и поле cbsymbolZone 0x1328
байт вперёд


r14 хранит структуру, соответствующую "this", то есть CClfsBaseFilePersisted, поскольку это this функции CClfsBaseFilePersisted::LoadContainerQ.

Структура CClfsBaseFilePersisted в памяти:

Итак, давайте создадим структуру длиной 0x21c0, чтобы заполнить её поля по мере реверс-инжиниринга (это недокументированная структура), мы назовём её struct_CClfsBaseFilePersisted

Внутри функции CClfsBaseFile::GetBaseLogRecord() получается указатель на _CLFS_BASE_RECORD_HEADER. и мы знаем, что "this" в этой функции — это структура: struct_CClfsBaseFilePersisted.

Считаны два поля (смещение 0x28 и 0x30)

Поле 0x28 — это слово (word) и имеет значение 6, поэтому мы меняем тип на word в структуре.



Пока мы переименовываем его в константу 6 (const_6)


Согласно документации, 6 — это количество блоков CLFS_METADATA_BLOCK_COUNT. Поле может относиться к этому значению.
И этот указатель находится по смещению 0x30.

Обратите внимание, что показанный там размер включает заголовок длиной 0x10


При вызове функции ExAllocatePoolWithTag запрашивается несколько байтов, но заголовок не включается, следовательно, в вызове будет запрошено 0x90 байт (0xa0 – 0x10).
При поиске по тексту +30h], инструкций, которые пишут по смещению 0x30, мы нашли длинный список, но фильтрация списка по типу объекта CClfsBaseFilePersisted оставляет нам немного результатов, и мы сразу находим, где выделяется этот размер, и тот же тег. (Совет: имена Create и Initialize функций — это первое, на что стоит смотреть)


Поскольку мы ещё не знаем имя, мы назовём её pool_0x90, это ещё одна недокументированная структура, и мы создадим структуру такого размера.


В памяти pool_0x90 имеет ещё один указатель по своему собственному смещению 0x30.

Этот другой указатель указывает на базовый блок в файле (базовый блок начинается со смещения 0x800)


Изображение взято из блога Zscaler:

Выделение очень большое, поскольку оно содержит весь базовый блок.



Итак, мы создадим новую структуру размером 0x7a00 и назовём её BASE_BLOCK

Первые 70 байт, как мы уже знаем, соответствуют _CLFS_LOG_BLOCK_HEADER, а следующие 0x1338 — _CLFS_BASE_RECORD_HEADER.

Итак, добавляя начало Base Block к смещению следующей записи (которое равно 0x70), мы получаем _CLFS_BASE_RECORD_HEADER

Структура _CLFS_BASE_RECORD_HEADER в памяти.

Рассматривая другие методы того же объекта CClfsBaseFilePersisted, в CClfsBaseFilePersisted::AddContainer также получается адрес _CLFS_BASE_RECORD_HEADER с помощью CClfsBaseFile::GetBaseLogRecord.

Далее вызывается CClfsBaseFile::OffsetToAddr с использованием cbOffset, он получает адрес _CLFS_CONTAINER_CONTEXT и сохраняет cboffset в массиве rgbcontainers, который находится по смещению 0x328 от _CLFS_BASE_RECORD_HEADER.

Функция CClfsBaseFile::OffsetToAddr используется для поиска адресов структур по смещению

На данный момент смещение контейнера, которое будет сохранено по адресу 0x328, всё ещё равно 0, поскольку мы ещё не добавили контейнер.

PoC вызывает CreateLogFile дважды: первый раз с повреждённым файлом MyLog.blf, а второй раз с обычным файлом MyLogxxx.blf, поэтому мы должны дважды останавливать отладку во всех вышеупомянутых местах и записывать в блокнот адреса вышеуказанных структур для обоих файлов.

Давайте немного перемотаем вперёд к CLFS!CClfsLogFcbPhysical::AllocContainer, установив на нём точку останова и запустив туда.
Когда в POC достигается AddLogContainer(), мы останавливаемся на точке останова.

Также установим точку останова на CClfsBaseFilePersisted::AddContainer+176, где мы ранее видели, что будет найдено смещение и указатель на структуру _CLFS_CONTAINER_CONTEXT.


Когда отладчик останавливается, мы видим, что смещение равно 0x1468.

В RAX будет возвращён адрес структуры _CLFS_CONTAINER_CONTEXT.

Структура всё ещё пуста, так как контейнер ещё не был добавлен.

Обратите внимание, что значение SignatureOffset=0x50, которое мы записали по смещению 0x868 в повреждённом файле, если вычесть 0x800 от начала базового блока, будет находиться в структуре _CLFS_LOG_BLOCK_HEADER по смещению 0x68.


Когда PoC вызывает функцию AddLogContainer() с повреждённым файлом, по смещению 0x68 от _CLFS_LOG_BLOCK_HEADER вместо значения 0x50, которое мы туда записали, сейчас в памяти находится 0xFFFF0050.

В какой-то момент это значение было изменено программой; чтобы увидеть, когда это произошло, при следующем выполнении мы установим точку останова по записи на память.
Смещение хранится по адресу r15 + 0x328 (r15 указывает на структуру _CLFS_BASE_RECORD_HEADER)


RBX хранит смещение 0x1468.

Итак, по адресу Base Block + 0x70 + смещение 0x1468, которое мы выяснили, будет находиться адрес контейнера CLFS_CONTAINER_CONTEXT.

В структуре CLFS_CONTAINER_CONTEXT по смещению 0x18 будет храниться указатель pContainer, мы можем установить точку останова по записи и посмотреть, когда он будет записан.


Это указатель, который мы должны испортить, поскольку в функции, где находится уязвимость, сначала читается CLFS_CONTAINER_CONTEXT, затем он перемещается в r15, а далее считывается значение r15+18, который является этим указателем, на который мы только что установили точку останова по записи.


он сохраняет pContainer по смещению 0x1c0 структуры struct_CClfsBaseFilePersisted.

После нескольких остановок мы достигаем момента, когда он портится. Верхняя часть адреса указателя изменилась с FF на ноль.

Это происходит, когда вызывается второй AddLogContainer() повреждённого файла; указатель предыдущего MyLogxxx портится.
Проблема возникает из-за того, что SignaturesOffset, который должен быть 0x50, теперь равен 0xFFFF0050, что позволяет записывать за границы в последующем вызове memset.


Функция memset() будет портить структуру _CLFS_CONTAINER_CONTEXT, расположенную ниже; эта структура соответствует файлу MyLogxxx, поскольку при их создании они располагались на расстоянии 0x11000 байт друг от друга.
Таким образом, он точно вычисляет, куда писать в следующую структуру, и обнуляет верхнюю часть указателя, так что он указывает на кучу пользователя, где был создан HeapSspray.
Структура базового блока повреждённого файла находится ровно на 0x11000 раньше, чем у файла MyLogxxx.
Повреждённый:

MyLogxxx


RCX меньше, чем RDX, поскольку к нему было добавлено 0xFFFF0050 вместо 0x50, как должно быть.

и мы добрались до функции memset(), чтобы установить количество 0xb0 байт нулями, при этом RCX указывает на структуру CLFS_CONTAINER_CONTEXT файла MyLogxxx, а именно на пять старших байтов pContainer.

Этот указатель будет испорчен перезаписью первых байтов:

осталось указывая на адрес памяти, ранее контролируемый нами с помощью HeapSpray


Затем дескриптор файла MyLogxxx будет закрыт, и достигается CClfsBaseFilePersisted::RemoveContainer; уязвимость наконец срабатывает.

Теперь, когда у нас больше информации, мы замечаем, что здесь считываются Base_Block.LOG_BLOCK_HEADER.SignaturesOffset и Base_Block. .LOG_BLOCK_HEADER.TotalSectorCount
В первой части исправления SignaturesOffset не должно превышать 0x7a00; в нашем случае изначально было 0x50; если бы оно пришло со значением больше 0x7a00, нас бы выкинуло.

Запустив PoC на исправленной машине, он сравнивает 0x50 с 0x7a00, и так как оно меньше, выполнение продолжается.

В следующем блоке повреждённый cbSymbolZone добавляется к значению конечного адреса _CLFS_BASE_RECORD_HEADER, и эта сумма сохраняется в result_1.

Затем адрес Base_Block складывается со значением SignatureOffset, которое в нормальном файле равно 0x7980.

Максимальный адрес base_block равен 0x7a00; теперь SymbolZone допускается до 0x80 перед пределом.
Он сохранит это в result_2, то есть это будет максимальный предел для SymbolZone внутри base block; затем он сравнивает оба результата; если первый больше второго, это означает выход за границы.


Очевидно, что первый член будет больше второго, и выполнение не продолжится, поскольку первая сумма cbSymbolZone + конечный адрес _CLFS_BASE_RECORD_HEADER превышает предел (который равен result_2) и приводит к "выходу за границы".

Последнее, что нам нужно выяснить, — где значение SignatureOffset 0x50 становится 0xFFFF0050.Итак, давайте начнём заново, перезагрузимся и остановимся на CLFS!CClfsBaseFilePersisted::LoadContainerQ, где значение ещё не изменено в памяти и равно 0x50.
Установим точку останова на запись по смещению 0x68 в SignatureOffset.

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

Эта функция не пропатчена, так что это может быть поведением, вызванным низким значением 0x50 и манипулированием остальными значениями.
Среди поддельных значений мы видим значение ccoffsetArray, имя которого в структуре _CLFS_BASE_RECORD_HEADER — rgClients, и оно представляет собой массив смещений, указывающих на объект Client Context.
Поле rgClients расположено по смещению 0x138 (0x9a8-0x800-0x70) от структуры _CLFS_BASE_RECORD_HEADER.


В PoC это значение искажено, чтобы указывать на поддельный объект клиентского контекста, называемый FakeClientContext

Это структура Client Context _CLFS_CLIENT_CONTEXT
struct _CLFS_CLIENT_CONTEXT
{
CLFS_NODE_ID cidNode;
CLFS_CLIENT_ID cidClient;
USHORT fAttributes;
ULONG cbFlushThreshold;
ULONG cShadowSectors;
ULONGLONG cbUndoCommitment;
LARGE_INTEGER llCreateTime;
LARGE_INTEGER llAccessTime;
LARGE_INTEGER llWriteTime;
CLFS_LSN lsnOwnerPage;
CLFS_LSN lsnArchiveTail;
CLFS_LSN lsnBase;
CLFS_LSN lsnLast;
CLFS_LSN lsnRestart;
CLFS_LSN lsnPhysicalBase;
CLFS_LSN lsnUnused1;
CLFS_LSN lsnUnused2;
CLFS_LOG_STATE eState;
union
{
HANDLE hSecurityContext;
ULONGLONG ullAlignment;
};
};
Значение eState находится по смещению 0x78 от начала структуры, в поддельном файле — 0x23a0+0x78.


Это значение показывает состояние журнала.
typedef UCHAR CLFS_LOG_STATE, *PCLFS_LOG_STATE;
const CLFS_LOG_STATE CLFS_LOG_UNINITIALIZED = 0x01;
const CLFS_LOG_STATE CLFS_LOG_INITIALIZED = 0x02;
const CLFS_LOG_STATE CLFS_LOG_ACTIVE = 0x04;
const CLFS_LOG_STATE CLFS_LOG_PENDING_DELETE = 0x08;
const CLFS_LOG_STATE CLFS_LOG_PENDING_ARCHIVE = 0x10;
const CLFS_LOG_STATE CLFS_LOG_SHUTDOWN = 0x20;
const CLFS_LOG_STATE CLFS_LOG_MULTIPLEXED = 0x40;
const CLFS_LOG_STATE CLFS_LOG_SECURE = 0x80;
это значение установлено в CLFS_LOG_STATE CLFS_LOG_SHUTDOWN =0x20
Другое поддельное значение — fAttributes, которое соответствует набору флагов FILE_ATTRIBUTE, связанных с базовым файлом журнала (например, System и Hidden).


Поскольку поле начинается на байт раньше, в 0xa, и занимает два байта, значение fAttributes равно 0x100.


Наконец, есть значение blocknameoffset, которое указывает на смещение 0x1bb8, то есть, добавляя 0x78 и 0x800, получаем смещение 0x2428 в файле.


Обратите внимание, что смещение до Client Context равно 0x1b30

Итак, Client Context находится по смещению 0x23a0.


А за 0x10 до него находится значение, соответствующее blocknameoffset.


Которое будет указывать на строку с именем.
Последнее — это blockattributeoffset, значение которого находится за 0xC до Client Context по адресу 0x2394.

Эти два последних значения принадлежат структуре, расположенной перед Client Context длиной 0x30 байт, называемой _CLFSHASHSYM
typedef struct _CLFSHASHSYM
{
CLFS_NODE_ID cidNode;
ULONG ulHash;
ULONG cbHash;
ULONGLONG ulBelow;
ULONGLONG ulAbove;
LONG cbSymName;
LONG cbOffset;
BOOLEAN fDeleted;
} CLFSHASHSYM, *PCLFSHASHSYM;


они находятся на расстоянии 0x20 и 0x24 байт от начала структуры _CLFSHASHSYM, так что в структуре _CLFSHASHSYM значение, называемое в PoC blockNameOffset, — это поле cbSymName, а blockAttributteoffset — это поле cbOffset.


Это поддельные значения, теперь нужно увидеть, как они влияют на изменение нашего SignaturesOffset со значения 0x50 на 0xFFFF0050.
Давайте посмотрим на функцию CClfsBaseFile::AcquireClientContext(), которая должна вернуть клиентский контекст.

она вызывает CClfsBaseFile::GetSymbol с четвёртым аргументом, который будет _CLFS_CLIENT_CONTEXT **, где будет сохранён указатель на Client Context.

Внутри функции CClfsBaseFile::GetSymbol мы передаём поддельное смещение ccoffsetArray в CClfsBaseFile::OffsetToAddr и получаем адрес клиентского контекста, давайте установим там точку останова, чтобы она остановилась при вызове файла, созданного с помощью CreatelogFile.

Здесь она остановилась с поддельным аргументом ccoffsetArray.


Функция CClfsBaseFile::OffsetToAddr возвращает ложный Client Context.

И проверяет, что значение cbOffset не равно нулю, так как перед структурой _CLFS_CLIENT_CONTEXT, находящейся в RAX, обнаружено 0xC.


Затем она сравнивает cbOffset с ccoffsetArray (находится в RSI), они должны быть равны, иначе мы получим ошибку.

Также она проверяет, что cbSymName равен cbOffset+0x88, иначе мы тоже получим ошибку.

И, наконец, она сравнивает байт cidClient с нулём.

Если все эти проверки пройдены, client context будет сохранён.

Результат функции r14 указывает на Client Context.

При выходе из CClfsLogFcbPhysical::Initialize у нас будет адрес CLFS_CLIENT_CONTEXT.

Теперь она читает значение fAttributes (0x100).

эта функция принадлежит классу CClfsLogFcbPhysical.


Который был выделен здесь, его размер 0x15d0, а тег “ClfC”.

Давайте создадим структуру для хранения того, что мы реверсируем, назовём её: struct_CClfsLogFcbPhysical.

Обратите внимание, что по смещению 0x2b0 сохраняется адрес структуры CClfsBaseFilePersisted.

После сохранения множества значений в структуре, она переходит к важной части: проверяет eState на 0x20.


Поскольку поддельное значение было 0x20, проверка вернёт 1.


Мы видим, что в конструкторе в vtable находится.

Она проверит, является ли файл мультиплексированным.

Итак, она идёт по нужному пути, достигая CClfsLogFcbPhysical::ResetLog.


Несколько полей инициализируются нулём, за исключением одного, которое инициализируется значением 0xFFFFFFFF00000000.

Здесь она извлекает Client Context.

она сохраняет значение 0xFFFFFFFF00000000.



Она записывает 0xFFFFFFFF по смещению 0x5c, которое является старшей частью CLFS_LSN lsnRestart.ullOffset.



Теперь мы выполняем функцию ClfsEncodeBlockPrivate(), которая отвечает за перезапись 0x50 на 0xFFFF0050, как мы видели ранее.
Там она читает значение SignatureOffset = 0x50, которое всё ещё такое, каким мы его установили в поддельном файле, и добавляет его к началу CLFS_LOG_BLOCK_HEADER.

Это цикл, который записывает 2 байта, так как SignatureOffset вместо указания на корректное значение, которое в нормальном файле является большим значением, например 0x3f8, что заставляет его писать дальше, здесь он будет писать в том же CLFS_LOG_BLOCK_HEADER.
Идея в том, чтобы изменить место назначения записи, пытаясь повредить значение SignatureOffset.
Нормальный файл.

В этот момент она начнёт цикл и будет записывать два байта.

Счётчик должен достичь значения 0x3d, чтобы выйти из цикла.

RCX увеличивается с 0x200, мы уже на третьем цикле, и его значение равно 0x600.

На итерации 0xe RCX равен 0x1a00.


Это было место, куда он записал 0xFFFFFFFF000000.


Он читает последние два байта FFFF.

И затем скопирует их в R8.


Как мы уже видели, это значение критично, так как позволяет обойти проверку и записать за границы, чтобы повредить указатель pContainer файла, следующий за memset(), и записать нули в начало, оставив его указывающим на нашу контролируемую память (HeapSpray).
В CClfsBaseFilePersisted::AllocSymbol та же сумма, которая будет использоваться для получения места назначения memset, то есть cbSymbolZone + конечный адрес CLFS_BASE_RECORD_HEADER, сравнивается перед этим с Base_block + 0xFFFF0050, так что искажённые значения присутствуют с обеих сторон уравнения.
CbSymbolZone = 0x1114B
Это поддельное значение, которое, будучи добавленным к конечному адресу CLFS_BASE_RECORD_HEADER, приведёт к записи за границы, а второй член сравнения, который должен быть адресом Base Block + SignatureOffset, остаётся SignatureOffset =0xFFFF0050, что позволяет пройти эту проверку и записать за границы в memset(), обнуляя верхнюю часть указателя, который будет указывать на наш HeapSpray.

Поскольку RCX меньше, чем RDX.

Как мы уже видели ранее. (Значения могут отличаться, так как принадлежат предыдущему выполнению).
Он повредит указатель, установив старшие байты в 0.

Оставляя его указывающим на область памяти, которую мы контролируем через HeapSpray.


Итак, когда уязвимость срабатывает, мы попадаем в CClfsBaseFilePersisted::RemoveContainer.

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

На этом этапе уязвимость эксплуатирована, что позволяет контролировать функции, которые позволяют читать токен SYSTEM и записывать в наш собственный процесс для достижения локального повышения привилегий.
Надеемся, вы найдёте это полезным. Если у вас есть вопросы, можете связаться с нами по адресам [email protected] и [email protected].
Приятного использования!