Рубрика:
Администрирование /
Продукты и решения
|
Facebook
Мой мир
Вконтакте
Одноклассники
Google+
|
Артем Баранов
Руткиты режима ядра: алгоритмы работы и защита
Случалось ли вам замечать, что в системе «живет» посторонний процесс. Но ни диспетчер задач, ни другие программы не могут его показать. Может быть, процесс скрывается и от них? Каковы методики скрытия в режиме ядра?
В последнее время в мире NT получает все большее распространение технология руткитов. По сути, руткит (rootkit) представляет собой технологию, предназначение которой скрывать что-либо. Под технологией руткитов обычно понимают программу, которая «документированно» или нет использует перехват API-вызовов для фильтрации возвращаемых результатов системных функций. В предыдущей статье (см. №10 за 2006 г.) давалась вводная информация, а также были рассмотрены алгоритмы работы руткитов пользовательского режима. В данной статье внимание уделено алгоритмам работы руткитов режима ядра.
Напомню, что руткиты режима ядра функционируют на нулевом кольце и имеют полный доступ к области памяти ядра NT, со всеми вытекающими отсюда последствиями.
Например, руткит может модифицировать структуры данных Windows и тем самым скорректировать поведение ядра для достижения заданных целей: маскировка файлов, процессов, сетевых соединений. В статье описываются техники, используемые в современных руткитах режима ядра, включая fu и Shadow Walker. Также описаны утилиты, с использованием которых можно выявлять скрытые руткитами объекты.
Классификация
Для скрытия в системе каких-либо объектов руткиты либо перехватывают системные сервисы ядра, аналогично тому, как это делают руткиты пользовательского режима для DLL, либо модифицируют структуры данных ядра. В соответствии с таким подходом руткиты делятся на две следующие группы:
- модифицирующие путь выполнения потока (modify execution path);
- модифицирующие объекты ядра (directly kernel object manipulation, DKOM).
Первые реализуются с использованием следующих методов:
- перехват функций через SSDT (таблица диспетчеризации системных сервисов);
- перехват прерываний;
- изменение первых машинных команд перехватываемой функции на jmp;
- изменение указателей в объектах ядра, например KTHREAD.pService DescriptorTable.
Руткиты, относящиеся к первому типу, заставляют поток, вызвавший системный сервис, отработать код руткита. Когда код руткита получает управление, он вызывает настоящий системный сервис по указателю, отфильтровывает данные и возвращает результат.
Второй тип руткитов менее заметен, т.к. код руткита напрямую не выполняется. Руткит модифицирует одну (или несколько) структур данных ядра. Таким образом заставляя системную функцию работать с ложным объектом (или очередью объектов). Такие руткиты воздействуют на работу ядра косвенно.
Рассмотрим каждый из методов более подробно.
Перехват функций через SSDT
Если руткиту нужно перехватить интерфейс ядра через таблицу диспетчеризации, он просто модифицирует указатель в этой таблице на свой обработчик, сохранив перед этим указатель на предыдущий.
Для исключения путанницы, связанной с терминологией, приведены структуры SST и SSDT.
typedef struct _SERVICE_DESCRIPTOR_TABLE
{
SYSTEM_SERVICE_TABLE ntoskrnl;
SYSTEM_SERVICE_TABLE win32k;
SYSTEM_SERVICE_TABLE Table3;
SYSTEM_SERVICE_TABLE Table4;
} SERVICE_DESCRIPTOR_TABLE,
typedef struct _SYSTEM_SERVICE_TABLE
{
PNTPROC ServiceTable
PDWORD CounterTable;
DWORD ServiceLimit;
PBYTE ArgumentTable;
} SYSTEM_SERVICE_TABLE;
На рис. 1 показано нормальное выполнение системного сервиса WriteFile, а на рис. 2 выполнение того же сервиса, но руткит уже заменил указатель в SSDT на свою функцию. Таким образом, руткит может контролировать выполнение системного сервиса, отфильтровывая «ненужные» данные.
Рисунок 1. Нормальный поток выполнения кода, вызвавшего WriteFile
Рисунок 2. Поток выполнения кода, вызвавшего WriteFile из переписанной SSDT, что приводит к активации руткита
Перехват прерываний
Иногда руткиту приходится перехватывать прерывания или исключения, подменяя адреса в таблице диспетчеризации прерываний (IDT). Это может быть сделано, например, для скрытия своего присутствия, или руткиту необходимо знать, что произошло какое-то событие в системе. При этом прерывания можно также перехватить методом замены первых байт функции на команду jmp (описан ниже). Для наглядной демонстрации на рис. 3 показаны IDT и функции ядра, обрабатывающие прерывания до того, как на них воздействовал руткит, а на рис. 4 показано, как руткит перехватывает исключения #GP (General Protection) и #PF (Page Fault). Видно, что #PF перехватывается через IDT, а #GP – путем записи команды безусловного перехода в начало обработчика.
Рисунок 3. Таблица дескрипторов прерываний не зараженной системы
Рисунок 4. Таблица дескрипторов прерываний, руткит перехватил Page Fault и General Protection
Изменение первых слов перехватываемой функции на jmp
Руткит может перехватить вызов функции, не только подменяя адреса в SSDT. Достаточно затереть первые пять байт перехватываемой функции на jmp на функцию руткита, сохранив перед этим оригинальные байты. Когда будет необходим вызов, восстанавливать эти байты и передавать управление оригинальной функции. Обычно руткит записывает первым байтом опкод ближнего jmp – E9, а в последующих четырех байтах смещение от начала следующей инструкции до целевой функции (функции руткита).
Изменение указателя KTHREAD.pServiceDescriptorTable
Когда поток вызывает системный сервис, ядро находит необходимую SDT (их две KeServiceDescriptorTable, KeServiceDescriptorTableShadow), обращаясь к структуре KTHREAD. KTHREAD является частью более общей структуры ETHREAD, которая представляет поток в ядре. Руткит может скопировать старую SDT (включая SSDT), подменить в SSDT указатели на свои обработчики и в KTHREAD.pServiceDescriptorTable записать новый адрес на вновь созданный дескриптор. Ниже на рис. 5 показаны два потока. Первый еще не вызывал USER или GDI-сервис, и его pServiceDescriptorTable указывает на сервисы, которые обслуживает ntoskrnl. PServiceDescriptorTable второго потока указывает на таблицу дескрипторов, включая дескриптор для обслуживания USER и GDI сервисов – win32k. На рис. 6 видно, что руткит скопировал старую SDT, подменил указатели в SSDT и записал указатель на новую таблицу дескрипторов в KTHREAD.
Рисунок 5. В структурах ETHREAD, указатель на таблицу SDT, указывает на SDT ядра
Рисунок 6. В структурах ETHREAD указатель на таблицу SDT, указывает на SDT руткита
Модификация объектов ядра
На сегодняшний день существуют руткиты, которые скрывают процессы с использованием модификации системной очереди объектов ядра. Яркий представитель – fu. Суть этого подхода в следующем. В NT 5.x процессы, которые работают на данный момент в системе, увязываются в одном двунаправленном списке. Fu просто переставляет указатели соседних объектов скрываемого процесса. Таким образом, когда ядро проходится по списку, то не видит этот процесс. На рис. 7 представлена системная очередь, а на рис. 8 видно, что руткит скрыл второй процесс.
Рисунок 7. Очередь процессов, выполняющихся в системе
Рисунок 8. Модифицированная руткитом очередь процессов
Каким образом руткит определяет адрес системной очереди? Ответов на этот вопрос несколько:
- через экспортируемую переменную ядра PsActive-ProcessHead; вычитая соответствующее смещение (0x88 в Windows XP), руткит определяет первый элемент очереди (всегда процесс System);
- через регистр fs; при переключении в привилегированный режим ядро помещает в регистр fs другой селектор, в результате fs будет адресоваться к структуре данных KPCR – Processor Control Region (описывает процессор в ядре); в KPCR содержится указатель на структуру KPRCB – Processor Control Block, которая содержит указатель на ETHREAD потока, выполняющегося в данный момент на процессоре (член CurrentThread); определив адрес ETHREAD, руткит, используя соответствующее поле, находит указатель на EPROCESS, которому принадлежит поток; этот подход использует fu;
- через функцию IoGetCurrentProcess, которая возвращает указатель на EPROCESS процесса, который активен в данный момент.
Такая техника вводит многих людей в замешательство. Они думают, что если такой процесс удалить из очереди, то он не сможет выполняться, т.к. система не будет его учитывать. Но все дело в том, что распределение процессорного времени в Windows NT осуществляется между исполняемыми сущностями – потоками. Процессы вообще не участвуют в перераспределении процессорного времени, и ядру без разницы – осуществлять переключение контекста на поток того же процесса или на поток другого процесса. Все дело в том, что существуют еще очереди планировщика (диспетчера потоков), которые он использует для осуществления операций, связанных с планированием. Таких очередей три: KiDispatcherReady ListHeader – очередь готовых к исполнению потоков, KiWaitInListHead, KiWaitOutListHead – две очереди потоков, которые пока не планируются, т.е. ждут какого-то события (например, завершение операции ввода-вывода). На самом деле первая очередь является таблицей из 32 элементов, каждый из которых адресует очередь для конкретного приоритета потока.
Shadow Walker
Shadow Walker является руткитом, который предназначен для скрытия fu в системе. Делает он это достаточно неординарным способом. Во всяком случае до Shadow Walker такой способ не применял ни один руткит. Сам fu работает в режиме ядра и предназначен для скрытия процессов, добавления привилегий в маркер и прочее. Но несмотря на предоставляемые богатые возможности, его можно обнаружить простым сигнатурным поиском в области памяти ядра, со всеми вытекающими отсюда последствиями. Противостоять сигнатурному поиску, а тем более в памяти достаточно сложно, если не применять стандартные методы (например, полиморфизм). А fu его не применяет. Нужно было придумать такой метод, при котором код может получать доступ к странице памяти при ее чтении и модифицировать ее данные. В Shadow Walker заложен следующий алгоритм:
- сделать страницы кода fu в памяти недействительными, т.е. сбросить первый бит – Valid в аппаратном PTE;
- когда произойдет ошибка страницы (при ее чтении) в связи с тем, что PTE помечен как недействительный, руткит должен определить, является ли страница той, которую необходимо скрыть; если да, то каким-то образом заменить данные на странице;
- если происходит выполнение на странице кода руткита, просто сделать PTE действительным и дать процессору таким образом доступ к странице.
Но каким образом можно определить, читает процессор данные из памяти или выполняет? Авторы руткита предлагают следующий прием. Если адрес в регистре CR2 (в этот управляющий регистр помещается линейный адрес, вызвавший ошибку страницы) совпадает с сохраненным в стеке адресом возврата (напомним, что ошибка в отличие от ловушки возвращает управление инструкции, которая вызвала ошибку), то процессор выполняет код на странице, в противном случае выполняется операция чтения.
Но как возвратить потоку ложные данные, когда тот пытается прочитать код руткита? Разработчики Shadow Walker и здесь нашли элегантное решение. Все дело в том, что в процессорах типа x86 для быстрого преобразования адресов используется специальный процессно-зависимый буфер преобразования адресов (Translate Lookaside Buffer, TLB). Записи TLB хранят соответствия между номерами виртуальных страниц (Virtual Page Number, VPN) и соответствующими номерами фреймов страниц (Page Frame Number, PFN). На самом деле в x386 существует несколько TLB, ITLB для кэширования проекций инструкций и DTLB для кэширования проекций данных. В синхронизированном виде ITLB и DTLB содержат одинаковые элементы кэширования страниц кода и данных, см. рис. 9. После того, как руткит изменит вхождение в TLB буферы не будут находиться в синхронном состоянии. В итоге поток, читающий данные, получит в использование «левую» страницу, что и демонстрируется на рис. 10.
Рисунок 9. Два буфера TLB, для кэширования проекций кода и данных в синхронизированном состоянии
Рисунок 10. Два буфера TLB для кэширования проекций кода и данных в рассинхронизированном состоянии
Когда процессору необходимо считать данные со страницы, он обращается к DTLB, если необходимо, выполнит код к ITLB. В результате рассинхронизации работы буферов преобразования адресов при чтении процессор видит одну страницу, а при записи – другую.
Привить руткиту стелс-алгоритм также можно следующим методом. В процедуре инициализации драйвера выделяем регион памяти (например, в резидентном пуле). Затем копируем туда функцию, которая будет осуществлять перехват сервиса. Устанавливаем атрибут страниц со скопированным кодом на «чтение». Переписываем указатель в KiServiceTable на скопированную функцию. Затем необходимо, чтобы ядро выгрузило драйвер. Этого можно достичь, если процедура DriverEntry вернет отличное от STATUS_SUCCESS значение.
Противодействие руткитам режима ядра
Рассмотрим программы, предназначенные для выявления руткитов, и алгоритмы их работы.
Klister
Автор Joanna Rutkowska (http://invisiblethings.org/tools/klister-0.4.zip). Эта программа предназначена для выявления скрытых процессов. Для этого она использует списки потоков планировщика (KiDispatcherReadyListHead, KiWaitOutList Head, KiWaitInListHead). Как уже упоминалось, эти списки указывают на структуры ETHREAD, с помощью которых исполнительная система представляет потоки в системе. А каждая из структур содержит член, указывающий на EPROCESS процесса, к которому принадлежит поток. Проходясь по этим спискам, klister собирает всю информацию по процессам и выводит ее пользователю. Соответственно, для выявления скрытого процесса нужно сравнить результаты, полученные с помощью klister, и те, которые отображает диспетчер задач. Klister состоит из драйвера режима ядра, принимающего запросы от программы пользовательского режима, – второй компонент klister. Драйвер и собирает всю статистику. К сожалению, символы KiDispatcherReady ListHead, KiWaitOutListHead, KiWaitInListHead не являются экспортируемыми, поэтому в klister еще на этапе компиляции были зашиты адреса списка потоков планировщика. В дистрибутиве с klister поставляются еще две программы: idt и sdt. Первая позволяет узнать адреса обработчиков прерываний и исключений, а вторая позволяет просмотреть таблицу дескрипторов системных сервисов.
BlackLight
Автор F-secure (http://www.f-secure.com/exclude/blacklight/blbeta.exe). Программа предназначена для выявления скрытых процессов, файлов и каталогов. Для выявления процессов используется метод, получивший название PID Bruteforce (PIDB). Заключается он в следующем: для всевозможных значений PID (от 0x0 до 0x4E1C) вызывается функция OpenProcess, если процесс существует, то BlackLight фиксирует его в своем списке. Затем создается снимок процессов (CreateToolhelp32Snapshot) и оба списка сравниваются. Если происходит несовпадение по какому-либо процессу, значит, он скрыт.
RootkitRevealer
Автор Sysinternals (http://download.sysinternals.com/Files/RootkitRevealer.zip). Еще одно средство, которое поможет в борьбе с руткитами. RootkitRevealer не занимается поиском скрытых процессов. Вместо этого он сосредоточен на поиске скрытых разделов реестра, файлов и папок. RootkitRevealer осуществляет техники выявления руткитов сканированием кустов реестра на низком (RAW) уровне, т.е. анализируя реестр без характерных API-вызовов, а затем используя Win32 API, и сравнивает полученные результаты. Аналогично RootkitRevealer выявляет и скрытые папки. Вначале сканирует раздел с использованием метаданных файловой системы, а затем с использованием Win32 API также сравнивает полученные результаты и выводит соответствующие скрытые объекты. На рис. 11 показано, что RootkitRevealer выявил разделы, которые скрыл из пользовательского режима HackerDefender.
Рисунок 11. RootkitRevealer обнаружил «невидимые» разделы реестра, скрытые руткитом HackerDefender
VICE
Авторы James Butler, Gregory Hoglund (http://www.rootkit.com/vault/fuzen_op/vice.zip). VICE также является программой для обнаружения руткитов. Его основная задача – обнаружение руткитов, модифицирующих путь выполнения (как в пользовательском режиме, так и в режиме ядра). После нажатия на кнопку сканирования в окне VICE начнет проверять систему на наличие руткитов. Процессы, на которые воздействует руткит, показаны в списке. Рядом с названиями процессов располагаются иконки, каждая из которых показывает, каким именно способом был заражен процесс руткитом.
RAIDE
Авторы James Butler, Peter Silberman (http://www.rootkit.com/vault/petersilberman/RAIDE_Beta_1.zip). Эта программа предоставляет следующие возможности: обнаружение скрытых процессов восстановление SSDT, восстановление первых байт функции. Также RAIDE позволяет сделать скрытый процесс видимым. Восстановление адресов в SSDT RAIDE осуществляет, считывая KeServiceDescriptorTable из файла ntoskrnl.exe на диске и определяя указатель на SSDT, сверяет соответствующие указатели в таблице SSDT в памяти и на диске. Аналогичным образом она восстанавливает поврежденную руткитом функцию, т. е. функцию, первые байты которой руткит переписал на команду перехода. Программа считывает первые три инструкции функции с файла на диске и переписывает их в соответствующее место в памяти.
Заключение
Руткиты режима ядра используют недокументированные возможности Windows NT, что не обеспечивает им переносимости на разных версиях NT. Microsoft не документирует устройство объектов ядра, т.к. оно меняется от версии к версии. Поэтому определения этих структур нельзя найти в DDK. Но при разработке руткита или средства его обнаружения можно опираться на определения структур, показанных Windbg (команда dt). Кроме того, Windbg предоставляет возможности ручного определения на предмет заражения системы. Например, можно продизассемблировать первые байты функции (u), а затем посмотреть, не стоит ли в первых же байтах команда jmp. Аналогично можно проверить, не перехвачена ли функция в SSDT. Для этого нужно посмотреть адреса в SSDT, например, командой dd KiServiceTable, а затем для адреса, указанного там, вывести идентификатор командой ln. Если для адреса выводится идентификатор, значит, функция не перехвачена, в противном случае есть подозрение, что это адрес функции руткита.
Несмотря на весьма широкий выбор программ по выявлению руткитов, лишь некоторые из них могут сказать, каким именно руткитом заражена система. В основном же эти средства узкоспециализированны и базируются только на определенных техниках выявления. Стоит также отметить, что техники, реализованные в тех или иных средствах, также не гарантированно могут обнаружить, например, скрываемый процесс. Так, IceSword и BlackLight не могут обнаружить руткит FuTo, который скрывает процессы таким образом, что те функции API, на которые опираются эти средства, работают с ложными данными, которые руткит уже модифицировал. А средства выявления руткитов, опирающиеся только на сигнатурный поиск в памяти, не смогут обнаружить Shadow Walker. Для выявления руткитов режима ядра нужен комплексный подход к анализу работы системы. Например, для выявления Shadow Walker прежде чем запускать сигнатурное сканирование в памяти, нужно посмотреть правильный ли адрес указан в IDT для обработчика ошибки страницы. Пока полнофункциональных средств, которые объединили бы все техники обнаружения, нет. Возможно, это обуславливается тем, что сама техника реализации DKOM используется с недавнего времени. Да и изощренные техники скрытия кода в памяти, применяемые в Shadow Walker, до этого не использовались.
Facebook
Мой мир
Вконтакте
Одноклассники
Google+
|