Все мало-мальски серьезные защитные приложения, будь то файрволы или антивирусы, используют собственные модули режима ядра (ring 0), через которые работает большинство их функций: защита процессов от завершения, фильтры различных событий, получение актуальной информации о состоянии сетевого трафика и количестве процессов в системе. Если у программы есть такой драйвер, то пробовать скрываться от нее из режима пользователя (ring 3) бессмысленно. Так же бесполезно пытаться на нее как-то воздействовать. Решение — написать собственный драйвер. В этой статье я покажу, как это делается.

INFO

Процессорные архитектуры 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 драйвер?

Загрузка ... Загрузка ...
 

Создание драйвера 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. Она, в свою очередь, указывает, где в системном реестре хранится информация о нашем драйвере.

WARNING

Любая ошибка в драйвере может вызвать общесистемный сбой и BSOD. Вероятна потеря данных и повреждение системы. Все эксперименты я рекомендую проводить в виртуальной машине.

Может ли зловред скрыть свой процесс от KMDF-драйвера?

Загрузка ... Загрузка ...
 

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_CONTROL
pDriverObject->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_FUNC
break;

default:
    status = STATUS_INVALID_DEVICE_REQUEST;
break;
}

return status;

Продолжение доступно только подписчикам

Вариант 1. Оформи подписку на «Хакер», чтобы читать все материалы на сайте

Подписка позволит тебе в течение указанного срока читать ВСЕ платные материалы сайта. Мы принимаем оплату банковскими картами, электронными деньгами и переводами со счетов мобильных операторов. Подробнее о подписке

Вариант 2. Купи один материал

Заинтересовала информация, но нет возможности оплатить подписку? Тогда этот вариант для тебя! Обрати внимание: этот способ покупки доступен только для материалов, опубликованных более двух месяцев назад.


4 комментария

  1. Arseniy

    14.03.2018 at 22:57

    Какова цель публикации самостоятельной статьи «для самых маленьких»? Те, кто этого не знал, все равно утонут в отладке, как только малейшие трудности возникнут. Возникнет вопрос про подпись явно. А те, кто с драйвером хоть немного знаком, будет разочарован потратив на этот текст время. Могу предложить автору написать серию про актуальное драйверописание в стиле Журнала (пусть данная статья будет номер 0)

  2. Jungle

    17.03.2018 at 17:50

    ОК, а как установить неподписанный драйвер? В 64-разрядных Windows, начиная с 7-ки, наличие ЭЦП у драйвера — обязательное требование. Да и скрывать процессы можно и без драйвера. Лет 15 назад обсасывалась на wasm.ru. Сегодня это вроде wasm.in.

Оставить мнение

Check Also

Творческая Iskra. Делаем аппаратный менеджер паролей своими руками

Давай рассмотрим настоящий тру-хакерский, тру-гиковский, удобный и безопасный способ храни…