
تصعيد الامتيازات عبر تلف Windows Kernel Pool (clfs.sys)
==============
تلف في تجمع نواة ويندوز (clfs.sys) يؤدي إلى تصعيد الامتيازات. (CVE-2023-36424)
يحتوي هذا المستودع على تحليل تقني واستغلال عملي.
المؤلف: Nassim Asrir (@p1k4l4) || https://www.linkedin.com/in/nassim-asrir-b73a57122/
================
يوجد فيض في تجمع (pool overflow) في برنامج تشغيل المرشح الصغير 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.
سنأخذ في الاعتبار إصدار clfs.sys 10.0.22621.2134 (ويندوز 11 22H2 22621.2215)
الدالة HsmFltProcessHSMControl مسؤولة عن معالجة FSCTLs الخاصة بمرشح السحابة. بالنسبة لعملية برمز 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 field) وحجم (Size) وإزاحة (Offset) لحقول البيانات.
أي نوع عنصر في أي مكان محدد بدقة مسبقًا. ولكن فقط للعناصر العشرة الأولى. على سبيل المثال، يجب أن يكون حقل النوع للعنصر الأول مساويًا لـ 0x7.
سينفذ برنامج التشغيل HsmpRpValidateBuffer [2*] للتحقق من البيانات المكتسبة. ثم سيتم [3*] تخصيص تجمع مقسم بصفحات (paged pool) بحجم ثابت 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. الحجم هو مضاعف للصفحة وبالتالي سيتم استخدام تخصيص المقطع (segment allocation) [3].
للاستغلال، تم استخدام التقنية الموصوفة هنا [4]. استدعاء NtAlpcCreateResourceReserve سينشئ العديد من المقابض (handles) وكتابة واحدة منها بمؤشر إلى كائن _KALPC_RESERVE مزيف سيعطينا القدرة على الكتابة إلى عنوان نواة عشوائي.
لتحضير الذاكرة، نقوم بتخصيص تجمعات متتالية بحجم 0x4000 باستخدام الأنابيب (pipes) [5]. ثم سنقوم بتحرير كل تجمع ثانٍ، لتوفير مكان للمخزن المؤقت القابل للاستغلال.

لقراءة عنوان نواة عشوائي، استغل التطبيق الأنابيب. لهذا الغرض سنقوم بكتابة مؤشر AttributeValue لبنية PipeAttribute.

وبعد ذلك، يمكننا سرقة رمز النظام (system token) لكتابة رمز (token) في العملية الهدف.
شكرًا على القراءة.