Следующая новость
Предыдущая новость

Прятки по хардкору. Как сделать свой драйвер режима ядра Windows и скрывать процессы

14.03.2018 13:35
Прятки по хардкору. Как сделать свой драйвер режима ядра Windows и скрывать процессы

Содержание статьи

  • Создание драйвера KMDF
  • Точка входа в драйвер
  • Interrupt Request Level (IRQL)
  • Пакеты запроса ввода-вывода (Input/Output Request Packet)
  • Создание устройства драйвера
  • Скрытие процессов методом DKOM (Direct Kernel Object Manipulation)
  • Загрузчик драйверов
  • Итоги

Все мало-мальски серьезные защитные приложения, будь то файрволы или антивирусы, используют собственные модули режима ядра (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 драйвер?

  • Для защиты своей утилиты от действий вредоносов и поиска зловредов, маскирующихся в режиме ядра
  • Для противодействия blue pill и другим руткитам, использующим режим аппаратной виртуализации
  • Для ускорения антивирусной проверки

Прятки по хардкору. Как сделать свой драйвер режима ядра Windows и скрывать процессы Загрузка ...

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

Прятки по хардкору. Как сделать свой драйвер режима ядра Windows и скрывать процессы

WARNING

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

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

  • Нет, даже если он сам работает в режиме ядра
  • Да, если он использует апаратную виртуализацию или SMM
  • Да, если зловред был загружен в память до него

Прятки по хардкору. Как сделать свой драйвер режима ядра Windows и скрывать процессы Загрузка ...

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. Чтобы драйвер ее обработал, мы должны настроить на нее ссылку, создав таким образом дополнительную точку входа в драйвер:

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 статей в месяц

Уже подписан?

Источник

Последние новости