Простейший WDM-драйвер

в 19:20, , рубрики: windows, драйверы, системное программирование, метки: ,

Приветствую!

В данной статье описан процесс написания простейшего драйвера, который выводит скан-коды нажатых клавиш.
Также в данной статье описан процесс настройки рабочего места для написания драйверов.
Если Вам интересно, прошу под кат.

Подготовка стенда

Установка необходимого ПО для написания простейшего драйвера

Необходимое ПО:

  1. Windows DDK (Driver Development Kit);
  2. VMware Workstation или Virtual Box;
  3. Windows XP;
  4. Visual Studio 2005;
  5. DDKWizard;
  6. KmdManager
  7. DebugView;

Я использую две виртуальные машины, пишу драйверы на одной, а запускаю на другой. Если вы тоже решите так делать то для той машины, на которой вы будете запускать драйверы, хватит 4 Гбайтового жесткого диска и 256 Мбайт оперативной памяти.

Настройка рабочего места

Установка DDK

Установка предельно проста. Единственное на что необходимо обратить внимание — это диалог, в котором Вам предлагается выбрать компоненты, которые будут установлены. Настоятельно рекомендую отметить всю документацию и примеры.

Установка и настройка Microsoft® Visual Studio 2005

Установка Microsoft® Visual Studio 2005 ничем не сложнее установки DDK. Если Вы будете использовать её только для написания драйверов, то когда инсталлятор спросит какие компоненты необходимо установить, выберите только Visual C++.
Далее можно установить Visual Assist X. С помощью этой программы (аддона) можно будет легко настроить подсказки для удобного написания драйверов.
После установки Visual Assist X в Visual Studio 2005 появится новое меню VAssistX. Далее в этом меню: Visual Assist X Options -> Projects -> C/C++ Directories -> Platform: Custom, Show Directories for: Stable include files. Нажимаем Ins или на иконку добавить новую директорию и в появившейся строке, если у вас Windows XP вписываем %WXPBASE%incddkwxp.

Установка и настройка DDKWizard

Для того чтобы в Visual Studio можно было компилировать драйверы нужно установить DDKWizard. Его можно скачать с сайта ddkwizard.assarbad.net. Также с этого сайта скачайте скрипт ddkbuild.cmd.
После того как мастер установится необходимо выполнить следующие шаги:

  • Создать системные (рекомендуется) или пользовательские переменные со следующими именами и значением, которое соответствует пути к DDK
    Версия DDK Имя переменной Путь по умолчанию
    Windows XP DDK WXPBASE C:WINDDK2600
    Windows 2003 Server DDK WNETBASE C:WINDDK3790.1830
    Windows Vista/Windows 2008 Server WDK WLHBASE
    Windows 7/Windows 2008 Server R2 WDK W7BASE

    Например, если я использую Windows XP DDK, то я должен создать переменную WXPBASE со значением, которое соответствует пути к DDK. Так как я не изменял путь установки, то значение у меня будет C:WINDDK2600.

  • Скопируйте скачанный скрипт ddkbuild.cmd, например, в папку с DDK. У меня это C:WINDDK.
  • Добавьте в конец системной переменной Path путь к скрипту ddkbuild.cmd.

Всё, машина, на которой будем запускать драйверы, готова.

Установка необходимого ПО для запуска драйверов

Теперь настроим машину, на которой будем запускать написанные драйверы.
Нам потребуются следющие программы:

  • DebugView (link) — это утилитка, которая позволяет просматривать отладочный вывод как режима пользователя так и режима ядра.
  • KmdManager (link) — утилита динамической загрузки/выгрузки драйверов

Всё, машина готова для запуска драйверов.

Постановка задачи

Задача: написать драйвер, который будет выводить в дебаг скан-коды нажатых клавиш и их комбинаций.

Немного теории

Драйвер — это набор функций, которые вызываются операционной системой при наступлении некоторых событий, приходящих от устройства или пользовательского режима.
Существует достаточно много типов драйверов, ниже перечисленны некоторые из них:

  • драйверы классов;
  • минидрайверы;
  • функциональные драйверы;
  • фильтрующие драйверы.

