Skip to content
KitploitKITPLOIT
HerramientasBlog
Enviar
HerramientasBlog
Enviar

¡Herramientas de Hacking, PenTest y Ciberseguridad para tu Arsenal de Seguridad!

Kitploit es un directorio de herramientas de hacking, ciberseguridad y pentesting. Descubre las últimas actualizaciones de proyectos para encontrar vulnerabilidades, analizar sistemas, automatizar pruebas y fortalecer tu seguridad.

··Feeds·Contacto·Privacidad·© 2026 Kitploit

Directorio de Herramientas

Categorías

Ver todas las categorías
Loading categories
CVE-2024-43630-POC — POC de desbordamiento de búfer en la pila de NtCopyFileChunk | Kitploit
Herramientas/GitHubGitHub/quasarbinary/cve-2024-43630-poc
Forensia de MemoriaAnálisis de VulnerabilidadesExplotaciónIngeniería InversaExplotación de Binarios
GitHubquasarbinary/cve-2024-43630-poc

CVE-2024-43630-POC

POC de desbordamiento de búfer en la pila de NtCopyFileChunk

Ver Repositorio
11hace 11 mesesAún no revisado

Más Populares

Ver todos →

Descubre las herramientas más usadas por nuestra comunidad.

Explora todas las herramientas

Explora nuestra colección de herramientas

Ver todas las herramientas →
Compartir

CVE-2024-43630-POC

Este repositorio contiene un POC que desencadena una escritura fuera de límites (OOB) en la pila cuando se ejecuta, provocando el bloqueo del sistema. Esta vulnerabilidad es una reliquia extremadamente interesante y poco común que demuestra las complejidades y peculiaridades de la programación de kernels, especialmente en la gestión de objetos. Además, la vulnerabilidad afecta al kernel de Windows 11 24h2, Windows 10 22h2/21h2, pero no a Windows 11 22h2/23h2, lo que solo aumenta el interés.

Parcheado el 12 de noviembre de 2024

Versiones de Windows afectadas

  • Windows 11 Versión 24H2
  • Windows 10 Versión 22H2
  • Windows 10 Versión 21H2
  • Windows Server 2025
  • Windows Server 2022, Edición 23H2
  • Windows Server 2022
  • Probado en: Windows 11 24h2 (x64) ntoskrnl.exe versión 10.0.26100.1742

Resumen de la vulnerabilidad

Existe una vulnerabilidad de desbordamiento de búfer basada en pila (más técnicamente, una escritura fuera de límites u OOB) en la función de syscall del kernel de Windows NtCopyFileChunk.

NtCopyFileChunk permite realizar dos operaciones en una única llamada al sistema: leer el archivo de origen y escribir en el archivo de destino.

NT_COPYFILE_DATA_BUFFER es una estructura que contiene todo lo necesario para la copia. Tenga en cuenta que esta estructura se obtuvo mediante ingeniería inversa y que su nombre fue inventado. Por lo tanto, tenga esto en cuenta.

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;
};

La función se ve más o menos así en pseudocódigo:

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;

}

En [1], podemos ver StackEvent, que es nuestro objeto problemático. En [2], si el archivo de destino se abrió en modo síncrono, el kernel utiliza un evento de pila para esperar de forma síncrona a que se complete la operación de escritura en el archivo de destino. Para ello, usa IopWaitForSynchronousIoEvent (no mostrado en el pseudocódigo) sobre el evento de pila en lugar del evento proporcionado por el usuario. Primero, el kernel espera el evento de pila y solo después actualiza el evento proporcionado por el usuario. De manera similar, en [3], se puede ver que UserEvent de WriteIrp apunta al evento de pila. Sin embargo, ¿qué sucede si formamos la solicitud correcta pero pasamos un evento de entrada incorrecto? En [4], podemos ver cómo hace referencia al evento de entrada, donde podemos pasar un identificador (handle) no válido (por ejemplo, el valor 1). Y luego la memoria se libera en [5]. Aquí es donde ocurre lo más interesante.

Analicemos la función IopFreeCopyObjectsFromDataBuffer y veamos qué sucede cuando se libera el IRP. Echemos un vistazo más de cerca a 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);
  [...]
}

