В. А. Прилепский
Донецкий национальный технический университет
В данной работе рассмотрена методика защиты исполнения процессов Windows в режиме ядра. Она включает в себя защиту от завершения, повреждения, внедрения кода и других вмешательств в работу процесса. Этот материал был составлен с учетом специфики Windows 2000 и XP. В настоящее время актуальность рассмотренного вопроса очень высока и связана с ростом числа вредоносных программ, целью которых является установка контроля над системой путем завершения процесса антивируса или другой системы безопасности. Разработанная методика защиты рассчитана на процессы сервисов, не предоставляющие GUI интерфейс.
Ключевым компонентом защиты объектов Windows является перехват функций, ответственных за работу с процессами. Рассмотрим путь, который проходит вызов функции от приложения до кода, который ее реализует. Здесь и далее под сервисом будет пониматься предоставляемая функция. Когда приложение вызывает функцию (например, OpenProcess), управление передается серверу подсистемы Win32 (процессу csrss) или соответствующей функции библиотеки ntdll.dll. Она предоставляет процессам пользовательского режима сервисы режима ядра в виде функций с префиксами Zw и Nt, имеющих здесь одну точку входа. Эти функции осуществляют работу с ключевыми структурами и объектами ядра. Реализация этих сервисов сводится к помещению в eax номера функции и вызову диспетчера системных сервисов специальной командой (syscall, sysenter или int), специфичной для архитектуры процессора. Код диспетчера выполняется уже в режиме ядра. Он анализирует содержимое регистра eax, после чего по таблице системных сервисов определяет адрес функции, соответствующей полученному номеру.
Указатель на таблицу системных сервисов SDT (Service Descriptor Table) хранится в системной переменной KeServiceDescriptorTable и доступен любому модулю режима ядра. SDT содержит в себе указатели на 4 таблицы системных сервисов SST: таблицу сервисов ядра и графической подсистемы, два других указателя зарезервированы. SST состоит из следующих полей: ServiceTable — указатель на массив точек входа в функции, CounterTable — указатель на массив счетчиков вызовов (используются только в отладочной сборке Windows), ServiceLimit &mdash поле, хранящее число системных сервисов, описанных данной SST, ArgumentLimit &mdash поле, содержащее количества параметров для каждой функции.
Осуществить надежный перехват функций в пользовательском режиме невозможно, так как приложение может напрямую вызвать сервис режима ядра с использованием упомянутой выше команды. Поэтому перехват должен осуществляться в режиме ядра. Наиболее простым в реализации методом является замена адресов точек входа в перехватываемые системные сервисы на адреса собственных обработчиков. Этот метод не позволяет контролировать вызовы модулями режима ядра функций с префиксом Zw, которые представлены здесь, в отличии от ntdll, отдельными функциями. Подобного рода защита рассчитана на противодействие только коду пользовательского режима. Но поскольку противодействовать коду режима ядра практически невозможно, описанный метод перехвата приемлем.
Первым этапом защиты является защита от открытия объекта процесса и получения его дескриптора другими процессами. Он реализуется путем перехвата функции NtOpenProcess. Новый обработчик сравнивает идентификатор процесса (далее Pid) с Pid защищаемого процесса и возвращает значение ACCESS_DENIED в случае совпадения или вызывает оригинальный обработчик в противном случае.
Но такая защита уязвима, поскольку при создании процесса дескриптор с полными правами доступа к нему получает процесс csrss. Если открыть его с правом копирования дескрипторов, затем получить информацию о них вызовом NtQuerySystemInformation, найти нужный дескриптор и скопировать его функцией DuplicateHandle, можно получить полный доступ к защищенному процессу. Поэтому для надежности метода необходимо перехватить все функции, способные повлиять на процесс (TerminateProcess, SetInformationProcess и другие). Список таких функций может быть определен по документации Platform SDK, а диаграмма вызовов построена с помощью дизассемблера.
Воздействовать на процесс можно путем воздействия на его потоки, при чем дескриптор потока, возможно, также можно получить описанным выше способом. Поэтому система защиты должна контролировать функции работы с потоками. При реализации перехвата необходимо по дескриптору потока определить Pid его родительского процесса. Это можно осуществить следующим способом: получить указатель на объект потока с помощью функции ObReferenceObjectByHandle и найти в нем Pid процесса, которому принадлежит поток. Структуры объектов потока и процесса отсутствуют в документации и специфичны для разных версий Windows, поэтому их целесообразно представить в виде смещений нужных полей.
Завершить процесс можно, присоединив его к объекту отладки. Вызов соответствующей функции сводится к вызову NtDebugActiveProcess, которую нужно перехватить. Завершение отлаживаемого процесса происходит при завершении процесса-отладчика или закрытии объекта отладки.
Также необходимо предусмотреть защиту от внедрения кода в адресное пространство процесса или его модификацию. Создание удаленного потока контролируется перехватом NtCreateThread, защита адресного пространства процесса от чтения и записи осуществляется перехватом функций NtProtectVirtualMemory (изменение уровней защиты), NtWriteVirtualMemory и NtReadVirtualMemory. Все эти функции используют дескриптор процесса, по которому можно определить, является ли он защищаемым.
Одним из ключевых вопросов реализации защиты является контроль над открытием секции физической памяти на запись функцией NtOpenSection, использование которой позволит процессу не только модифицировать ключевые структуры памяти, но и проникнуть в режим ядра и установить полный контроль над системой.
Последним аспектом защиты в этой работе является защита драйвера от завершения, приостановки и другого доступа. Она реализуется путем удаления объекта драйвера из первичного пространства имен после его загрузки. Активацию защиты можно организовать как открытие символьной ссылки устройства, созданного драйвером, или привязать защищаемый процесс к драйверу через контрольную сумму файла-образа.
В ходе работы был разработан драйвер защиты, работа которого была проверена на программе Simple Process Termination, завершающей процесс 16 способами. Результаты тестирования показали, что защита успешно противостоит всем 16 способам. Следовательно, разработанный драйвер может быть успешно применен в целях защиты важных системных процессов.