Пишем драйвер WDM на Ассемблере.

(Комментарии к исходникам AsmDrv)

Часть 1. Цель работы. Постановка задачи.

Признаюсь честно: мне нравится Ассемблер. Вернее, даже не сам Ассемблер, а стиль общения с компьютером через него.

В сети есть несколько примеров создания драйверов виртуальных устройств VxD на Ассемблере.

Но нет ни одного аналогичного примера для драйвера WDM.

Так исправим же эту досадную оплошность!

Создание несложного драйвера с использованием только лишь Ассемблера – довольно трудоёмкое занятие.

По двум причинам:

1) Отсутствие ассемблерных заголовочных файлов для использования драйверного API.

2) Методически трудная отладка драйверов в системе Windows.

Первая причина может быть некритичной. Были бы руки да голова. Ведь известно, что значительная часть заголовков Win32 API была переведена энтузиастами на Ассемблер. И работа эта немалая.

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

Однако, теперь у вас будет хороший кусок работающего кода. Мы его сейчас напишем! Изменяя и дополняя его, вы сможете создать свой собственный драйвер, довольно быстро и легко.

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

Что нам потребуется? Вот что:

1) Текстовый редактор.

Notepad. Но лучше что-нибудь поудобнее, например, Патриот XP.

2) MS Windows DDK.

DDK содержит почти всё, что требуется для создания драйверов. Но нам важны: справка DDK, Ассемблер masm 6.1, компоновщик Link, также оттуда мы возьмём библиотеки и заголовочные файлы для C (что с ними делать – см. далее).

3) Утилита для визуализации отладочного вывода. Я использую DbgView , который можно взять с сайта www.sysinternals.com

4) Delphi для компиляции тестовой программы.

Но вам необязательно набирать текст с нуля. К счастью, я сделал это до вас :)

Скачайте файл с исходниками проекта AsmDrv и распакуйте его в подкаталог \NTDDK\src\AsmDrv.

(http://progrex.narod.ru/sources/AsmDrv.zip)

Вот, кажется, всё. Можно начинать!

Часть 2. API для WDM драйвера.

Большинство функций драйверного API, которые нас интересуют, предоставляются модулем ntoskrnl.exe.

Для их использования надо сделать следующее:

1) Объявить типы данных и определить константы.

Большинство определений для C находятся в файлах ntdef.h и wdm.h.

2) Объявить прототипы функций, которые мы намерены использовать.

Эти определения для C также находятся в wdm.h

3) Выполнить сборку драйвера с подключением библиотеки wdm.lib

Все три файла (wdm.h, ntdef.h и wdm.lib) входят в состав Windows DDK.

Я перевёл часть заголовков на Ассемблер и поместил их в файл usewdm.inc, который находится в базовом каталоге проекта.

Часть 3. Пишем рыбу.

3.1. Итак, приступим.

Вы можете проследить за последовательностью и содержанием действий, открыв файл main.asm для просмотра.

Начнём, пожалуй, так:

.586p ; Процессор Intel Pentium, разрешены инструкции защищённого режима

.model flat, stdcall ; Здесь всё ясно. Плоская модель адресации и тип вызовов stdcall.

option casemap:none ; "case-sensitive"

Дальше нужно задействовать файл включений usewdm.inc и библиотеку wdm.lib, чтобы мы смогли использовать драйверный API:

.include usewdm.inc

.includelib wdm.lib

Затем размещаем два сегмента – данных и кода:

.data

; […]

.code

; […]

3.2. Процедура инициализации

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

У нас такая процедура называется DriverEntry. Объявим её как

Driver Entry proc near public, DriverObject:PDRIVER_OBJECT, RegistryPath:PUNICODE_STRING

DriverObject – это указатель на служебную структуру, сопоставленную драйверу. Она используется системой для вызова процедур драйвера. Её-то и следует инициализировать – записать в эту структуру адреса соответствующих процедур нашего драйвера.

