
NtCopyFileChunk تجاوز سعة المخزن المؤقت للمكدس إثبات المفهوم
يحتوي هذا المستودع على POC يؤدي إلى كتابة خارج الحدود (OOB) في مكدس (stack) عند تنفيذه، مما يتسبب في تعطل النظام. هذه الثغرة الأمنية هي بقايا نادرة ومثيرة للاهتمام للغاية توضح تعقيدات وخصوصيات برمجة النواة، خاصة إدارة الكائنات. علاوة على ذلك، تؤثر الثغرة على نواة Windows 11 24h2 و Windows 10 22h2/21h2، ولكن ليس Windows 11 22h2/23h2، مما يزيد من الاهتمام بها.
تم التصحيح في 12 نوفمبر 2024
توجد ثغرة تجاوز سعة المخزن المؤقت في المكدس (بشكل أكثر دقة، كتابة خارج الحدود) في وظيفة استدعاء النظام NtCopyFileChunk في نواة Windows.
تتيح NtCopyFileChunk إجراء عمليتين في استدعاء نظام واحد: قراءة الملف المصدر والكتابة إلى الملف الوجهة.
NT_COPYFILE_DATA_BUFFER هي بنية تحتوي على كل ما هو ضروري للنسخ. يرجى ملاحظة أن هذه البنية تم الحصول عليها من خلال الهندسة العكسية، وتم اختراع اسمها. لذا، كن على علم بذلك.
struct NT_COPYFILE_DATA_BUFFER // sizeof=0x48
{
DWORD64 UnknownQword1;
DWORD64 UnknownQword2;
DWORD64 UnknownQword3;
DWORD64 UnknownQword4;
PIRP WriteIrp;
PDEVICE_OBJECT HighestDeviceObject;
PFILE_OBJECT DestFileObject;
PFILE_OBJECT SourceFileObject;
DWORD64 SourceOffsetQuadPart;
};
تبدو الوظيفة شيئًا كهذا في الكود الزائف (pseudocode):
// Pseudocode for the function nt!NtCopyFileChunk in win11 24h2
__int64 __fastcall NtCopyFileChunk(
void* SourceHandle,
void* DestHandle,
void* UserInputHandleEvent,
struct _IO_STATUS_BLOCK* IoStatusBlock,
ULONG Length,
__int64 SourceOffset,
struct _KTHREAD** DestOffset,
ULONG* SourceKey,
_DWORD* DestKey,
int Flags)
{
[...]
NTSTATUS Status;
char is_alertable_io;
DWORD64 SourceOffsetStack;
struct _KTHREAD* DestOffsetValue;
_OBJECT_HANDLE_INFORMATION* HandleInformation;
NT_COPYFILE_DATA_BUFFER* DataBuffer_3;
PVOID UserInputEventObject;
_FILE_OBJECT* pSourceFileObject;
PIRP WriteIrp;
struct _KEVENT StackEvent; // [1]
[...]
memset(&StackEvent, 0, sizeof(StackEvent));
DataBuffer = (NT_COPYFILE_DATA_BUFFER*)ExAllocatePool2(0x43u, Length + sizeof(NT_COPYFILE_DATA_BUFFER), 'pCoI');
ArbDataBuffer = DataBuffer + sizeof(NT_COPYFILE_DATA_BUFFER); //point after NT_COPYFILE_DATA_BUFFER
// Reference source file by handle
ret = IopReferenceFileObject(SourceHandle, 1u, PreviousMode, (PVOID*)&DataBuffer_2->SourceFileObject, 0);
if (ret < 0)
goto RET;
// Reference destination file by handle
ret = ObReferenceFileObjectForWrite(
(ULONG_PTR)DestHandle,
PreviousMode,
(_FILE_OBJECT*)&DataBuffer->DestFileObject,
(_OBJECT_HANDLE_INFORMATION*)&HandleInformation);
[...]
//Fill ArbDataBuffer with data that we will write to the dest file
ret = IopPopulateCopyWriteWorkerData(
(__int64)DestFileObj,
(__int64)IoStatusBlock,
(__int64)ArbDataBuffer,
Length,
v28,
(__int64)pSourceFileObject,
UserInputHandleEvent_1,
DestOffset,
DestKey,
SHIDWORD(HandleInformation),
(__int64)&DataBuffer_2->WriteIrp);
[...]
if (DestFileObj->Flags & FO_SYNCHRONOUS_IO)
{
// [2]
KeInitializeEvent(&StackEvent, SynchronizationEvent, 0);
// [3]
DataBuffer_3->WriteIrp->UserEvent = &StackEvent; //WriteIrp contains pointer to stack event!
DataBuffer->WriteIrp->Flags |= IRP_MJ_WRITE;
}
else
{
//for asynchronous mode, we don't need that
[...]
}
UserInputEventObject = 0;
// [4]
ret = ObReferenceObjectByHandle(
UserInputHandleEvent,
2u,
(POBJECT_TYPE)ExEventObjectType,
PreviousMode,
&UserInputEventObject,
0);
if (ret >= 0)
{
//If the user has submitted the correct event, we proceed to the main logic for copying one file to another.
//I have omitted that section of code for simplicity.
KeResetEvent((PRKEVENT)UserInputEventObject);
goto NEXT_PATH_TO_READ_FILE_QUERY;
}
RET:
//Here it is! Free the DataBuffer structure (remember WriteIrp, which contains a pointer to the stack event).
// [5]
if (ArbDataBuffer)
IopFreeCopyObjectsFromDataBuffer((__int64)ArbDataBuffer, 1);
if (UserInputEventObject_1)
ObfDereferenceObject(UserInputEventObject_1);
return (unsigned int)ret;
}
في [1]، يمكننا رؤية StackEvent، وهو كائن المشكلة لدينا. في [2]، إذا تم فتح الملف الوجهة في الوضع المتزامن، تستخدم النواة حدثًا في المكدس للانتظار بشكل متزامن لعملية الكتابة إلى الملف الوجهة. للقيام بذلك، تستخدم IopWaitForSynchronousIoEvent (غير موضحة في الكود الزائف) على حدث المكدس بدلاً من الحدث الذي مرره المستخدم. أولاً، تنتظر النواة حدث المكدس، ثم تقوم فقط بتحديث الحدث الذي مرره المستخدم. وبالمثل، في [3]، يمكنك رؤية أن UserEvent الخاص بـ WriteIrp يشير إلى حدث المكدس. ومع ذلك، ماذا لو قمنا بتكوين الطلب الصحيح ولكننا مررنا حدث إدخال غير صحيح؟ في [4]، يمكننا رؤية كيفية الإشارة إلى حدث الإدخال، حيث يمكننا تمرير مقبض غير صالح (على سبيل المثال، القيمة 1). ثم يتم مسح الذاكرة في [5]. هذا هو المكان الذي يحدث فيه الشيء الأكثر إثارة للاهتمام.
دعنا نحلل وظيفة IopFreeCopyObjectsFromDataBuffer ونرى ما يحدث عند مسح irp. دعنا نلقي نظرة فاحصة على WriteIrp->UserEvent.
void __fastcall IopFreeCopyObjectsFromDataBuffer(__int64 ArbDataBuffer, char to_clear_irp)
{
NT_COPYFILE_DATA_BUFFER *DataBuffer;
PFILE_OBJECT SourceFileObject;
PIRP WriteIrp;
PFILE_OBJECT DestFileObject;
DataBuffer = (NT_COPYFILE_DATA_BUFFER *)(ArbDataBuffer - 0x48);
if ( to_clear_irp )
{
WriteIrp = DataBuffer->WriteIrp;
DestFileObject = DataBuffer->DestFileObject;
if ( WriteIrp )
{
IopFreeIrpExtension((__int64)DataBuffer->WriteIrp, 9, 1);
//We are moving deeper, closely monitoring UserEvent
IopExceptionCleanupEx((ULONG_PTR)DestFileObject, WriteIrp, WriteIrp->UserEvent, 0, 0);
return;
}
if ( DestFileObject )
ObfDereferenceObjectWithTag(DataBuffer->DestFileObject, 0x746C6644u);
}
SourceFileObject = DataBuffer->SourceFileObject;
if ( SourceFileObject )
ObfDereferenceObjectWithTag(SourceFileObject, 0x746C6644u);
ExFreePoolWithTag(DataBuffer, 0);
}
LONG_PTR __fastcall IopExceptionCleanupEx(ULONG_PTR DestFileObject, PIRP Irp, PVOID UserEvent, PVOID P, char a5)
{
[...]
if ( Irp )
{
MasterIrp = Irp->AssociatedIrp.MasterIrp;
if ( MasterIrp )
ExFreePoolWithTag(MasterIrp, 0);
[...]
IoFreeIrp(Irp);
}
//Oh, that's it! But how can you decrement the reference counter for a stack object that doesn't have an OBJECT_HEADER?
//Vuln!
if ( UserEvent )
ObfDereferenceObject(UserEvent);
if ( P )
ExFreePoolWithTag(P, 0);
[...]
}
حسنًا، تقوم النواة بتنفيذ ObfDereferenceObject على UserEvent. ولكن ما الخطأ هنا؟ المشكلة هي، كما كنت أؤكد طوال هذا الوقت، أن هذا هو KEVENT موجود على المكدس. ليس له OBJECT_HEADER وبالتالي ليس له عداد مراجع، لأن عمره محدود بإطار المكدس. بينما كانت وظيفة ZwCreateEvent ستسمح لك بإنشاء مثل هذا الرأس للحدث، لأنه عندها يتم تخصيص الذاكرة في تجمع النظام (system pool)، ويجب تحريرها عندما ينخفض العداد إلى الصفر. لكن هذا لا يستخدم في حالتنا. OBJECT_HEADER يقع دائمًا قبل كل كائن. هذا هو سبب حدوث الكتابة خارج الحدود (OOB)، لأن ObfDereferenceObject يشير إلى إزاحة سالبة 0x30 ويقلل عدد مراجع وهمي. هذا يسمح لك بتقليل شيء موجود في المتغيرات المحلية لإطار مكدس NtCopyFileChunk بشكل تعسفي. من العشوائي تمامًا ما هي المتغيرات المحلية الموجودة هناك. علاوة على ذلك، إذا انخفض العداد إلى الصفر، ستحاول النواة تحرير المكدس كما لو كان تجمع نظام...
هذا هو السؤال الأكثر أهمية. سيمكننا من فهم كيف يمكن أن تنشأ مثل هذه الثغرة الواضحة في هذه الظروف.
دعنا ننظر إلى نفس الجزء تمامًا من الكود داخل NtCopyFileChunk داخل ntoskrnl في Windows 11 23h2. في الكود الزائف المذكور أعلاه لـ NtCopyFileChunk لـ 24h2، يتم تهيئة الحدث في [2].