
POC переполнения буфера стека NtCopyFileChunk
Этот репозиторий содержит POC, который при выполнении вызывает запись за пределами стека (OOB write), приводящую к краху системы. Эта уязвимость — чрезвычайно интересный и редкий артефакт, демонстрирующий сложность и особенности программирования ядра, особенно управления объектами. Кроме того, уязвимость затрагивает ядро Windows 11 24h2, Windows 10 22h2/21h2, но не Windows 11 22h2/23h2, что только добавляет интереса.
Исправлено 12 ноября 2024 года
Уязвимость переполнения буфера на основе стека (технически — внеплановая запись, OOB write) существует в функции системного вызова ядра 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] видно, что UserEvent структуры WriteIrp указывает на стековое событие. Однако что будет, если сформировать правильный запрос, но передать неправильное входное событие? В [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);
[...]
}