
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
이유는 드라이버가 NTFS reparse point에서 비롯된 데이터를 충분히 검사하지 않기 때문입니다.
여기서는 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*]는 reparse point 데이터를 검색합니다. 이 데이터에는 구조화된 항목의 개수를 지정하는 WORD 크기 값 *((_WORD *)v8 + 7)이 포함되어 있습니다. 각 항목에는 Type 필드, Size 및 데이터 필드로의 Offset이 있습니다.
어느 위치에 어떤 항목 유형이 와야 하는지는 엄격하게 미리 결정되어 있습니다. 그러나 처음 10개 항목에 대해서만 그렇습니다. 예를 들어, 첫 번째 항목의 유형 필드는 0x7과 같은 값을 가져야 합니다.
드라이버는 획득한 데이터를 검증하기 위해 HsmpRpValidateBuffer [2*]를 실행합니다. 그런 다음 [3*]에서 고정된 0x4000바이트 크기의 페이징 풀이 할당됩니다. 그리고 reparse point 데이터의 Count 값이 10보다 크면, 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 객체에 대한 포인터로 덮어쓰면 임의의 커널 주소에 쓸 수 있게 됩니다.
메모리를 준비하기 위해 파이프[5]를 사용하여 0x4000 크기의 풀을 순차적으로 할당합니다. 그런 다음 두 번째 풀마다 해제하여 취약한 버퍼를 위한 공간을 마련합니다.

임의의 커널 주소를 읽기 위해 익스플로잇은 파이프를 활용했습니다. 이를 위해 PipeAttribute 구조체의 AttributeValue 포인터를 덮어씁니다.

그리고 이후에는 시스템 토큰을 탈취하여 대상 프로세스의 토큰을 덮어쓸 수 있습니다.
읽어 주셔서 감사합니다.