Чаще всего при анализе поведения подозрительной программы наблюдают за пользовательским режимом, а код в ядре остается вне поля зрения аналитика. Отчасти это правильно, поскольку больше всего вредоносной деятельности ведется именно в пользовательском пространстве. Тем не менее вредоносный код в ядре может нанести больше ущерба, и его сложнее обнаружить. Однако это возможно.
С появлением KPP (Kernel Patch Protection), или сокращенно PatchGuard, в Windows стало сложнее модифицировать ядро без последствий. Если раньше, например, таблицу системных вызовов с целью фильтрации системных вызовов перехватывали даже такие легитимные программы, как антивирусы, то с появлением KPP это стало не так просто. Тем не менее для руткитов PatchGuard не представляет особой угрозы, поскольку техники его обхода можно найти в открытом доступе, они развиваются и актуальны по сей день. В этой статье будет рассматриваться множество техник модификации ядра, включая те, что обнаруживает PatchGuard, на примере нового плагина для DRAKVUF — rootkitmon.
Мы уже несколько лет разрабатываем песочницу для рискориентированной защиты, использующую фреймворк DRAKVUF. DRAKVUF — безагентная песочница, основанная на библиотеке LIBVMI и гипервизоре XEN. Все возможности DRAKVUF реализованы в плагинах. Каждый из двадцати с лишним плагинов выполняет определенную работу: для обнаружения доступа к файлам есть плагин filetracer, для трассировки системных вызовов — плагин syscalls. Rootkitmon — новый плагин, позволяющий отслеживать вредоносную активность в ядре средствами DRAKVUF.
Существует множество определений понятия «руткит», мы же будем опираться на следующее: «компьютерная программа, которая использует недокументированные и (или) запрещенные техники для манипуляции ядром операционной системы в своих целях».
Inline-перехваты обычно состоят из трех частей:
Схема системного вызова
Поскольку код находится в страницах с правами только на чтение и выполнение, для записи в такие страницы необходимо либо выделить новую виртуальную страницу с правами на запись и спроецировать ее на физическую страницу, где находится код, либо отключить бит Write Protect в специальном регистре управления CR0, что позволит выполнять запись в страницы в обход их прав для текущего ядра.
Структура регистра управления CR0
Описание значения флага Write Protect
Обнаружение таких перехватов сводится к подсчету контрольной суммы секций драйвера в момент начала анализа и пересчету, сверке контрольной суммы в конце. В отличие от PatchGuard, который защищает только небольшой список системных драйверов, мы можем проверять абсолютно все загруженные драйверы из списка PsLoadedModules.
PsLoadedModules — двусвязный список структур _KLDR_DATA_TABLE_ENTRY, описывающих загруженный драйвер: его базовый адрес, размер, имя, характеристики и прочее.
Двусвязный список загруженных модулей
https://st768.s3.eu-central-1.amazonaws.com/a3a40bf47e56509b90045a971f96a1f0/23149/image5.png
Для перечисления загруженных модулей ядра с помощью DRAKVUF API мы реализовали метод drakvuf_enumerate_drivers. В плагине rootkitmon список PsLoadedModules проходится два раза: в начале инициализации плагина — для подсчета контрольных сумм секций драйверов — и в конце — для сравнения значений.
Перечисление и подсчет контрольных сумм секций драйвера
В случае расхождения в логе появится строка:
Для отработки в конце анализа мы добавляем перехват часто вызываемой ядерной функции KiDeliverApc. Поскольку мы заранее не знаем, какой поток вызовет эту функцию и в каком контексте процесса этот поток находился, имя процесса, его PID и PPID, которые DRAKVUF автоматически сохраняет в журнале, не имеют отношения к детекту, в то время как поле Reason и все поля, следующие за ним, имеют. В данном случае поле Reason означает обнаруженную вредоносную технику, а поле Driver — название модифицированного драйвера.
На данный момент в ОС Windows существует два таких массива под символами nt!KiServiceTable и win32k!W32pServiceTable для системных вызовов к модулю ntoskrnl.exe и графической подсистеме win32k.sys соответственно. Количество элементов в этих массивах сохранено в переменных nt!KiServiceLimit и win32k!W32pServiceLimit.
Значения структур nt!KiServiceTable и win32k!W32pServiceTable
Указатель на SSDT находится в специальной структуре KSYSTEM_SERVICE_TABLE, или SST, под именем ServiceTableBase:
В то же время массив этих структур лежит в таблицах nt!KeServiceDescriptorTable и nt!KeServiceDescriptorTableShadow. Если в первой хранится только SST для обработчиков системных вызовов NT, то во второй добавляется SST-таблица графической подсистемы win32k.
Значения таблиц nt!KeServiceDescriptorTable и nt!KeServiceDescriptorTableShadow
Можно заметить, что первая SST-структура в обеих таблицах одинакова. При системном вызове ОС Windows использует нижние 12 бит регистра eax как индекс в одной из SSDT-таблиц, а биты 12–13 указывают, какую SDT-структуру выбрать: nt!KeServiceDescriptorTable или nt!KeServiceDescriptorTableShadow.
В первой части статьи уже шла речь о том, что на 64-битных системах SSDT содержит массив смещений, а виртуальный адрес обработчика системного вызова вычисляется по формуле
Сдвиг на 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 этого процесса для трансляции виртуального адреса таблицы в физический:
После чего выставляются ловушки на эти физические страницы, и, поскольку обе таблицы находятся в секциях READ ONLY, любая запись в них может расцениваться как вредоносная.
IDT (Interrupt Descriptor Table) — таблица прерываний в архитектуре х86, которая служит для корректного ответа на прерывания и исключения. В IDT используются следующие типы прерываний: аппаратные, программные и прерывания, зарезервированные процессором, — они называются исключениями (первые 32 штуки) и используются на случай возникновения некоторых недопустимых событий, таких как деление на ноль, ошибка трассировки, переполнение.
Регистр IDTR содержит базовый адрес (32 бита в защищенном режиме, 64 бита в режиме IA-32e) и 16-битный размер таблицы в байтах. Инструкции LIDT и SIDT загружают и сохраняют регистр IDTR соответственно.
Структура регистров системных таблиц
Пример значений таблицы прерываний
Таблица дескрипторов прерываний используется для того, чтобы показать процессору, какую процедуру обслуживания прерывания (ISR) вызывать для обработки исключения. Для этой же цели существует ассемблерная инструкция int N, где N — это номер прерывания.
Каждый элемент в таблице прерываний на 64-битных системах имеет структуру IDTENTRY64.
Описание структуры KIDTENTRY64
Реальный же адрес обработчика прерывания вычисляется по следующей формуле:
Пример подсчета адреса обработчика прерываний
В плагине мы подсчитываем контрольную сумму всей IDT-таблицы на каждом логическом ядре и сохраняем для проверки в конце анализа.
Код перечисления и сохранения значений дескрипторов
На рисунке выше видно, что, помимо IDT, мы также перечисляем и сохраняем GDT-таблицу. GDT (Global Descriptor Table) — структура в архитектуре х86, помогающая процессору распределить память по сегментам. Каждый элемент таблицы, называемый сегментным дескриптором, содержит адрес, размер (лимит), флаги прав и атрибутов таких областей.
Структура дескриптора сегмента
Адрес и размер таблицы хранится в отдельном системном регистре GDTR.
Структура системного регистра GDTR
В режиме х64 из‑за линейной модели памяти каждый сегмент в таблице охватывает все адресное пространство и различие можно наблюдать лишь в флагах доступа к данным сегментам. К сожалению, мы не обнаружили руткиты, которые как‑либо модифицировали таблицу GDT. Но поскольку PatchGuard защищает ее, мы также добавили проверку на добавление, удаление и модификацию элементов таблицы.
Если IDT- или GDT-таблицы расходятся в начале и в конце анализа образца, плагин выписывает следующие строчки:
Значение регистра MSR LSTAR
Как видно, первая инструкция в ядре после системного вызова — 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 текущего ядра.
Описание использования MSR LSTAR
В отличие от SSDT-перехватов, перезапись LSTAR позволяет перехватывать и фильтровать все системные вызовы сразу. Но для этого руткиту нужно сделать дополнительную работу, а именно: вызвать инструкцию swapgs, сохранить пользовательский стек, загрузить ядерный стек. Часто в перехваченном обработчике LSTAR можно встретить следующий код.
Пример перехваченного KiSystemCall64
Rootkitmon обнаруживает модификацию MSR LSTAR, сверяя его значение с адресом функции KiSystemCall64 в конце анализа. Мониторинг записи в MSR LSTAR в DRAKVUF на данный момент невозможен. Во‑первых, перехватываться могут только записи во все MSR-регистры. Во‑вторых, перехват MSR на запись сильно нагружает систему, поскольку на каждом системном вызове инструкция swapgs записывает в IA32_KERNEL_GS_BASE MSR, как говорилось выше. В‑третьих, мониторинг MSR LSTAR на запись часто приводит к ложным срабатываниям, поскольку, как мы заметили, PatchGuard сам перезаписывает этот MSR во время выполнения своих проверок.
Код перехвата MSR LSTAR
Описание структуры _DRIVER_OBJECT
Перезапись указателей на обработчики, как и в случае с SSDT, приведет к перехвату всех вызовов определенного типа запроса к драйверу. Особенность этой техники состоит в том, что PatchGuard защищает только небольшой список объектов драйверов и перехват любого драйвера не из списка не приведет к BSOD’у системы.
Помимо массива MajorFunction, также существует массив быстрых IO-операций — FastIoDispatch. В отличие от MajorFunction, быстрые IO-операции предназначены исключительно для драйверов файловой системы и сетевых драйверов. Рассмотрим, что такое объекты и как можно их перечислять непосредственно из контекста гипервизора.
В Windows каждый ресурс (процесс, поток, файл и так далее) представлен в виде объекта и управляется диспетчером объектов Windows. Например, в Windows 10 1803 существует около 64 различных типов объектов.
Каждый объект состоит из двух частей:
Описание структуры _OBJECT_HEADER
Многие объекты содержат данные, которые постоянны для всех объектов одного типа. Для экономии памяти диспетчер объектов хранит статичные данные одного типа объектов в специальной структуре OBJECT_TYPE.
Описание структуры _OBJECT_TYPE
Все типы объектов хранятся в массиве под символом nt!ObTypeIndexTable. В то же время в заголовке объекта находится поле TypeIndex, которое указывает на индекс в данном массиве. Для наглядности найдем структуру типа объекта EPROCESS.
Пример нахождения типа объекта
Объекты хранятся в специальной директории _OBJECT_DIRECTY. Указатель на корневую директорию содержится в nt!ObpRootDirectoryObject.
Описание структуры _OBJECT_DIRECTORY
Всего в директории может быть 37 записей, где каждая запись — указатель на связный список _OBJECT_DIRECTORY_ENTRY со следующей структурой.
Описание структуры _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 имеет указатели на обработчики запросов со следующим прототипом.
Прототип функции обработки IRP-запросов
Первым аргументом всегда будет указатель на девайс‑объект, к которому был отправлен запрос. Соответственно, драйвер может иметь бесконечно много различных девайсов и обрабатывать каждый по‑своему.
Windows позволяет формировать стек девайс‑объектов. Обычно, когда на девайс оправляется запрос, его помогают обработать несколько драйверов. Каждый из этих драйверов связан с девайс‑объектом, и такие объекты расположены в стеке. Последовательность девайс‑объектов с соответствующими драйверами называется стеком девайс‑объектов. Например, так выглядит стек девайс‑объектов жесткого диска.
Пример стека девайс‑объектов
Видно, что запрос пройдет несколько «высокоуровневых» драйверов, прежде чем оказаться в обработчике контроллера жесткого диска stornvme. Такие запросы можно перехватывать, регистрируя свой драйвер в стек‑девайсе объектов с помощью IoAttachDeviceToDeviceStack. Также Windows позволяет регистрировать специальный драйвер — мини‑фильтр с помощью FltRegisterFilter, который может вызываться на различных IO-запросах. Отличный пример работы мини‑фильтров показан на рисунке ниже.
Описание работы драйвера мини‑фильтра
Хотя регистрация мини‑фильтра не считается вредоносной, перехват этой функции может дать отличное представление о возможностях анализируемого драйвера.
Код регистрации перехвата FltRegisterFilter
Если стек девайсов модифицирован или зарегистрирован мини‑фильтр, DRAKVUF покажет следующие строки:
Описание двусвязного списка структур EPROCESS
Так, при вызове команды cmd /c tasklist или открытии Task Manager Windows просматривает связный список процессов и возвращает информацию по каждому из них. Чтобы процесс не высвечивался в данных списках, достаточно удалить его из цепочки. Для этого необходимо, чтобы Flink предыдущего процесса в цепочке указывал на структуру следующего, а Blink следующего процесса указывал на предыдущий. Также во избежание BSOD’ов ссылки скрытого процесса должны указывать друг на друга, как показано на рисунке ниже.
Пример скрытого процесса
Для мониторинга сокрытия процесса из связного списка плагин rootkitmon перехватывает две системные функции: PspInsertProcess и PspProcessDelete. Как можно догадаться по названию, первая функция вставляет только что созданный процесс в связный список структур EPROCESS, а вторая удаляет.
Также плагин содержит внутренний список всех запущенных процессов в системе, который заполняется при инициализации плагина и пополняется (или отчищается) при создании и завершении процесса.
Соответственно, если на завершении процесса его указатели Flink и Blink замкнуты — процесс был скрыт.
Код обнаружения скрытого процесса
Но что, если процесс не был завершен в конце анализа? Для этого плагин перечисляет связный список структур EPROCESS и сравнивает его с имеющимся в момент завершения: если процесс из старого списка не был найден в новом, он был скрыт.
Код проверки списка процессов в конце анализа
Такой подход не нагружает систему и очень эффективен. Если будет обнаружен скрытый процесс, в логах DRAKVUF появится такая строка:
где HiddenPid — PID скрытого процесса.
Microsoft предоставляет «официальный» способ перехвата системных событий с помощью регистрации функций обратного вызова (колбэков). Например, чтобы получать оповещения о создании или завершении процессов, драйвер может зарегистрировать колбэк с помощью API PsSetCreateProcessNotifyRoutine, для подписки на создание потоков — PsSetCreateThreadNotifyRoutine.
Только документированных колбэков больше пятнадцати, в DRAKVUF мы перехватываем следующие API регистрации колбэков:
Код перехвата регистрации колбэк‑функций
Автор: Павел Максютин
хакер.ру
С появлением 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-перехваты обычно состоят из трех частей:
- подмена первых нескольких инструкций целевой функции для перенаправления потока выполнения на код своего приложения;
- обработка целевой функции: изменение параметров, фильтрация, логирование;
- выполнение подмененных инструкций и возврат на оригинальную функцию.
Схема системного вызова
Поскольку код находится в страницах с правами только на чтение и выполнение, для записи в такие страницы необходимо либо выделить новую виртуальную страницу с правами на запись и спроецировать ее на физическую страницу, где находится код, либо отключить бит Write Protect в специальном регистре управления CR0, что позволит выполнять запись в страницы в обход их прав для текущего ядра.
Структура регистра управления CR0
Описание значения флага Write Protect
Обнаружение таких перехватов сводится к подсчету контрольной суммы секций драйвера в момент начала анализа и пересчету, сверке контрольной суммы в конце. В отличие от PatchGuard, который защищает только небольшой список системных драйверов, мы можем проверять абсолютно все загруженные драйверы из списка PsLoadedModules.
PsLoadedModules — двусвязный список структур _KLDR_DATA_TABLE_ENTRY, описывающих загруженный драйвер: его базовый адрес, размер, имя, характеристики и прочее.
Двусвязный список загруженных модулей
https://st768.s3.eu-central-1.amazonaws.com/a3a40bf47e56509b90045a971f96a1f0/23149/image5.png
Для перечисления загруженных модулей ядра с помощью 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"
Перехват таблицы системных вызовов
SSDT (System Service Descriptor Table) — массив указателей на обработчики системных вызовов в 32-битных системах или список смещений относительно базового адреса таблицы на 64-битных ОС. Как говорилось ранее, до появления PatchGuard эта таблица активно использовалась легитимными программами, а также руткитами для фильтрации и мониторинга системных вызовов. Перезапись одного указателя в такой таблице равносильна перехвату всех вызовов определенного обработчика, что намного проще inline-перехватов, рассмотренных до этого.На данный момент в ОС Windows существует два таких массива под символами nt!KiServiceTable и win32k!W32pServiceTable для системных вызовов к модулю ntoskrnl.exe и графической подсистеме win32k.sys соответственно. Количество элементов в этих массивах сохранено в переменных nt!KiServiceLimit и win32k!W32pServiceLimit.
Значения структур nt!KiServiceTable и win32k!W32pServiceTable
Указатель на SSDT находится в специальной структуре KSYSTEM_SERVICE_TABLE, или SST, под именем ServiceTableBase:
Описание структуры KSYSTEM_SERVICE_TABLE
Код:
typedef struct _KSYSTEM_SERVICE_TABLE
{
PULONG ServiceTableBase;
PULONG ServiceCounterTableBase;
ULONG NumberOfService;
ULONG ParamTableBase;
} KSYSTEM_SERVICE_TABLE, *PKSYSTEM_SERVICE_TABLE;
Значения таблиц nt!KeServiceDescriptorTable и nt!KeServiceDescriptorTableShadow
Можно заметить, что первая SST-структура в обеих таблицах одинакова. При системном вызове ОС Windows использует нижние 12 бит регистра eax как индекс в одной из SSDT-таблиц, а биты 12–13 указывают, какую SDT-структуру выбрать: nt!KeServiceDescriptorTable или nt!KeServiceDescriptorTableShadow.
В первой части статьи уже шла речь о том, что на 64-битных системах SSDT содержит массив смещений, а виртуальный адрес обработчика системного вызова вычисляется по формуле
Код:
RoutineAddress = ServiceTableBase + (ServiceTableBase[index] >> 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
Код:
vmi_pid_t gui_pid = 0;
if (!drakvuf_get_process_pid(drakvuf, gui_process, &gui_pid))
{
PRINT_DEBUG("[SSDTMON] Failed to get PID of "explorer.exe"\n");
throw -1;
}
addr_t w32k_base = 0;
vmi_lock_guard vmi(drakvuf);
// 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");
throw -1;
}
// Read ssdt shadow size
if (VMI_SUCCESS != vmi_read_32_va(vmi, w32k_base + w32psl_rva, gui_pid, &this->w32pservicelimit))
{
PRINT_DEBUG("[SSDTMON] Failed to read W32pServiceLimit\n");
throw -1;
}
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");
throw -1;
}
Перехваты 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.
Описание структуры KIDTENTRY64
Реальный же адрес обработчика прерывания вычисляется по следующей формуле:
Код:
RoutineAddress = OffsetHigh << 32 + OffsetMiddle << 16 + OffsetLow
Пример подсчета адреса обработчика прерываний
В плагине мы подсчитываем контрольную сумму всей IDT-таблицы на каждом логическом ядре и сохраняем для проверки в конце анализа.
Код перечисления и сохранения значений дескрипторов
На рисунке выше видно, что, помимо IDT, мы также перечисляем и сохраняем GDT-таблицу. GDT (Global Descriptor Table) — структура в архитектуре х86, помогающая процессору распределить память по сегментам. Каждый элемент таблицы, называемый сегментным дескриптором, содержит адрес, размер (лимит), флаги прав и атрибутов таких областей.
Структура дескриптора сегмента
Адрес и размер таблицы хранится в отдельном системном регистре GDTR.
Структура системного регистра 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.
Значение регистра MSR LSTAR
Как видно, первая инструкция в ядре после системного вызова — 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 текущего ядра.
Описание использования MSR LSTAR
В отличие от SSDT-перехватов, перезапись LSTAR позволяет перехватывать и фильтровать все системные вызовы сразу. Но для этого руткиту нужно сделать дополнительную работу, а именно: вызвать инструкцию swapgs, сохранить пользовательский стек, загрузить ядерный стек. Часто в перехваченном обработчике LSTAR можно встретить следующий код.
Пример перехваченного KiSystemCall64
Rootkitmon обнаруживает модификацию MSR LSTAR, сверяя его значение с адресом функции KiSystemCall64 в конце анализа. Мониторинг записи в MSR LSTAR в DRAKVUF на данный момент невозможен. Во‑первых, перехватываться могут только записи во все MSR-регистры. Во‑вторых, перехват MSR на запись сильно нагружает систему, поскольку на каждом системном вызове инструкция swapgs записывает в IA32_KERNEL_GS_BASE MSR, как говорилось выше. В‑третьих, мониторинг MSR LSTAR на запись часто приводит к ложным срабатываниям, поскольку, как мы заметили, PatchGuard сам перезаписывает этот MSR во время выполнения своих проверок.
Код перехвата MSR LSTAR
ПЕРЕХВАТЫ ФУНКЦИЙ DRIVER_OBJECT
У каждого загруженного драйвера в системе есть структура DRIVER_OBJECT. Она описывает состояние драйвера, который отвечает за ее инициализацию во время своей загрузки. В частности, для коммуникации с пользовательскими программами драйвер должен инициализировать массив MajorFunction. Он, в свою очередь, содержит адреса обработчиков для всех запросов, на которые драйвер может отвечать. Например, когда пользовательское ПО будет использовать API ReadFile с описателем драйвера, Windows создаст специальный IRP-запрос и вызовет обработчик из массива MajorFunction этого драйвера — в данном случае IRP_MJ_READ.
Описание структуры _DRIVER_OBJECT
Перезапись указателей на обработчики, как и в случае с SSDT, приведет к перехвату всех вызовов определенного типа запроса к драйверу. Особенность этой техники состоит в том, что PatchGuard защищает только небольшой список объектов драйверов и перехват любого драйвера не из списка не приведет к BSOD’у системы.
Помимо массива MajorFunction, также существует массив быстрых IO-операций — FastIoDispatch. В отличие от MajorFunction, быстрые IO-операции предназначены исключительно для драйверов файловой системы и сетевых драйверов. Рассмотрим, что такое объекты и как можно их перечислять непосредственно из контекста гипервизора.
В Windows каждый ресурс (процесс, поток, файл и так далее) представлен в виде объекта и управляется диспетчером объектов Windows. Например, в Windows 10 1803 существует около 64 различных типов объектов.
Каждый объект состоит из двух частей:
- OBJECT_HEADER — специальный заголовок фиксированного размера, который находится перед телом объекта и которым владеет диспетчер объектов;
- Body — часть объекта, представляющая системный ресурс (EPROCESS для процессов, ETHREAD для потоков, FILE_OBJECT для файлов и так далее).
Описание структуры _OBJECT_HEADER
Многие объекты содержат данные, которые постоянны для всех объектов одного типа. Для экономии памяти диспетчер объектов хранит статичные данные одного типа объектов в специальной структуре OBJECT_TYPE.
Описание структуры _OBJECT_TYPE
Все типы объектов хранятся в массиве под символом nt!ObTypeIndexTable. В то же время в заголовке объекта находится поле TypeIndex, которое указывает на индекс в данном массиве. Для наглядности найдем структуру типа объекта EPROCESS.
Пример нахождения типа объекта
Объекты хранятся в специальной директории _OBJECT_DIRECTY. Указатель на корневую директорию содержится в nt!ObpRootDirectoryObject.
Описание структуры _OBJECT_DIRECTORY
Всего в директории может быть 37 записей, где каждая запись — указатель на связный список _OBJECT_DIRECTORY_ENTRY со следующей структурой.
Описание структуры _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 имеет указатели на обработчики запросов со следующим прототипом.
Прототип функции обработки IRP-запросов
Первым аргументом всегда будет указатель на девайс‑объект, к которому был отправлен запрос. Соответственно, драйвер может иметь бесконечно много различных девайсов и обрабатывать каждый по‑своему.
Windows позволяет формировать стек девайс‑объектов. Обычно, когда на девайс оправляется запрос, его помогают обработать несколько драйверов. Каждый из этих драйверов связан с девайс‑объектом, и такие объекты расположены в стеке. Последовательность девайс‑объектов с соответствующими драйверами называется стеком девайс‑объектов. Например, так выглядит стек девайс‑объектов жесткого диска.
Пример стека девайс‑объектов
Видно, что запрос пройдет несколько «высокоуровневых» драйверов, прежде чем оказаться в обработчике контроллера жесткого диска stornvme. Такие запросы можно перехватывать, регистрируя свой драйвер в стек‑девайсе объектов с помощью IoAttachDeviceToDeviceStack. Также Windows позволяет регистрировать специальный драйвер — мини‑фильтр с помощью FltRegisterFilter, который может вызываться на различных IO-запросах. Отличный пример работы мини‑фильтров показан на рисунке ниже.
Описание работы драйвера мини‑фильтра
Хотя регистрация мини‑фильтра не считается вредоносной, перехват этой функции может дать отличное представление о возможностях анализируемого драйвера.
Код регистрации перехвата FltRegisterFilter
Если стек девайсов модифицирован или зарегистрирован мини‑фильтр, 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 — указатель на связный список процессов.
Описание двусвязного списка структур EPROCESS
Так, при вызове команды 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
РЕГИСТРАЦИЯ СИСТЕМНЫХ ФУНКЦИЙ ОБРАТНОГО ВЫЗОВА
Напоследок разберем несколько индикаторов, которые не являются вредоносными, но могут таковыми оказаться.Microsoft предоставляет «официальный» способ перехвата системных событий с помощью регистрации функций обратного вызова (колбэков). Например, чтобы получать оповещения о создании или завершении процессов, драйвер может зарегистрировать колбэк с помощью API PsSetCreateProcessNotifyRoutine, для подписки на создание потоков — PsSetCreateThreadNotifyRoutine.
Только документированных колбэков больше пятнадцати, в DRAKVUF мы перехватываем следующие API регистрации колбэков:
- PspSetCreateProcessNotifyRoutine — подписка на создание/завершение процесса;
- PspSetCreateThreadNotifyRoutine — подписка на создание/завершение потока;
- PsSetLoadImageNotifyRoutine — подписка на загрузку модулей;
- CmpRegisterCallbackInternal — подписка на работу с реестром;
- ObRegisterCallbacks — подписка на получение описателей процесса, потока и рабочего стола;
- FsRtlRegisterFileSystemFilterCallbacks — подписка на работу с файловой системой.
Код перехвата регистрации колбэк‑функций
ЗАКЛЮЧЕНИЕ
Плагин rootkitmon находится на ранних стадиях разработки, но уже позволяет обнаружить такие руткиты, как Moriya, TDSS, Necurs и многие другие. Rootkitmon не нагружает систему и поддерживает как Windows 7 x64, так и Windows 10 x64. Дальше количество техник обнаружения вредоносной активности будет только увеличиваться.Автор: Павел Максютин
хакер.ру