Довольно часто диванные эксперты критикуют Microsoft, однако именно их инженеры представляют миру свежие идеи и системные механизмы, одним из которых является нашумевший защитник Patch-Guard. Углубившись в его детали с отладчиком WinDbg в руках быстро приходит понимание того, насколько оригинально продумана эта защита, отодрать которую из ядра не так просто, как может показаться на первый взгляд. В данной статье мы рассмотрим лишь наиболее привлекательные моменты, и она не претендует на полное изложение всех деталей работы PG, т.к. группа исследователей из компании "Tetrane" уже продемонстрировала в документе из 61-ой страницы свой тех-обзор.
Содержание:
1. Основные моменты
Идея защиты ядра ОС от модификации не нова - первое семя было брошено ещё на невспаханное поле 64-битных WinXP и Server-2003. C тех пор прошло без малого четверть века и защитник Patch-Guard уже совсем не тот, кем был в младенчестве. Бесконечные атаки наоборот способствовали его интеллектуальному росту, а поскольку PG как самая младшая дочь матрёшки глубоко интегрирован в ядро, инженеры решали проблемы не выпуском очередной заплатки, а точечным и полным обновлением конкретных его узлов.
PG страдает передозировкой внимания к себе, поскольку демонстрирует классическую гонку вооружений с малварью в сфере безопасности. Его существование призвано защищать такие системные компоненты как: таблицы GDT, IDT, SSDT (дескрипторы вирт.памяти, диспетчеризация прерываний, указатели на сервисы ядра соответственно), модельно-специфичные регистры процессора MSR и отладки DR0-DR7, область памяти глобальных переменных и стека ядра, а так-же программная секция .pdata самого файла Ntoskrnl.exe + код драйвера поддержки сети Ndis.sys и HAL.dll.
Как видим список весьма внушительный, и перед запуском ОС сторож пересчитывает контрольные суммы всех этих блоков. Если кто-то посмеет нарушить их целостность позже, то в рандомный период времени он сгенерит BSOD с ошибкой
Расширение
Основное препятствие на пути к реверсу PG - это устойчивая защита от отладки. Чтобы переключить ядро в режим дебага, нужно конфигуратором
Более того, в дефолте всё тело Patch-Guard хранится в зашифрованном виде, и динамически разворачивается лишь для проверок контрольных сумм своих подопечных, после чего опять шифруется. Поэтому если и удастся нам найти в ядерной памяти его код, дизасм-листинг будет похож на почерк стермитов.
PG часто упоминается вместе с DSE, который проверяет драйвера на подпись. Их семейный союз создаёт мощный кордон - DSE не даёт загрузить в систему левый дров, а PG запрещает даже подписанному драйверу модифицировать ядро.
Но что интересно, доверенные центры отказываются подписывать драйвера, если обнаружат в них открытую на запись секцию-кода, хотя страницы самого PG свободно используют атрибут записи в код, т.к. это жизненно важно для расшифровки его тела. В общем майки в очередной раз придерживаются политики "Следуй моим словам, а не моим поступкам".
Взяв на вооружение этот тезис, исследователь Can Boluk предложил довольно интересную идею поиска страниц памяти сторожа PG. Поскольку память по любому выделяется из невыгружаемого пула NonPaged диапазон которого заранее известен по флагам MI_SYSTEM_VA_TYPE, достаточно проверить все вирт.страницы в этом регионе на наличие атрибутов RWX. С высокой степенью вероятности, в них и будет лежать контекст Patch-Guard'a.
Константы VA_TYPE по сути представляют собой индексы во-второй половине каталога PML4/PXE (записи 256-511), а потому имея на руках известный индекс(5) невыгружаемого пула, мы сможем легко определить требуемый диапазон для сканирования памяти. Как только найдём страницы с атрибутами RWX, тут-же выставляем в их записях РТЕ самый старший бит NX "NoExecute", в результате чего PatchGuard по прежнему распакует свой код, однако выполнить его уже не сможет.
Собрать пруфы на эту теорию можно плагинами WinDbg
Как видим, резерв для пула невыгружаемой памяти NonPaged выставлен на 11 гигов, хотя на данный момент из них может быть реально использовано всего порядка 100 МБ (см.диспетчер задач, вкладку быстродействие).
А вот выхлоп второго плагина от Сатоши - к нему прилагается справка, где сказано:
Проверяем указанную область памяти и точно - в ней абсолютно нет нулей, а значит содержимое зашифровано и это может быть контекстом защитника. Кстати контекстов может быть не один, а сразу несколько, т.к. механизм PG выбирает область для загрузки своего кода случайным образом из шести вариантов, которые обсуждаются далее. Только здесь не понятно, почему инженеры не сбрасывают атрибут RWE в уже отработанной странице памяти, в результате чего он остаётся в качестве артефакта для исследователей. Эта ошибка не была исправлена вплоть да наших дней в Win11. Может тому есть своё объяснение и PG использует старый контекст повторно? Не знаю..
2. Алгоритм инициализации
Инициализация механизма PG происходит на ранней стадии загрузки ОС, чтобы снять хэши с подконтрольных блоков на девственной системе до загрузки драйверов. Первой из вызываемых ядром функцией PG является
Для этого функция использует 2 переменные ядра, флаги в которых имеют значение True=1 в обычном состоянии, и сбрасываются в False=0 если к системе подключён отладчик. Вот имена и состояние этих флагов в LiveKD. Как видим оба они сейчас взведены, а значит система не под отладкой:
Значение KdPitchDebugger определяется параметрами загрузки системы в bcdedit.exe на глобальном уровне. Если загрузка была выполнена без ключа
Вот листинг процедуры инициализации PG, где происходит жонглирование Debug-переменными. Если отладчика нет, делимое в паре
А вообще WinDbg в данном случае плохой помощник, т.к. скрывает самую главную суть фишки: -"Что происходит в момент исключения?". Но если открыть файл NtosKrnl.exe в дизассемблере IDA-Pro, можно увидеть сл.картину. Оказывается
Как и следует из её названия, эта функция настраивает контекст проверки. Сначала расшифровывает тушку PG операцией
3. Выбор метода и период проверок
Структура контекста PatchGuard разделена на три части.
Шифрованием кода туда-обратно занимается функция ядра
Вызов проверки выполняется с помощью множества случайных процедур, запускаемых таймерами, процедурами DPC через поля в блоке управления процессором
Саму-же проверку инженеры возложили на безымянную функцию, которой исследователи дали своё имя
4. Потенциальные уязвимости
Поскольку PatchGuard работает на том-же уровне, что и любой драйвер, его всегда можно отключить, ...если сможем найти. Технология во многом полагается на обфускацию своего кода (запутывание), чтобы его было сложно найти и проанализировать. В современных исследованиях прослеживается переход к более тонким методам, не оставляющим явных следов в памяти:
1. Hell's Hollow. Техника только для Win11.
Позволяет осуществлять SSDT-хукинг без прямого изменения самой таблицы. Метод злоупотребляет недокументированным механизмом альтернативных системных вызовов (Alt Syscalls), чтобы изменять
2. Обход целостности колбэков.
Вместо патча кода, атакующий может подменить целевую функцию типа
3. Буткиты EfiGuard.
Эти инструменты показывает, как можно деактивировать PG и DSE на этапе загрузки, до старта ядра Win - это делается через UEFI. Подобные атаки не могут деактивировать HVCI, т.к. он работает на более высоком уровне привилегий.
Современная защита Win это не просто PatchGuard, а целая экосистема, построенная на Virtualization-Based Security (VBS). Даже при наличии прав админа в VTL0 (обычное ядро), атакующий не может вмешаться в работу механизмов работающих в VTL1 (гипервизор). Так появился аналогичный по смыслу HyperGuard (Secure Kernel Patch Guard), как более эволюционное решение, которое стало возможным с появлением технологий виртуализации VBS. В отличие от своего предшественника, он не нуждается в обфускации, поскольку его код и данные находятся вне досягаемости для вредоносного кода в ядре. Это делает его намного более надёжным и простым для анализа (например, он реализован в модуле SecureKernel.exe).
5. Заключение
Ну и под зановес рассмотрим в 2-х словах эволюцию защиты между версиями х64 Win:
- WinVista (2007) - первое появление PG. Он обеспечивает целостность ядра посредством обфусцированных проверок критически важных структур.
- Win7 (2009) - полностью сохранила архитектуру эпохи Vista.
- Win8 (2012) - защита расширилась и охватила более широкие подсистемы ОС, с реализациями через несколько ядер.
- Win10 (2015) - были представлены разнообразные векторы инициализации, такие как вероятностное 4% создание потоков и сокрытие внутри областей PRCB. Одновременно появился HyperGuard, ранее известный как Secure Kernel Patch Guard (SKPG).
- Win11 (2021) - развила эти основы, и такие обновления как 24H2 от 2024 г. использовали спец.настройки во время загрузки, включая
Пдфка от группы "Tetrane" со всеми деталями:
https://blog.tetrane.com/downloads/Tetrane_PatchGuard_Analysis_RS4_v1.01.pdf
Интересное исследование для Win8:
https://pt-corp.storage.yandexcloud.net/upload/corporate/ru-ru/analytics/Windows_81_Kernel_Patch_Protection_Analysis.pdf#2#2
Сатоши Танда делится своим наблюдением:
Some Tips to Analyze PatchGuard
Содержание:
1. Основные моменты
2. Алгоритм инициализации
3. Выбор метода и период проверок
4. Потенциальные уязвимости
5. Заключение
1. Основные моменты
Идея защиты ядра ОС от модификации не нова - первое семя было брошено ещё на невспаханное поле 64-битных WinXP и Server-2003. C тех пор прошло без малого четверть века и защитник Patch-Guard уже совсем не тот, кем был в младенчестве. Бесконечные атаки наоборот способствовали его интеллектуальному росту, а поскольку PG как самая младшая дочь матрёшки глубоко интегрирован в ядро, инженеры решали проблемы не выпуском очередной заплатки, а точечным и полным обновлением конкретных его узлов.
PG страдает передозировкой внимания к себе, поскольку демонстрирует классическую гонку вооружений с малварью в сфере безопасности. Его существование призвано защищать такие системные компоненты как: таблицы GDT, IDT, SSDT (дескрипторы вирт.памяти, диспетчеризация прерываний, указатели на сервисы ядра соответственно), модельно-специфичные регистры процессора MSR и отладки DR0-DR7, область памяти глобальных переменных и стека ядра, а так-же программная секция .pdata самого файла Ntoskrnl.exe + код драйвера поддержки сети Ndis.sys и HAL.dll.
Как видим список весьма внушительный, и перед запуском ОС сторож пересчитывает контрольные суммы всех этих блоков. Если кто-то посмеет нарушить их целостность позже, то в рандомный период времени он сгенерит BSOD с ошибкой
0x109. Обратите внимание, что система рухнет не сразу, а через произвольное время от 10 сек до 2 мин. Это потому, что период проверки хэшей подконтрольных блоков PG выбирает случайно по счётчику TSC (time-stamp-counter), чтобы малварь не могла использовать для своих атак известное окно в промежутках между проверками.Расширение
!analyze отладчика WinDbg специально введено для анализа BSOD'ов, и в данном случае характер ошибки будет указан в последнем аргументе(4). Не смотря на всю коварность голубых экранов смерти, здесь он как-нельзя кстати. Чтобы поймать кота за хвост, достаточно в свойствах системы активировать галку "Сохранять дамп ядра при сбоях", после чего вскормить этот дамп расширению ниже. Так можно будет раскрутить стек назад и фактически попасть в прошлое, где обнаружим всю цепочку вызовов PG.
Код:
0: kd> !analyze -show 0x00000109
CRITICAL_STRUCTURE_CORRUPTION (109)
This bugcheck is generated when the kernel detects that critical kernel code or
data have been corrupted. There are generally three causes for a corruption:
1) A driver has inadvertently or deliberately modified critical kernel code
or data. See http://www.microsoft.com/whdc/driver/kernel/64bitPatching.mspx
2) A developer attempted to set a normal kernel breakpoint using a kernel
debugger that was not attached when the system was booted. Normal breakpoints,
"bp", can only be set if the debugger is attached at boot time. Hardware
breakpoints, "ba", can be set at any time.
3) A hardware corruption occurred, e.g. failing RAM holding kernel code or data.
Arguments:
Arg1: 0000000000000000, Reserved
Arg2: 0000000000000000, Reserved
Arg3: 0000000000000000, Failure type dependent information
Arg4: 0000000000000000, Type of corrupted region, can be
0 : A generic data region
1 : Modification of a function or .pdata
2 : A processor IDT
3 : A processor GDT
4 : Type 1 process list corruption
5 : Type 2 process list corruption
6 : Debug routine modification
7 : Critical MSR modification
0: kd>
Основное препятствие на пути к реверсу PG - это устойчивая защита от отладки. Чтобы переключить ядро в режим дебага, нужно конфигуратором
bcdedit включить рубильник в загрузчике ОС, что отражается на состоянии системных переменных размером в байт KdPitchDebugger и KdDebuggerNotPresent. На этапе инициализации, PG хитро манипулирует этими флагами, и если обнаруживает факт отладки, полностью пропадает с радаров, тупо прекращая любую свою деятельность. Таким образом защитник работает только на живой системе, и попытка поставить на него брейк в отладчике обречена на провал.Более того, в дефолте всё тело Patch-Guard хранится в зашифрованном виде, и динамически разворачивается лишь для проверок контрольных сумм своих подопечных, после чего опять шифруется. Поэтому если и удастся нам найти в ядерной памяти его код, дизасм-листинг будет похож на почерк стермитов.
PG часто упоминается вместе с DSE, который проверяет драйвера на подпись. Их семейный союз создаёт мощный кордон - DSE не даёт загрузить в систему левый дров, а PG запрещает даже подписанному драйверу модифицировать ядро.
Но что интересно, доверенные центры отказываются подписывать драйвера, если обнаружат в них открытую на запись секцию-кода, хотя страницы самого PG свободно используют атрибут записи в код, т.к. это жизненно важно для расшифровки его тела. В общем майки в очередной раз придерживаются политики "Следуй моим словам, а не моим поступкам".
Взяв на вооружение этот тезис, исследователь Can Boluk предложил довольно интересную идею поиска страниц памяти сторожа PG. Поскольку память по любому выделяется из невыгружаемого пула NonPaged диапазон которого заранее известен по флагам MI_SYSTEM_VA_TYPE, достаточно проверить все вирт.страницы в этом регионе на наличие атрибутов RWX. С высокой степенью вероятности, в них и будет лежать контекст Patch-Guard'a.
Константы VA_TYPE по сути представляют собой индексы во-второй половине каталога PML4/PXE (записи 256-511), а потому имея на руках известный индекс(5) невыгружаемого пула, мы сможем легко определить требуемый диапазон для сканирования памяти. Как только найдём страницы с атрибутами RWX, тут-же выставляем в их записях РТЕ самый старший бит NX "NoExecute", в результате чего PatchGuard по прежнему распакует свой код, однако выполнить его уже не сможет.
Код:
0: kd> dt _MI_SYSTEM_VA_TYPE ;<---- перечисление типов памяти
nt!_MI_SYSTEM_VA_TYPE
MiVaUnused = 0
MiVaSessionSpace = 1
MiVaProcessSpace = 2
MiVaBootLoaded = 3
MiVaPfnDatabase = 4
MiVaNonPagedPool = 5 ;<----
MiVaPagedPool = 6
MiVaSpecialPoolPaged = 7
MiVaSystemCache = 8
MiVaSystemPtes = 9
MiVaHal = 10
MiVaSessionGlobalSpace = 11
MiVaDriverImages = 12
MiVaSpecialPoolNonPaged = 13
MiVaKernelStack = 14 ;<---- начиная с Win10+
MiVaSecureNonPagedPool = 15
MiVaKernelShadowStack = 16
MiVaKasan = 17
MiVaMaximumType = 18
Собрать пруфы на эту теорию можно плагинами WinDbg
!cmkd от "CodeMachine", и !findpg от крутого спеца из Японии Сатоши Танда. Последний плаг как-раз сканирует NonPagedPool на предмет уникальных сигнатур PG, и прав доступа к найденным страницам памяти.
Код:
0: kd> !cmkd.kvas -v
### Start End Length Type
000 ffff0800`00000000 fffff67f`ffffffff ee8000000000 ( 238 TB) SystemSpace
001 fffff680`00000000 fffff6ff`ffffffff 8000000000 ( 512 GB) PageTables
002 fffff700`00000000 fffff77f`ffffffff 8000000000 ( 512 GB) HyperSpace
003 fffff780`00000000 fffff780`00000fff 1000 ( 4 KB) SharedSystemPage
004 fffff780`00001000 fffff7ff`ffffffff 7ffffff000 ( 511 GB) CacheWorkingSet
005 fffff800`00000000 fffff87f`ffffffff 8000000000 ( 512 GB) LoaderMappings
006 fffff880`00000000 fffff89f`ffffffff 2000000000 ( 128 GB) SystemPTEs
007 fffff8a0`00000000 fffff8bf`ffffffff 2000000000 ( 128 GB) PagedPool
008 fffff900`00000000 fffff97f`ffffffff 8000000000 ( 512 GB) SessionSpace
009 fffff980`00000000 fffffa7f`ffffffff 10000000000 ( 1 TB) DynamicKernelVa
010 fffffa80`00000000 fffffa80`0c5fffff c600000 ( 198 MB) PfnDatabase
011 fffffa80`0c400000 fffffa83`033fffff 2f7000000 ( 11 GB) NonPagedPool ;<---------
012 ffffffff`ffc00000 ffffffff`ffffffff 400000 ( 4 MB) HalReserved
Как видим, резерв для пула невыгружаемой памяти NonPaged выставлен на 11 гигов, хотя на данный момент из них может быть реально использовано всего порядка 100 МБ (см.диспетчер задач, вкладку быстродействие).
А вот выхлоп второго плагина от Сатоши - к нему прилагается справка, где сказано:
1. Первое поле Randomness - это счётчик значений 0x00 в первых 100 байтах страницы. Если страница зашифрована, число должно быть меньше 8.
2. Второе поле Randomness - это счётчик уникальных байт в первых 100 байтах страницы. Если пейдж зашифрован, число должно быть больше 70.
3. Если Pooltag выглядит как легитимный тег, страница всё-равно может принадлежать PatchGuard.
Код:
0: kd> !findpg
PatchGuard context base: fffffa80`11290000, size: 0x0001b000, Randomness 2:79
Pooltag CcBc: Cache Manager, Binary : nt!cc
Проверяем указанную область памяти и точно - в ней абсолютно нет нулей, а значит содержимое зашифровано и это может быть контекстом защитника. Кстати контекстов может быть не один, а сразу несколько, т.к. механизм PG выбирает область для загрузки своего кода случайным образом из шести вариантов, которые обсуждаются далее. Только здесь не понятно, почему инженеры не сбрасывают атрибут RWE в уже отработанной странице памяти, в результате чего он остаётся в качестве артефакта для исследователей. Эта ошибка не была исправлена вплоть да наших дней в Win11. Может тому есть своё объяснение и PG использует старый контекст повторно? Не знаю..
Код:
0: kd> db fffffa80`11290000 L100
fffffa80`11290000 76 3c 03 34 90 13 c4 1e-d3 3d 14 94 af 4f 44 95 v<.4.....=...OD.
fffffa80`11290010 7b 3c 2d f4 ae ab c4 09-a8 fc 24 ac ad 8c 24 ed {<-.......$...$.
fffffa80`11290020 81 dc 2b c8 2e b1 94 da-ec bc 23 c4 ad 91 84 4e ..+.......#....N
fffffa80`11290030 c2 9d cf 40 1a 26 73 e8-90 5d b8 18 1b ff 52 d3 ...@.&s..]....R.
fffffa80`11290040 12 3d c2 d4 99 17 43 be-2a 1d c8 10 19 3c 33 2b .=....C.*....<3+
fffffa80`11290050 7d ff ee 6c 96 a4 23 88-43 bf d0 24 96 5d 03 f7 }..l..#.C..$.]..
fffffa80`11290060 ef 9c da e0 18 6a f3 61-98 7f e3 3c 94 92 e3 ce .....j.a...<....
fffffa80`11290070 90 5f e6 58 15 87 53 3c-56 1a 8b 30 01 30 b2 4a ._.X..S<V..0.0.J
fffffa80`11290080 3b fa 83 cc 81 10 a2 3e-fb db 9c a8 00 6d 12 21 ;......>.....m.!
fffffa80`11290090 8e ba 76 63 83 c5 01 a4-aa 7a 8c 9c 82 2e 62 79 ..vc.....z....by
fffffa80`112900a0 de 5a a5 f8 1d 8b d2 5d-82 3a bd f4 9c eb c2 c1 .Z.....].:......
fffffa80`112900b0 15 1a 95 f0 1f 48 b2 45-19 da 9b c8 00 71 92 a2 .....H.E.....q..
fffffa80`112900c0 64 ba 93 c4 9f 51 82 16-ca 98 5f 5f 08 66 71 a0 d....Q....__.fq.
fffffa80`112900d0 af 78 46 5b 89 06 61 1c-9a c7 32 d3 8b d7 40 06 .xF[..a...2...@.
fffffa80`112900e0 d3 e7 38 2f 0b fc 30 f3-75 f9 7e 6b 84 e4 21 40 ..8/..0.u.~k..!@
fffffa80`112900f0 56 d9 61 07 04 99 91 bf-e7 99 6a ff 06 aa f1 19 V.a.......j.....
2. Алгоритм инициализации
Инициализация механизма PG происходит на ранней стадии загрузки ОС, чтобы снять хэши с подконтрольных блоков на девственной системе до загрузки драйверов. Первой из вызываемых ядром функцией PG является
KeInitAmd64SpecificState() - её задача определить, находится-ли система под отладкой и если да, то молча пропустить этап инициализации до сл.ребута. Для этого функция использует 2 переменные ядра, флаги в которых имеют значение True=1 в обычном состоянии, и сбрасываются в False=0 если к системе подключён отладчик. Вот имена и состояние этих флагов в LiveKD. Как видим оба они сейчас взведены, а значит система не под отладкой:
Код:
0: kd> db nt!KdPitchDebugger L1
fffff800`023df23a 01
0: kd> db nt!KdDebuggerNotPresent L1
fffff800`0246e9f1 01
Значение KdPitchDebugger определяется параметрами загрузки системы в bcdedit.exe на глобальном уровне. Если загрузка была выполнена без ключа
/debug в конфигураторе, значение этой переменной будет равно 1. А вот вторая переменная KdDebuggerNotPresent является просто флагом на светофоре, подключён отладчик ядра на данный момент, или нет. Когда WinDbg реально коннектится к системе (через com или usb-порт), он отправляет ядру специальный "ResetPacket", который сбрасывает этот флаг в нуль.Вот листинг процедуры инициализации PG, где происходит жонглирование Debug-переменными. Если отладчика нет, делимое в паре
EDX:EAX превысит допустимое значение со-знаком, в результате чего после idiv получим исключение в ядре ERROR_ARITHMETIC_OVERFLOW =0x216. Роль главной скрипки играет здесь инструкция neg ecx, по результату которой взводится флаг процессора CF, и уже от него формируется значение делителя через sbb r8d,r8d.
Код:
0: kd> uf KeInitAmd64SpecificState
nt!KeInitAmd64SpecificState:
fffff800`027bbeb0 4883ec28 sub rsp,28h
fffff800`027bbeb4 0fb615367bd1ff movzx edx,byte ptr [nt!KdDebuggerNotPresent (fffff800`024d39f1)]
fffff800`027bbebb 0fb6057883c8ff movzx eax,byte ptr [nt!KdPitchDebugger (fffff800`0244423a)]
fffff800`027bbec2 0bd0 or edx,eax
fffff800`027bbec4 8bca mov ecx,edx
fffff800`027bbec6 f7d9 neg ecx ;-- CF = 1 если EDX=1, иначе нуль
fffff800`027bbec8 451bc0 sbb r8d,r8d ;-- R8d = 0xFFFFFFFF если EDX=1, иначе нуль
fffff800`027bbecb 4183e0ee and r8d,0FFFFFFEEh
fffff800`027bbecf 4183c011 add r8d,11h ;-- R8d = 0x11 если EDX был 0 (делитель)
fffff800`027bbed3 d1ca ror edx,1 ;-- EDX=0x80000000 если был 1, иначе нуль
fffff800`027bbed5 8bc2 mov eax,edx ;-- EAX=0x80000000 (без отладчика)
fffff800`027bbed7 99 cdq ;-- EDX=0xFFFFFFFF (без отладчика)
fffff800`027bbed8 41f7f8 idiv eax,r8d ;-- Без отладчика = исключение!!!
fffff800`027bbedb 89442430 mov dword ptr [rsp+30h],eax
fffff800`027bbedf eb00 jmp nt!KeInitAmd64SpecificState+0x31 (fffff800`027bbee1)
nt!KeInitAmd64SpecificState+0x31:
fffff800`027bbee1 4883c428 add rsp,28h
fffff800`027bbee5 c3 ret
0: kd>
А вообще WinDbg в данном случае плохой помощник, т.к. скрывает самую главную суть фишки: -"Что происходит в момент исключения?". Но если открыть файл NtosKrnl.exe в дизассемблере IDA-Pro, можно увидеть сл.картину. Оказывается
idiv на самом деле завёрнут в блок _try\except, и при ошибке управление по цепочке Chunk получает его обработчик, от куда вызывается уже функция KiFilterFiberContext(), которая и занимается в конечном счёте реальной инициализацией сторожа PatchGuard.Как и следует из её названия, эта функция настраивает контекст проверки. Сначала расшифровывает тушку PG операцией
XOR+ROL, после чего опять шифрует тело случайным ключом на основе счётчика rdtsc, который сохраняет для следующего декрипта рядом с хэшами в своём контексте. Таким образом, ключ расшифровки меняется после каждой проверки, и его заранее не знает даже сам PatchGuard.KiFilterFiberContext() может вызываться либо с аргументом-указателем на структуру KI_FILTER_FIBER_PARAM, либо с NULL. Её основная задача - начать процедуру инициализации контекста с конкретными аргументами. Эти аргументы определяют, какой метод использовать для запуска проверки. Поскольку эта основная функция уже известна, в узких кругах её обычно называют KiInitPatchGuardContext().
Код:
struct KI_FILTER_FIBER_PARAM
code_prefetch_rcx_retn dd 0
padding dd 0
pPsCreateSystemThread dq 0
Pg_Method3StubToCheckRoutine dq 0
pKiBalanceSetManagerPeriodicDpc dq 0
ends
;//---------------------------
0: kd> uf /i nt!KiFilterFiberContext
nt!KiFilterFiberContext: 135 instructions scanned
fffff800`02768c14 48895c2408 mov qword ptr [rsp+08h],rbx
fffff800`02768c19 48896c2410 mov qword ptr [rsp+10h],rbp
fffff800`02768c1e 4889742418 mov qword ptr [rsp+18h],rsi
fffff800`02768c23 57 push rdi
fffff800`02768c24 4154 push r12
fffff800`02768c26 4155 push r13
fffff800`02768c28 4883ec20 sub rsp,20h
fffff800`02768c2c fa cli
fffff800`02768c2d 803dbd7dd1ff00 cmp byte ptr [nt!KdDebuggerNotPresent (fffff800`024809f1)],0
fffff800`02768c34 7502 jne nt!KiFilterFiberContext+0x24 (fffff800`02768c38)
nt!KiFilterFiberContext+0x22:
fffff800`02768c36 ebfe jmp nt!KiFilterFiberContext+0x22 (fffff800`02768c36)
nt!KiFilterFiberContext+0x24:
fffff800`02768c38 fb sti
fffff800`02768c39 0f31 rdtsc
fffff800`02768c3b 48c1e220 shl rdx,20h
fffff800`02768c3f 48bd0120000480001070 mov rbp,7010008004002001h
fffff800`02768c49 bb01000000 mov ebx,1
fffff800`02768c4e 480bc2 or rax,rdx
fffff800`02768c51 488bc8 mov rcx,rax
fffff800`02768c54 488bd0 mov rdx,rax
fffff800`02768c57 488bc5 mov rax,rbp
fffff800`02768c5a 48c1c903 ror rcx,3
fffff800`02768c5e 4833d1 xor rdx,rcx
fffff800`02768c61 48f7e2 mul rax,rdx
fffff800`02768c64 488bca mov rcx,rdx
fffff800`02768c67 4833c8 xor rcx,rax
fffff800`02768c6a 48b8cdcccccccccccccc mov rax,0CCCCCCCCCCCCCCCDh
fffff800`02768c74 48f7e1 mul rax,rcx
fffff800`02768c77 48c1ea03 shr rdx,3
fffff800`02768c7b 488d0492 lea rax,[rdx+rdx*4]
fffff800`02768c7f 4803c0 add rax,rax
fffff800`02768c82 482bc8 sub rcx,rax
fffff800`02768c85 4883f906 cmp rcx,6
fffff800`02768c89 1bf6 sbb esi,esi
fffff800`02768c8b f7de neg esi
fffff800`02768c8d 03f3 add esi,ebx
fffff800`02768c8f 0f31 rdtsc
fffff800`02768c91 48c1e220 shl rdx,20h
fffff800`02768c95 480bc2 or rax,rdx
fffff800`02768c98 488bc8 mov rcx,rax
fffff800`02768c9b 488bd0 mov rdx,rax
fffff800`02768c9e 488bc5 mov rax,rbp
fffff800`02768ca1 48c1c903 ror rcx,3
fffff800`02768ca5 4833d1 xor rdx,rcx
fffff800`02768ca8 48f7e2 mul rax,rdx
fffff800`02768cab 4c8bd2 mov r10,rdx
fffff800`02768cae 4c8bc8 mov r9,rax
fffff800`02768cb1 0f31 rdtsc
fffff800`02768cb3 48c1e220 shl rdx,20h
fffff800`02768cb7 4d33d1 xor r10,r9
fffff800`02768cba 49bdabaaaaaaaaaaaaaa mov r13,0AAAAAAAAAAAAAAABh
fffff800`02768cc4 480bc2 or rax,rdx
fffff800`02768cc7 49bcc54eecc44eecc44e mov r12,4EC4EC4EC4EC4EC5h
fffff800`02768cd1 488bc8 mov rcx,rax
fffff800`02768cd4 4c8bc0 mov r8,rax
fffff800`02768cd7 488bc5 mov rax,rbp
fffff800`02768cda 48c1c903 ror rcx,3
fffff800`02768cde 4c33c1 xor r8,rcx
fffff800`02768ce1 49f7e0 mul rax,r8
fffff800`02768ce4 448bc6 mov r8d,esi
fffff800`02768ce7 488bfa mov rdi,rdx
fffff800`02768cea 4833f8 xor rdi,rax
fffff800`02768ced 498bc5 mov rax,r13
fffff800`02768cf0 48f7e7 mul rax,rdi
fffff800`02768cf3 48d1ea shr rdx,1
fffff800`02768cf6 488d0452 lea rax,[rdx+rdx*2]
fffff800`02768cfa 482bf8 sub rdi,rax
fffff800`02768cfd 498bc4 mov rax,r12
fffff800`02768d00 49f7e2 mul rax,r10
fffff800`02768d03 48c1ea02 shr rdx,2
fffff800`02768d07 486bd20d imul rdx,rdx,0Dh
fffff800`02768d0b 4c2bd2 sub r10,rdx
fffff800`02768d0e 8bd7 mov edx,edi
fffff800`02768d10 418bca mov ecx,r10d
fffff800`02768d13 e8f062ffff call nt!VfOrderDependentThunks <PERF> (nt+0x54c008) (fffff800`0275f008)
fffff800`02768d18 84c0 test al,al
fffff800`02768d1a 0f8484000000 je nt!KiFilterFiberContext+0x190 (fffff800`02768da4)
nt!KiFilterFiberContext+0x10c:
fffff800`02768d20 448d4301 lea r8d,[rbx+1]
fffff800`02768d24 413bf0 cmp esi,r8d
fffff800`02768d27 7577 jne nt!KiFilterFiberContext+0x18c (fffff800`02768da0)
nt!KiFilterFiberContext+0x115:
fffff800`02768d29 0f31 rdtsc
fffff800`02768d2b 48c1e220 shl rdx,20h
fffff800`02768d2f 480bc2 or rax,rdx
fffff800`02768d32 488bc8 mov rcx,rax
fffff800`02768d35 488bd0 mov rdx,rax
fffff800`02768d38 488bc5 mov rax,rbp
fffff800`02768d3b 48c1c903 ror rcx,3
fffff800`02768d3f 4833d1 xor rdx,rcx
fffff800`02768d42 48f7e2 mul rax,rdx
fffff800`02768d45 4c8bca mov r9,rdx
fffff800`02768d48 4c33c8 xor r9,rax
fffff800`02768d4b 498bc4 mov rax,r12
fffff800`02768d4e 49f7e1 mul rax,r9
fffff800`02768d51 48c1ea02 shr rdx,2
fffff800`02768d55 486bd20d imul rdx,rdx,0Dh
fffff800`02768d59 4c2bca sub r9,rdx
nt!KiFilterFiberContext+0x148:
fffff800`02768d5c 0f31 rdtsc
fffff800`02768d5e 48c1e220 shl rdx,20h
fffff800`02768d62 480bc2 or rax,rdx
fffff800`02768d65 488bc8 mov rcx,rax
fffff800`02768d68 488bd0 mov rdx,rax
fffff800`02768d6b 488bc5 mov rax,rbp
fffff800`02768d6e 48c1c903 ror rcx,3
fffff800`02768d72 4833d1 xor rdx,rcx
fffff800`02768d75 48f7e2 mul rax,rdx
fffff800`02768d78 488bca mov rcx,rdx
fffff800`02768d7b 4833c8 xor rcx,rax
fffff800`02768d7e 498bc5 mov rax,r13
fffff800`02768d81 48f7e1 mul rax,rcx
fffff800`02768d84 48d1ea shr rdx,1
fffff800`02768d87 488d0452 lea rax,[rdx+rdx*2]
fffff800`02768d8b 482bc8 sub rcx,rax
fffff800`02768d8e 85ff test edi,edi
fffff800`02768d90 7404 je nt!KiFilterFiberContext+0x182 (fffff800`02768d96)
nt!KiFilterFiberContext+0x17e:
fffff800`02768d92 3bcf cmp ecx,edi
fffff800`02768d94 74c6 je nt!KiFilterFiberContext+0x148 (fffff800`02768d5c)
nt!KiFilterFiberContext+0x182:
fffff800`02768d96 8bd1 mov edx,ecx
fffff800`02768d98 418bc9 mov ecx,r9d
fffff800`02768d9b e86862ffff call nt!VfOrderDependentThunks <PERF> (nt+0x54c008) (fffff800`0275f008)
nt!KiFilterFiberContext+0x18c:
fffff800`02768da0 84c0 test al,al
fffff800`02768da2 7502 jne nt!KiFilterFiberContext+0x192 (fffff800`02768da6)
nt!KiFilterFiberContext+0x190:
fffff800`02768da4 33db xor ebx,ebx
nt!KiFilterFiberContext+0x192:
fffff800`02768da6 fa cli
fffff800`02768da7 803d437cd1ff00 cmp byte ptr [nt!KdDebuggerNotPresent (fffff800`024809f1)],0
fffff800`02768dae 7502 jne nt!KiFilterFiberContext+0x19e (fffff800`02768db2)
nt!KiFilterFiberContext+0x19c:
fffff800`02768db0 ebfe jmp nt!KiFilterFiberContext+0x19c (fffff800`02768db0)
nt!KiFilterFiberContext+0x19e:
fffff800`02768db2 fb sti
fffff800`02768db3 488b6c2448 mov rbp,qword ptr [rsp+48h]
fffff800`02768db8 488b742450 mov rsi,qword ptr [rsp+50h]
fffff800`02768dbd 8bc3 mov eax,ebx
fffff800`02768dbf 488b5c2440 mov rbx,qword ptr [rsp+40h]
fffff800`02768dc4 4883c420 add rsp,20h
fffff800`02768dc8 415d pop r13
fffff800`02768dca 415c pop r12
fffff800`02768dcc 5f pop rdi
fffff800`02768dcd c3 ret
0: kd>
3. Выбор метода и период проверок
Структура контекста PatchGuard разделена на три части.
1. Небольшой загрузчик, который расшифровывает (и обратно) основное тело PG.
2. Собственно рабочая тушка, которая занимается проверкой критических блоков ядра.
3. Содержит информацию для проверок, которая содержит в себе сл.данные для каждого из блоков:
а) 0x00 - Тип структуры.
б) 0x08 - Указатель на блок данных для проверки.
в) 0x10 - Размер блока данных.
г) 0х18 - Хэш (контрольная сумма), вычисленная во время инициализации Win.
Шифрованием кода туда-обратно занимается функция ядра
CmpAppendDllSection(). Обратите внимание на префикс Cmp_xx, который вообще-то выделен менеджеру конфигов, хотя в данном случае это не он. Сами-же проверки начинаются асинхронно, чтобы избежать предсказуемого времени. Это гарантирует, что логика будет защищена от подделки и реверса.
Код:
0: kd> uf /i CmpAppendDllSection
Flow analysis was incomplete, some code may be missing. 41 instructions scanned
nt!CmpAppendDllSection:
fffff800`02756de0 2e483111 xor qword ptr cs:[rcx],rdx
fffff800`02756de4 48315108 xor qword ptr [rcx+08h],rdx
fffff800`02756de8 48315110 xor qword ptr [rcx+10h],rdx
fffff800`02756dec 48315118 xor qword ptr [rcx+18h],rdx
fffff800`02756df0 48315120 xor qword ptr [rcx+20h],rdx
fffff800`02756df4 48315128 xor qword ptr [rcx+28h],rdx
fffff800`02756df8 48315130 xor qword ptr [rcx+30h],rdx
fffff800`02756dfc 48315138 xor qword ptr [rcx+38h],rdx
fffff800`02756e00 48315140 xor qword ptr [rcx+40h],rdx
fffff800`02756e04 48315148 xor qword ptr [rcx+48h],rdx
fffff800`02756e08 48315150 xor qword ptr [rcx+50h],rdx
fffff800`02756e0c 48315158 xor qword ptr [rcx+58h],rdx
fffff800`02756e10 48315160 xor qword ptr [rcx+60h],rdx
fffff800`02756e14 48315168 xor qword ptr [rcx+68h],rdx
fffff800`02756e18 48315170 xor qword ptr [rcx+70h],rdx
fffff800`02756e1c 48315178 xor qword ptr [rcx+78h],rdx
fffff800`02756e20 48319180000000 xor qword ptr [rcx+80h],rdx
fffff800`02756e27 48319188000000 xor qword ptr [rcx+88h],rdx
fffff800`02756e2e 48319190000000 xor qword ptr [rcx+90h],rdx
fffff800`02756e35 48319198000000 xor qword ptr [rcx+98h],rdx
fffff800`02756e3c 483191a0000000 xor qword ptr [rcx+0A0h],rdx
fffff800`02756e43 483191a8000000 xor qword ptr [rcx+0A8h],rdx
fffff800`02756e4a 483191b0000000 xor qword ptr [rcx+0B0h],rdx
fffff800`02756e51 483191b8000000 xor qword ptr [rcx+0B8h],rdx
fffff800`02756e58 483191c0000000 xor qword ptr [rcx+0C0h],rdx
fffff800`02756e5f 3111 xor dword ptr [rcx],edx
fffff800`02756e61 488bc2 mov rax,rdx
fffff800`02756e64 488bd1 mov rdx,rcx
fffff800`02756e67 8b8ac4000000 mov ecx,dword ptr [rdx+0C4h]
nt!CmpAppendDllSection+0x8d:
fffff800`02756e6d 483184cac00000 xor qword ptr [rdx+rcx*8+0C0h],rax
fffff800`02756e75 48d3c8 ror rax,cl
fffff800`02756e78 e2f3 loop nt!CmpAppendDllSection+0x8d (fffff800`02756e6d)
nt!CmpAppendDllSection+0x9a:
fffff800`02756e7a 8b8288020000 mov eax,dword ptr [rdx+288h]
fffff800`02756e80 4803c2 add rax,rdx
fffff800`02756e83 4883ec28 sub rsp,28h
fffff800`02756e87 ffd0 call rax
fffff800`02756e89 4883c428 add rsp,28h
fffff800`02756e8d 4c8b80e8000000 mov r8,qword ptr [rax+0E8h]
fffff800`02756e94 488d8840020000 lea rcx,[rax+240h]
fffff800`02756e9b ba01000000 mov edx,1
fffff800`02756ea0 41ffe0 jmp r8
0: kd>
Вызов проверки выполняется с помощью множества случайных процедур, запускаемых таймерами, процедурами DPC через поля в блоке управления процессором
PRCB (таких как AcpiReserved или HalReserved), рабочими потоками системы, или перехватчиками периодических функций ядра KiBalanceSetManagerPeriodicDpc() каждые 120-130 сек. Каждая процедура сканирует указанные структуры, пересчитывая контрольные суммы и сравнивая их с сохранёнными значениями - несоответствия в любой защищённой области приводят к немедленному сбою системы с рассмотренным выше кодом 0х109.Саму-же проверку инженеры возложили на безымянную функцию, которой исследователи дали своё имя
KiInitPatchGuardContext(). Как выяснилось, она может принимать разные параметры 0,1,2,3,4,5,7, от которых зависит метод проверки. Описывать все эти методы подробно здесь не будем, т.к. это уже освятила в своей работе группа из компании "Tetrane" - в конце статьи я просто оставлю линк на их доку PDF.4. Потенциальные уязвимости
Поскольку PatchGuard работает на том-же уровне, что и любой драйвер, его всегда можно отключить, ...если сможем найти. Технология во многом полагается на обфускацию своего кода (запутывание), чтобы его было сложно найти и проанализировать. В современных исследованиях прослеживается переход к более тонким методам, не оставляющим явных следов в памяти:
1. Hell's Hollow. Техника только для Win11.
Позволяет осуществлять SSDT-хукинг без прямого изменения самой таблицы. Метод злоупотребляет недокументированным механизмом альтернативных системных вызовов (Alt Syscalls), чтобы изменять
KTRAP_FRAME и перехватывать вызовы. Однако по утверждениям авторов, этот метод блокируется HVCI.2. Обход целостности колбэков.
Вместо патча кода, атакующий может подменить целевую функцию типа
JMP RАX, где аргументом является адрес управляемого им кода. Таким образом, код ядра не изменяется, что позволяет избежать обнаружения PG. Однако и эта техника имеет ограничения по переносимости и может быть обнаружена.3. Буткиты EfiGuard.
Эти инструменты показывает, как можно деактивировать PG и DSE на этапе загрузки, до старта ядра Win - это делается через UEFI. Подобные атаки не могут деактивировать HVCI, т.к. он работает на более высоком уровне привилегий.
Современная защита Win это не просто PatchGuard, а целая экосистема, построенная на Virtualization-Based Security (VBS). Даже при наличии прав админа в VTL0 (обычное ядро), атакующий не может вмешаться в работу механизмов работающих в VTL1 (гипервизор). Так появился аналогичный по смыслу HyperGuard (Secure Kernel Patch Guard), как более эволюционное решение, которое стало возможным с появлением технологий виртуализации VBS. В отличие от своего предшественника, он не нуждается в обфускации, поскольку его код и данные находятся вне досягаемости для вредоносного кода в ядре. Это делает его намного более надёжным и простым для анализа (например, он реализован в модуле SecureKernel.exe).
5. Заключение
Ну и под зановес рассмотрим в 2-х словах эволюцию защиты между версиями х64 Win:
- WinVista (2007) - первое появление PG. Он обеспечивает целостность ядра посредством обфусцированных проверок критически важных структур.
- Win7 (2009) - полностью сохранила архитектуру эпохи Vista.
- Win8 (2012) - защита расширилась и охватила более широкие подсистемы ОС, с реализациями через несколько ядер.
- Win10 (2015) - были представлены разнообразные векторы инициализации, такие как вероятностное 4% создание потоков и сокрытие внутри областей PRCB. Одновременно появился HyperGuard, ранее известный как Secure Kernel Patch Guard (SKPG).
- Win11 (2021) - развила эти основы, и такие обновления как 24H2 от 2024 г. использовали спец.настройки во время загрузки, включая
KiFilterFiberContext().Пдфка от группы "Tetrane" со всеми деталями:
https://blog.tetrane.com/downloads/Tetrane_PatchGuard_Analysis_RS4_v1.01.pdf
Интересное исследование для Win8:
https://pt-corp.storage.yandexcloud.net/upload/corporate/ru-ru/analytics/Windows_81_Kernel_Patch_Protection_Analysis.pdf#2#2
Сатоши Танда делится своим наблюдением:
Some Tips to Analyze PatchGuard