Драйверы классов — это драйверы, котрые пишет Microsoft. Это общие драйвера для определенного класса (неужели!) устройств.
Минидрайверы — драйверы, которые используеют драйвер класса для управления устройством.
Функциональные драйверы — это драйверы, которые работают самостоятельно и определяет все что связано с устройством.
Фильтрующие драйверы — драйверы, которые используются для мониторинга или изменения логики другого драйвера путем изменения данных, которые идут к нему.

Необязательно определять все возожные функции в своем драйвере, но он обязательно должен содержать DriverEntry и AddDevice.

IRP — это структура, которая используется драйверами для обмена данными.

Итак, для того чтобы выводить скан-коды (что это?) в дебаг, будем использовать фильтрующий драйвер.
Существует два типа фильтрующих драйверов:

  • верхние фильтрующие драйверы;
  • нижние фильтрующие драйверы.

То, к какому типу относится ваш драйвер, зависит от того где этот драйвер находится в стеке драйверов устройств. Если Ваш драйвер находится выше функционального драйвера, то его называют верхним фильтрующим драйвером, если ниже, то, нижним фильтрующим драйвером.

Отличия между верхними и нижними фильтрующими драйверами

Через верхние фильтрующие драйверы проходят все запросы, а это значит, что они могут изменять и/или фильтровать информацию, идущую к функциональному драйверу, ну и далее, возможно, к устройству.
Пример использования верхних фильтрующих драйверов:
Фильтр-хук драйвер, который устанавливает свою хук-функцию для системного драйвера IpFilterDirver, для отслеживания и фильтрации траффика. Такие драйверы используются в брандмауэрах.

Через нижние фильтрующие драйверы проходит меньше запросов потому что большинство запросов выполняет и завершает функциональный драйвер.

Проблемы синхронизации

В драйвере, который мы будем писать, есть несколько «проблемных» секций. Для нашего драйвера вполне достаточно использования ассемблерных вставок:

__asm {
	lock dec «переменная, которую нужно уменьшить на единицу»
}

или

__asm {
	lock inc «переменная, которую нужно увеличить на единицу»
}

Префикс lock позволяет безопасно выполнить идущую за ним команду. Она блокирует остальные процессоры, пока выполняется команда.

Экшен

Для начала необходимо включить заголовочные файлы «ntddk.h», «ntddkbd.h»

extern "C"
{
	#include "ntddk.h"
}
#include "ntddkbd.h"

Также необходимо описать структуру DEVICE_EXTENSION

typedef struct _DEVICE_EXTENSION{
	PDEVICE_OBJECT pLowerDO;
} DEVICE_EXTENSION, *PDEVICE_EXTENSION;

Объект pLowerDO это объект устройства, который находится ниже нас в стеке. Он нужен нам для того чтобы знать кому дальше отправлять IRP-пакеты.
Еще для работы нашего драйвера нам нужна переменная, в которой будет храниться количество не завершенных запросов.

int gnRequests;

Начнем с функции, которая является главной точкой входа нашего драйвера.

extern "C" NTSTATUS DriverEntry(IN PDRIVER_OBJECT theDriverObject, IN PUNICODE_STRING ustrRegistryPath)

theDriverObject – объект драйвера, содержит указатели на все необходимые операционной системе функции, которые мы должны будем инициализировать.
ustrRegistryPath – имя раздела в реестре, где хранится информация о данном драйвере.
Для начала необходимо объявить и обнулить переменные:

gnRequests = 0;
NTSTATUS status = {0};

Далее, как я и писал выше, нужно инициализировать указатели на функции

for (int i = 0; i<IRP_MJ_MAXIMUM_FUNCTION; ++i)
	{
	theDriverObject->MajorFunction[i] = DispatchThru;
	}
theDriverObject->MajorFunction[IRP_MJ_READ] = DispatchRead;
theDriverObject->DriverUnload = DriverUnload;

