Все мало-мальски серьезные защитные приложения, будь то файрволы или антивирусы, используют собственные модули режима ядра (ring 0), через которые работает большинство их функций: защита процессов от завершения, фильтры различных событий, получение актуальной информации о состоянии сетевого трафика и количестве процессов в системе. Если у программы есть такой драйвер, то пробовать скрываться от нее из режима пользователя (ring 3) бессмысленно. Так же бесполезно пытаться на нее как-то воздействовать. Решение — написать собственный драйвер. В этой статье я покажу, как это делается.
Процессорные архитектуры x86 и x64 имеют четыре кольца защиты, из которых в Windows по факту используются всего два — это ring 3 (режим пользователя) и ring 0 (режим ядра). Бытует мнение, что код режима ядра — самый привилегированный и «ниже» ничего нет. На самом деле архитектура x86/x64 позволяет опускаться еще ниже: это технология виртуализации (hypervisor mode), которая считается кольцом -1 (ring -1), и режим системного управления (System Management Mode, SMM), считающийся кольцом -2 (ring -2), которому доступна память режима ядра и гипервизора.
Итак, мы решили писать собственный драйвер. Начнем с выбора инструментария. Я советую использовать Microsoft Visual Studio, как наиболее user-friendly IDE. Также необходимо будет установить Windows SDK и Windows Driver Kit (WDK) для твоей версии ОС. Кроме того, я крайне рекомендую запастись такими утилитами, как DebugView (просмотр отладочного вывода), DriverView (позволяет получить список всех установленных драйверов) и KmdManager (удобный загрузчик драйверов).
Драйверы в Windows начиная с Vista могут быть как режима пользователя (User-Mode Driver Framework, UMDF), так и режима ядра (Kernel-Mode Driver Framework, KMDF). Более ранние драйверы Windows Driver Model (WDM) появились в Windows 98 и сейчас считаются устаревшими.
Драйверы UMDF имеют намного более ограниченные права, чем KMDF, однако они используются, например, для управления устройствами, подключенными по USB. Помимо ограничений, у них есть очевидные плюсы: их намного проще отлаживать, а ошибка в их написании не вызовет глобальный системный сбой и синий экран смерти. Такие драйверы имеют расширение dll.
Что до драйверов режима ядра (KMDF), то им дозволено куда больше, а расширение файлов, закрепленное за ними, — это sys. В этой статье мы научимся писать простые драйверы режима ядра, напишем драйвер для скрытия процессов методом DKOM (Direct Kernel Object Manipulation) и его загрузчик.
Зачем специалисту по ИБ может понадобиться написать kernel-mode драйвер?
Загрузка ...
После того как ты создашь проект драйвера, Visual Studio автоматически настроит некоторые параметры. Проект будет компилироваться в бинарный файл в соответствии с тем, какая выбрана подсистема. Наш вариант — это NATIVE, подсистема низкого уровня, как раз для того, чтобы писать драйверы.
Строго говоря, точка входа в драйвер может быть любой — мы можем сами ее определить, добавив к параметрам компоновки проекта -entry:[DriverEntry]
, где [DriverEntry]
— название функции, которую мы хотим сделать стартовой. Если в обычных приложениях основная функция обычно называется main, то в драйверах точку входа принято называть DriverEntry.
Выглядеть это будет так:
NTSTATUS DriverEntry (PDRIVER_OBJECT pDriverObject, PUNICODE_STRING pRegistryPath);
Давай пройдемся по параметрам, которые передаются DriverEntry
. pDriverObject
имеет тип PDRIVER_OBJECT
, это значит, что это указатель на структуру DRIVER_OBJECT
, которая содержит информацию о нашем драйвере. Мы можем менять некоторые поля этой структуры, тем самым меняя свойства драйвера. Второй параметр имеет тип PUNICODE_STRING
, который означает указатель на строку типа UNICODE
. Она, в свою очередь, указывает, где в системном реестре хранится информация о нашем драйвере.
Любая ошибка в драйвере может вызвать общесистемный сбой и BSOD. Вероятна потеря данных и повреждение системы. Все эксперименты я рекомендую проводить в виртуальной машине.
Может ли зловред скрыть свой процесс от KMDF-драйвера?
Загрузка ...
IRQL — это своеобразный «приоритет» для драйверов. Чем выше IRQL, тем меньшее число других драйверов будут прерывать выполнение нашего кода. Существует несколько уровней IRQL: Passive, APC, Dispatch и DIRQL. Если открыть документацию MSDN по функциям WinAPI, то можно увидеть примечания, которые регламентируют уровень IRQL, который требуется для обращения к каждой функции. Чем выше этот уровень, тем меньше WinAPI нам доступно для использования. Первые три уровня IRQL используются для синхронизации программных частей ОС, уровень DIRQL считается аппаратным и самым высоким по сравнению с программными уровнями.
IRP — это запросы, которые поступают к драйверу. Именно при помощи IRP один драйвер может «попросить» сделать что-то другой драйвер либо получить запрос от программы, которая им управляет. IRP используются диспетчером ввода-вывода ОС. Чтобы научить программу воспринимать наши IRP, мы должны зарегистрировать функцию обратного вызова и настроить на нее массив указателей на функции. Код весьма прост:
for(x = 0; x < IRP_MJ_MAXIMUM_FUNCTION; ++x) pDriverObject->MajorFunction[x] = MyCallbackFunc;
А вот код функции-заглушки, которая всегда возвращает статусный код STATUS_SUCCESS
. В этой функции мы обрабатываем запрос IRP.
NTSTATUS MyCallbackFunk(PDEVICE_OBJECT pDeviceObject, PIRP pIrp) { pIrp->IoStatus.Status = STATUS_SUCCESS; IoCompleteRequest(pIrp, IO_NO_INCREMENT); return pIrp->IoStatus.Status; }
Теперь любой запрос к нашему драйверу вызовет функцию-заглушку, которая всегда возвращает STATUS_SUCCESS
. Но что, если нам нужно попросить драйвер сделать что-то конкретное, например вызвать определенную функцию? Для этого регистрируем управляющую процедуру:
#define IRP_MY_FUNC 0x801
Здесь мы объявили процедуру с именем IRP_MY_FUNC
и ее кодом — 0x801
. Чтобы драйвер ее обработал, мы должны настроить на нее ссылку, создав таким образом дополнительную точку входа в драйвер:
for(x = 0; x < IRP_MJ_MAXIMUM_FUNCTION; ++x) pDriverObject->MajorFunction[x] = MyCallbackFunc; // Заполнили все коды IRP ссылкой на функцию-заглушку pDriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = MyCallbackControl; // Настроили вызов функции MyCallbackControl на запрос IRP_MJ_DEVICE_CONTROL
После этого нам нужно получить указатель на стек IRP, который мы будем обрабатывать. Это делается при помощи функции IoGetCurrentIrpStackLocation
, на вход которой подается указатель на пакет. Кроме этого, необходимо будет получить от диспетчера ввода-вывода размеры буферов ввода-вывода, чтобы иметь возможность передавать и получать данные от пользовательского приложения. Шаблонный код каркаса обработчика управляющей процедуры:
PIO_STACK_LOCATION pIrpSt = IoGetCurrentIrpStackLocation(pIrp); // Получаем указатель на стек IRP пакета ULONG InBufLen = IrpStack->Parameters.DeviceIoControl.InputBufferLength; // Получаем размер буфера ввода ULONG OutBufLen = IrpStack->Parameters.DeviceIoControl.OutputBufferLength; // Получаем размер буфера вывода ULONG CtrlCode = IrpStack->Parameters.DeviceIoControl.IoControlCode; // Получаем код управляющей процедуры NTSTATUS status = STATUS_SUCCESS; swich(CtrlCode) { case IRP_MY_FUNC: // Здесь код, который будет вызываться управляющей процедурой IRP_MY_FUNC break; default: status = STATUS_INVALID_DEVICE_REQUEST; break; } return status;
Материалы из последних выпусков можно покупать отдельно только через два месяца после публикации. Чтобы продолжить чтение, необходимо купить подписку.
Подписка позволит тебе в течение указанного срока читать ВСЕ платные материалы сайта. Мы принимаем оплату банковскими картами, электронными деньгами и переводами со счетов мобильных операторов. Подробнее о подписке
1 год6490 р. Экономия 1400 рублей! |
1 месяц720 р. 25-30 статей в месяц |
Уже подписан?
Читайте также
Последние новости