Skip to content
KitploitKITPLOIT
أدواتالمدونة
إرسال
أدواتالمدونة
إرسال

أدوات الاختراق واختبار الاختراق والأمن السيبراني لترسانتك الأمنية!

Kitploit هو دليل لأدوات الاختراق والأمن السيبراني واختبار الاختراق. اكتشف آخر تحديثات المشاريع للعثور على الثغرات وتحليل الأنظمة وأتمتة الاختبارات وتعزيز أمنك.

··الخلاصات·اتصال·الخصوصية·© 2026 Kitploit

دليل الأدوات

الفئات

عرض جميع الفئات
Loading categories
CVE-2024-43630-POC — NtCopyFileChunk تجاوز سعة المخزن المؤقت للمكدس إثبات المفهوم | Kitploit
أدوات/GitHubGitHub/quasarbinary/cve-2024-43630-poc
تحليل الذاكرة الجنائيتحليل الثغرات الأمنيةالاستغلالالهندسة العكسيةاستغلال الملفات الثنائية
GitHubquasarbinary/cve-2024-43630-poc

CVE-2024-43630-POC

NtCopyFileChunk تجاوز سعة المخزن المؤقت للمكدس إثبات المفهوم

عرض المستودع
11منذ 11 أشهرلم تتم المراجعة بعد

الأكثر شعبية

عرض الكل →

اكتشف الأدوات الأكثر استخدامًا من قبل مجتمعنا.

استكشف جميع الأدوات

تصفح مجموعتنا من الأدوات

عرض جميع الأدوات →
مشاركة

CVE-2024-43630-POC

يحتوي هذا المستودع على POC يؤدي إلى كتابة خارج الحدود (OOB) في مكدس (stack) عند تنفيذه، مما يتسبب في تعطل النظام. هذه الثغرة الأمنية هي بقايا نادرة ومثيرة للاهتمام للغاية توضح تعقيدات وخصوصيات برمجة النواة، خاصة إدارة الكائنات. علاوة على ذلك، تؤثر الثغرة على نواة Windows 11 24h2 و Windows 10 22h2/21h2، ولكن ليس Windows 11 22h2/23h2، مما يزيد من الاهتمام بها.

تم التصحيح في 12 نوفمبر 2024

إصدارات Windows المتأثرة

  • Windows 11 Version 24H2
  • Windows 10 Version 22H2
  • Windows 10 Version 21H2
  • Windows Server 2025
  • Windows Server 2022, 23H2 Edition
  • Windows Server 2022
  • تم الاختبار على: Windows 11 24h2 (x64) ntoskrnl.exe الإصدار 10.0.26100.1742

نظرة عامة على الثغرة

توجد ثغرة تجاوز سعة المخزن المؤقت في المكدس (بشكل أكثر دقة، كتابة خارج الحدود) في وظيفة استدعاء النظام NtCopyFileChunk في نواة Windows.

تتيح NtCopyFileChunk إجراء عمليتين في استدعاء نظام واحد: قراءة الملف المصدر والكتابة إلى الملف الوجهة.

NT_COPYFILE_DATA_BUFFER هي بنية تحتوي على كل ما هو ضروري للنسخ. يرجى ملاحظة أن هذه البنية تم الحصول عليها من خلال الهندسة العكسية، وتم اختراع اسمها. لذا، كن على علم بذلك.

root@kitploit:~
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):

root@kitploit:~
// 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.

root@kitploit:~
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);
}
root@kitploit:~
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 بشكل تعسفي. من العشوائي تمامًا ما هي المتغيرات المحلية الموجودة هناك. علاوة على ذلك، إذا انخفض العداد إلى الصفر، ستحاول النواة تحرير المكدس كما لو كان تجمع نظام...

لماذا Windows 11 23h2/22h2 غير معرض للخطر؟

هذا هو السؤال الأكثر أهمية. سيمكننا من فهم كيف يمكن أن تنشأ مثل هذه الثغرة الواضحة في هذه الظروف.

دعنا ننظر إلى نفس الجزء تمامًا من الكود داخل NtCopyFileChunk داخل ntoskrnl في Windows 11 23h2. في الكود الزائف المذكور أعلاه لـ NtCopyFileChunk لـ 24h2، يتم تهيئة الحدث في [2].

root@kitploit:~
*(_QWORD *)&ObjectAttributes.Length = 48;
memset(&ObjectAttributes.Attributes + 1, 0, 20);
ObjectAttributes.RootDirectory = 0;
ObjectAttributes.Attributes = PreviousMode == 0 ? 0x200 : 0;
ObjectAttributes.ObjectName = 0;
//Not even a stack event! It has an OBJECT_HEADER and stores a reference counter.
status = ZwCreateEvent(&EventHandle, 0x1F0003u, &ObjectAttributes, SynchronizationEvent, 0);