Функция DispatchRead будет обрабатывать запросы на чтение. Она будет вызываться, когда нажата или отпущена клавиша клавиатуры.
Функция DriverUnload вызывается, когда драйвер уже не нужен и его можно выгрузить из памяти, или когда пользователь сам выгружает драйвер. В данной функции должна производиться «зачистка», т.е. освобождаться ресурсы, которые использовались драйвером, завершаться все незавершенные запросы и т.д.
Функция DispatchThru это функция-заглушка. Все что она делает это передача IRP-пакета следующему драйверу (драйверу который находится под нашим в стеке, т.е. pLowerDO из DEVICE_EXTENSION).
Далее мы вызываем нашу функцию, для создания и установки нашего устройства в стек устройств:

status = InstallFilter(theDriverObject);

Эту функцию я опишу чуть ниже.
Возвращаем status, в котором, если функция InstallFilter завершилась удачей, хранится значение STATUS_SUCCESS.
Переходим к функции InstallFilter. Вот её прототип:

NTSTATUS InstallFilter(IN PDRIVER_OBJECT theDO);

Эта функция создает объект устройства, настраивает его и включает в стек устройств поверх \Device\KeyboardClass0

Объявляем переменные:

PDEVICE_OBJECT pKeyboardDevice;
NTSTATUS status = {0};

pKeyboardDevice – это объект устройсва, которое мы должны создать.
Вызываем IoCreateDevice для создания нового устройства

status = IoCreateDevice(theDO, sizeof(DEVICE_EXTENSION), NULL, FILE_DEVICE_KEYBOARD, 0, FALSE, &pKeyboardDevice);

Разберем подробнее параметры:

  • Первый аргумент это объект драйвера, который мы получили как параметр функции InstallFilter. Он передается в IoCreateDevice для того чтобы установить связь между нашим драйвером и новым устройством.
  • Третий параметр это имя устройства
  • Четвертый параметр это тип устройства
  • Пятый параметр это флаги, которые обычно устанавливаются для запоминающих устройств.
  • Шестой параметр описывает можно ли открывать манипуляторы устройства в количестве больше одного. Если FALSE можно открыть только один манипулятор. Иначе можно открыть любое количество манипуляторов.
  • Седьмой параметр это память, в которой будем сохранен созданный объект устройства.

Далее устанавливаем флаги устройства.

pKeyboardDevice->Flags = pKeyboardDevice->Flags | (DO_BUFFERED_IO | DO_POWER_PAGABLE);
pKeyboardDevice->Flags = pKeyboardDevice->Flags & ~DO_DEVICE_INITIALIZING;

Флаги, которые мы устанавливаем для нашего устройства, должны быть эквивалентными флагам устройства, поверх которого мы включаемся в стек.
Далее мы должны выполнить преобразования имени устройства, которое мы включаем в стек.

CCHAR cName[40] = "\Device\KeyboardClass0";
STRING strName;
UNICODE_STRING ustrDeviceName;

RtlInitAnsiString(&strName, cName);
RtlAnsiStringToUnicodeString(&ustrDeviceName, &strName, TRUE);

Функция IoAttachDevice внедряет наше устройство в стек. В pdx->pLowerDO будет храниться объект следующего (нижнего) устройства.

IoAttachDevice(pKeyboardDevice, &ustrDeviceName, &pdx->pLowerDO);

Освобождаем ресурсы:

RtlFreeUnicodeString(&ustrDeviceName);

Далее разберем функцию DispatchRead с прототипом:

NTSTATUS DispatchRead(IN PDEVICE_OBJECT pDeviceObject, IN PIRP pIrp);

Данная функция будет вызываться операционной системой при нажатии или отпускании клавиши клавиатуры
Увеличиваем счетчик незавершенных запросов

__asm {
	lock inc gnRequests
}

Перед тем как передать запрос следующему драйверу мы должны настроить указатель стека для драйвера. IoCopyCurrentIrpStackLocationToNext копирует участок памяти, который принадлежит текущему драйверу, в область памяти следующего драйвера.

IoCopyCurrentIrpStackLocationToNext(theIrp);

Когда запрос идет вниз по стеку в нем еще нет нужных нам данных, поэтому мы должны задать функцию, которая вызовется, когда запрос будет идти вверх по стеку с нужными нам данными.

IoSetCompletionRoutine(theIrp, ReadCompletionRoutine, pDeviceObject, TRUE, TRUE, TRUE)

