
Повышение привилегий через повреждение пула ядра Windows (clfs.sys)
==============
Повышение привилегий через повреждение пула ядра Windows (clfs.sys). (CVE-2023-36424)
Этот репозиторий содержит технический анализ и рабочий эксплойт.
Автор: Nassim Asrir (@p1k4l4) || https://www.linkedin.com/in/nassim-asrir-b73a57122/
================
В драйвере мини-фильтра clfs.sys происходит переполнение пула. Подробнее об этом можно прочитать здесь:
1 - https://googleprojectzero.blogspot.com/2021/01/hunting-for-bugs-in-windows-mini-filter.html
2 - https://www.zerodayinitiative.com/blog/2021/7/19/cve-2021-31969-underflowing-in-the-clouds
Причина в том, что драйвер недостаточно проверяет данные, поступающие из точки повторной обработки (reparse point) NTFS.
Мы рассмотрим clfs.sys версии 10.0.22621.2134 (Windows 11 22H2 22621.2215)
Функция HsmFltProcessHSMControl отвечает за обработку FSCTL облачного фильтра. Для операции с кодом 0xC0000003 она в конечном итоге вызывает HsmFltProcessUpdatePlaceholder.
После некоторых действий поток выполнения достигает HsmiOpUpdatePlaceholderDirectory и затем, наконец, HsmpRpCommitNoLock:
__int64 __fastcall HsmpRpCommitNoLock(__int64 a1, __int64 a2, struct _FILE_OBJECT *a3, char a4, char a5)
{ .....
v26 = FileObject; LODWORD(v9) = HsmpRpReadBuffer(*(PFLT_INSTANCE *)(v164 + 32), FileObject, (unsigned __int16 **)&P); // [1*]
HsmDbgBreakOnStatus((unsigned int)v9);
if ( (_DWORD)v9 == -1073741195 ) .....
goto LABEL_55; }
if ( (v9 & 0x80000000) != 0i64 )
goto LABEL_9;}
if ( (*(_DWORD *)P & 0xFFFF0FFF) != dword_1C0027650 )// Is
Cloud Reparse Tag?
{
LODWORD(v9) = 0xC000CF0B;
.....
goto LABEL_54;
}
v32 = *((unsigned __int16 *)P + 2);
v9 = (unsigned int)HsmpRpValidateBuffer((__int64)P + 8, v32); [2*]
.....
Pool2 = ExAllocatePool2(0x100i64, 0x4000i64, 'pRsH'); // [3*]
v146 = (_DWORD *)Pool2;
v13 = (void *)Pool2;
if ( Pool2 )
{
v64 = v159_10;
v65 = Pool2 + 4;
if ( v8 && *((_WORD *)v8 + 7) > 0xAu )
v64 = *((_WORD *)v8 + 7);
v9 = Pool2 + 20;
v66 = (unsigned int *)(Pool2 + 12);
*(_OWORD *)v65 = 0i64;
*(_WORD *)(Pool2 + 16) = 0;
*(_WORD *)(Pool2 + 18) = v64;
*(_DWORD *)(Pool2 + 12) = 8 * v64 + 16;
*(_DWORD *)v65 = 'pReF';
memset((void *)(Pool2 + 20), 0, 8i64 * v64);
.....
if ( v8 )
{
v127 = 10;
if ( *((_WORD *)v8 + 7) > 0xAu ) // [4*]
{
if ( WPP_GLOBAL_Control !=
(PDEVICE_OBJECT)&WPP_GLOBAL_Control
&& (HIDWORD(WPP_GLOBAL_Control->Timer) & 1) != 0
&& BYTE1(WPP_GLOBAL_Control->Timer) >= 4u )
{
WPP_SF_qiq(WPP_GLOBAL_Control->AttachedDevice, v86,
v87, a2, *(_QWORD *)(v156 + 32), FileObject);
}
while ( v127 < *((_WORD *)v8 + 7) )
{
*(_QWORD *)(v65 + 8i64 * v127 + 16) = *(_QWORD
*)&v8[8 * v127 + 16];
memmove(
(void *)(v65 + *v66),
&v8[*(unsigned int *)&v8[8 * v127 + 20]],
*(unsigned __int16 *)&v8[8 * v127 + 18]); //
[5*]
*(_DWORD *)(v65 + 8i64 * v127 + 20) = *v66;
*v66 += *(unsigned __int16 *)(v65 + 8i64 * v127++ +
18);
}
}
}
.....
}
HsmpRpReadBuffer [1*] получает данные точки повторной обработки. Эти данные содержат значение размером WORD — *((_WORD *)v8 + 7), задающее количество структурированных элементов. Каждый элемент имеет поле Type, Size и Offset, указывающее на поля данных.
Тип элемента и его расположение строго предопределены. Но только для первых 10 элементов. Например, поле типа первого элемента должно быть равно 0x7.
Драйвер выполняет HsmpRpValidateBuffer [2*] для проверки полученных данных. Затем [3*] выделяется страничный пул фиксированного размера 0x4000 байт. И если данные точки повторной обработки содержат значение Count больше 10, то данные элементов после десятого копируются в этот пул фиксированного размера [4*] без каких-либо дополнительных проверок.
Проверка внутри HsmpRpValidateBuffer недостаточна, поскольку он проверяет только первые 10 записей.
__int64 __fastcall HsmpRpValidateBuffer(__int64 pBuf, unsigned int a2)
{
.....
v2 = a2 - 4;
pBuf2 = pBuf + 4;
LOBYTE(v5) = 0;
v6 = 0i64;
if ( a2 <= 4 )
v2 = 0;
v7 = 0;
v8 = *(_DWORD *)pBuf & 0xF;
if ( !v8 )
{
.....
return IsReparseBufferSupported;
}
if ( v8 > 1 )
{
....
}
v9 = 0;
v66 = 0;
if ( v2 < 0x18 )
goto ERROR_EXIT;
v9 = 1;
if ( *(_DWORD *)pBuf2 != 'pReF' )
goto ERROR_EXIT;
v9 = 2;
v10 = (unsigned int *)(pBuf + 0xC);
if ( (*(_BYTE *)(pBuf + 16) & 2) != 0 && *(_DWORD *)(pBuf +
8) != RtlComputeCrc32(0, (PUCHAR)(pBuf + 0xC), v2 - 8) )
goto ERROR_EXIT;
v11 = *v10;
v9 = 3;
if ( v2 < (unsigned int)v11 )
goto ERROR_EXIT;
v12 = *(unsigned __int16 *)(pBuf2 + 0xE);
v9 = 4;
if ( !(_WORD)v12 )
goto ERROR_EXIT;
v13 = 8 * v12 + 16;
v9 = 5;
if ( v13 >= v11 )
goto ERROR_EXIT;
v9 = 0x10000;
for ( i = 0; ; ++i )
{
v15 = *(unsigned __int16 *)(pBuf2 + 0xE);
if ( (unsigned int)v12 >= 0xA ) // [1*]
v15 = 10;
if ( i >= v15 )
break;
}
Как мы видим [1*], код проверяет только первые 10 элементов и игнорирует случай, когда записей больше.
=================
Размер уязвимого пула равен 0x4000. Этот размер кратен размеру страницы, поэтому будет использоваться сегментное выделение [3].
Для эксплуатации использовалась техника, описанная здесь [4]. Вызов NtAlpcCreateResourceReserve создаст множество дескрипторов, и перезапись одного из них указателем на сконструированный поддельный объект _KALPC_RESERVE даст нам возможность записи в произвольный адрес ядра.
Для подготовки памяти мы последовательно выделяем пулы размером 0x4000, используя каналы (pipes) [5]. Затем мы освобождаем каждый второй пул, освобождая место для уязвимого буфера.

Для чтения произвольного адреса ядра эксплойт использует каналы. Для этого мы перезаписываем указатель AttributeValue в структуре PipeAttribute.

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