كما نرى، في Windows 11 23h2/22h2، بدلاً من حدث المكدس، يتم استخدام حدث مخصص في التجمع عبر ZwCreateEvent و OBJECT_HEADER. لذلك، لا توجد ثغرة هنا. ObfDereferenceObject هي عملية مسموح بها على مثل هذا الكائن. بالمناسبة، هذه هي بالضبط الطريقة التي قاموا بها بتصحيح هذه الثغرة في 24h2. كان جذر المشكلة في عدم التزامن في الكود بين الإصدارات المختلفة. ربما في مرحلة ما، قرر المهندسون توفير الذاكرة وتعريف حدث على المكدس (هذه ممارسة قياسية). لكنهم نسوا أن وظيفة التنظيف IopFreeCopyObjectsFromDataBuffer تقوم بتقليل عدد المراجع، لأنه إذا لم يحدث هذا، سيكون هناك تسرب في 23h2. من المدهش أن كود NtCopyFileChunk ليس مشتركًا بين جميع إصدارات Windows 11. هذا يسلط الضوء على تعقيد تطوير النواة ويظهر مدى سهولة ارتكاب أخطاء جسيمة.

الاستغلال؟

وفقًا لتقييم Microsoft نفسه، فإن استغلال هذه الثغرة 'أكثر احتمالاً'. هذا البيان أثار اهتمامي في البداية. لذا دعنا نكتشف الحقيقة.

دعنا ننتبه إلى إطار المكدس الذي تحدث فيه الوصولات خارج الحدود (OOB).

root@kitploit:~
struct _KTHREAD* DestOffset; //-0x30
OBJECT_HANDLE_INFORMATION HandleInformation; // -0x28
NT_COPYFILE_DATA_BUFFER* DataBuffer; // -0x20
PVOID UserInputEventObject; // -0x18 
_FILE_OBJECT* pSourceFileObject; //-0x10
PIRP WriteIrp; //-0x8
struct _KEVENT StackEvent; // our vuln event

وكيف يصل ObfDereferenceObject إلى إطار المكدس الخاص بنا.

root@kitploit:~
;rcx = ptr to StackEvent
ObfDereferenceObject proc near
...
;rdi = beginning of OBJECT_HEADER,
;but in our case it is ptr to DestOffset(user controlled)
lea     rdi, [rcx-30h]
...
mov     rbx, -1
lock xadd [rdi], rbx ; decrement DestOffset
sub     rbx, 1
jg      short RET ;if refcnt was >= 2, dont delete object, just exit

;The inevitable path to freeing the object, it will crash the system...

mov     rcx, [rdi+8] ;rcx = HandleInformation(_OBJECT_HANDLE_INFORMATION struct)
test    rcx, rcx 
jnz     short BSOD ; HandleInformation must be zero, or we will BSOD

test    rbx, rbx ;check for negative reference counter
js     short BSOD

...more code...

كما ترى، هناك طرق كثيرة جدًا للتسبب في تعطل النظام (BSOD). نحن أيضًا محظوظون جدًا لأن DestOffset يتداخل مع _OBJECT_HEADER.PointerCount، وهو جزء من استدعاء NtCopyFileChunk (المعامل السابع). وبالتالي، إذا مررنا أي DestOffset موجب أكبر من 2، فإننا ببساطة نقلل DestOffset، ولن يفعل ذلك شيئًا هناك، وسنخرج من الوظيفة بهدوء. ولكن إذا مررنا DestOffset = 1، فستحاول النواة تحرير حدث المكدس الخاص بنا كتجمع نظام.

قبل تحرير تجمع النظام، يتم استدعاء رد اتصال (callback) خاص بنوع الكائن المحدد في _OBJECT_HEADER.TypeIndex، والذي يتداخل مع UserInputEventObject، الذي نتحكم فيه أيضًا، مما يعني أنه يمكننا خداع النظام لقبول أي نوع كائن نريده. هذا يفتح مجموعة كاملة من الاحتمالات. هدفنا الرئيسي هو اختطاف RIP قبل عملية التحرير من التجمع، على سبيل المثال، من خلال ارتباك نوع الكائن (object type confusion)، لاستبدال رد الاتصال في مكان ما.

لكن هناك شيء واحد مزعج للغاية يلغي خططنا. كما ترى أعلاه، هناك فحص لوجود _OBJECT_HEADER.HandleCount، وإذا كان غير صفري، تستدعي النواة تعطل النظام (BSOD). في حالتنا، هذا يتداخل مع HandleInformation. وبما أن هذه ليست معلمة يتحكم فيها المستخدم، فهناك حاجة إلى مزيد من البحث حول كيفية جعلها مساوية للصفر.

root@kitploit:~
//0x8 bytes (sizeof)
struct _OBJECT_HANDLE_INFORMATION
{
    ULONG HandleAttributes;                                                 //0x0
    ULONG GrantedAccess;                                                    //0x4
}; 

كما اتضح، هذه البنية تنتمي إلى DestHandle. هذا يعني أنه يجب علينا فتح مقبض على الوجهة مع GrantedAccess يساوي صفر. هذا يعني أيضًا أن HANDLE_TABLE_ENTRY.GrantedAccessBits الخاص بـ DestHandle الخاص بنا يجب أن يكون صفرًا. ومع ذلك، نظرًا لأنه يجب علينا أيضًا فتحه للكتابة (ObReferenceFileObjectForWrite)، فلا يمكن أن يكون صفرًا بأي حال من الأحوال.

هذا يقودنا إلى طريق مسدود.

تنزيل الأداة