PDA

Просмотр полной версии : Rootkit обнаружение Windows: DKOM, SSDT hooking, minifilter и callback-манипуляции через WinDbg и Volatility


Сергей Попов
15.04.2026, 12:30
https://forum.antichat.xyz/attachments/4951472/img_6ef858b08c.png

Антивирус говорит «система чиста», а сетевой трафик тем временем утекает на подозрительный C2-сервер. Знакомая картина? Значит, вы наткнулись на kernel-mode руткит. Не на ту поделку, что прячет файлы через перехват FindFirstFile в user-mode, а на зверя, который работает на одном уровне привилегий с самой ОС и напрямую ковыряет структуру ядра.

По данным Positive Technologies, руткиты - меньше 1% от всех вредоносных программ. Но каждый такой случай - это серьёзный инцидент: APT-кампании, скрытый майнинг на десятках тысяч машин. Причина простая: руткит режима ядра - верхняя точка эскалации, после которой атакующий контролирует всё, что видит (и чего не видит) операционная система.

В MITRE ATT&CK руткиты описаны как техника T1014 (Rootkit). Но реальный руткит - это не одна техника, а комбо (полную классификацию с матрицей обнаружения см. в обзоре техник руткитов (https://forum.antichat.xyz/threads/592911/)): DKOM для скрытия процессов, SSDT hooking для перехвата системных вызовов, minifilter-драйверы для фильтрации файлового I/O и манипуляции с kernel callbacks, чтобы ослепить средства защиты (T1562.001, Disable or Modify Tools). Дальше разберём каждую из этих техник на уровне структур ядра Windows и покажем, как их ловить с помощью WinDbg и Volatility - не по туториалу из первой ссылки в Google, а по реальной методологии анализа.
DKOM атака Windows: как работает скрытие процессов через ActiveProcessLinks
DKOM (Direct Kernel Object Manipulation) - классика kernel-mode руткитов. Атакующий напрямую правит структуры данных ядра без штатных API. Самый ходовой сценарий - скрытие процесса через удаление из двусвязного списка

ActiveProcessLinks

.
Механика на уровне ядра
Каждый процесс в Windows представлен структурой

EPROCESS

. Эти структуры связаны через поле

ActiveProcessLinks

- элемент типа

LIST_ENTRY

с указателями

Flink

(forward link, на следующий процесс) и

Blink

(backward link, на предыдущий). Главный списка - глобальная переменная ядра

PsActiveProcessHead

.

Когда диспетчер задач или Process Explorer запрашивают список процессов через

NtQuerySystemInformation

, ядро обходит именно этот список. Руткит делает классическое удаление узла из двусвязного списка:

Код:



// Концептуальный псевдокод DKOM-скрытия
// Для демонстрации механизма - не рабочий эксплойт

PEPROCESS targetProcess; // EPROCESS скрываемого процесса
PLIST_ENTRY prevEntry = targetProcess->ActiveProcessLinks.Blink;
PLIST_ENTRY nextEntry = targetProcess->ActiveProcessLinks.Flink;

// Перелинковка: предыдущий указывает на следующий, минуя целевой
prevEntry->Flink = nextEntry;
nextEntry->Blink = prevEntry;

// Замыкаем ссылки скрытого процесса на самого себя
targetProcess->ActiveProcessLinks.Flink = &targetProcess->ActiveProcessLinks;
targetProcess->ActiveProcessLinks.Blink = &targetProcess->ActiveProcessLinks;


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

PsActiveProcessHead

. Именно так работали руткиты FU и его потомки.

Нюанс: DKOM не ограничивается процессами. Та же техника применяется к спискам драйверов (

PsLoadedModuleList

), потокам и другим объектам ядра. По MITRE ATT&CK скрытие объектов ядра через DKOM - это T1014 (Rootkit) и более общая T1564 (Hide Artifacts).
Обнаружение DKOM в WinDbg
Первый шаг - получить список процессов двумя независимыми способами и сравнить. В WinDbg в kernel-mode сессии:

Код:



kd> !process 0 0


Эта команда обходит

PsActiveProcessHead

- тот самый список, из которого DKOM удаляет записи. Скрытый процесс здесь не всплывёт.

Теперь сравним с другим источником - пулом памяти ядра. Каждый

EPROCESS

распределён из nonpaged pool с тегом

Proc

:

Код:



kd> !poolfind Proc 0


Если pool scan находит

EPROCESS

-блок, чей PID отсутствует в выводе

!process 0 0

- это прямой индикатор DKOM. Имейте в виду:

!poolfind

может молотить десятки минут на больших дампах. На Windows 10 19H1+ (build 18362) и новее с segment heap pool scanning через

!poolfind

менее надёжен (можно предварительно проверить наличие тега через

!poolused Proc

) - в таких случаях лучше использовать

psscan

в Volatility. Также полезно проверить структуру конкретного подозрительного процесса:

Код:



kd> dt nt!_EPROCESS


Смотрим поля

ActiveProcessLinks.Flink

и

ActiveProcessLinks.Blink

- если оба указывают на саму структуру (замкнуты на себя), процесс был unlinkован из списка. Это как отпечаток пальца на месте преступления.

Для проверки целостности цепочки можно пройти весь список вручную:

Код:



kd> dl nt!PsActiveProcessHead 200 2


Команда

dl

проходит по двусвязному списку. Если при обходе вперёд и назад получаем разное количество элементов - список повреждён, что тоже указывает на DKOM.
Обнаружение DKOM в Volatility
Volatility решает проблему обнаружения DKOM через кросс-валидацию - сравнение разных источников информации о процессах. Ключевой плагин -

psxview

(только в Volatility 2). В Volatility 3 встроенного аналога нет - кросс-валидация делается вручную через сравнение выводов

windows.pslist

и

windows.psscan

.

Bash:



# Volatility 2
python vol.py -f memory.dmp --profile
=
Win10x64 psxview
# Volatility 3
python vol.py -f memory.dmp windows.pslist
python vol.py -f memory.dmp windows.psscan



psxview

сравнивает результаты из нескольких источников:

ИсточникЧто проверяетОбходится DKOMPsActiveProcessHeadДвусвязный список EPROCESSДаETHREAD scanningПотоки, ссылающиеся на процессНетPool tag scanning (psscan)Сигнатуры EPROCESS в памятиНетPspCidTableТаблица объектов по PID/TIDЧастичноCsrss handlesХэндлы csrss.exe к процессамЧастичноSession listСписок процессов в сессииЧастично

Если процесс виден в

psscan

(сканирование пула), но отсутствует в

pslist

(обход

ActiveProcessLinks

) - перед вами DKOM. Классический паттерн: в столбце

pslist

значение False, а в

psscan

- True. Лично у меня это был самый частый способ обнаружения - просто diff двух выводов.

Bash:



# Прямое сравнение двух плагинов
python vol.py -f memory.dmp windows.pslist
>
pslist_output.txt
python vol.py -f memory.dmp windows.psscan
>
psscan_output.txt
# Любой PID из psscan, отсутствующий в pslist - кандидат на DKOM


SSDT hooking rootkit: перехват таблицы системных вызовов
SSDT (System Service Descriptor Table) - таблица с адресами обработчиков системных вызовов ядра. Когда user-mode код вызывает

NtCreateFile

, процессор через

syscall

/

sysenter

переходит в kernel-mode, где диспетчер

KiSystemService

использует номер системного вызова как индекс в SSDT для вызова нужной функции. В MITRE ATT&CK SSDT hooking описывается техникой T1014 (Rootkit), тактика Defense Evasion. Примечание: тесты Atomic Red Team для T1014 есть только для Linux; на Windows валидация требует других подходов. Ранее существовала техника T1179 (Hooking), покрывавшая Persistence, Privilege Escalation и Credential Access, но её отозвали (deprecated) начиная с ATT&CK v8 (октябрь 2020); часть функциональности мигрировала в T1056.004 (Credential API Hooking) и другие суб-техники.
Механика SSDT hook
Руткит подменяет адрес в таблице SSDT, заставляя ядро вызывать свой обработчик вместо оригинального. Через это можно перехватить что угодно: скрывать файлы через хук

NtQueryDirectoryFile

, процессы через хук

NtQuerySystemInformation

, сетевые соединения через хук

NtDeviceIoControlFile

.

На современных 64-битных Windows SSDT хранит не абсолютные адреса, а относительные смещения. Каждая запись - 4-байтовое значение, где верхние 28 бит - смещение от базы

KeServiceDescriptorTable

, а нижние 4 бита - количество аргументов, передаваемых через стек. Это усложняет hooking по сравнению с 32-битными системами, но не делает его невозможным.

На 64-битных Windows с активным PatchGuard (Kernel Patch Protection) прямая модификация SSDT вызывает BSOD с кодом

CRITICAL_STRUCTURE_CORRUPTION

. Однако техники обхода PatchGuard задокументированы, и некоторые руткиты - Turla, FiveSys - успешно их применяли. PatchGuard проверяет целостность периодически, а не в реальном времени, что создаёт окно для атаки. Так-что не стоит считать его непробиваемым щитом.
Обнаружение SSDT hooking через WinDbg
Смотрим базу SSDT и проверяем, что все адреса указывают внутрь ntoskrnl.exe:

Код:



kd> dps nt!KiServiceTable L 0x200


Команда выведет таблицу с адресами и символами. Все записи должны резолвиться (DNS resolution) в функции

nt!Nt*

. Если какая-то запись указывает на адрес вне диапазона ntoskrnl - это хук, и тут уже пора напрягаться.

Проверяем диапазон ntoskrnl:

Код:



kd> lm m nt


Запоминаем Start и End адреса. Любой адрес в SSDT, выходящий за этот диапазон, - аномалия. На 64-битных системах с относительными смещениями:

Код:



kd> dd nt!KiServiceTable L 0x10


Каждое 4-байтовое значение - relative offset. Для преобразования в абсолютный адрес:

базовый_адрес + (offset >> 4)

. Если результат выходит за пределы ntoskrnl - хук.

Для проверки конкретного системного вызова, например

NtCreateFile

(syscall номер зависит от версии Windows):

Код:



kd> u nt!NtCreateFile


Если первые инструкции содержат

jmp

на адрес вне ntoskrnl - это inline hook. Ещё один вариант перехвата, при котором сама SSDT не тронута, но код функции подменён. Хитрее, но и обнаруживается несложно.
Обнаружение SSDT hooks в Volatility

Bash:



# Volatility 2
python vol.py -f memory.dmp --profile
=
Win10x64 ssdt
# Volatility 3
python vol.py -f memory.dmp windows.ssdt


Плагин

ssdt

выводит каждую запись SSDT с resolved символами. Ключевой индикатор: если функция резолвится не в

ntoskrnl.exe

или

win32k.sys

(для Shadow SSDT), а в неизвестный модуль или адрес вообще без модуля - это hooking.

Код:



# Пример подозрительного вывода (для демонстрации концепции)
Entry 0x0049: 0xfffff880012345678 unknown_driver.sys!HookedNtQueryDirectoryFile


Нормальные записи выглядят так:

Код:



Entry 0x0049: 0xfffff80001234567 ntoskrnl.exe!NtQueryDirectoryFile


Minifilter driver abuse: невидимая фильтрация файловых операций
Minifilter-драйверы - легитимный фреймворк Windows (Filter Manager,

fltmgr.sys

) для перехвата файловых операций ввода-вывода. Антивирусы, DLP-системы, средства шифрования - все используют minifilter для сканирования файлов на лету. Но тот же механизм с удовольствием эксплуатируют руткиты.
Как работает minifilter
Filter Manager организует minifilter-драйверы в стек по altitude - числовому значению, определяющему приоритет обработки. Антивирусные фильтры обычно имеют altitude 320000–329999, фильтры шифрования - 140000–149999. Каждый minifilter регистрирует callback-функции для конкретных операций (Pre-Operation и Post-Operation): создание файла, чтение, запись, запрос директории.

Вредоносный minifilter может:

Скрывать файлы руткита, фильтруя результаты

IRP_MJ_DIRECTORY_CONTROL


Блокировать доступ антивируса к вредоносным файлам через

IRP_MJ_CREATE


Подменять содержимое файлов при чтении через

IRP_MJ_READ


Перехватывать и эксфильтровать записываемые данные
По MITRE ATT&CK это связано с T1547.006 (Kernel Modules and Extensions) - сохранение состояния системы с помощью драйвера, T1014 (Rootkit) - скрытие файлов через minifilter callbacks, и T1562.001 (Disable or Modify Tools) - блокировка доступа антивируса к вредоносным файлам.
Обнаружение вредоносных minifilter-драйверов
В WinDbg проверяем загруженные minifilter-драйверы:

Код:



kd> !fltkd.filters


Расширение

!fltkd

даёт полный доступ к внутренним структурам Filter Manager. Команда выведет все зарегистрированные фильтры с их altitude и количеством зарегистрированных операций.

Код:



kd> !fltkd.frames


Показывает фреймы Filter Manager и все присоединённые фильтры. На что смотреть:

Фильтры с необычными именами или без цифровой подписи

Фильтры с altitude, не соответствующим их заявленному назначению (антивирус с altitude шифровальщика - подозрительно)

Фильтры, зарегистрировавшие callbacks для

IRP_MJ_DIRECTORY_CONTROL

или

IRP_MJ_CREATE

- именно они используются для скрытия файлов
Для проверки конкретного фильтра:

Код:



kd> !fltkd.filter


Покажет все зарегистрированные Pre/Post callback-функции. Адрес каждого callback должен резолвиться в известный модуль.

В Volatility minifilter-анализ доступен через плагин

driverirp

и анализ загруженных модулей:

Bash:



# driverirp -r fltmgr показывает IRP dispatch table самого fltmgr.sys,
# а НЕ callback-функции зарегистрированных minifilter-драйверов.
# Для анализа minifilter-стека используйте WinDbg с !fltkd.filters.
python vol.py -f memory.dmp --profile
=
Win10x64 driverirp -r
"fltmgr"


Для обнаружения скрытых minifilter-драйверов через Volatility используйте

driverscan

и

modscan

- они находят драйверы, скрытые из штатного списка загруженных модулей (

PsLoadedModuleList

), но всё ещё присутствующие в памяти. Для полного анализа minifilter-стека (зарегистрированные callback-функции, altitude, перехватываемые операции) основной метод - WinDbg с расширением

!fltkd.filters

; в продвинутых случаях возможен ручной анализ структур

FLT_FILTER

в дампе памяти, но это уже для любителей острых ощущений.
Kernel callback manipulation: нейтрализация защитных механизмов
Ядро Windows предоставляет набор notification callback-механизмов, которые средства защиты используют для мониторинга системных событий. Руткиты манипулируют этими callback-таблицами, чтобы «ослепить» антивирусы и EDR.
Целевые callback-механизмы

CallbackФункция регистрацииНазначениеProcess creationPsSetCreateProcessNotifyRoutine(Ex)Уве омление о создании/завершении процессовThread creationPsSetCreateThreadNotifyRoutineУведом ление о создании потоковImage loadPsSetLoadImageNotifyRoutineУведомлен е о загрузке образов (DLL, EXE)RegistryCmRegisterCallbackExУведомлен ие об операциях с реестромObject accessObRegisterCallbacksФильтрация доступа к объектам (процессам, потокам)

EDR-решения регистрируют свои callback-функции через эти API. Руткит может:

Удалить callback из массива (по аналогии с DKOM - unlinking)

Подменить адрес callback-функции на заглушку, которая тупо ничего не делает

Пропатчить код самой callback-функции (inline hook), заставляя её сразу возвращать управление
Это прямая реализация техники Disable or Modify Tools (T1562.001, Defense Evasion) - атакующий не уничтожает средство защиты, а тихо выключает ему глаза и уши.
Обнаружение callback-манипуляций в WinDbg
Проверяем массив notification routines для процессов:

Код:



kd> dt nt!_EX_CALLBACK_ROUTINE_BLOCK


Для проверки зарегистрированных Process notification callbacks:

Код:



kd> x nt!PspCreateProcessNotifyRoutine
kd> dps nt!PspCreateProcessNotifyRoutine L 0x40


Каждый ненулевой элемент - указатель на

EX_CALLBACK_ROUTINE_BLOCK

. Значения должны резолвиться в известные модули защитных средств. Если слот обнулён или указывает на неизвестный модуль - callback был удалён или подменён. Вот тут-то и начинается самое интересное.

Аналогично для потоков и загрузки образов:

Код:



kd> dps nt!PspLoadImageNotifyRoutine L 0x40
kd> dps nt!PspCreateThreadNotifyRoutine L 0x40


Для ObRegisterCallbacks проверка сложнее - нужно пройти по списку

ObTypeInitializer

для объектов типа

Process

и

Thread

:

Код:



kd> !object \ObjectTypes\Process
kd> dt nt!_OBJECT_TYPE -r1


Обнаружение через Volatility

Bash:



# Volatility 2
python vol.py -f memory.dmp --profile
=
Win10x64 callbacks
# Volatility 3
python vol.py -f memory.dmp windows.callbacks


Плагин

callbacks

покажет все зарегистрированные kernel notification callbacks с указанием модуля-владельца. Красные флаги:

Callback, принадлежащий неизвестному или неподписанному модулю

Отсутствие callbacks от EDR, который точно установлен на системе (EDR есть, а его callbacks нет - значит, кто-то их вычистил)

Callback-функция в области памяти без привязки к модулю - это шеллкод
Практический workflow обнаружения руткитов Windows
Ниже - пошаговая методология для реального дампа памяти подозрительной системы. Последовательность выстроена так, чтобы каждый шаг сужал область поиска.
Шаг 1: Снятие дампа памяти
Используем WinPmem или DumpIt для получения raw memory dump. Если подозревается руткит - не доверяйте инструментам, работающим из-под скомпрометированной системы. Идеально - DMA-устройство (PCILeech) или дамп через гипервизор. На практике часто приходится довольствоваться WinPmem с USB-носителя - не идеал, но лучше, чем ничего.

Bash:



# WinPmem (запуск с USB-носителя)
winpmem_mini_x64.exe output.raw


Шаг 2: Кросс-валидация списков процессов

Bash:



python vol.py -f output.raw windows.pslist
>
step2_pslist.txt
python vol.py -f output.raw windows.psscan
>
step2_psscan.txt


Сравниваем PID из обоих файлов. Любое расхождение - первый индикатор DKOM.
Шаг 3: Проверка SSDT и драйверов

Bash:



python vol.py -f output.raw windows.ssdt
>
step3_ssdt.txt
python vol.py -f output.raw windows.modules
>
step3_modules.txt
python vol.py -f output.raw windows.driverscan
>
step3_drivers.txt


В выводе

ssdt

все записи должны указывать на

ntoskrnl.exe

или

win32k.sys

. В

driverscan

ищем драйверы, отсутствующие в

modules

(аналог DKOM, только для модулей).
Шаг 4: Анализ callbacks

Bash:



python vol.py -f output.raw windows.callbacks
>
step4_callbacks.txt


Проверяем наличие callbacks от установленных защитных средств. Если EDR установлен, но его callbacks отсутствуют - руткит их вычистил.
Шаг 5: Углублённый анализ в WinDbg
Если на шагах 2-4 обнаружены аномалии, открываем дамп в WinDbg для детальной процедуры:

Код:



kd> .sympath srv*c:\symbols*https://msdl.microsoft.com/download/symbols
kd> .reload /f

;// Проверяем подозрительный процесс из psscan
kd> dt nt!_EPROCESS
kd> !process 7

;// Смотрим потоки скрытого процесса
kd> !process 4


Поле

ActiveProcessLinks

у скрытого процесса покажет замкнутые на себя указатели. Поле

ImageFileName

- имя исполняемого файла руткита.
Шаг 6: Восстановление полной картины
Для каждого обнаруженного артефакта фиксируем:

PID и имя скрытого процесса

Загруженные им модули (

!process 7

покажет VAD-дерево)

Сетевые соединения (

windows.netscan

в Volatility)

Файлы, открытые процессом (

windows.handles

)
PatchGuard и современные ограничения руткитов
На 64-битных Windows PatchGuard (Kernel Patch Protection) защищает критические структуры ядра: SSDT, GDT, IDT, системные образы, структуры объектов процессов. Периодические проверки целостности вызывают BSOD при обнаружении модификаций.

Классический SSDT hooking на современных 64-битных Windows - техника с высоким риском словить BSOD. Поэтому современные руткиты смещают фокус:

Minifilter-драйверы - легитимный механизм, PatchGuard не проверяет

Callback-манипуляции - менее заметны, чем прямые модификации SSDT

DKOM с осторожным восстановлением после проверок PatchGuard

Обход через уязвимости в подписанных драйверах (BYOVD - Bring Your Own Vulnerable Driver)
Руткит FiveSys использовал украденную цифровую подпись Microsoft WHQL, что позволяло ему загружаться как легитимный драйвер и обходить проверки целостности. Это наглядно показывает, что даже PatchGuard и DSE (Driver Signature Enforcement) - не абсолютная защита. На заборе тоже написано «защищено», а на деле..
Что упускают стандартные средства защиты
Большинство антивирусов работают в user-mode или используют ограниченный набор kernel callbacks. Kernel-mode руткит, работающий на том же уровне привилегий, может:

Скрыть свой процесс через DKOM - антивирус не увидит его в списке процессов

Удалить callbacks антивируса - тот перестанет получать уведомления о событиях

Зарегистрировать minifilter с более высоким altitude - обработать запрос к файлу раньше, чем до него доберётся антивирус
Именно поэтому для обнаружения kernel-mode руткитов нужен офлайн-анализ памяти через Volatility (https://forum.antichat.xyz/threads/592233/) или анализ через отладчик ядра (WinDbg) - подходы, которые работают извне скомпрометированного ядра и не зависят от API, которые руткит может контролировать.

Rootkit обнаружение Windows - это всегда комбинация: кросс-валидация (сравнение нескольких источников данных о процессах), проверка целостности критических структур (SSDT, callback-массивы) и анализ загруженных драйверов (minifilter-стек, неподписанные модули). Ни один инструмент не ловит всё - поэтому WinDbg и Volatility работают в паре, компенсируя ограничения друг друга. Попробуйте прогнать свой дамп по workflow из шагов 1-6 - даже на «чистой» системе результаты бывают... познавательными.