На страже ядра. Обнаруживаем руткиты с помощью нового плагина DRAKVUF
Содержание статьи
- Техники
- Inline-перехваты
- Перехват таблицы системных вызовов
- Перехваты IDT/GDT
- MSR LSTAR
- Перехваты функций DRIVER_OBJECT
- Техника сокрытия процесса от Task Manager
- Регистрация системных функций обратного вызова
- Заключение
Чаще всего при анализе поведения подозрительной программы наблюдают за пользовательским режимом, а код в ядре остается вне поля зрения аналитика. Отчасти это правильно, поскольку больше всего вредоносной деятельности ведется именно в пользовательском пространстве. Тем не менее вредоносный код в ядре может нанести больше ущерба, и его сложнее обнаружить. Однако это возможно.
С появлением KPP (Kernel Patch Protection), или сокращенно PatchGuard, в Windows стало сложнее модифицировать ядро без последствий. Если раньше, например, таблицу системных вызовов с целью фильтрации системных вызовов перехватывали даже такие легитимные программы, как антивирусы, то с появлением KPP это стало не так просто. Тем не менее для руткитов PatchGuard не представляет особой угрозы, поскольку техники его обхода можно найти в открытом доступе, они развиваются и актуальны по сей день. В этой статье будет рассматриваться множество техник модификации ядра, включая те, что обнаруживает PatchGuard, на примере нового плагина для DRAKVUF — rootkitmon.
Мы уже несколько лет разрабатываем песочницу для рискориентированной защиты, использующую фреймворк DRAKVUF. DRAKVUF — безагентная песочница, основанная на библиотеке LIBVMI и гипервизоре XEN. Все возможности DRAKVUF реализованы в плагинах. Каждый из двадцати с лишним плагинов выполняет определенную работу: для обнаружения доступа к файлам есть плагин filetracer, для трассировки системных вызовов — плагин syscalls. Rootkitmon — новый плагин, позволяющий отслеживать вредоносную активность в ядре средствами DRAKVUF.
Существует множество определений понятия «руткит», мы же будем опираться на следующее: «компьютерная программа, которая использует недокументированные и (или) запрещенные техники для манипуляции ядром операционной системы в своих целях».
ТЕХНИКИ
В «джентельменский набор» плагина входят возможности детектировать следующие типы перехватов:
- модификация кода загруженных драйверов в памяти (inline-перехваты);
- модификация таблиц системных вызовов, таблиц прерываний и таблиц дескрипторов;
- модификация регистра
MSR LSTAR
; - модификация указателей на функции
DRIVER_OBJECT
, стекDEVICE_OBJECT
; - сокрытие процесса из списка
EPROCESS
; - регистрация различных системных функций обратного вызова.
Поскольку руткитов намного меньше, чем вредоносного ПО в пользовательском пространстве, и время выполнения образца всегда ограниченно, уменьшение нагрузки плагина на работающую систему было приоритетной задачей. Для этого во многих случаях было решено сверять целостность критических структур в начале анализа и в конце, а не выполнять непосредственный перехват на запись страниц памяти.
Inline-перехваты
Рассмотрим самый распространенный тип перехватов и, возможно, самый легко обнаруживаемый — inline-перехваты. Inline-перехваты очень популярны, и даже Microsoft предоставляет возможность перехватить API-библиотеки, добавляя перед функциями двухбайтовый пролог вроде mov edi, edi для быстрого редактирования функциональности уже загруженных и работающих компонентов. Конечно, такие перехваты возможны только в пользовательском режиме, а в ядре караются синим экраном с кодом ошибки 0x109
, если PatchGuard не выключен.
Inline-перехваты обычно состоят из трех частей:
- подмена первых нескольких инструкций целевой функции для перенаправления потока выполнения на код своего приложения;
- обработка целевой функции: изменение параметров, фильтрация, логирование;
- выполнение подмененных инструкций и возврат на оригинальную функцию.
Рассмотрим простой пример вызова CreateFileW
из библиотеки kernel32.dll
. Пройдя все библиотеки, в итоге код окажется в ядерной функции nt!NtCreateFile
. Если бы руткит установил перехват на эту функцию, он бы мог выглядеть следующим образом.
Поскольку код находится в страницах с правами только на чтение и выполнение, для записи в такие страницы необходимо либо выделить новую виртуальную страницу с правами на запись и спроецировать ее на физическую страницу, где находится код, либо отключить бит Write Protect в специальном регистре управления CR0
, что позволит выполнять запись в страницы в обход их прав для текущего ядра.
Обнаружение таких перехватов сводится к подсчету контрольной суммы секций драйвера в момент начала анализа и пересчету, сверке контрольной суммы в конце. В отличие от PatchGuard, который защищает только небольшой список системных драйверов, мы можем проверять абсолютно все загруженные драйверы из списка PsLoadedModules
.
PsLoadedModules
— двусвязный список структур _KLDR_DATA_TABLE_ENTRY
, описывающих загруженный драйвер: его базовый адрес, размер, имя, характеристики и прочее.
Для перечисления загруженных модулей ядра с помощью DRAKVUF API мы реализовали метод drakvuf_enumerate_drivers
. В плагине rootkitmon список PsLoadedModules
проходится два раза: в начале инициализации плагина — для подсчета контрольных сумм секций драйверов — и в конце — для сравнения значений.
В случае расхождения в логе появится строка:
"\Device\HarddiskVolume2\Windows\System32\explorer.exe":KiDeliverApc SessionID:0 PID:1968 PPID:476 Reason:"Driver section modification" Driver:"ntoskrnl.exe"
Для отработки в конце анализа мы добавляем перехват часто вызываемой ядерной функции KiDeliverApc
. Поскольку мы заранее не знаем, какой поток вызовет эту функцию и в каком контексте процесса этот поток находился, имя процесса, его PID и PPID, которые DRAKVUF автоматически сохраняет в журнале, не имеют отношения к детекту, в то время как поле Reason и все поля, следующие за ним, имеют. В данном случае поле Reason означает обнаруженную вредоносную технику, а поле Driver — название модифицированного драйвера.
Перехват таблицы системных вызовов
SSDT (System Service Descriptor Table) — массив указателей на обработчики системных вызовов в 32-битных системах или список смещений относительно базового адреса таблицы на 64-битных ОС. Как говорилось ранее, до появления PatchGuard эта таблица активно использовалась легитимными программами, а также руткитами для фильтрации и мониторинга системных вызовов. Перезапись одного указателя в такой таблице равносильна перехвату всех вызовов определенного обработчика, что намного проще inline-перехватов, рассмотренных до этого.
На данный момент в ОС Windows существует два таких массива под символами nt!KiServiceTable
и win32k!W32pServiceTable
для системных вызовов к модулю ntoskrnl.exe
и графической подсистеме win32k.sys
соответственно. Количество элементов в этих массивах сохранено в переменных nt!KiServiceLimit
и win32k!W32pServiceLimit
.
Указатель на SSDT находится в специальной структуре KSYSTEM_SERVICE_TABLE
, или SST
, под именем ServiceTableBase
:
Описание структуры KSYSTEM_SERVICE_TABLE
typedef struct _KSYSTEM_SERVICE_TABLE
PULONG ServiceCounterTableBase;
} KSYSTEM_SERVICE_TABLE, *PKSYSTEM_SERVICE_TABLE;
В то же время массив этих структур лежит в таблицах nt!KeServiceDescriptorTable
и nt!KeServiceDescriptorTableShadow
. Если в первой хранится только SST для обработчиков системных вызовов NT, то во второй добавляется SST-таблица графической подсистемы win32k.
Можно заметить, что первая SST-структура в обеих таблицах одинакова. При системном вызове ОС Windows использует нижние 12 бит регистра eax
как индекс в одной из SSDT-таблиц, а биты 12–13 указывают, какую SDT-структуру выбрать: nt!KeServiceDescriptorTable
или nt!KeServiceDescriptorTableShadow
.
В первой части статьи уже шла речь о том, что на 64-битных системах SSDT содержит массив смещений, а виртуальный адрес обработчика системного вызова вычисляется по формуле
RoutineAddress = ServiceTableBase + (ServiceTableBase[index] >> 4)
Сдвиг на 4 необходим, поскольку первые 4 бита содержат количество параметров, передаваемых через стек.
Если взглянуть на пример с вызовом CreateFileW
, учитывая вышеизложенное, он будет выглядеть следующим образом.
Мониторинг модификации SSDT-таблиц заключается в выставлении ловушек на запись в физические страницы памяти данных таблиц и реализован в плагине ssdtmon.
Перехват nt!KiServiceTable
уже был реализован в этом плагине, и мы лишь добавили поддержку таблицы win32k!W32pServiceTable
. Также в плагине вредоносной считается запись, совершенная на 8 байт ниже начала таблицы. Это связано с тем, что с помощью 16-битных XMM-инструкций можно переписать первые 12 байт таблицы, записывая на 4 байта ниже ее начала. Тем самым можно обойти тривиальную проверку границ. Стоит заметить, что в то время, как таблица nt!KiServiceTable
находится в секции данных модуля ntoskrnl.exe
и доступна из всего ядерного пространства, win32k!W32pServiceTable
принадлежит модулю win32k.sys
и для доступа к данному драйверу нужно находиться в контексте какого‑либо графического процесса, например explorer.exe
.
Чтобы получить физический адрес страницы таблицы win32k!W32pServiceTable
, плагин находит PID графического процесса explorer.exe
и использует регистр CR3 этого процесса для трансляции виртуального адреса таблицы в физический:
Код получения значений win32k!W32pServiceTable и win32k!W32pServiceLimit
if (!drakvuf_get_process_pid(drakvuf, gui_process, &gui_pid))
PRINT_DEBUG("[SSDTMON] Failed to get PID of "explorer.exe"\n");
// Locate Win32k.sys base address
if (!get_driver_base(vmi, this, "win32k.sys", &w32k_base))
PRINT_DEBUG("[SSDTMON] Failed to find win32k.sys in PsLoadedModuleList\n");
if (VMI_SUCCESS != vmi_read_32_va(vmi, w32k_base + w32psl_rva, gui_pid, &this->w32pservicelimit))
PRINT_DEBUG("[SSDTMON] Failed to read W32pServiceLimit\n");
if (VMI_SUCCESS != vmi_translate_uv2p(vmi, w32k_base + w32pst_rva, gui_pid, &this->w32pservicetable))
PRINT_DEBUG("[SSDTMON] Failed to translate win32k!W32pServiceTable to physical address\n");
После чего выставляются ловушки на эти физические страницы, и, поскольку обе таблицы находятся в секциях READ ONLY, любая запись в них может расцениваться как вредоносная.
Перехваты IDT/GDT
Продолжая тему перехвата таблиц, отметим, что существует еще два типа, которые могут представлять интерес: таблицы IDT и GDT.
IDT (Interrupt Descriptor Table) — таблица прерываний в архитектуре х86, которая служит для корректного ответа на прерывания и исключения. В IDT используются следующие типы прерываний: аппаратные, программные и прерывания, зарезервированные процессором, — они называются исключениями (первые 32 штуки) и используются на случай возникновения некоторых недопустимых событий, таких как деление на ноль, ошибка трассировки, переполнение.
Регистр IDTR
содержит базовый адрес (32 бита в защищенном режиме, 64 бита в режиме IA-32e) и 16-битный размер таблицы в байтах. Инструкции LIDT
и SIDT
загружают и сохраняют регистр IDTR соответственно.
Таблица дескрипторов прерываний используется для того, чтобы показать процессору, какую процедуру обслуживания прерывания (ISR) вызывать для обработки исключения. Для этой же цели существует ассемблерная инструкция int N
, где N
— это номер прерывания.
Каждый элемент в таблице прерываний на 64-битных системах имеет структуру IDTENTRY64.
Реальный же адрес обработчика прерывания вычисляется по следующей формуле:
RoutineAddress = OffsetHigh << 32 + OffsetMiddle << 16 + OffsetLow
В плагине мы подсчитываем контрольную сумму всей IDT-таблицы на каждом логическом ядре и сохраняем для проверки в конце анализа.
На рисунке выше видно, что, помимо IDT, мы также перечисляем и сохраняем GDT-таблицу. GDT (Global Descriptor Table) — структура в архитектуре х86, помогающая процессору распределить память по сегментам. Каждый элемент таблицы, называемый сегментным дескриптором, содержит адрес, размер (лимит), флаги прав и атрибутов таких областей.
Адрес и размер таблицы хранится в отдельном системном регистре GDTR.
В режиме х64 из‑за линейной модели памяти каждый сегмент в таблице охватывает все адресное пространство и различие можно наблюдать лишь в флагах доступа к данным сегментам. К сожалению, мы не обнаружили руткиты, которые как‑либо модифицировали таблицу GDT. Но поскольку PatchGuard защищает ее, мы также добавили проверку на добавление, удаление и модификацию элементов таблицы.
Если IDT- или GDT-таблицы расходятся в начале и в конце анализа образца, плагин выписывает следующие строчки:
"\Device\HarddiskVolume2\Windows\System32\explorer.exe":KiDeliverApc SessionID:0 PID:1968 PPID:476 Reason:"IDT modification"
"\Device\HarddiskVolume2\Windows\System32\explorer.exe":KiDeliverApc SessionID:0 PID:1968 PPID:476 Reason:"GDT modification"
MSR LSTAR
Еще одна возможная цель руткитов — перезапись системного регистра MSR LSTAR (0xc0000082)
. Согласно спецификации, после системного вызова инструкцией syscall
в регистр RIP
будет загружено значение MSR LSTAR
, которое в Windows указывает на адрес системной функции KiSystemCall64
.
Как видно, первая инструкция в ядре после системного вызова — swapgs
. Согласно документации Intel, SWAPGS exchanges the current GS base register value with the value contained in MSR address C0000102H (IA32_KERNEL_GS_BASE).
В то время как пользовательский регистр gs
всегда указывает на TEB текущего потока, в ядре gs
будет указывать на Processor control region текущего ядра.
В отличие от SSDT-перехватов, перезапись LSTAR
позволяет перехватывать и фильтровать все системные вызовы сразу. Но для этого руткиту нужно сделать дополнительную работу, а именно: вызвать инструкцию swapgs
, сохранить пользовательский стек, загрузить ядерный стек. Часто в перехваченном обработчике LSTAR
можно встретить следующий код.
Rootkitmon обнаруживает модификацию MSR LSTAR
, сверяя его значение с адресом функции KiSystemCall64
в конце анализа. Мониторинг записи в MSR LSTAR
в DRAKVUF на данный момент невозможен. Во‑первых, перехватываться могут только записи во все MSR-регистры. Во‑вторых, перехват MSR на запись сильно нагружает систему, поскольку на каждом системном вызове инструкция swapgs
записывает в IA32_KERNEL_GS_BASE MSR
, как говорилось выше. В‑третьих, мониторинг MSR LSTAR
на запись часто приводит к ложным срабатываниям, поскольку, как мы заметили, PatchGuard сам перезаписывает этот MSR во время выполнения своих проверок.
ПЕРЕХВАТЫ ФУНКЦИЙ DRIVER_OBJECT
У каждого загруженного драйвера в системе есть структура DRIVER_OBJECT
. Она описывает состояние драйвера, который отвечает за ее инициализацию во время своей загрузки. В частности, для коммуникации с пользовательскими программами драйвер должен инициализировать массив MajorFunction
. Он, в свою очередь, содержит адреса обработчиков для всех запросов, на которые драйвер может отвечать. Например, когда пользовательское ПО будет использовать API ReadFile с описателем драйвера, Windows создаст специальный IRP-запрос и вызовет обработчик из массива MajorFunction
этого драйвера — в данном случае IRP_MJ_READ
.
Перезапись указателей на обработчики, как и в случае с SSDT, приведет к перехвату всех вызовов определенного типа запроса к драйверу. Особенность этой техники состоит в том, что PatchGuard защищает только небольшой список объектов драйверов и перехват любого драйвера не из списка не приведет к BSOD’у системы.
Помимо массива MajorFunction
, также существует массив быстрых IO-операций — FastIoDispatch
. В отличие от MajorFunction
, быстрые IO-операции предназначены исключительно для драйверов файловой системы и сетевых драйверов. Рассмотрим, что такое объекты и как можно их перечислять непосредственно из контекста гипервизора.
В Windows каждый ресурс (процесс, поток, файл и так далее) представлен в виде объекта и управляется диспетчером объектов Windows. Например, в Windows 10 1803 существует около 64 различных типов объектов.
Каждый объект состоит из двух частей:
- OBJECT_HEADER — специальный заголовок фиксированного размера, который находится перед телом объекта и которым владеет диспетчер объектов;
- Body — часть объекта, представляющая системный ресурс (
EPROCESS
для процессов,ETHREAD
для потоков,FILE_OBJECT
для файлов и так далее).
Многие объекты содержат данные, которые постоянны для всех объектов одного типа. Для экономии памяти диспетчер объектов хранит статичные данные одного типа объектов в специальной структуре OBJECT_TYPE
.
Все типы объектов хранятся в массиве под символом nt!ObTypeIndexTable
. В то же время в заголовке объекта находится поле TypeIndex
, которое указывает на индекс в данном массиве. Для наглядности найдем структуру типа объекта EPROCESS
.
Объекты хранятся в специальной директории _OBJECT_DIRECTY
. Указатель на корневую директорию содержится в nt!ObpRootDirectoryObject
.
Всего в директории может быть 37 записей, где каждая запись — указатель на связный список _OBJECT_DIRECTORY_ENTRY
со следующей структурой.
Object
— это тело объекта. Директории также могут быть вложенными, для них существует специальный тип Directory
, и, если Object
имеет данный тип, он трактуется как OBJECT_DIRECTORY
.
Соответственно, для перечисления всех объектов драйверов в системе rootkitmon рекурсивно обходит каждую директорию и сохраняет объект, если он относится к типу Driver.
У каждого полученного объекта подсчитывается контрольная сумма массивов MajorFunction
и FastIoDispatch
, а также указателей DriverStartIo
и DriverUnload
. Несовпадение контрольной суммы по результатам анализа означает вредоносную модификацию этих структур.
Помимо DRIVER_OBJECT
, мы также сохраняем каждый DEVICE_OBJECT
в стеке девайсов. DEVICE_OBJECT
может представлять логическое, физическое устройство или просто некие возможности драйвера. Сама структура DEVICE_OBJECT
всегда создается драйвером функцией IoCreateDevice
. Когда система посылает IRP-запрос драйверу, она направляет его какому‑либо девайсу. Массив MajorFunction
имеет указатели на обработчики запросов со следующим прототипом.
Первым аргументом всегда будет указатель на девайс‑объект, к которому был отправлен запрос. Соответственно, драйвер может иметь бесконечно много различных девайсов и обрабатывать каждый по‑своему.
Windows позволяет формировать стек девайс‑объектов. Обычно, когда на девайс оправляется запрос, его помогают обработать несколько драйверов. Каждый из этих драйверов связан с девайс‑объектом, и такие объекты расположены в стеке. Последовательность девайс‑объектов с соответствующими драйверами называется стеком девайс‑объектов. Например, так выглядит стек девайс‑объектов жесткого диска.
Видно, что запрос пройдет несколько «высокоуровневых» драйверов, прежде чем оказаться в обработчике контроллера жесткого диска stornvme. Такие запросы можно перехватывать, регистрируя свой драйвер в стек‑девайсе объектов с помощью IoAttachDeviceToDeviceStack
. Также Windows позволяет регистрировать специальный драйвер — мини‑фильтр с помощью FltRegisterFilter
, который может вызываться на различных IO-запросах. Отличный пример работы мини‑фильтров показан на рисунке ниже.
Хотя регистрация мини‑фильтра не считается вредоносной, перехват этой функции может дать отличное представление о возможностях анализируемого драйвера.
Если стек девайсов модифицирован или зарегистрирован мини‑фильтр, DRAKVUF покажет следующие строки:
"\Device\HarddiskVolume2\Windows\System32\explorer.exe":KiDeliverApc SessionID:0 PID:1968 PPID:476 Reason:"Driver stack modification"
"\Device\HarddiskVolume2\Windows\System32\explorer.exe":KiDeliverApc SessionID:0 PID:1968 PPID:476 Reason:"FltRegisterFilter"
ТЕХНИКА СОКРЫТИЯ ПРОЦЕССА ОТ TASK MANAGER
Одна из классических техник сокрытия процесса — удаление его из связного списка структур EPROCESS
. EPROCESS
— ядерная структура, описывающая состояние запущенного процесса. Другими словами, на каждый запущенный процесс заводится отдельная структура EPROCESS
. Система связывает каждую структуру EPROCESS
с другими с помощью двойного связного списка LIST_ENTRY
. Сам список состоит из двух элементов — указателя Flink
на следующую структуру EPROCESS
в цепочке и указателя Blink
на предыдущую структуру в цепочке. PsProcessActiveHead
— указатель на связный список процессов.
Так, при вызове команды cmd /c tasklist
или открытии Task Manager Windows просматривает связный список процессов и возвращает информацию по каждому из них. Чтобы процесс не высвечивался в данных списках, достаточно удалить его из цепочки. Для этого необходимо, чтобы Flink
предыдущего процесса в цепочке указывал на структуру следующего, а Blink
следующего процесса указывал на предыдущий. Также во избежание BSOD’ов ссылки скрытого процесса должны указывать друг на друга, как показано на рисунке ниже.
Для мониторинга сокрытия процесса из связного списка плагин rootkitmon перехватывает две системные функции: PspInsertProcess
и PspProcessDelete
. Как можно догадаться по названию, первая функция вставляет только что созданный процесс в связный список структур EPROCESS
, а вторая удаляет.
Также плагин содержит внутренний список всех запущенных процессов в системе, который заполняется при инициализации плагина и пополняется (или отчищается) при создании и завершении процесса.
Соответственно, если на завершении процесса его указатели Flink
и Blink
замкнуты — процесс был скрыт.
Но что, если процесс не был завершен в конце анализа? Для этого плагин перечисляет связный список структур EPROCESS
и сравнивает его с имеющимся в момент завершения: если процесс из старого списка не был найден в новом, он был скрыт.
Такой подход не нагружает систему и очень эффективен. Если будет обнаружен скрытый процесс, в логах DRAKVUF появится такая строка:
"\Device\HarddiskVolume2\Windows\System32\explorer.exe":KiDeliverApc SessionID:0 PID:1968 PPID:476 Reason:"Hidden process" HiddenPid:1624
где HiddenPid
— PID скрытого процесса.
РЕГИСТРАЦИЯ СИСТЕМНЫХ ФУНКЦИЙ ОБРАТНОГО ВЫЗОВА
Напоследок разберем несколько индикаторов, которые не являются вредоносными, но могут таковыми оказаться.
Microsoft предоставляет «официальный» способ перехвата системных событий с помощью регистрации функций обратного вызова (колбэков). Например, чтобы получать оповещения о создании или завершении процессов, драйвер может зарегистрировать колбэк с помощью API PsSetCreateProcessNotifyRoutine
, для подписки на создание потоков — PsSetCreateThreadNotifyRoutine
.
Только документированных колбэков больше пятнадцати, в DRAKVUF мы перехватываем следующие API регистрации колбэков:
PspSetCreateProcessNotifyRoutine
— подписка на создание/завершение процесса;PspSetCreateThreadNotifyRoutine
— подписка на создание/завершение потока;PsSetLoadImageNotifyRoutine
— подписка на загрузку модулей;CmpRegisterCallbackInternal
— подписка на работу с реестром;ObRegisterCallbacks
— подписка на получение описателей процесса, потока и рабочего стола;FsRtlRegisterFileSystemFilterCallbacks
— подписка на работу с файловой системой.
Список не исчерпывающий и в будущих ревизиях плагина может изменяться. Сама по себе регистрация колбэка не считается вредоносной, но дает хорошее представление, что делает анализируемый драйвер в песочнице. На текущий момент плагин просто перехватывает функции регистрации колбэков и фиксирует названия функций регистрации, если такие были вызваны.
ЗАКЛЮЧЕНИЕ
Плагин rootkitmon находится на ранних стадиях разработки, но уже позволяет обнаружить такие руткиты, как Moriya, TDSS, Necurs и многие другие. Rootkitmon не нагружает систему и поддерживает как Windows 7 x64, так и Windows 10 x64. Дальше количество техник обнаружения вредоносной активности будет только увеличиваться.