![](/uploads/posts/2022-08/loading.gif)
Содержание статьи
Итак, мы решили писать собственный драйвер. Начнем с выбора инструментария. Я советую использовать 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 драйвер?
- Для защиты своей утилиты от действий вредоносов и поиска зловредов, маскирующихся в режиме ядра
- Для противодействия blue pill и другим руткитам, использующим режим аппаратной виртуализации
- Для ускорения антивирусной проверки
![Загрузка ... Загрузка ...](/uploads/posts/2022-08/loading.gif)
Создание драйвера KMDF
После того как ты создашь проект драйвера, 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
. Она, в свою очередь, указывает, где в системном реестре хранится информация о нашем драйвере.
Может ли зловред скрыть свой процесс от KMDF-драйвера?
- Нет, даже если он сам работает в режиме ядра
- Да, если он использует апаратную виртуализацию или SMM
- Да, если зловред был загружен в память до него
![Загрузка ... Загрузка ...](/uploads/posts/2022-08/loading.gif)
Interrupt Request Level (IRQL)
IRQL — это своеобразный «приоритет» для драйверов. Чем выше IRQL, тем меньшее число других драйверов будут прерывать выполнение нашего кода. Существует несколько уровней IRQL: Passive, APC, Dispatch и DIRQL. Если открыть документацию MSDN по функциям WinAPI, то можно увидеть примечания, которые регламентируют уровень IRQL, который требуется для обращения к каждой функции. Чем выше этот уровень, тем меньше WinAPI нам доступно для использования. Первые три уровня IRQL используются для синхронизации программных частей ОС, уровень DIRQL считается аппаратным и самым высоким по сравнению с программными уровнями.
Пакеты запроса ввода-вывода (Input/Output Request Packet)
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
. Чтобы драйвер ее обработал, мы должны настроить на нее ссылку, создав таким образом дополнительную точку входа в драйвер:
// Заполним все коды IRP ссылкой на функцию-заглушкуfor(x = 0; x < IRP_MJ_MAXIMUM_FUNCTION; ++x) pDriverObject->MajorFunction[x] = MyCallbackFunc;// Настроим вызов функции MyCallbackControl на запрос IRP_MJ_DEVICE_CONTROLpDriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = MyCallbackControl;
После этого нам нужно получить указатель на стек IRP, который мы будем обрабатывать. Это делается при помощи функции IoGetCurrentIrpStackLocation
, на вход которой подается указатель на пакет. Кроме этого, необходимо будет получить от диспетчера ввода-вывода размеры буферов ввода-вывода, чтобы иметь возможность передавать и получать данные от пользовательского приложения. Шаблонный код каркаса обработчика управляющей процедуры:
// Получаем указатель на стек IRP пакетаPIO_STACK_LOCATION pIrpSt = IoGetCurrentIrpStackLocation(pIrp);// Получаем размер буфера ввода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_FUNCbreak;default: status = STATUS_INVALID_DEVICE_REQUEST;break;}return status;
Скачать:
Скриншоты:
Важно:
Все статьи и материал на сайте размещаются из свободных источников. Приносим свои глубочайшие извинения, если Ваша статья или материал была опубликована без Вашего на то согласия.
Напишите нам, и мы в срочном порядке примем меры.