где ReadCompletionRoutine наша функция.
Передаем IRP следующему драйверу:

return IoCallDriver(((PDEVICE_EXTENSION) pDeviceObject->DeviceExtension)->pLowerDO ,theIrp);

Теперь разберем функцию, которая будет вызываться каждый раз при завершении IRP. Прототип:

NTSTATUS ReadCompletionRoutine(IN PDEVICE_OBJECT pDeviceObject, IN PIRP theIrp, IN PVOID Context);

Получаем DEVICE_EXTENSION:

PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)pDeviceObject->DeviceExtension;

Структура PKEYBOARD_INPUT_DATA используется для описания нажатой клавиши.

PKEYBOARD_INPUT_DATA kidData;

Проверяем, удачно завершен запрос или нет

if (NT_SUCCESS(theIrp->IoStatus.Status))

Чтобы достать структуру KEYBOARD_INPUT_DATA нужно обратиться к системному буферу IRP-пакета.

kidData = (PKEYBOARD_INPUT_DATA)theIrp->AssociatedIrp.SystemBuffer;

Узнаем количество клавиш

int n = theIrp->IoStatus.Information / sizeof(KEYBOARD_INPUT_DATA);

И выводим каждую клавишу:

for(int i = 0; i<n; ++i)
	DbgPrint("Code: %xn", kidData[i].MakeCode);

И не забываем уменьшать количество не обработанных запросов

__asm {
	lock dec gnRequests
}

Возвращаем статус запроса

return theIrp->IoStatus.Status;

Разберем функцию завершения работы. Прототип:

VOID DriverUnload(IN PDRIVER_OBJECT theDO);

Получаем DEVICE_EXTENSION:

PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)theDO->DeviceObject->DeviceExtension;

Извлекаем устройство из стека:

IoDetachDevice(pdx->pLowerDO);

Удаляем устройство:

IoDeleteDevice(theDO->DeviceObject);

Проверяем есть незавершенные запросы или нет. Если мы выгрузим драйвер без этой проверки, при первом нажатии на клавишу после выгрузки будет БСоД.

if (gnRequests != 0)
{
	KTIMER ktTimer;
	LARGE_INTEGER liTimeout;
	liTimeout.QuadPart = 1000000;
	KeInitializeTimer(&ktTimer);
	Задаем таймер и пока не завершены все запросы, крутим цикл
	while(gnRequests > 0)
	{
		KeSetTimer(&ktTimer, liTimeout, NULL); // Устанавливаем таймер
		KeWaitForSingleObject(&ktTimer, Executive, KernelMode, FALSE, NULL); // Ждем пока истечет время

	}
}

Код драйвера:

extern "C"
{
	#include "ntddk.h"
}

#include "ntddkbd.h"

typedef struct _DEVICE_EXTENSION{
	PDEVICE_OBJECT pLowerDO;
} DEVICE_EXTENSION, *PDEVICE_EXTENSION;

int gnRequests;

NTSTATUS DispatchThru(PDEVICE_OBJECT theDeviceObject, PIRP theIrp)
{
	IoSkipCurrentIrpStackLocation(theIrp);
	return IoCallDriver(((PDEVICE_EXTENSION) theDeviceObject->DeviceExtension)->pLowerDO ,theIrp);
}

NTSTATUS InstallFilter(IN PDRIVER_OBJECT theDO)
{
	PDEVICE_OBJECT pKeyboardDevice;
	NTSTATUS status = {0};

	status = IoCreateDevice(theDO, sizeof(DEVICE_EXTENSION), NULL, FILE_DEVICE_KEYBOARD, 0, FALSE, &pKeyboardDevice);
	if (!NT_SUCCESS(status))
	{
		DbgPrint("IoCreateDevice error..");
		return status;
	}
	pKeyboardDevice->Flags = pKeyboardDevice->Flags | (DO_BUFFERED_IO | DO_POWER_PAGABLE);
	pKeyboardDevice->Flags = pKeyboardDevice->Flags & ~DO_DEVICE_INITIALIZING;

	RtlZeroMemory(pKeyboardDevice->DeviceExtension, sizeof(DEVICE_EXTENSION));

	PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)pKeyboardDevice->DeviceExtension;

	CCHAR cName[40] = "\Device\KeyboardClass0";
	STRING strName;
	UNICODE_STRING ustrDeviceName;

	RtlInitAnsiString(&strName, cName);
	RtlAnsiStringToUnicodeString(&ustrDeviceName, &strName, TRUE);

	IoAttachDevice(pKeyboardDevice, &ustrDeviceName, &pdx->pLowerDO);
	//DbgPrint("After IoAttachDevice");
	RtlFreeUnicodeString(&ustrDeviceName);
	
	return status;
}

