Windows wdm device driver

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

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

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

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

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

  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%\inc\ddk\wxp .

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Экшен

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

DRIVER_INITIALIZE callback function (wdm.h)

DriverEntry is the first routine called after a driver is loaded, and is responsible for initializing the driver.

Syntax

Parameters

[in] A pointer to a DRIVER_OBJECT structure. This is the driver’s driver object.

[in] A pointer to a counted Unicode string specifying the path to the driver’s registry key.

Return value

If the routine succeeds, it must return STATUS_SUCCESS. Otherwise, it must return one of the error status values defined in Ntstatus.h.

Remarks

The DriverObject parameter supplies the DriverEntry routine with a pointer to the driver’s driver object, which is allocated by the I/O manager. The DriverEntry routine must fill in the driver object with entry points for the driver’s standard routines.

The DriverObject pointer gives the driver access to DriverObject->HardwareDatabase, which points to a counted Unicode string that specifies a path to the registry’s \Registry\Machine\Hardware tree.

The registry path string pointed to by RegistryPath is of the form \Registry\Machine\System\CurrentControlSet\Services\DriverName. A driver can use this path to store driver-specific information; see Registry Keys for Drivers. The DriverEntry routine should save a copy of the Unicode string, not the pointer, since the I/O manager frees the RegistryPath buffer after DriverEntry returns.

For more information about implementing a DriverEntry routine, see Writing a DriverEntry Routine.

While it is possible to name this routine something other than DriverEntry, doing so is not recommended. The DDK-supplied build tools automatically inform the linker that the driver’s entry point is called DriverEntry, so giving the routine another name requires you to modify the build tools. For more information about build tools, see Building a Driver.

Examples

To define a DriverEntry callback routine, you must first provide a function declaration that identifies the type of callback routine you’re defining. Windows provides a set of callback function types for drivers. Declaring a function using the callback function types helps Code Analysis for Drivers, Static Driver Verifier (SDV), and other verification tools find errors, and it’s a requirement for writing drivers for the Windows operating system.

To define a DriverEntry callback routine, use the DRIVER_INITIALIZE type as shown in this code example:

Then, implement your callback routine as follows:

The DRIVER_INITIALIZE function type is defined in the Wdm.h header file. To more accurately identify errors when you run the code analysis tools, be sure to add the Use_decl_annotations annotation to your function definition. The Use_decl_annotations annotation ensures that the annotations that are applied to the DRIVER_INITIALIZE function type in the header file are used. For more information about the requirements for function declarations, see Declaring Functions by Using Function Role Types for WDM Drivers. For information about Use_decl_annotations, see Annotating Function Behavior.

Controlling Device Access (WDM)

Access to a device is controlled by a security descriptor (and the ACL it contains). A security descriptor for a device object can be specified when the device object is created, or set in the registry.

Controlling Device Access for WDM Drivers

When a WDM driver (other than certain bus drivers) creates a device object, the Plug and Play manager determines a security descriptor for the device. The order of operations is as follows.

The PnP manager calls the driver’s AddDevice routine.

The driver’s AddDevice routine calls IoCreateDevice to create the device object and attach it to the device object stack.

The PnP manager updates the security descriptor for the newly-created device object.

For a WDM driver, the PnP manager determines the security descriptor for the device object as follows.

If the device has a security descriptor setting in the registry, it is applied to every object in the device stack.

Otherwise, if the device’s setup class has a security descriptor setting in the registry, it is applied to every object in the device stack.

Otherwise, the PnP manager leaves the default security descriptor for each object unchanged. In this case, the default security descriptor for the stack is determined by the device type and device characteristics of the PDO.

For most device types and characteristics, the default security descriptor gives full access (GENERIC_ALL) to administrators, and read, write, and execute access (GENERIC_READ | GENERIC_WRITE | GENERIC_EXECUTE) access to everyone else.

For more information about how to set a security descriptor for a device or device setup class in the registry, see Setting Device Object Properties in the Registry.

If a device is operated in raw mode, then the PnP manager cannot determine a security descriptor for the device object. In that case, the bus driver must provide a security descriptor; see below.

Controlling Device Access for WDM Bus Drivers

A WDM bus driver must provide a security descriptor for the PDO of every device that can be operated in raw mode. Use IoCreateDeviceSecure to create the device object with a security descriptor.

If the bus driver does not operate a device in raw mode, then it is not required to supply a security descriptor. The PnP manager determines the security descriptor, as described above. The bus driver can supply a security descriptor if it must ensure that its PDOs have stricter security settings than the default descriptor. Any descriptor specified by the bus driver is overridden by settings in the registry.

For more information about creating device objects, see Creating a Device Object.

Controlling Device Access for Non-WDM Drivers

Non-WDM drivers must specify a default security descriptor and class GUID for any named device objects they create.

Use the IoCreateDeviceSecure routine to create the named device object and to specify the default security descriptor and class GUID for that device. The security descriptor is specified in a subset of SDDL. For more information, see SDDL for Device Objects.

The system overrides the default security descriptor with any security settings in the registry for the specified class GUID. The driver must specify its own unique GUID for the device. Use the GuidGen tool to generate a unique GUID. (GuidGen is included in the Microsoft Windows SDK.)

Читайте также:  Linux find print date
Оцените статью