
NtCopyFileChunk overflow del buffer dello stack POC
Questa repository contiene un POC che, quando eseguito, innesca una scrittura OOB nello stack causando il crash del sistema. Questa vulnerabilità è un reperto estremamente interessante e raro che dimostra la complessità e le peculiarità della programmazione del kernel, in particolare della gestione degli oggetti. Inoltre, la vulnerabilità interessa il kernel di Windows 11 24h2, Windows 10 22h2/21h2, ma non Windows 11 22h2/23h2, il che la rende ancora più interessante.
Corretto il 12 novembre 2024
Una vulnerabilità di buffer overflow basato sullo stack (più tecnicamente, scrittura OOB) esiste nella funzione syscall del kernel Windows NtCopyFileChunk.
NtCopyFileChunk consente di eseguire due operazioni in un'unica syscall: la lettura del file sorgente e la scrittura sul file di destinazione.
NT_COPYFILE_DATA_BUFFER è una struttura che contiene tutto il necessario per la copia. Si noti che questa struttura è stata ottenuta tramite reverse engineering e il suo nome è stato inventato. Pertanto, tenetelo presente.
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 funzione appare più o meno così in pseudocodice:
// Pseudocodice per la funzione 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 dopo NT_COPYFILE_DATA_BUFFER
// Riferimento al file sorgente tramite handle
ret = IopReferenceFileObject(SourceHandle, 1u, PreviousMode, (PVOID*)&DataBuffer_2->SourceFileObject, 0);
if (ret < 0)
goto RET;
// Riferimento al file di destinazione tramite handle
ret = ObReferenceFileObjectForWrite(
(ULONG_PTR)DestHandle,
PreviousMode,
(_FILE_OBJECT*)&DataBuffer->DestFileObject,
(_OBJECT_HANDLE_INFORMATION*)&HandleInformation);
[...]
//Riempie ArbDataBuffer con i dati che scriveremo sul file di destinazione
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 contiene un puntatore all'evento di stack!
DataBuffer->WriteIrp->Flags |= IRP_MJ_WRITE;
}
else
{
//per la modalità asincrona, non ne abbiamo bisogno
[...]
}
UserInputEventObject = 0;
// [4]
ret = ObReferenceObjectByHandle(
UserInputHandleEvent,
2u,
(POBJECT_TYPE)ExEventObjectType,
PreviousMode,
&UserInputEventObject,
0);
if (ret >= 0)
{
//Se l'utente ha fornito l'evento corretto, procediamo con la logica principale per copiare un file in un altro.
//Ho omesso quella sezione di codice per semplicità.
KeResetEvent((PRKEVENT)UserInputEventObject);
goto NEXT_PATH_TO_READ_FILE_QUERY;
}
RET:
//Ecco qua! Libera la struttura DataBuffer (ricorda WriteIrp, che contiene un puntatore all'evento di stack).
// [5]
if (ArbDataBuffer)
IopFreeCopyObjectsFromDataBuffer((__int64)ArbDataBuffer, 1);
if (UserInputEventObject_1)
ObfDereferenceObject(UserInputEventObject_1);
return (unsigned int)ret;
}
In [1], possiamo vedere StackEvent, che è il nostro oggetto problematico. In [2], se il file di destinazione è stato aperto in modalità sincrona, il kernel usa un evento di stack per attendere in modo sincrono il completamento dell'operazione di scrittura sul file di destinazione. Per fare ciò, usa IopWaitForSynchronousIoEvent (non mostrato nello pseudocodice) sull'evento di stack anziché sull'evento passato dall'utente. Innanzitutto, il kernel attende l'evento di stack, e solo successivamente aggiorna l'evento passato dall'utente. Allo stesso modo, in [3], si può vedere che UserEvent di WriteIrp punta all'evento di stack. Tuttavia, cosa succede se formiamo la richiesta corretta ma passiamo un evento di input errato? In [4], possiamo vedere come viene referenziato l'evento di input, dove possiamo passare un handle non valido (ad esempio, il valore 1). E poi la memoria viene liberata in [5]. È qui che accade la cosa più interessante.
Analizziamo la funzione IopFreeCopyObjectsFromDataBuffer e vediamo cosa succede quando l'IRP viene liberato. Diamo un'occhiata più da vicino a 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);
//Ci spostiamo più in profondità, monitorando attentamente 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, ecco! Ma come si può decrementare il contatore di riferimento per un oggetto di stack che non ha un OBJECT_HEADER?
//Vuln!
if ( UserEvent )
ObfDereferenceObject(UserEvent);
if ( P )
ExFreePoolWithTag(P, 0);
[...]
}