Наш драйвер довольно прост. Он будет отрабатывать только 4 стандартных запроса:

IRP_MJ_CREATE – Вызов CreateFile() в приложении пользователя для установления связи с драйвером;

IRP_MJ_CLOSE – Вызов CloseHandle() в приложении пользователя для разрыва связи с драйвером;

IRP_MJ_DEVICE_CONTROL – Вызов DeviceIoControl() в приложении пользователя для запроса выполнения какой-либо функции в драйвере.

Все эти три запроса мы адресуем некоей диспетчерской функции OnDispatch. Мы узнаем о ней позже.

Четвёртый запрос – на выгрузку. Об этом пойдёт речь ниже.

А пока необходимо сделать ещё 2 важные вещи – создать логический объект устройства при помощи функции IoCreateDevice() и символическую связь, имя которой пользовательские приложения будут использовать для связи с драйвером при помощи функции CreateFile(). Символическая связь создаётся при помощи вызова IoCreateSymbolicLink():

; Инициализируем юникодовые строки с именами устройства и линка

invoke RtlInitUnicodeString, offset NtDeviceName, offset wsNtDeviceName

invoke RtlInitUnicodeString, offset Win32DeviceName, offset wsWin32DeviceName

; […]

; Создаём логический объект устройства

invoke IoCreateDevice, DriverObject, 0, offset NtDeviceName, FILE_DEVICE_UNKNOWN,0,FALSE,offset DeviceObject;

cmp eax,STATUS_SUCCESS ; Проверим, не было ли ошибки.

jnz @F

; Создаём symbolic link

invoke IoCreateSymbolicLink, offset Win32DeviceName, offset NtDeviceName ; в eax останется код результата

@@:

ret

Итак, только что мы завершили разбор процедуры инициализации.

3.3. Процедура выгрузки.

У нас она реализуется функцией OnUnload. Эта функция производит действия, обратные процедуре инициализации по отношению к связанным объектам: она удаляет символическую связь (вызов IoDeleteSymbolicLink()), и затем логическое устройство, сопоставленное драйверу (IoDeleteDevice()):

; Удаляем символическую связь

invoke IoDeleteSymbolicLink, offset Win32DeviceName

; Удаляем логическое устройство

invoke IoDeleteDevice, DeviceObject

3.4. Главная диспетчерская процедура.

Она называется OnDispatch и объявлена как

OnDispatch proc near, pDeviceObject:PDEVICE_OBJECT, pIrp:PIRP

Здесь нам важен указатель на структуру с данными запроса pIrp. Данная структура довольно сложна. Вы можете найти её объявление в файле usewdm.inc.

Но нам понадобятся лишь некоторые данные.

Сначала мы должны определить код запроса – он будет один из трёх: IRP_MJ_CREATE, IRP_MJ_CLOSE или IRP_MJ_DEVICE_CONTROL.

Мы получаем этот код из структуры IO_STACK_LOCATION, указатель на которую мы получаем из структуры IRP(в свою очередь, указатель на irp был передан нам в пераметре pIrp):

mov ebx,pIrp

mov eax,(_IRP ptr [ebx]).Tail.Overlay.CurrentStackLocation ; Восстанавливаем указатель на структуру IO_STACK_LOCATION

mov pIrpStack,eax

mov ebx,pIrpStack

mov al,(IO_STACK_LOCATION ptr [ebx]).MajorFunction ; al – Код сообщения

Дальше отрабатываем запросы по-разному.

Для IRP_MJ_CREATE и IRP_MJ_CLOSEобработка фиктивная. Мы просто возвращаем код успеха STATUS_SUCCESS в регистреeax.

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

Мы размещаем эти данные в локальных переменных, чтобы потом вызвать вторичную функцию DeviceIoControlHandler, где и будет выполнена обработка.

Часть 4. Детализация.

Детализация заключается в размещении пользовательского кода во вторичной процедуре обработчика IRP_MJ_DEVICE_CONTROL.

