Skip to content
KitploitKITPLOIT
工具漏洞利用博客
Log in
提交
工具漏洞利用博客
提交

黑客、渗透测试和网络安全工具,武装您的安全武器库!

Kitploit 是一个黑客、网络安全和渗透测试工具的目录。发现最新的项目更新,查找漏洞、分析系统、自动化测试并加强你的安全。

··订阅源·联系·隐私·© 2026 Kitploit

工具目录

分类

查看所有分类
Loading categories
CVE-2024-43630-POC — NtCopyFileChunk 栈缓冲区溢出 POC | Kitploit
工具/GitHubGitHub/quasarbinary/cve-2024-43630-poc
内存取证漏洞分析漏洞利用逆向工程二进制利用
GitHubquasarbinary/cve-2024-43630-poc

CVE-2024-43630-POC

NtCopyFileChunk 栈缓冲区溢出 POC

查看仓库
1191年前尚未审核

最受欢迎

查看全部 →

发现我们社区最常用的工具。

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享

CVE-2024-43630-POC

此仓库包含一个POC,执行时会触发栈上越界写入,导致系统崩溃。该漏洞是一个非常有趣且罕见的遗物,展示了内核编程(尤其是对象管理)的复杂性和特殊性。此外,该漏洞影响Windows 11 24h2、Windows 10 22h2/21h2的内核,但不影响Windows 11 22h2/23h2,这更增添了其趣味性。

于2024年11月12日修补

受影响的Windows版本

  • Windows 11 版本 24H2
  • Windows 10 版本 22H2
  • Windows 10 版本 21H2
  • Windows Server 2025
  • Windows Server 2022, 23H2 版
  • Windows Server 2022
  • 测试环境: Windows 11 24h2 (x64) ntoskrnl.exe 版本 10.0.26100.1742

漏洞概述

Windows内核系统调用函数NtCopyFileChunk中存在一个基于栈的缓冲区溢出漏洞(更技术地说,是越界写入)。

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 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]中,可以看到WriteIrp的UserEvent指向栈事件。 然而,如果我们形成一个正确的请求但传递了一个错误的输入事件呢?在[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);
  [...]
}

好的,内核在UserEvent上执行ObfDereferenceObject。但这里有什么问题呢?问题在于,我一直强调的,这是一个位于栈上的KEVENT。它没有OBJECT_HEADER,因此也没有引用计数器,因为它的生命周期受栈帧限制。但ZwCreateEvent函数会允许为事件创建这样的头部,因为那样内存是在系统池中分配的,并且当计数器降至零时需要释放。然而,在我们的案例中并未使用这种方法。OBJECT_HEADER始终位于每个对象之前。这就是为什么会发生越界写入,因为ObfDereferenceObject引用了一个负偏移量0x30,并递减了一个虚构的引用计数。这允许你任意递减NtCopyFileChunk栈帧局部变量中的某个值。那里有什么局部变量完全是随机的。此外,如果计数器降至零,内核会试图释放栈,就好像它是系统池一样...

为什么Windows 11 23h2/22h2不受影响?

这是最关键的问题。它将使我们理解,如此明显的漏洞是如何在这些情况下出现的。

让我们看看Windows 11 23h2中ntoskrnl内部NtCopyFileChunk完全相同的代码片段。在上述24h2的NtCopyFileChunk伪代码中,事件在[2]处初始化。

*(_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所有版本通用。这凸显了内核开发的复杂性,并展示了犯下关键错误是何等容易。

利用?

根据微软自身的评估,利用此漏洞的可能性为“较可能”。这一描述最初激起了我的兴趣。那么,让我们找出真相。

让我们注意发生越界访问的栈帧。

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如何访问我们的栈帧。

;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...
下载工具