Skip to content
KitploitKITPLOIT
OutilsBlog
Soumettre
OutilsBlog
Soumettre

Outils de Hacking, PenTest et Cybersécurité pour votre Arsenal de Sécurité !

Kitploit est un répertoire d'outils de hacking, de cybersécurité et de pentesting. Découvrez les dernières mises à jour des projets pour trouver des vulnérabilités, analyser des systèmes, automatiser les tests et renforcer votre sécurité.

··Flux·Contact·Confidentialité·© 2026 Kitploit

Répertoire d'outils

Catégories

Voir toutes les catégories
Loading categories
CVE-2024-43630-POC — POC de dépassement de tampon de pile NtCopyFileChunk | Kitploit
Outils/GitHubGitHub/quasarbinary/cve-2024-43630-poc
Criminalistique MémoireAnalyse des VulnérabilitésExploitationRétro-ingénierieExploitation de Binaires
GitHubquasarbinary/cve-2024-43630-poc

CVE-2024-43630-POC

POC de dépassement de tampon de pile NtCopyFileChunk

Voir le dépôt
113il y a 11 moisPas encore vérifié

Populaires

Voir tout →

Découvrez les outils les plus utilisés par notre communauté.

Explorer tous les outils

Parcourez notre collection d'outils

Voir tous les outils →
Partager

CVE-2024-43630-POC

Ce dépôt contient une POC qui déclenche une écriture OOB sur la pile lorsqu'elle est exécutée, provoquant un crash du système.
Cette vulnérabilité est un vestige extrêmement intéressant et rare qui démontre les complexités et les particularités de la programmation noyau, en particulier la gestion des objets.
De plus, la vulnérabilité affecte le noyau de Windows 11 24h2, Windows 10 22h2/21h2, mais pas Windows 11 22h2/23h2, ce qui ne fait qu'ajouter à l'intérêt.

Corrigé le 12 novembre 2024

Versions Windows affectées

  • Windows 11 Version 24H2
  • Windows 10 Version 22H2
  • Windows 10 Version 21H2
  • Windows Server 2025
  • Windows Server 2022, 23H2 Edition
  • Windows Server 2022
  • Testé sur : Windows 11 24h2 (x64) ntoskrnl.exe version 10.0.26100.1742

Aperçu de la vulnérabilité

Une vulnérabilité de débordement de tampon basé sur la pile (plus techniquement, une écriture OOB) existe dans la fonction d'appel système du noyau Windows NtCopyFileChunk.

NtCopyFileChunk permet d'effectuer deux opérations en un seul appel système : lire le fichier source et écrire dans le fichier de destination.

NT_COPYFILE_DATA_BUFFER est une structure qui contient tout le nécessaire pour la copie. Veuillez noter que cette structure a été obtenue par rétro-ingénierie et que son nom a été inventé. Soyez-en conscient.

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 fonction ressemble à ceci en pseudo-code :

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], nous pouvons voir StackEvent, qui est notre objet problématique. En [2], si le fichier de destination a été ouvert en mode synchrone, le noyau utilise un événement de pile pour attendre de manière synchrone l'opération d'écriture vers le fichier de destination. Pour ce faire, il utilise IopWaitForSynchronousIoEvent (non montré dans le pseudo-code) sur l'événement de pile plutôt que sur l'événement passé par l'utilisateur. D'abord, le noyau attend l'événement de pile, et seulement ensuite met à jour l'événement passé par l'utilisateur. De même, en [3], vous pouvez voir que UserEvent de WriteIrp pointe vers l'événement de pile. Cependant, que se passe-t-il si nous formons la requête correcte mais passons un événement d'entrée incorrect ? En [4], nous voyons comment il référence l'événement d'entrée, où nous pouvons passer un handle invalide (par exemple, la valeur 1). Et ensuite la mémoire est libérée en [5]. C'est là que se produit la chose la plus intéressante.

Analysons la fonction IopFreeCopyObjectsFromDataBuffer et voyons ce qui se passe lorsque l'IRP est libéré. Examinons de plus près 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);
  [...]
}