Мы будем отрабатывать 2 запроса:

IOCTL_USER_REQUEST_1 – отправка переданной строки в отладочный вывод, и

IOCTL_USER_REQUEST_2 – перевод литер переданной строки в нижний регистр.

Коды запросов объявлены в файле-включении ioctlcodes.inc

В принципе, реализация этих вещей довольно проста и не требует комментариев.

Часть 5. Компиляция и сборка.

Для компиляции программы следует выполнить командный файл assemble.cmd.

Его содержимое:

..\..\bin\ml.exe –coff –Fl –c –Foasmdrv.obj main.asm

В результате мы получим листинг main.lst и объектный модульasmdrv.obj.

Дальше мы должны собрать бинарник драйвера из объектного модуля. Для этой цели существует команда link.cmd:

..\..\bin\link.exe @linkcmd.rsp

в файле linkcmd.rsp размещены настройки линкера. Полный список выглядит так:

-MACHINE:IX86

-STACK:32768,4096

-OPT:REF

-OPT:ICF

-INCREMENTAL:NO

-FORCE:MULTIPLE

-RELEASE

-DEFAULTLIB:wdm.lib

-DRIVER

-ALIGN:0x20

-SUBSYSTEM:NATIVE

-BASE:0x10000

-ENTRY:DriverEntry@8

-OUT:disk1\asmdrv.sys

asmdrv.obj

В результате сборки мы получаем файлAsmDrv.sys в подкаталоге Disk1.

Часть 6. Как правильно установить драйвер.

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

Это так называемый inf-файл.

Опять-таки, к счастью для вас, я уже написал этот файл – asmdrv.inf. Вы можете открыть его для просмотра и изучить.

Файл находится в подкаталоге Disk1 проекта.

Ну что же? – Пробуем установиться.

Открываем Панель управления, запускаем Мастер установки оборудования и указываем ему путь к файлу asmdrv.inf в режиме ручного выбора.

Завершаем установку.

Windows 98 на этом месте может попросить перезагрузки. Не отказывайте ей :)

Windows 2000/XP запускает драйвер сразу.

Вы можете проконтролировать установку, убедившись в наличии устройства "Простейший WDM драйвер на Ассемблере" в списке менеджера устройств.

Поздравляю, если вы всё сделали правильно, наш драйвер – о, чудо! – работает.

Часть 7. Тестовая программа.

Проект тестовой программы расположен в подкаталоге TestApp.

Откройте его в Delphi и перекомпилируйте.

В результате вы получите файл AsmDrvTest.exe, который нужно будет запустить.

В принципе, это одна из самых простых программ в мире. Она занимается отправкой драйверу AsmDrv.sys запросов IOCTL_USER_REQUEST_1 и _2по требованию пользователя, передавая драйверу строку символов.

Работу этой программы рекомендуется изучить самостоятельно.

Одновременно с тестовой программой используйте утилиту DbgView для просмотра отладочного вывода.

Часть 8. Напоследок.

Мы убедились ещё раз, что не боги горшки обжигают.

Следует ли писать драйверы WDM на ассемблере? –

Зависит от желания и возможностей.

Однако вопрос оставлю открытым.

В любом случае, мне интересно было бы узнать, использовал ли кто-нибудь этот пример для создания своего драйвера? Пишите.

Content

Пишем драйвер WDM на Ассемблере.
(Комментарии к исходникам AsmDrv)

Часть 1. Цель работы. Постановка задачи.

Часть 2. API для WDM драйвера.

Часть 3. Пишем рыбу.

3.1. Итак, приступим.

3.2. Процедура инициализации

3.3. Процедура выгрузки.

3.4. Главная диспетчерская процедура.

Часть 4. Детализация.

Часть 5. Компиляция и сборка.

Часть 6. Как правильно установить драйвер.

Часть 7. Тестовая программа.

Часть 8. Напоследок.