De acuerdo, el kernel realiza ObfDereferenceObject sobre UserEvent. Pero, ¿qué es lo que falla aquí? La cuestión es que, como he estado enfatizando todo este tiempo, se trata de un KEVENT ubicado en la pila. No tiene OBJECT_HEADER y, por lo tanto, no tiene contador de referencias, porque su vida útil está limitada por el marco de pila. Pero la función ZwCreateEvent permitiría crear ese encabezado para el evento, porque entonces la memoria se asigna en el grupo del sistema (system pool) y debe liberarse cuando el contador llegue a cero. Sin embargo, esto no se utiliza en nuestro caso. OBJECT_HEADER siempre se encuentra antes de cada objeto. Esta es la razón por la que se produce la escritura fuera de límites: ObfDereferenceObject accede a un desplazamiento negativo 0x30 y decrementa un contador de referencias ficticio. Esto permite decrementar arbitrariamente algo situado en las variables locales del marco de pila de NtCopyFileChunk. Es completamente aleatorio qué variables locales habrá allí. Además, si el contador llega a cero, el kernel intentará liberar la pila como si fuera un grupo del sistema...

¿Por qué Windows 11 23h2/22h2 no es vulnerable?

Esta es la pregunta más crucial. Nos permitirá entender cómo pudo surgir una vulnerabilidad tan evidente en estas circunstancias.

Veamos exactamente el mismo fragmento de código dentro de NtCopyFileChunk en ntoskrnl en Windows 11 23h2. En el pseudocódigo mencionado anteriormente para NtCopyFileChunk en 24h2, el evento se inicializa en [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);

Como podemos ver, en Windows 11 23h2/22h2, en lugar de un evento de pila, se utiliza un evento asignado en el grupo (pool) mediante ZwCreateEvent y OBJECT_HEADER. Por lo tanto, aquí no hay vulnerabilidad. ObfDereferenceObject es una operación permitida sobre dicho objeto. Por cierto, así es exactamente como se parcheó esta vulnerabilidad en 24h2. La raíz del problema radicaba en la desincronización del código en diferentes versiones. Quizás en algún momento los ingenieros decidieron ahorrar memoria y definir un evento en la pila (esto es una práctica estándar). Pero olvidaron que la función de limpieza IopFreeCopyObjectsFromDataBuffer realiza un decremento del contador de referencias, porque si esto no sucediera, habría una fuga en 23h2. Es sorprendente que el código de NtCopyFileChunk no sea común a todas las versiones de Windows 11. Esto pone de manifiesto la complejidad del desarrollo de kernels y muestra lo fácil que es cometer errores críticos.

¿Explotación?

Según la propia evaluación de Microsoft, la explotación de esta vulnerabilidad es 'Más probable'. Esta afirmación despertó inicialmente mi interés. Así que averigüemos la verdad.

Prestemos atención al marco de pila sobre el que se producen los accesos fuera de límites.

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

Y cómo ObfDereferenceObject accede a nuestro marco de pila.

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...

Como se puede ver, aquí hay demasiadas formas de provocar un BSOD. También tenemos mucha suerte de que DestOffset se superponga con _OBJECT_HEADER.PointerCount, que forma parte de la llamada a NtCopyFileChunk (séptimo parámetro). Por lo tanto, si pasamos cualquier DestOffset positivo mayor que 2, simplemente decrementamos DestOffset y esto no hará nada allí; saldremos de la función con normalidad. Pero si pasamos DestOffset = 1, entonces el kernel intentará liberar nuestro evento de pila como si fuera un grupo del sistema.

Antes de que se libere el grupo del sistema, se invoca una devolución de llamada (callback) específica del tipo de objeto indicado en _OBJECT_HEADER.TypeIndex, que se superpone con UserInputEventObject, el cual también controlamos. Esto significa que podemos engañar al sistema para que acepte cualquier tipo de objeto que queramos. Esto abre un amplio abanico de posibilidades. Nuestro objetivo principal es secuestrar RIP antes de la operación de liberación del grupo, por ejemplo, mediante confusión de tipos de objeto, para sobrescribir la devolución de llamada en algún lugar.

Pero hay una cosa muy desagradable que cancela nuestros planes. Como se puede ver arriba, hay una comprobación de la presencia de _OBJECT_HEADER.HandleCount y, si no es cero, el kernel llama a BSOD. En nuestro caso, esto se superpone con HandleInformation. Y dado que este no es un parámetro controlado por el usuario, se necesita más investigación sobre cómo lograr que sea igual a cero.

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

Resulta que esta estructura pertenece a DestHandle. Esto implica que debemos abrir un identificador (handle) sobre el destino con GrantedAccess igual a cero. Esto también significa que HANDLE_TABLE_ENTRY.GrantedAccessBits de nuestro DestHandle debe ser cero. Sin embargo, como también debemos abrirlo para escritura (ObReferenceFileObjectForWrite), no puede ser cero en ningún caso.

Esto nos lleva a un callejón sin salida.

Descargar herramienta