D'accord, le noyau exécute ObfDereferenceObject sur UserEvent. Mais qu'est-ce qui ne va pas ici ? Le fait est, comme je l'ai souligné tout ce temps, qu'il s'agit d'un KEVENT situé sur la pile. Il n'a pas d'OBJECT_HEADER et, par conséquent, pas de compteur de références, car sa durée de vie est limitée par la trame de pile. Mais la fonction ZwCreateEvent permettrait de créer un tel en-tête pour l'événement, car la mémoire est alors allouée dans le pool système, et elle doit être libérée lorsque le compteur tombe à zéro. Cependant, ce n'est pas le cas dans notre situation. OBJECT_HEADER est toujours situé avant chaque objet. C'est pourquoi une écriture OOB se produit, car ObfDereferenceObject se réfère à un décalage négatif de 0x30 et décrémente un compteur de références fictif. Cela permet de décrémenter arbitrairement quelque chose situé dans les variables locales de la trame de pile de NtCopyFileChunk. C'est complètement aléatoire quelles variables locales s'y trouveront. De plus, si le compteur tombe à zéro, le noyau essaiera de libérer la pile comme s'il s'agissait d'un pool système...

Pourquoi Windows 11 23h2/22h2 n'est-il pas vulnérable ?

C'est la question cruciale. Elle nous permettra de comprendre comment une vulnérabilité aussi évidente a pu apparaître dans ces circonstances.

Regardons le même morceau de code dans NtCopyFileChunk dans ntoskrnl sous Windows 11 23h2. Dans le pseudo-code susmentionné pour NtCopyFileChunk pour 24h2, l'événement est initialisé 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);

Comme nous pouvons le voir, dans Windows 11 23h2/22h2, au lieu d'un événement de pile, un événement alloué dans le pool via ZwCreateEvent et OBJECT_HEADER est utilisé. Par conséquent, il n'y a pas de vulnérabilité ici. ObfDereferenceObject est une opération autorisée sur un tel objet. Accessoirement, c'est exactement ainsi qu'ils ont corrigé cette vulnérabilité dans 24h2. La racine du problème résidait dans la désynchronisation du code entre différentes versions. Peut-être qu'à un certain stade, les ingénieurs ont décidé d'économiser de la mémoire et de définir un événement sur la pile (c'est une pratique courante). Mais ils ont oublié que la fonction de nettoyage IopFreeCopyObjectsFromDataBuffer effectue une décrémentation du compteur de références, car si cela ne se produisait pas, il y aurait une fuite dans 23h2. Il est surprenant que le code de NtCopyFileChunk ne soit pas commun à toutes les versions de Windows 11. Cela souligne la complexité du développement du noyau et montre à quel point il est facile de commettre des erreurs critiques.

Exploitation ?

Selon l'évaluation de Microsoft elle-même, l'exploitation de cette vulnérabilité est 'Plus probable'. Cette déclaration a initialement piqué mon intérêt. Découvrons donc la vérité.

Faisons attention à la trame de pile sur laquelle les accès OOB se produisent.

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

Et comment ObfDereferenceObject accède à notre trame de pile.

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

Comme vous pouvez le voir, il y a trop de façons de provoquer un BSOD ici. Nous avons aussi beaucoup de chance que DestOffset chevauche _OBJECT_HEADER.PointerCount, qui fait partie de l'appel NtCopyFileChunk (7ème paramètre). Ainsi, si nous passons un DestOffset positif supérieur à 2, nous décrémentons simplement DestOffset, et cela ne fera rien, nous quitterons calmement la fonction. Mais si nous passons DestOffset = 1, alors le noyau essaiera de libérer notre événement de pile comme un pool système.

Avant que le pool système ne soit libéré, un callback spécifique au type de l'objet spécifié dans _OBJECT_HEADER.TypeIndex est appelé, qui chevauche UserInputEventObject, que nous contrôlons également, ce qui signifie que nous pouvons tromper le système pour accepter n'importe quel type d'objet que nous voulons. Cela ouvre tout un éventail de possibilités. Notre objectif principal est de détourner RIP avant l'opération de libération du pool, par exemple via une confusion de type d'objet, pour écraser le callback quelque part.

Mais il y a une chose très désagréable qui annule nos plans. Comme vous pouvez le voir ci-dessus, il y a une vérification de la présence de _OBJECT_HEADER.HandleCount, et si elle est non nulle, le noyau appelle BSOD. Dans notre cas, cela chevauche HandleInformation. Et comme ce n'est pas un paramètre contrôlé par l'utilisateur, des recherches supplémentaires sont nécessaires pour le rendre égal à zéro.

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

Il s'avère que cette structure appartient à DestHandle. Cela implique que nous devons ouvrir un handle sur la destination avec GrantedAccess à zéro. Cela signifie également que HANDLE_TABLE_ENTRY.GrantedAccessBits de notre DestHandle doit être zéro. Cependant, comme nous devons également l'ouvrir en écriture (ObReferenceFileObjectForWrite), il ne peut en aucun cas être zéro.

Cela nous mène à une impasse.

Télécharger l’outil