Статья Секреты механизма PatchGuard

Довольно часто диванные эксперты критикуют Microsoft, однако именно их инженеры представляют миру свежие идеи и системные механизмы, одним из которых является нашумевший защитник Patch-Guard. Углубившись в его детали с отладчиком WinDbg в руках быстро приходит понимание того, насколько оригинально продумана эта защита, отодрать которую из ядра не так просто, как может показаться на первый взгляд. В данной статье мы рассмотрим лишь наиболее привлекательные моменты, и она не претендует на полное изложение всех деталей работы PG, т.к. группа исследователей из компании "Tetrane" уже продемонстрировала в документе из 61-ой страницы свой тех-обзор.

Содержание:
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.

PageDir.webp

Константы 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 МБ (см.диспетчер задач, вкладку быстродействие).

Npool.webp

А вот выхлоп второго плагина от Сатоши - к нему прилагается справка, где сказано:

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.

InitAmd.webp

Как и следует из её названия, эта функция настраивает контекст проверки. Сначала расшифровывает тушку 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).

ProtList.webp


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
 
Мы в соцсетях:

Взломай свой первый сервер и прокачай скилл — Начни игру на HackerLab

🚀 Первый раз на Codeby?
Гайд для новичков: что делать в первые 15 минут, ключевые разделы, правила
Начать здесь →
🧭 Навигатор · ИБ 2026
Не знаешь, какой трек твой?
5 направлений ИБ, реальные зарплаты и точка входа для каждого — в одном треде.
JuniorSenior+
100K → 600K+ ₽ /мес
Открыть навигатор →
🔴 Свежие CVE, 0-day и инциденты
То, о чём ChatGPT ещё не знает — обсуждаем в реальном времени
Threat Intel →
💼 Вакансии и заказы в ИБ
Pentest, SOC, DevSecOps, bug bounty — работа и проекты от проверенных компаний
Карьера в ИБ →

HackerLab