VOID DriverUnload(IN PDRIVER_OBJECT theDO)
{
	PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)theDO->DeviceObject->DeviceExtension;
	IoDetachDevice(pdx->pLowerDO);
	IoDeleteDevice(theDO->DeviceObject);
	if (gnRequests != 0)
	{
		KTIMER ktTimer;
		LARGE_INTEGER liTimeout;
		liTimeout.QuadPart = 1000000;
		KeInitializeTimer(&ktTimer);
		
		while(gnRequests > 0)
		{
			KeSetTimer(&ktTimer, liTimeout, NULL);
			KeWaitForSingleObject(&ktTimer, Executive, KernelMode, FALSE, NULL);
		}
	}
}

NTSTATUS ReadCompletionRoutine(IN PDEVICE_OBJECT pDeviceObject, IN PIRP theIrp, IN PVOID Context)
{
		PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)pDeviceObject->DeviceExtension;
	PKEYBOARD_INPUT_DATA kidData;
	if (NT_SUCCESS(theIrp->IoStatus.Status))
	{
		kidData = (PKEYBOARD_INPUT_DATA)theIrp->AssociatedIrp.SystemBuffer;
		int n = theIrp->IoStatus.Information / sizeof(KEYBOARD_INPUT_DATA);
		for(int i = 0; i<n; ++i)
		{
			DbgPrint("Code: %xn", kidData[i].MakeCode);
		}
	}
	if(theIrp->PendingReturned)
		IoMarkIrpPending(theIrp);
	__asm{
		lock dec gnRequests
	}
	return theIrp->IoStatus.Status;
}

NTSTATUS DispatchRead(IN PDEVICE_OBJECT pDeviceObject, IN PIRP theIrp)
{
	__asm{
		lock inc gnRequests
	}
	IoCopyCurrentIrpStackLocationToNext(theIrp);
	IoSetCompletionRoutine(theIrp, ReadCompletionRoutine, pDeviceObject, TRUE, TRUE, TRUE);
	return IoCallDriver(((PDEVICE_EXTENSION) pDeviceObject->DeviceExtension)->pLowerDO ,theIrp);
}

extern "C" NTSTATUS DriverEntry(IN PDRIVER_OBJECT theDriverObject, IN PUNICODE_STRING RegistryPath)
{
	NTSTATUS status = {0};
	gnRequests = 0;
	for (int i = 0; i<IRP_MJ_MAXIMUM_FUNCTION; ++i)
	{
		theDriverObject->MajorFunction[i] = DispatchThru;
	}
	theDriverObject->MajorFunction[IRP_MJ_READ] = DispatchRead;
	
	status = InstallFilter(theDriverObject);
	
	theDriverObject->DriverUnload = DriverUnload;
	return status;
}

MAKEFILE:

#
# DO NOT EDIT THIS FILE!!!  Edit .sources. if you want to add a new source
# file to this component.  This file merely indirects to the real make file
# that is shared by all the driver components of the Windows NT DDK
#

!INCLUDE $(NTMAKEENV)makefile.def
SOURCES:
TARGETNAME=sysfile
TARGETPATH=BIN
TARGETTYPE=DRIVER

SOURCES = DriverMain.cpp

Как запустить драйвер и просмотреть отладочную информацию

Для запуска драйвера я использовал утилиту KmdManager. Для просмотра отладочной информации использовалась утилита DbgView.

P. S. Статью писал давно, ещё на третьем курсе, сейчас уже почти ничего не помню. Но если есть вопросы, постараюсь ответить.

Автор: bespechniy

* - обязательные к заполнению поля