Как пишут драйвера для устройств

Facebook Twitter VK Telegram Youtube Яндекс Дзен

Техническая поддержка

Вернуться на старую версию

© 2006–2023, Habr

Предыстория

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

После 20 минут поисков по сети я наткнулся на Github Павла Иосифовича (zodiacon — Overview). Личность легендарная в своих кругах, достаточно посмотреть на его репозиторий, публикации и выступления на именитых конференциях. Помимо этого, Павел является автором/соавтором нескольких книг: «Windows Internals» (книга, имеющаяся у меня на полке, которая принесла немало пользы), и «Windows Kernel Programming» 2019 года выпуска (бегло пролистав 11 Глав или 390 страниц, я понял – это то, что нужно!).
Кстати, книгу вы можете купить прямо на сайте Павла

Ссылка скрыта от гостей

Книгу я приобрёл в бумажной версии, чтобы хоть и немного, но поддержать автора. Безупречное качество, несмотря на то, что она издается в мягком переплете. Хорошие плотные листы формата А4 и качественная краска. (книга без проблем пережила вылитую на нее кружку горячего кофе).
Пока я сидел на балконе и читал четвёртую главу книги, в голову пришла мысль: а почему бы не сделать ряд статей на тему «Программирования драйвера под Windows», так сказать, совместить полезное, с еще более полезным.

И вот я здесь, пишу предысторию.

Как я вижу этот цикл статей и что от него ожидать:
Это будут статьи, которые будут базироваться на вышеупомянутой книге, своеобразный вольный и сокращенный перевод, с дополнениями и примечаниями.

Базовые понятия о внутреннем устройстве Windows (Windows Internals)

Для того, чтобы начать разрабатывать Драйвер под Windows, то есть работать на уровне с ядром ОС, необходимо базовое понимание того, как эта ОС утроена. Так как я хочу сосредоточиться на написании драйвера, а не на теории об операционных системах, подробно описывать понятия я не буду, чтобы не растягивать статью, вместо этого прикреплю ссылки для самостоятельного изучения.

Следовательно, нам стоит ознакомиться с такими базовыми понятиями как:

Ссылка скрыта от гостей

Процесс – это объект, который управляет запущенной инстанцией программы.​

Ссылка скрыта от гостей

Технология, позволяющая создавать закрытые пространства памяти для процессов. В своем роде — это песочница.​

Ссылка скрыта от гостей

Это сущность, которая содержится внутри процесса и использует для работы ресурсы, выделенные процессом — такие, как виртуальная память. По сути, как раз таки потоки и запускают код.​

Ссылка скрыта от гостей

В своем роде это прокладка, которая позволяет программе отправлять запросы в Ядро операционной системы, для выполнения нужных ей операций.​

Ссылка скрыта от гостей

Это сложно описать словами коротко, проще один раз увидеть картинку.​

В упрощённом виде это выглядит так:​

image1.png

Ссылка скрыта от гостей

Дескрипторы и объекты необходимы для регулирования доступа к системным ресурсам.​

Объект — это структура данных, представляющая системный ресурс, например файл, поток или графическое изображение.​

Дескриптор – это некая абстракция, которая позволяет скрыть реальный адрес памяти от Программы в пользовательском режиме.​

Для более глубокого понимания Операционных систем могу посоветовать следующие материалы:
Книги:

  • Таненбаум, Бос: Современные операционные системы
  • Windows Internals 7th edition (Part 1)

Видео:

Настройка рабочего пространства

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

Что нам понадобится:

1. Visual Studio 2017 и старше.​

(Community Version хватает с головой) Также во вкладке „Individual components” необходимо установить

Код:

MSVC v142 - VS 2019 C++ ARM build tools (Latest)
MSVC v142 - VS 2019 C++ ARM Spectre-mitigated libs (Latest)
MSVC v142 - VS 2019 C++ ARM64 build tools (Latest)
MSVC v142 - VS 2019 C++ ARM64 Spectre-mitigated libs (Latest)
MSVC v142 - VS 2019 C++ ARM64EC build tools (Latest - experimental)
MSVC v142 - VS 2019 C++ ARM64EC Spectre-mitigated libs (Latest - experimental)
MSVC v142 - VS 2019 C++ x64/x86 build tools (Latest)
MSVC v142 - VS 2019 C++ x64/x86 Spectre-mitigated libs (Latest)

image10.png

и далее по списку.

2. Windows 10/11 SDK (последней версии)​

Ссылка скрыта от гостей

image3.png

Тут все просто. Качаем iso файл, монтируем и запускаем установщик.

3. Windows 10/11 Driver Kit (WDK)​

Ссылка скрыта от гостей

image4.png

В конце установки вам будет предложено установить расширение для Visual Studio. Обязательно установите его!

image11.png

После закрытия окна установки WDK появится установщик Расширения VisualStudio

image2.png

4. Sysinternals Suite

Ссылка скрыта от гостей

Скачайте и распакуйте в удобное для вас место. Это набор полезных утилит, которые пригодятся для исследования Windows, дебага драйвера и прочего.

image8.png

5. Виртуальная Машина с Windows для тестов.​

Выбор ПО для виртуализации на ваше усмотрение. Я буду использовать «VMware Workstation 16 pro».
Написанные драйверы лучше тестировать именно в виртуальной машине, так как Ядро — ошибок не прощает, и вы будете часто улетать в синий экран смерти.

После того, как все было установлено, пора запускать Visual Studio и начинать писать драйвер.

Создание проекта

Запускаем Visual Studio и создаем новый проект. Создадим пустой проект „Empty WDM Driver“

image21.png

Называем его как душе угодно.

image24.png

И вот он, наш свеженький чистенький проект для нашего первого драйвера.

image27.png

Теперь необходимо создать cpp файл, в котором мы будем писать сам драйвер.

image7.png

image14.png

Вот и все. Настройку системы и среды мы закончили.

image22.png

Первый драйвер

Сначала импортируем ntddk.h эта одна из базовых библиотек для работы с ядром. Больше информации

Ссылка скрыта от гостей

. Как и у любой программы, у драйвера должна быть точка входа DriverEntry, как функция Main в обычной программе. Готовый прототип этой функции выглядит так

C++:

#include <ntddk.h>
NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) {
/*
    In_ это часть SAL(Source Code Ananotation Language) Аннотации не видимы для компилятора,
    но содержат метаданные которые, улучшают анализ и чтение кода.
*/
    return STATUS_SUCCESS;
}

Если мы попробуем собрать наш проект, то получим следующие ошибки и предупреждения.

image15.png

В данном случае пункт 1 является следствием пунктов 2 и 3. Дело в том, что по дефолту в Visual Studio некоторые “предупреждения” расцениваются как ошибки.
Чтобы решить эту проблему есть 2 пути.

  1. Отключить эту фичу в Visual Studio, что делать не рекомендуется. Так как сообщения об ошибках могут быть полезны и сэкономят вам время и нервы в дальнейшем.
  2. Более правильный и классический метод это использовать макросы в c++. Как видно из сообщения с кодом C4100 объекты RegistryPath и DriverObject не упомянуты в теле функции. Подробнее

    Ссылка скрыта от гостей

    .

Для того, чтобы избавиться от предупреждений, и заставить наш код работать, стоит поместить объекты в макрос UNREFERENCED_PARAMETER(ObjectName)

C++:

include <ntddk.h>

NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) {
    UNREFERENCED_PARAMETER(DriverObject);
    UNREFERENCED_PARAMETER(RegistryPath);
    return STATUS_SUCCESS;
}

Теперь, если пересобрать проект, то мы увидим, что ошибка С220 и предупреждение C4100 пропали, но к ним на смену пришли LNK2019 и LNK1120. Однако это уже не ошибки компиляции — это ошибки линкера. А кто говорил что будет легко?
О том, что такое линкер можно почитать

Ссылка скрыта от гостей

.

Дело в том, что наша функция не представлена в стандартном линкере С++ и вообще она девушка капризная и хочет Си-линкер. Удовлетворим желание дамы и дадим ей то, чего она хочет.

Делается это просто. Перед функцией надо добавить extern "C" так наш линкер будет понимать, что эта функция должна линковаться С-линкером.

Собираем проект заново и вуаля — Драйвер собрался.

image17.png

Что на данный момент умеет наш драйвер? Сейчас это по сути пустышка, которая после загрузки, в случае успеха, вернет нам сообщения об удачном запуске. Давайте заставим его нас поприветствовать и проверим его работоспособность. Выводить сообщения мы будем при помощи функции KdPrint(()); да именно в двойных кавычках.

Итоговый код драйвера будет выглядеть так:

C++:

#include <ntddk.h>

//Указываем линкеру, что DriverEntry должна линковаться С-линкером
extern "C"
NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath)
{
    //Убираем варнинг C4100 и связанную с ним ошибку C220
    UNREFERENCED_PARAMETER(DriverObject);
    UNREFERENCED_PARAMETER(RegistryPath);
    //Выводим сообщение
    KdPrint(("Hi Codeby, this is our first driver! Yuhu!n"));
   
    return STATUS_SUCCESS;
}

Собираем или пересобираем драйвер.

image6.png

Важно! Сборка драйвера должна происходить в режиме Debug!!!

image12.png

После чего в папке нашего проекта мы сможем найти результаты нашего труда. Вы только посмотрите на него, какой маленький и хорошенький.

image5.png

Но что делать дальше? Как проверить его работоспособность?
Для этого нам и понадобится наша виртуальная машина с Windows, но перед запуском на ней драйвера, нам придется проделать пару манипуляций. Дело в том, что в Windows есть встроенная защита, и если драйвер не подписан «нужной» подписью ака сертификатом, то драйвер просто не загрузится.

Дальнейшие действия нужно проделать в Windows на виртуальной машине.
Чтобы отключить эту проверку подписи, а точенее перевести Windows в тестовый режим, запустите cmd.exe от имени администратора и введите следующую команду bcdedit /set testsigning on.

image13.png

Перезагрузите виртуальную машину.
Если все прошло удачно, в правом нижнем углу вы увидите следующую надпись (2 нижнее строчки могут отличиться в зависимости от версии Windows)

image9.png

Возвращаемся в папку с драйвером и копируем его в виртуальную машину. Теперь нам надо создать службу для запуска драйвер. Открываем консоль от имени администратора и вводим следующую команду:
sc create Name type= kernel binPaht= PATH_TO_DRIVER

в моем случае это выглядит так:

image19.png

Также проверить успешность создания можно через реестр.

image16.png

В той же консоли мы можем попробовать запустить нашу службу.
sc start CodebyDriver

image18.png

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

Создадим новый ключ в реестре и назовем его Debug Print Filter.

image23.png

В качестве значения задаем DWORD с именем DEFAULT и определяем данные для значения как 8.

image25.png

Перезагружаем виртуальную машину.

После перезапуска запускаем DebugView данный инструмент находится в архиве Sysinternals, который мы ранее скачали. Ее можно смело скопировать в виртуальную машину.

Запускаем DebugView от имени Администратора и ставим галочку “Capture Kerner”

image20.png

Capture Win32 и Capture Global Win32 можно снять, если летит много сообщений.

Затем запускаем консоль от имени администратора и запускаем службу загрузки драйвера.

image26.png

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

Спасибо за чтение!

P.S: Я сам только начал изучать тему работы с драйверами. Так что если у вас есть предложения или правки по технической части статьи, прошу отписать в комментарии, чтобы я мог внести изменения в статью.
P.P.S: Как вы могли заметить, писать мы будем преимущественно на С++, посему могу посоветовать отличный канал с уроками по С++ — The Cherno.

Урок 3. Сборка и запуск драйвера

Еще один, особый, вид программ – драйверы. В отличие от остальных – выполняются в особом режиме, где они имеют ряд возможностей, недоступных для обычных приложений Windows. Однако это осложняется тем, что в этом режиме практически отсутствуют средства для взаимодействия с пользователем. Для тестирования драйвера можно пользоваться функцией DbgPrint, выдающей отладочные сообщения отладчику. Но отладчика может не оказаться и этих сообщений никто в таком случае не увидит. Для обычного же взаимодействия с пользователем драйвер должен полагаться на прикладные приложения, обмениваясь с ними запросами ввода/вывода.

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

Создание проекта будем вести следующим образом. Создаем папку DrvHello, где будем разрабатывать приложение-драйвер. Внутри нее создаем такие файлы:

• 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=drvhello
TARGETTYPE=DRIVER
TARGETPATH=obj
SOURCES=main.c

Задает параметры сборки. Здесь в параметре SOURCES необходимо перечислить через пробел все файлы с исходным кодом, которые нужно будет скомпилировать.

• main.c – это наш файл с исходным кодом. Может носить любое имя, их может быть много, а может быть очень много. Главное, не забыть их все перечислить в файле sources

#include "ntddk.h"

#pragma code_seg("INIT")
NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject,
    IN PUNICODE_STRING RegistryPath)
{
    DbgPrint("Hello, world!");			// Выдача отладочного сообщения
    return STATUS_DEVICE_CONFIGURATION_ERROR;	// Выдача ошибки заставит систему сразу же выгрузить драйвер
}
#pragma code_seg()

Основная функция у нас здесь носит название DriverEntry. Запускаться она будет при попытке загрузки драйвера. А чтобы сделать такую попытку – нужна программа-загрузчик, которую напишем чуть поздней.

Откомпилируем драйвер. Чтобы мы могли что-то увидеть в отладчике, драйвер должен быть собран в отладочном режиме. Напомню, что отладочный режим у программистов драйверов называется checked, а финальный — free (прикладные программисты так не заморачиваются и называют их debug и release соответственно). В этом режиме программный код не оптимизируется и исполняемый файл содержит много избыточной информации, совершенно не нужной для работы, но очень полезной при изучении того, что будет делать этот код.
Для сборки открываем меню Start, затем Programs/Windows Driver Kits/WDK <номер версии>/Build Environment/Windows Server 2003 (или Windows XP)/ x86 Checked Build Environment. Щелкаем и попадаем в консольное окно. Если FAR был добавлен в PATH, то можно набрать far и перейти в директорию проекта обычным способом, иначе туда придется добираться используя команду cd (и может быть смену диска). Когда добрались до папки DrvHello (там где лежат три файла, которые были созданы выше) – набираем команду nmake. Если сборка прошла без ошибок, то будет созданы директории objchk_wnet_x86/i386, а в ней уже можно обнаружить файл drvhello.sys. Это и есть наш собранный драйвер.

Теперь вернемся к загрузчику. Это у нас будет консольное приложение (см. главу 1). Имя я ему дал DrvHelloloader и разместил, как всегда, в c:Projects. Имя файла с исходным кодом такое же – main.c, содержимое:

#include <windows.h>
#include <shlwapi.h>
#pragma comment(lib, "shlwapi.lib") 

void main(int argc, char* argv[])
{
	char serviceName[] = "drvhello";
	char driverBinary[MAX_PATH] = "";
	SC_HANDLE hSc;
	SC_HANDLE hService;

	// Чтобы запустить драйвер, потребуется полный путь к нему
	// Предполагаем, что он лежит в той же папке, что и экзешник
	strcpy(driverBinary, argv[0]);			// argv[0] - здесь будет имя экзешника с полным путем
	PathRemoveFileSpec(driverBinary);		// Выкидываем имя экзешника, остается только путь к папке
	strcat(driverBinary, "\drvhello.sys");	// Добавляем имя драйвера.
					// Бэкслэш в строках Си надо удваивать, из-за его специального значения.

	hSc = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);	// Открываем SCM (Service Control Management)
								// Это такая штука, которая позволяет запускать драйверы
								// из пользовательского режима
	CreateService(hSc, serviceName, serviceName,
		SERVICE_ALL_ACCESS, SERVICE_KERNEL_DRIVER, SERVICE_DEMAND_START, SERVICE_ERROR_NORMAL,
		driverBinary, NULL, NULL, NULL, NULL, NULL);				// Загрузка в 3 этапа - создаем службу
	hService = OpenService(hSc, serviceName, SERVICE_ALL_ACCESS);	// Открываем ее
	StartService(hService, 0, NULL);	// И запускаем. Вообще-то было бы неплохо
						// еще и закрыть ее... Как нибудь потом.
}

Выбираем в меню cтудии – Build/Rebuild Solution. Произойдет пересборка проекта без запуска. В принципе, можно и запустить. Находим экзешник в папке проекта, докидываем в ту же папку drvhello.sys. Далее запускаем DebugView, включаем галочку Capture/Capture Kernel, как показано на рисунке:

Изображение
Рис. 3.1 – Настройка DebugView

Теперь запускаем программу. Если все прошло успешно – видим следующую картину:

Изображение
Рис. 3.2 – Проверка драйвера

Вот такой простой код позволяет уже очень много – работать на уровне ядра. Но следует помнить, что если допустить ошибку в прикладной программе – упадет она одна; если в драйвере – вся система. К примеру, если в код драйвера перед DbgPrint добавить строку:

int a = *((int *)0);

которая делает попытку прочесть содержимое памяти по нулевому адресу – то картинка будет уже иной:

Изображение
Рис. 3.3 – Попытка чтения из нулевого адреса

Если бы это был не драйвер, а просто программа пользовательского режима, то при выполнении этого кода она бы просто закрылась, выдав ошибку:

Изображение
Рис. 3.4 – Та же ошибка в программе пользовательского режима

  • Download source files — 10.4 Kb

This tutorial will attempt to describe how to write a simple device driver for Windows NT. There are various resources and tutorials on the internet for writing device drivers, however, they are somewhat scarce as compared to writing a “hello world” GUI program for Windows. This makes the search for information on starting to write device drivers a bit harder. You may think that if there’s already one tutorial, why do you need more? The answer is that more information is always better especially when you are first beginning to understand a concept. It is always good to see information from different perspectives. People write differently and describe certain pieces of information in a different light depending on how familiar they are with a certain aspect or how they think it should be explained. This being the case, I would recommend anyone who wants to write device drivers not to stop here or somewhere else. Always find a variety of samples and code snippets and research the differences. Sometimes there are bugs and things omitted. Sometimes there are things that are being done that aren’t necessary, and sometimes there’s information incorrect or just incomplete.

This tutorial will describe how to create a simple device driver, dynamically load and unload it, and finally talk to it from user mode.

Creating a Simple Device Driver

What is a subsystem?

I need to define a starting ground before we begin to explain how to write a device driver. The starting point for this article will be the compiler. The compiler and linker generate a binary in a format that the Operating System understands. In Windows, this format is “PE” for “Portable Executable” format. In this format, there is an idea called a subsystem. A subsystem, along with other options specified in the PE header information, describes how to load an executable which also includes the entry point into the binary.

Many people use the VC++ IDE to simply create a project with some default pre-set options for the compiler’s (and linker) command line. This is why a lot of people may not be familiar with this concept even though they are most likely already using it if they have ever written Windows applications. Have you ever written a console application? Have you ever written a GUI application for Windows? These are different subsystems in Windows. Both of these will generate a PE binary with the appropriate subsystem information. This is also why a console application uses “main” where a WINDOWS application uses “WinMain”. When you choose these projects, VC++ simply creates a project with /SUBSYSTEM:CONSOLE or /SUBSYSTEM:WINDOWS. If you accidentally choose the wrong project, you can simply change this in the linker options menu rather than needing to create a new project.

There’s a point to all of this? A driver is simply linked using a different subsystem called “NATIVE”. MSDN Subsystem compiler options.

The Driver’s “main”

After the compiler is setup with the appropriate options, it’s probably good to start thinking about the entry point to a driver. The first section lied a little bit about the subsystem. “NATIVE” can also be used to run user-mode applications which define an entry point called “NtProcessStartup”. This is the “default” type of executable that is made when specifying “NATIVE” in the same way “WinMain” and “main” are found when the linker is creating an application. You can override the default entry point with your own, simply by using the “-entry:<functionname>” linker option. If we know we want this to be a driver, we simply need to write an entry point whose parameter list and return type matches that of a driver. The system will then load the driver when we install it and tell the system that it is a driver.

The name we use can be anything. We can call it BufferFly() if we want. The most common practice used by driver developers and Microsoft is using the name “DriverEntry” as its initial entry point. This means we add “-entry:DriverEntry” to the linker’s command line options. If you are using the DDK, this is done for you when you specify “DRIVER” as the type of executable to build. The DDK contains an environment that has pre-set options in the common make file directory which makes it simpler to create an application as it specifies the default options. The actual driver developer can then override these settings in the make file or simply use them as a connivance. This is essentially how “DriverEntry” became the somewhat “official” name for driver entry points.

Remember, DLLs actually are also compiled specifying “WINDOWS” as the subsystem, but they also have an additional switch called /DLL. There is a switch which can also be used for drivers: /DRIVER:WDM (which also sets NATIVE behind the scenes) as well as a /DRIVER:UP which means this driver cannot be loaded on a multi-processor system.

The linker builds the final binary, and based on what the options are in the PE header and how the binary is attempting to be loaded (run as an EXE through the loader, loaded by LoadLibrary, or attempting to be loaded as a driver) will define how the loading system behaves. The loading system attempts to perform some level of verification, that the image being loaded is indeed supposed to be loaded in this manner, for example. There is even, in some cases, startup code added to the binary that executes before your entry point is reached (WinMainCRTStartup calling WinMain, for example, to initialize the CRT). Your job is to simply write the application based on how you want it to be loaded and then set the correct options in the linker so it knows how to properly create the binary. There are various resources on the details of the PE format which you should be able to find if you are interested in further investigation into this area.

The options we will set for the linker will end up being the following:

/SUBSYSTEM:NATIVE /DRIVER:WDM –entry:DriverEntry

Before creating the “DriverEntry”

There are some things we need to go over before we simply sit down and write the “DriverEntry”. I know that a lot of people simply want to jump right into writing the driver and seeing it work. This is generally the case in most programming scenarios as you usually just take the code, change it around, compile it, and test it out. If you remember back to when you were first learning Windows development, it was probably the same way. Your application probably didn’t work right away, probably crashed, or just disappeared. This was a lot of fun and you probably learned a lot, but you know that with a driver, the adventure is a little different. Not knowing what to do can end up in blue screening the system, and if your driver is loaded on boot and executes that code, you now have a problem. Hopefully, you can boot in safe mode or restore to a previous hardware configuration. That being the case, we have a few things to go over before you write the driver in order to help educate you on what you are doing before you actually do it.

The first rule of thumb is do not just take a driver and compile it with some of your changes. If you do not understand how the driver is working or how to program correctly in the environment, you are likely to cause problems. Drivers can corrupt the integrity of the whole system, they can have bugs that don’t always occur but in some rare circumstances. Application programs can have the same type of bugs in behavior but not in root cause. As an example, there are times when you cannot access memory that is pagable. If you know how Virtual Memory works, you know that the Operating System will remove pages from memory to pull in pages that are needed, and this is how more applications can run than would have been physically possible given the memory limitations of the machine. There are places, however, when pages cannot be read into memory from disk. At these times, those “drivers” who work with memory can only access memory that cannot be paged out.

Where am I going with this? Well, if you allow a driver which runs under these constraints to access memory that is “pagable”, it may not crash as the Operating System usually tries to keep all pages in memory as long as possible. If you close an application that was running, it may still be in memory, for example! This is why a bug like this may go undetected (unless you try doing things like driver verifier) and eventually may trap. When it does, if you do not understand the basic concepts like this, you would be lost as to what the problem is and how to fix it.

There are a lot of concepts behind everything that will be described in this document. On IRQL alone, there is a twenty page document you can find on MSDN. There’s an equally large document on IRP. I will not attempt to duplicate this information nor point out every single little detail. What I will attempt to do is give a basic summary and point you in the direction of where to find more information. It’s important to at least know that these concepts exist and understand some basic idea behind them, before writing the driver.

What is IRQL?

The IRQL is known as the “Interrupt ReQuest Level”. The processor will be executing code in a thread at a particular IRQL. The IRQL of the processor essentially helps determine how that thread is allowed to be interrupted. The thread can only be interrupted by code which needs to run at a higher IRQL on the same processor. Interrupts requiring the same IRQL or lower are masked off so only interrupts requiring a higher IRQL are available for processing. In a multi-processor system, each processor operates independently at its own IRQL.

There are four IRQL levels which you generally will be dealing with, which are “Passive”, “APC”, “Dispatch” and “DIRQL”. Kernel APIs documented in MSDN generally have a note which specifies the IRQL level at which you need to be running in order to use the API. The higher the IRQL you go, the less APIs that are available for use. The documentation on MSDN defines what IRQL the processor will be running at when the particular entry point of the driver is called. “DriverEntry”, for example, will be called at PASSIVE_LEVEL.

PASSIVE_LEVEL

This is the lowest IRQL. No interrupts are masked off and this is the level in which a thread executing in user mode is running. Pagable memory is accessible.

APC_LEVEL

In a processor running at this level, only APC level interrupts are masked. This is the level in which Asynchronous Procedure Calls occur. Pagable memory is still accessible. When an APC occurs, the processor is raised to APC level. This, in turn, also disables other APCs from occurring. A driver can manually raise its IRQL to APC (or any other level) in order to perform some synchronization with APCs, for example, since APCs can’t be invoked if you are already at APC level. There are some APIs which can’t be called at APC level due to the fact that APCs are disabled, which, in turn, may disable some I/O Completion APCs.

DISPATCH_LEVEL

The processor running at this level has DPC level interrupts and lower masked off. Pagable memory cannot be accessed, so all memory being accessed must be non-paged. If you are running at Dispatch Level, the APIs that you can use greatly decrease since you can only deal with non-paged memory.

<A name=OLE_LINK4>DIRQL </A>(Device IRQL)

Generally, higher level drivers do not deal with IRQLs at this level, but all interrupts at this level or less are masked off and do not occur. This is actually a range of IRQLs, and this is a method to determine which devices have priority over other devices.

In this driver, we will basically only be working at PASSIVE_LEVEL, so we won’t have to worry about the gotchas. However, it is necessary for you to be aware of what IRQL is, if you intend to continue writing device drivers.

For more information on IRQLs and thread scheduling, refer to the following documentation, and another good source of information is here.

What is an IRP?

The “IRP” is called the “I/O Request Packet”, and it is passed down from driver to driver in the driver stack. This is a data structure that allows drivers to communicate with each other and to request work to be done by the driver. The I/O manager or another driver may create an IRP and pass it down to your driver. The IRP includes information about the operation that is being requested.

A description of the IRP data structure can be found here.

The description and usage of an IRP can go from simple to complex very easily, so we will only be describing, in general, what an IRP will mean to you. There is an article on MSDN which describes in a lot more detail (about twenty pages) of what exactly an IRP is and how to handle them. That article can be found here.

The IRP will also contain a list of “sub-requests” also known as the “IRP Stack Location”. Each driver in the device stack will generally have its own “sub request” of how to interpret the IRP. This data structure is the “IO_STACK_LOCATION” and is described on MSDN.

To create an analogy of the IRP and IO_STACK_LOCATION, perhaps you have three people who do different jobs such as carpentry, plumbing and welding. If they were going to build a house, they could have a common overall design and perhaps a common set of tools like their tool box. This includes things like power drills, etc. All of these common tools and overall design of building a house would be the IRP. Each of them has an individual piece they need to work on to make this happen, for example, the plumber needs the plans on where to put the pipe, how much pipe he has, etc. These could be interpreted as the IO_STACK_LOCATION as his specific job is to do the piping. The carpenter could be building the framework for the house and the details of that would be in his IO_STACK_LOCATION. So, while the entire IRP is a request to build a house, each person in the stack of people has their own job as defined by the IO_STACK_LOCATION to make this happen. Once everyone has completed their job, they then complete the IRP.

The device driver we will be building will not be that complex and will basically be the only driver in the stack.

Things to Avoid

There are a lot of pitfalls that you will need to avoid but they are mostly unrelated to our simple driver. To be more informed, however, here is a list of items called “things to avoid” when it comes to driver development.

Create the DriverEntry routine

There is so much to explain, however, I think it’s time we simply started to develop the driver and explain as we go. It is hard to digest theory or even how code is supposed to work, without actually doing anything. You need some hands on experience so you can bring these ideas out of space and into reality.

The prototype for the DriverEntry is the following.

NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject, PUNICODE_STRING pRegistryPath);

The DRIVER_OBJECT is a data structure used to represent this driver. The DriverEntry routine will use it to populate it with other entry points to the driver for handling specific I/O requests. This object also has a pointer to a DEVICE_OBJECT which is a data structure which represents a particular device. A single driver may actually advertise itself as handling multiple devices, and as such, the DRIVER_OBJECT maintains a linked list pointer to all the devices this particular driver services request for. We will simply be creating one device.

The “Registry Path” is a string which points to the location in the registry where the information for the driver was stored. The driver can use this location to store driver specific information.

The next part is to actually put things in the DriverEntry routine. The first thing we will do is create the device. You may be wondering how we are going to create a device and what type of device we should create. This is generally because a driver is usually associated with hardware but this is not the case. There are a variety of different types of drivers which operate at different levels, not all drivers work or interface directly with hardware. Generally, you maintain a stack of drivers each with a specific job to do. The highest level driver is the one that communicates with user mode, and the lowest level drivers generally just talk to other drivers and hardware. There are network drivers, display drivers, file system drivers, etc., and each has their own stack of drivers. Each place in the stack breaks up a request into a more generic or simpler request for the lower level driver to service. The highest level drivers are the ones which communicate themselves to user mode, and unless they are a special device with a particular framework (like display drivers), they can behave generally the same as other drivers just as they implement different types of operations.

As an example, take the hard disk drive. The driver which communicates to user mode does not talk directly to hardware. The high level driver simply manages the file system itself and where to put things. It then communicates where it wants to read or write from the disk to the lower level driver which may or may not talk directly to hardware. There may be another layer which then communicates that request to the actual hardware driver which then physically reads or writes a particular sector off a disk and then returns it to the higher level. The highest level may interpret them as file data, but the lowest level driver may simply be stupid and only manage requests as far as when to read a sector based off where the read/write head is located on the disk. It could then determine what sector read requests to service, however, it has no idea what the data is and does not interpret it.

Let’s take a look at the first part of our “DriverEntry”.

NTSTATUS DriverEntry(PDRIVER_OBJECT  pDriverObject, PUNICODE_STRING  pRegistryPath)
{
    NTSTATUS NtStatus = STATUS_SUCCESS;
    UINT uiIndex = 0;
    PDEVICE_OBJECT pDeviceObject = NULL;
    UNICODE_STRING usDriverName, usDosDeviceName;

    DbgPrint("DriverEntry Called rn");

    RtlInitUnicodeString(&usDriverName, L"\Device\Example");
    RtlInitUnicodeString(&usDosDeviceName, L"\DosDevices\Example"); 

    NtStatus = IoCreateDevice(pDriverObject, 0,
                              &usDriverName, 
                              FILE_DEVICE_UNKNOWN,
                              FILE_DEVICE_SECURE_OPEN, 
                              FALSE, &pDeviceObject);

The first thing you will notice is the DbgPrint function. This works just like “printf” and it prints messages out to the debugger or debug output window. You can get a tool called “DBGVIEW” from www.sysinternals.com and all of the information in those messages will be displayed.

You will then notice that we use a function called “RtlInitUnicodeString” which basically initializes a UNICODE_STRING data structure. This data structure contains basically three entries. The first is the size of the current Unicode string, the second is the maximum size that the Unicode string can be, and the third is a pointer to the Unicode string. This is used to describe a Unicode string and used commonly in drivers. The one thing to remember with UNICODE_STRING is that they are not required to be NULL terminated since there is a size parameter in the structure! This causes problems for people new to driver development as they assume a UNICODE_STRING is NULL terminated, and they blue-screen the driver. Most Unicode strings passing into your driver will not be NULL terminated, so this is something you need to be aware of.

Devices have names just like anything else. They are generally named Device<somename> and this is the string we were creating to pass into IoCreateDevice. The second string, “DosDevicesExample”, we will get into later as it’s not used in the driver yet. To the IoCreateDevice, we pass in the driver object, a pointer to the Unicode string we want to call the driver, and we pass in a type of driver “UNKNOWN” as it’s not associated with any particular type of device, and we also pass in a pointer to receive the newly created device object. The parameters are explained in more detail at “IoCreateDevice”.

The second parameter we passed 0, and it says to specify the number of bytes to create for the device extension. This is basically a data structure that the driver writer can define which is unique to that device. This is how you can extend the information being passed into a device and create device contexts, etc. in which to store instance data. We will not be using this for this example.

Now that we have successfully created our DeviceExample device driver, we need to setup the Driver Object to call into our driver when certain requests are made. These requests are called IRP Major requests. There are also Minor requests which are sub-requests of these and can be found in the stack location of the IRP.

The following code populates certain requests:

for(uiIndex = 0; uiIndex < IRP_MJ_MAXIMUM_FUNCTION; uiIndex++)
     pDriverObject->MajorFunction[uiIndex] = Example_UnSupportedFunction;

pDriverObject->MajorFunction[IRP_MJ_CLOSE]             = Example_Close;
pDriverObject->MajorFunction[IRP_MJ_CREATE]            = Example_Create;
pDriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL]    = Example_IoControl;
pDriverObject->MajorFunction[IRP_MJ_READ]              = Example_Read;
pDriverObject->MajorFunction[IRP_MJ_WRITE]             = USE_WRITE_FUNCTION;

We populate the Create, Close, IoControl, Read and Write. What do these refer to? When communicating with the user-mode application, certain APIs call directly to the driver and pass in parameters!

  • CreateFile -> IRP_MJ_CREATE
  • CloseHandle -> IRP_MJ_CLEANUP & IRP_MJ_CLOSE
  • WriteFile -> IRP_MJ_WRITE
  • ReadFile-> IRP_MJ_READ
  • DeviceIoControl -> IRP_MJ_DEVICE_CONTROL

To explain, one difference is IRP_MJ_CLOSE is not called in the context of the process which created the handle. If you need to perform process related clean up, then you need to handle IRP_MJ_CLEANUP as well.

So as you can see, when a user mode application uses these functions, it calls into your driver. You may be wondering why the user mode API says “file” when it doesn’t really mean “file”. That is true, these APIs can talk to any device which exposes itself to user mode, they are not only for accessing files. In the last piece of this article, we will be writing a user mode application to talk to our driver and it will simply do CreateFile, WriteFile, CloseHandle. That’s how simple it is. USE_WRITE_FUNCTION is a constant I will explain later.

The next piece of code is pretty simple, it’s the driver unload function.

pDriverObject->DriverUnload =  Example_Unload;

You can technically omit this function but if you want to unload your driver dynamically, then it must be specified. If you do not specify this function once your driver is loaded, the system will not allow it to be unloaded.

The code after this is actually using the DEVICE_OBJECT, not the DRIVER_OBJECT. These two data structures may get a little confusing since they both start with “D” and end with “_OBJECT”, so it’s easy to confuse which one we’re using.

pDeviceObject->Flags |= IO_TYPE;
pDeviceObject->Flags &= (~DO_DEVICE_INITIALIZING);

We are simply setting the flags. “IO_TYPE” is actually a constant which defines the type of I/O we want to do (I defined it in example.h). I will explain this in the section on handling user-mode write requests.

The “DO_DEVICE_INITIALIZING” tells the I/O Manager that the device is being initialized and not to send any I/O requests to the driver. For devices created in the context of the “DriverEntry”, this is not needed since the I/O Manager will clear this flag once the “DriverEntry” is done. However, if you create a device in any function outside of the DriverEntry, you need to manually clear this flag for any device you create with IoCreateDevice. This flag is actually set by the IoCreateDevice function. We cleared it here just for fun even though we weren’t required to.

The last piece of our driver is using both of the Unicode strings we defined above. “DeviceExample” and “DosDevicesExample”.

IoCreateSymbolicLink(&usDosDeviceName, &usDriverName);

IoCreateSymbolicLink” does just that, it creates a “Symbolic Link” in the object manager. To view the object manager, you may download my tool “QuickView”, or go to www.sysinternals.com and download “WINOBJ”. A Symbolic Link simply maps a “DOS Device Name” to an “NT Device Name”. In this example, “Example” is our DOS Device Name and “DeviceExample” is our NT Device Name.

To put this into perspective, different vendors have different drivers and each driver is required to have its own name. You cannot have two drivers with the same NT Device name. Say, you have a memory stick which can display itself to the system as a new drive letter which is any available drive letter such as E:. If you remove this memory stick and say you map a network drive to E:. Application can talk to E: the same way, they do not care if E: is a CD ROM, Floppy Disk, memory stick or network drive. How is this possible? Well, the driver needs to be able to interpret the requests and either handle them within themselves such as the case of a network redirector or pass them down to the appropriate hardware driver. This is done through symbolic links. E: is a symbolic link. The network mapped drive may map E: to DeviceNetworkRedirector and the memory stick may map E: to DeviceFujiMemoryStick, for example.

This is how applications can be written using a commonly defined name which can be abstracted to point to any device driver which would be able to handle requests. There are no rules here, we could actually map DeviceExample to E:. We can do whatever we wish to do, but in the end, however, the application attempts to use the device as how the device driver needs to respond and act. This means supporting IOCTLs commonly used by those devices as applications will try to use them. COM1, COM2, etc. are all examples of this. COM1 is a DOS name which is mapped to an NT Device name of a driver which handles serial requests. This doesn’t even need to be a real physical serial port!

So we have defined “Example” as a DOS Device which points to “DeviceExample”. In the “communicating with usermode” portion, we will learn more about how to use this mapping.

Create the Unload Routine

The next piece of code we will look at is the unload routine. This is required in order to be able to unload the device driver dynamically. This section will be a bit smaller as there is not much to explain.

VOID Example_Unload(PDRIVER_OBJECT  DriverObject)
{    
    
    UNICODE_STRING usDosDeviceName;
    
    DbgPrint("Example_Unload Called rn");
    
    RtlInitUnicodeString(&usDosDeviceName, L"\DosDevices\Example");
    IoDeleteSymbolicLink(&usDosDeviceName);

    IoDeleteDevice(DriverObject->DeviceObject);
}

You can do whatever you wish in your unload routine. This unload routine is very simple, it just deletes the symbolic link we created and then deletes the only device that we created which was DeviceExample.

Creating the IRP_MJ_WRITE

The rest of the functions should be self explanatory as they don’t do anything. This is why I am only choosing to explain the “Write” routine. If this article is liked, I may write a second tutorial on implementing the IO Control function.

If you have used WriteFile and ReadFile, you know that you simply pass a buffer of data to write data to a device or read data from a device. These parameters are sent to the device in the IRP as we explained previously. There is more to the story though as there are actually three different methods that the I/O Manager will use to marshal this data before giving the IRP to the driver. That also means that how the data is marshaled is how the driver’s Read and Write functions need to interpret the data.

The three methods are “Direct I/O”, “Buffered I/O” and “Neither”.

#ifdef __USE_DIRECT__
#define IO_TYPE DO_DIRECT_IO
#define USE_WRITE_FUNCTION  Example_WriteDirectIO
#endif
 
#ifdef __USE_BUFFERED__
#define IO_TYPE DO_BUFFERED_IO
#define USE_WRITE_FUNCTION  Example_WriteBufferedIO
#endif

#ifndef IO_TYPE
#define IO_TYPE 0
#define USE_WRITE_FUNCTION  Example_WriteNeither
#endif

The code was written so if you define “__USE_DIRECT__” in the header, then IO_TYPE is now DO_DIRECT_IO and USE_WRITE_FUNCTION is now Example_WriteDirectIO. If you define “__USE_BUFFERED__” in the header, then IO_TYPE is now DO_BUFFERED_IO and USE_WRITE_FUNCTION is now Example_WriteBufferedIO. If you don’t define __USE_DIRECT__ or __USE_BUFFERED__, then IO_TYPE is defined as 0 (neither) and the write function is Example_WriteNeither.

We will now go over each type of I/O.

Direct I/O

The first thing I will do is simply show you the code for handling direct I/O.

NTSTATUS Example_WriteDirectIO(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
    NTSTATUS NtStatus = STATUS_SUCCESS;
    PIO_STACK_LOCATION pIoStackIrp = NULL;
    PCHAR pWriteDataBuffer;

    DbgPrint("Example_WriteDirectIO Called rn");
    
    
    pIoStackIrp = IoGetCurrentIrpStackLocation(Irp);
    
    if(pIoStackIrp)
    {
        pWriteDataBuffer = 
          MmGetSystemAddressForMdlSafe(Irp->MdlAddress, NormalPagePriority);
    
        if(pWriteDataBuffer)
        {                             
            
           if(Example_IsStringTerminated(pWriteDataBuffer, 
              pIoStackIrp->Parameters.Write.Length))
           {
                DbgPrint(pWriteDataBuffer);
           }
        }
    }

    return NtStatus;
}

The entry point simply provides the device object for the device for which this request is being sent for. If you recall, a single driver can create multiple devices even though we have only created one. The other parameter is as was mentioned before which is an IRP!

The first thing we do is call “IoGetCurrentIrpStackLocation”, and this simply provides us with our IO_STACK_LOCATION. In our example, the only parameter we need from this is the length of the buffer provided to the driver, which is at Parameters.Write.Length.

The way buffered I/O works is that it provides you with a “MdlAddress” which is a “Memory Descriptor List”. This is a description of the user mode addresses and how they map to physical addresses. The function we call then is “MmGetSystemAddressForMdlSafe” and we use the Irp->MdlAddress to do this. This operation will then give us a system virtual address which we can then use to read the memory.

The reasoning behind this is that some drivers do not always process a user mode request in the context of the thread or even the process in which it was issued. If you process a request in a different thread which is running in another process context, you would not be able to read user mode memory across process boundaries. You should know this already, as you run two applications they can’t just read/write to each other without Operating System support.

So, this simply maps the physical pages used by the user mode process into system memory. We can then use the returned address to access the buffer passed down from user mode.

This method is generally used for larger buffers since it does not require memory to be copied. The user mode buffers are locked in memory until the IRP is completed which is the downside of using direct I/O. This is the only downfall and is why it’s generally more useful for larger buffers.

Buffered I/O

The first thing I will do is simply show you the code for handling buffered I/O.

NTSTATUS Example_WriteBufferedIO(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
    NTSTATUS NtStatus = STATUS_SUCCESS;
    PIO_STACK_LOCATION pIoStackIrp = NULL;
    PCHAR pWriteDataBuffer;

    DbgPrint("Example_WriteBufferedIO Called rn");
    
    
    pIoStackIrp = IoGetCurrentIrpStackLocation(Irp);
    
    if(pIoStackIrp)
    {
        pWriteDataBuffer = (PCHAR)Irp->AssociatedIrp.SystemBuffer;
    
        if(pWriteDataBuffer)
        {                             
            
           if(Example_IsStringTerminated(pWriteDataBuffer, 
                   pIoStackIrp->Parameters.Write.Length))
           {
                DbgPrint(pWriteDataBuffer);
           }
        }
    }

    return NtStatus;
}

As mentioned above, the idea is to pass data down to the driver that can be accessed from any context such as another thread in another process. The other reason would be to map the memory to be non-paged so the driver can also read it at raised IRQL levels.

The reason you may need to access memory outside the current process context is that some drivers create threads in the SYSTEM process. They then defer work to this process either asynchronously or synchronously. A driver at a higher level than your driver may do this or your driver itself may do it.

The downfall of using “Buffered I/O” is that it allocates non-paged memory and performs a copy. This is now overhead in processing every read and write into the driver. This is one of the reasons this is best used on smaller buffers. The whole user mode page doesn’t need to be locked in memory as with Direct I/O, which is the plus side of this. The other problem with using this for larger buffers is that since it allocates non-paged memory, it would need to allocate a large block of sequential non-paged memory.

Neither Buffered nor Direct

The first thing I will do is show you the code for handling neither Buffered nor Direct I/O.

NTSTATUS Example_WriteNeither(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
    NTSTATUS NtStatus = STATUS_SUCCESS;
    PIO_STACK_LOCATION pIoStackIrp = NULL;
    PCHAR pWriteDataBuffer;

    DbgPrint("Example_WriteNeither Called rn");
    
    
    pIoStackIrp = IoGetCurrentIrpStackLocation(Irp);
    
    if(pIoStackIrp)
    {
        
        __try {
        
                ProbeForRead(Irp->UserBuffer, 
                  pIoStackIrp->Parameters.Write.Length, 
                  TYPE_ALIGNMENT(char));
                pWriteDataBuffer = Irp->UserBuffer;
            
                if(pWriteDataBuffer)
                {                             
                    
                   if(Example_IsStringTerminated(pWriteDataBuffer, 
                          pIoStackIrp->Parameters.Write.Length))
                   {
                        DbgPrint(pWriteDataBuffer);
                   }
                }

        } __except( EXCEPTION_EXECUTE_HANDLER ) {

              NtStatus = GetExceptionCode();     
        }

    }

    return NtStatus;
}

In this method, the driver accesses the user mode address directly. The I/O manager does not copy the data, it does not lock the user mode pages in memory, it simply gives the driver the user mode address buffer.

The upside of this is that no data is copied, no memory is allocated, and no pages are locked into memory. The downside of this is that you must process this request in the context of the calling thread so you will be able to access the user mode address space of the correct process. The other downside of this is that the process itself can attempt to change access to the pages, free the memory, etc., on another thread. This is why you generally want to use “ProbeForRead” and “ProbeForWrite” functions and surround all the code in an exception handler. There’s no guarantee that at any time the pages could be invalid, you can simply attempt to make sure they are, before you attempt to read or write. This buffer is stored at Irp->UserBuffer.

What’s this #pragma stuff?

These directives you see simply let the linker know what segment to put the code and what options to set on the pages. The “DriverEntry”, for example, is set as “INIT” which is a discardable page. This is because you only need that function during initialization.

Homework!

Your homework is to create the Read routines for each type of I/O processing. You can use the Write routines as reference to figure out what you need to do.

Dynamically Loading and Unloading the Driver

A lot of tutorials will go and explain the registry, however, I have chosen not to at this time. There is a simple user mode API that you can use to load and unload the driver without having to do anything else. This is what we will use for now.

int _cdecl main(void)
{
    HANDLE hSCManager;
    HANDLE hService;
    SERVICE_STATUS ss;

    hSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_CREATE_SERVICE);
    
    printf("Load Drivern");

    if(hSCManager)
    {
        printf("Create Servicen");

        hService = CreateService(hSCManager, "Example", 
                                 "Example Driver", 
                                  SERVICE_START | DELETE | SERVICE_STOP, 
                                  SERVICE_KERNEL_DRIVER,
                                  SERVICE_DEMAND_START, 
                                  SERVICE_ERROR_IGNORE, 
                                  "C:\example.sys", 
                                  NULL, NULL, NULL, NULL, NULL);

        if(!hService)
        {
            hService = OpenService(hSCManager, "Example", 
                       SERVICE_START | DELETE | SERVICE_STOP);
        }

        if(hService)
        {
            printf("Start Servicen");

            StartService(hService, 0, NULL);
            printf("Press Enter to close servicern");
            getchar();
            ControlService(hService, SERVICE_CONTROL_STOP, &ss);

            DeleteService(hService);

            CloseServiceHandle(hService);
            
        }

        CloseServiceHandle(hSCManager);
    }
    
    return 0;
}

This code will load the driver and start it. We load the driver with “SERVICE_DEMAND_START” which means this driver must be physically started. It will not start automatically on boot, that way we can test it, and if we blue-screen, we can fix the issue without having to boot to safe mode.

This program will simply pause. You can then run the application that talks to the service, in another window. The code above should be pretty easy to understand that you need to copy the driver to C:example.sys in order to use it. If the service fails to create, it knows it has already been created and opens it. We then start the service and pause. Once you press Enter, we stop the service, delete it from the list of services, and exit. This is very simple code and you can modify it to serve your purposes.

Communicating to the Device Driver

The following is the code that communicates to the driver.

int _cdecl main(void)
{
    HANDLE hFile;
    DWORD dwReturn;

    hFile = CreateFile("\\.\Example", 
            GENERIC_READ | GENERIC_WRITE, 0, NULL, 
            OPEN_EXISTING, 0, NULL);

    if(hFile)
    {
        WriteFile(hFile, "Hello from user mode!", 
                  sizeof("Hello from user mode!"), &dwReturn, NULL); 
        CloseHandle(hFile);
    }
    
    return 0;
}

This is probably simpler than you thought. If you compile the driver three times using the three different methods of I/O, the message sent down from user mode should be printed in DBGVIEW. As you notice, you simply need to open the DOS Device Name using \.<DosName>. You could even open Device<Nt Device Name> using the same method. You will then create a handle to the device and you can call WriteFile, ReadFile, CloseHandle, DeviceIoControl! If you want to experiment, simply perform actions and use DbgPrint to show what code is being executed in your driver.

Conclusion

This article showed a simple example of how to create a driver, install it, and access it via a simple user mode application. You may use the associated source files to change and experiment. If you wish to write drivers, it’s best to read up on many of the basic concepts of drivers, especially, some of the ones linked to in this tutorial.

  • Download source files — 10.4 Kb

This tutorial will attempt to describe how to write a simple device driver for Windows NT. There are various resources and tutorials on the internet for writing device drivers, however, they are somewhat scarce as compared to writing a “hello world” GUI program for Windows. This makes the search for information on starting to write device drivers a bit harder. You may think that if there’s already one tutorial, why do you need more? The answer is that more information is always better especially when you are first beginning to understand a concept. It is always good to see information from different perspectives. People write differently and describe certain pieces of information in a different light depending on how familiar they are with a certain aspect or how they think it should be explained. This being the case, I would recommend anyone who wants to write device drivers not to stop here or somewhere else. Always find a variety of samples and code snippets and research the differences. Sometimes there are bugs and things omitted. Sometimes there are things that are being done that aren’t necessary, and sometimes there’s information incorrect or just incomplete.

This tutorial will describe how to create a simple device driver, dynamically load and unload it, and finally talk to it from user mode.

Creating a Simple Device Driver

What is a subsystem?

I need to define a starting ground before we begin to explain how to write a device driver. The starting point for this article will be the compiler. The compiler and linker generate a binary in a format that the Operating System understands. In Windows, this format is “PE” for “Portable Executable” format. In this format, there is an idea called a subsystem. A subsystem, along with other options specified in the PE header information, describes how to load an executable which also includes the entry point into the binary.

Many people use the VC++ IDE to simply create a project with some default pre-set options for the compiler’s (and linker) command line. This is why a lot of people may not be familiar with this concept even though they are most likely already using it if they have ever written Windows applications. Have you ever written a console application? Have you ever written a GUI application for Windows? These are different subsystems in Windows. Both of these will generate a PE binary with the appropriate subsystem information. This is also why a console application uses “main” where a WINDOWS application uses “WinMain”. When you choose these projects, VC++ simply creates a project with /SUBSYSTEM:CONSOLE or /SUBSYSTEM:WINDOWS. If you accidentally choose the wrong project, you can simply change this in the linker options menu rather than needing to create a new project.

There’s a point to all of this? A driver is simply linked using a different subsystem called “NATIVE”. MSDN Subsystem compiler options.

The Driver’s “main”

After the compiler is setup with the appropriate options, it’s probably good to start thinking about the entry point to a driver. The first section lied a little bit about the subsystem. “NATIVE” can also be used to run user-mode applications which define an entry point called “NtProcessStartup”. This is the “default” type of executable that is made when specifying “NATIVE” in the same way “WinMain” and “main” are found when the linker is creating an application. You can override the default entry point with your own, simply by using the “-entry:<functionname>” linker option. If we know we want this to be a driver, we simply need to write an entry point whose parameter list and return type matches that of a driver. The system will then load the driver when we install it and tell the system that it is a driver.

The name we use can be anything. We can call it BufferFly() if we want. The most common practice used by driver developers and Microsoft is using the name “DriverEntry” as its initial entry point. This means we add “-entry:DriverEntry” to the linker’s command line options. If you are using the DDK, this is done for you when you specify “DRIVER” as the type of executable to build. The DDK contains an environment that has pre-set options in the common make file directory which makes it simpler to create an application as it specifies the default options. The actual driver developer can then override these settings in the make file or simply use them as a connivance. This is essentially how “DriverEntry” became the somewhat “official” name for driver entry points.

Remember, DLLs actually are also compiled specifying “WINDOWS” as the subsystem, but they also have an additional switch called /DLL. There is a switch which can also be used for drivers: /DRIVER:WDM (which also sets NATIVE behind the scenes) as well as a /DRIVER:UP which means this driver cannot be loaded on a multi-processor system.

The linker builds the final binary, and based on what the options are in the PE header and how the binary is attempting to be loaded (run as an EXE through the loader, loaded by LoadLibrary, or attempting to be loaded as a driver) will define how the loading system behaves. The loading system attempts to perform some level of verification, that the image being loaded is indeed supposed to be loaded in this manner, for example. There is even, in some cases, startup code added to the binary that executes before your entry point is reached (WinMainCRTStartup calling WinMain, for example, to initialize the CRT). Your job is to simply write the application based on how you want it to be loaded and then set the correct options in the linker so it knows how to properly create the binary. There are various resources on the details of the PE format which you should be able to find if you are interested in further investigation into this area.

The options we will set for the linker will end up being the following:

/SUBSYSTEM:NATIVE /DRIVER:WDM –entry:DriverEntry

Before creating the “DriverEntry”

There are some things we need to go over before we simply sit down and write the “DriverEntry”. I know that a lot of people simply want to jump right into writing the driver and seeing it work. This is generally the case in most programming scenarios as you usually just take the code, change it around, compile it, and test it out. If you remember back to when you were first learning Windows development, it was probably the same way. Your application probably didn’t work right away, probably crashed, or just disappeared. This was a lot of fun and you probably learned a lot, but you know that with a driver, the adventure is a little different. Not knowing what to do can end up in blue screening the system, and if your driver is loaded on boot and executes that code, you now have a problem. Hopefully, you can boot in safe mode or restore to a previous hardware configuration. That being the case, we have a few things to go over before you write the driver in order to help educate you on what you are doing before you actually do it.

The first rule of thumb is do not just take a driver and compile it with some of your changes. If you do not understand how the driver is working or how to program correctly in the environment, you are likely to cause problems. Drivers can corrupt the integrity of the whole system, they can have bugs that don’t always occur but in some rare circumstances. Application programs can have the same type of bugs in behavior but not in root cause. As an example, there are times when you cannot access memory that is pagable. If you know how Virtual Memory works, you know that the Operating System will remove pages from memory to pull in pages that are needed, and this is how more applications can run than would have been physically possible given the memory limitations of the machine. There are places, however, when pages cannot be read into memory from disk. At these times, those “drivers” who work with memory can only access memory that cannot be paged out.

Where am I going with this? Well, if you allow a driver which runs under these constraints to access memory that is “pagable”, it may not crash as the Operating System usually tries to keep all pages in memory as long as possible. If you close an application that was running, it may still be in memory, for example! This is why a bug like this may go undetected (unless you try doing things like driver verifier) and eventually may trap. When it does, if you do not understand the basic concepts like this, you would be lost as to what the problem is and how to fix it.

There are a lot of concepts behind everything that will be described in this document. On IRQL alone, there is a twenty page document you can find on MSDN. There’s an equally large document on IRP. I will not attempt to duplicate this information nor point out every single little detail. What I will attempt to do is give a basic summary and point you in the direction of where to find more information. It’s important to at least know that these concepts exist and understand some basic idea behind them, before writing the driver.

What is IRQL?

The IRQL is known as the “Interrupt ReQuest Level”. The processor will be executing code in a thread at a particular IRQL. The IRQL of the processor essentially helps determine how that thread is allowed to be interrupted. The thread can only be interrupted by code which needs to run at a higher IRQL on the same processor. Interrupts requiring the same IRQL or lower are masked off so only interrupts requiring a higher IRQL are available for processing. In a multi-processor system, each processor operates independently at its own IRQL.

There are four IRQL levels which you generally will be dealing with, which are “Passive”, “APC”, “Dispatch” and “DIRQL”. Kernel APIs documented in MSDN generally have a note which specifies the IRQL level at which you need to be running in order to use the API. The higher the IRQL you go, the less APIs that are available for use. The documentation on MSDN defines what IRQL the processor will be running at when the particular entry point of the driver is called. “DriverEntry”, for example, will be called at PASSIVE_LEVEL.

PASSIVE_LEVEL

This is the lowest IRQL. No interrupts are masked off and this is the level in which a thread executing in user mode is running. Pagable memory is accessible.

APC_LEVEL

In a processor running at this level, only APC level interrupts are masked. This is the level in which Asynchronous Procedure Calls occur. Pagable memory is still accessible. When an APC occurs, the processor is raised to APC level. This, in turn, also disables other APCs from occurring. A driver can manually raise its IRQL to APC (or any other level) in order to perform some synchronization with APCs, for example, since APCs can’t be invoked if you are already at APC level. There are some APIs which can’t be called at APC level due to the fact that APCs are disabled, which, in turn, may disable some I/O Completion APCs.

DISPATCH_LEVEL

The processor running at this level has DPC level interrupts and lower masked off. Pagable memory cannot be accessed, so all memory being accessed must be non-paged. If you are running at Dispatch Level, the APIs that you can use greatly decrease since you can only deal with non-paged memory.

<A name=OLE_LINK4>DIRQL </A>(Device IRQL)

Generally, higher level drivers do not deal with IRQLs at this level, but all interrupts at this level or less are masked off and do not occur. This is actually a range of IRQLs, and this is a method to determine which devices have priority over other devices.

In this driver, we will basically only be working at PASSIVE_LEVEL, so we won’t have to worry about the gotchas. However, it is necessary for you to be aware of what IRQL is, if you intend to continue writing device drivers.

For more information on IRQLs and thread scheduling, refer to the following documentation, and another good source of information is here.

What is an IRP?

The “IRP” is called the “I/O Request Packet”, and it is passed down from driver to driver in the driver stack. This is a data structure that allows drivers to communicate with each other and to request work to be done by the driver. The I/O manager or another driver may create an IRP and pass it down to your driver. The IRP includes information about the operation that is being requested.

A description of the IRP data structure can be found here.

The description and usage of an IRP can go from simple to complex very easily, so we will only be describing, in general, what an IRP will mean to you. There is an article on MSDN which describes in a lot more detail (about twenty pages) of what exactly an IRP is and how to handle them. That article can be found here.

The IRP will also contain a list of “sub-requests” also known as the “IRP Stack Location”. Each driver in the device stack will generally have its own “sub request” of how to interpret the IRP. This data structure is the “IO_STACK_LOCATION” and is described on MSDN.

To create an analogy of the IRP and IO_STACK_LOCATION, perhaps you have three people who do different jobs such as carpentry, plumbing and welding. If they were going to build a house, they could have a common overall design and perhaps a common set of tools like their tool box. This includes things like power drills, etc. All of these common tools and overall design of building a house would be the IRP. Each of them has an individual piece they need to work on to make this happen, for example, the plumber needs the plans on where to put the pipe, how much pipe he has, etc. These could be interpreted as the IO_STACK_LOCATION as his specific job is to do the piping. The carpenter could be building the framework for the house and the details of that would be in his IO_STACK_LOCATION. So, while the entire IRP is a request to build a house, each person in the stack of people has their own job as defined by the IO_STACK_LOCATION to make this happen. Once everyone has completed their job, they then complete the IRP.

The device driver we will be building will not be that complex and will basically be the only driver in the stack.

Things to Avoid

There are a lot of pitfalls that you will need to avoid but they are mostly unrelated to our simple driver. To be more informed, however, here is a list of items called “things to avoid” when it comes to driver development.

Create the DriverEntry routine

There is so much to explain, however, I think it’s time we simply started to develop the driver and explain as we go. It is hard to digest theory or even how code is supposed to work, without actually doing anything. You need some hands on experience so you can bring these ideas out of space and into reality.

The prototype for the DriverEntry is the following.

NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject, PUNICODE_STRING pRegistryPath);

The DRIVER_OBJECT is a data structure used to represent this driver. The DriverEntry routine will use it to populate it with other entry points to the driver for handling specific I/O requests. This object also has a pointer to a DEVICE_OBJECT which is a data structure which represents a particular device. A single driver may actually advertise itself as handling multiple devices, and as such, the DRIVER_OBJECT maintains a linked list pointer to all the devices this particular driver services request for. We will simply be creating one device.

The “Registry Path” is a string which points to the location in the registry where the information for the driver was stored. The driver can use this location to store driver specific information.

The next part is to actually put things in the DriverEntry routine. The first thing we will do is create the device. You may be wondering how we are going to create a device and what type of device we should create. This is generally because a driver is usually associated with hardware but this is not the case. There are a variety of different types of drivers which operate at different levels, not all drivers work or interface directly with hardware. Generally, you maintain a stack of drivers each with a specific job to do. The highest level driver is the one that communicates with user mode, and the lowest level drivers generally just talk to other drivers and hardware. There are network drivers, display drivers, file system drivers, etc., and each has their own stack of drivers. Each place in the stack breaks up a request into a more generic or simpler request for the lower level driver to service. The highest level drivers are the ones which communicate themselves to user mode, and unless they are a special device with a particular framework (like display drivers), they can behave generally the same as other drivers just as they implement different types of operations.

As an example, take the hard disk drive. The driver which communicates to user mode does not talk directly to hardware. The high level driver simply manages the file system itself and where to put things. It then communicates where it wants to read or write from the disk to the lower level driver which may or may not talk directly to hardware. There may be another layer which then communicates that request to the actual hardware driver which then physically reads or writes a particular sector off a disk and then returns it to the higher level. The highest level may interpret them as file data, but the lowest level driver may simply be stupid and only manage requests as far as when to read a sector based off where the read/write head is located on the disk. It could then determine what sector read requests to service, however, it has no idea what the data is and does not interpret it.

Let’s take a look at the first part of our “DriverEntry”.

NTSTATUS DriverEntry(PDRIVER_OBJECT  pDriverObject, PUNICODE_STRING  pRegistryPath)
{
    NTSTATUS NtStatus = STATUS_SUCCESS;
    UINT uiIndex = 0;
    PDEVICE_OBJECT pDeviceObject = NULL;
    UNICODE_STRING usDriverName, usDosDeviceName;

    DbgPrint("DriverEntry Called rn");

    RtlInitUnicodeString(&usDriverName, L"\Device\Example");
    RtlInitUnicodeString(&usDosDeviceName, L"\DosDevices\Example"); 

    NtStatus = IoCreateDevice(pDriverObject, 0,
                              &usDriverName, 
                              FILE_DEVICE_UNKNOWN,
                              FILE_DEVICE_SECURE_OPEN, 
                              FALSE, &pDeviceObject);

The first thing you will notice is the DbgPrint function. This works just like “printf” and it prints messages out to the debugger or debug output window. You can get a tool called “DBGVIEW” from www.sysinternals.com and all of the information in those messages will be displayed.

You will then notice that we use a function called “RtlInitUnicodeString” which basically initializes a UNICODE_STRING data structure. This data structure contains basically three entries. The first is the size of the current Unicode string, the second is the maximum size that the Unicode string can be, and the third is a pointer to the Unicode string. This is used to describe a Unicode string and used commonly in drivers. The one thing to remember with UNICODE_STRING is that they are not required to be NULL terminated since there is a size parameter in the structure! This causes problems for people new to driver development as they assume a UNICODE_STRING is NULL terminated, and they blue-screen the driver. Most Unicode strings passing into your driver will not be NULL terminated, so this is something you need to be aware of.

Devices have names just like anything else. They are generally named Device<somename> and this is the string we were creating to pass into IoCreateDevice. The second string, “DosDevicesExample”, we will get into later as it’s not used in the driver yet. To the IoCreateDevice, we pass in the driver object, a pointer to the Unicode string we want to call the driver, and we pass in a type of driver “UNKNOWN” as it’s not associated with any particular type of device, and we also pass in a pointer to receive the newly created device object. The parameters are explained in more detail at “IoCreateDevice”.

The second parameter we passed 0, and it says to specify the number of bytes to create for the device extension. This is basically a data structure that the driver writer can define which is unique to that device. This is how you can extend the information being passed into a device and create device contexts, etc. in which to store instance data. We will not be using this for this example.

Now that we have successfully created our DeviceExample device driver, we need to setup the Driver Object to call into our driver when certain requests are made. These requests are called IRP Major requests. There are also Minor requests which are sub-requests of these and can be found in the stack location of the IRP.

The following code populates certain requests:

for(uiIndex = 0; uiIndex < IRP_MJ_MAXIMUM_FUNCTION; uiIndex++)
     pDriverObject->MajorFunction[uiIndex] = Example_UnSupportedFunction;

pDriverObject->MajorFunction[IRP_MJ_CLOSE]             = Example_Close;
pDriverObject->MajorFunction[IRP_MJ_CREATE]            = Example_Create;
pDriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL]    = Example_IoControl;
pDriverObject->MajorFunction[IRP_MJ_READ]              = Example_Read;
pDriverObject->MajorFunction[IRP_MJ_WRITE]             = USE_WRITE_FUNCTION;

We populate the Create, Close, IoControl, Read and Write. What do these refer to? When communicating with the user-mode application, certain APIs call directly to the driver and pass in parameters!

  • CreateFile -> IRP_MJ_CREATE
  • CloseHandle -> IRP_MJ_CLEANUP & IRP_MJ_CLOSE
  • WriteFile -> IRP_MJ_WRITE
  • ReadFile-> IRP_MJ_READ
  • DeviceIoControl -> IRP_MJ_DEVICE_CONTROL

To explain, one difference is IRP_MJ_CLOSE is not called in the context of the process which created the handle. If you need to perform process related clean up, then you need to handle IRP_MJ_CLEANUP as well.

So as you can see, when a user mode application uses these functions, it calls into your driver. You may be wondering why the user mode API says “file” when it doesn’t really mean “file”. That is true, these APIs can talk to any device which exposes itself to user mode, they are not only for accessing files. In the last piece of this article, we will be writing a user mode application to talk to our driver and it will simply do CreateFile, WriteFile, CloseHandle. That’s how simple it is. USE_WRITE_FUNCTION is a constant I will explain later.

The next piece of code is pretty simple, it’s the driver unload function.

pDriverObject->DriverUnload =  Example_Unload;

You can technically omit this function but if you want to unload your driver dynamically, then it must be specified. If you do not specify this function once your driver is loaded, the system will not allow it to be unloaded.

The code after this is actually using the DEVICE_OBJECT, not the DRIVER_OBJECT. These two data structures may get a little confusing since they both start with “D” and end with “_OBJECT”, so it’s easy to confuse which one we’re using.

pDeviceObject->Flags |= IO_TYPE;
pDeviceObject->Flags &= (~DO_DEVICE_INITIALIZING);

We are simply setting the flags. “IO_TYPE” is actually a constant which defines the type of I/O we want to do (I defined it in example.h). I will explain this in the section on handling user-mode write requests.

The “DO_DEVICE_INITIALIZING” tells the I/O Manager that the device is being initialized and not to send any I/O requests to the driver. For devices created in the context of the “DriverEntry”, this is not needed since the I/O Manager will clear this flag once the “DriverEntry” is done. However, if you create a device in any function outside of the DriverEntry, you need to manually clear this flag for any device you create with IoCreateDevice. This flag is actually set by the IoCreateDevice function. We cleared it here just for fun even though we weren’t required to.

The last piece of our driver is using both of the Unicode strings we defined above. “DeviceExample” and “DosDevicesExample”.

IoCreateSymbolicLink(&usDosDeviceName, &usDriverName);

IoCreateSymbolicLink” does just that, it creates a “Symbolic Link” in the object manager. To view the object manager, you may download my tool “QuickView”, or go to www.sysinternals.com and download “WINOBJ”. A Symbolic Link simply maps a “DOS Device Name” to an “NT Device Name”. In this example, “Example” is our DOS Device Name and “DeviceExample” is our NT Device Name.

To put this into perspective, different vendors have different drivers and each driver is required to have its own name. You cannot have two drivers with the same NT Device name. Say, you have a memory stick which can display itself to the system as a new drive letter which is any available drive letter such as E:. If you remove this memory stick and say you map a network drive to E:. Application can talk to E: the same way, they do not care if E: is a CD ROM, Floppy Disk, memory stick or network drive. How is this possible? Well, the driver needs to be able to interpret the requests and either handle them within themselves such as the case of a network redirector or pass them down to the appropriate hardware driver. This is done through symbolic links. E: is a symbolic link. The network mapped drive may map E: to DeviceNetworkRedirector and the memory stick may map E: to DeviceFujiMemoryStick, for example.

This is how applications can be written using a commonly defined name which can be abstracted to point to any device driver which would be able to handle requests. There are no rules here, we could actually map DeviceExample to E:. We can do whatever we wish to do, but in the end, however, the application attempts to use the device as how the device driver needs to respond and act. This means supporting IOCTLs commonly used by those devices as applications will try to use them. COM1, COM2, etc. are all examples of this. COM1 is a DOS name which is mapped to an NT Device name of a driver which handles serial requests. This doesn’t even need to be a real physical serial port!

So we have defined “Example” as a DOS Device which points to “DeviceExample”. In the “communicating with usermode” portion, we will learn more about how to use this mapping.

Create the Unload Routine

The next piece of code we will look at is the unload routine. This is required in order to be able to unload the device driver dynamically. This section will be a bit smaller as there is not much to explain.

VOID Example_Unload(PDRIVER_OBJECT  DriverObject)
{    
    
    UNICODE_STRING usDosDeviceName;
    
    DbgPrint("Example_Unload Called rn");
    
    RtlInitUnicodeString(&usDosDeviceName, L"\DosDevices\Example");
    IoDeleteSymbolicLink(&usDosDeviceName);

    IoDeleteDevice(DriverObject->DeviceObject);
}

You can do whatever you wish in your unload routine. This unload routine is very simple, it just deletes the symbolic link we created and then deletes the only device that we created which was DeviceExample.

Creating the IRP_MJ_WRITE

The rest of the functions should be self explanatory as they don’t do anything. This is why I am only choosing to explain the “Write” routine. If this article is liked, I may write a second tutorial on implementing the IO Control function.

If you have used WriteFile and ReadFile, you know that you simply pass a buffer of data to write data to a device or read data from a device. These parameters are sent to the device in the IRP as we explained previously. There is more to the story though as there are actually three different methods that the I/O Manager will use to marshal this data before giving the IRP to the driver. That also means that how the data is marshaled is how the driver’s Read and Write functions need to interpret the data.

The three methods are “Direct I/O”, “Buffered I/O” and “Neither”.

#ifdef __USE_DIRECT__
#define IO_TYPE DO_DIRECT_IO
#define USE_WRITE_FUNCTION  Example_WriteDirectIO
#endif
 
#ifdef __USE_BUFFERED__
#define IO_TYPE DO_BUFFERED_IO
#define USE_WRITE_FUNCTION  Example_WriteBufferedIO
#endif

#ifndef IO_TYPE
#define IO_TYPE 0
#define USE_WRITE_FUNCTION  Example_WriteNeither
#endif

The code was written so if you define “__USE_DIRECT__” in the header, then IO_TYPE is now DO_DIRECT_IO and USE_WRITE_FUNCTION is now Example_WriteDirectIO. If you define “__USE_BUFFERED__” in the header, then IO_TYPE is now DO_BUFFERED_IO and USE_WRITE_FUNCTION is now Example_WriteBufferedIO. If you don’t define __USE_DIRECT__ or __USE_BUFFERED__, then IO_TYPE is defined as 0 (neither) and the write function is Example_WriteNeither.

We will now go over each type of I/O.

Direct I/O

The first thing I will do is simply show you the code for handling direct I/O.

NTSTATUS Example_WriteDirectIO(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
    NTSTATUS NtStatus = STATUS_SUCCESS;
    PIO_STACK_LOCATION pIoStackIrp = NULL;
    PCHAR pWriteDataBuffer;

    DbgPrint("Example_WriteDirectIO Called rn");
    
    
    pIoStackIrp = IoGetCurrentIrpStackLocation(Irp);
    
    if(pIoStackIrp)
    {
        pWriteDataBuffer = 
          MmGetSystemAddressForMdlSafe(Irp->MdlAddress, NormalPagePriority);
    
        if(pWriteDataBuffer)
        {                             
            
           if(Example_IsStringTerminated(pWriteDataBuffer, 
              pIoStackIrp->Parameters.Write.Length))
           {
                DbgPrint(pWriteDataBuffer);
           }
        }
    }

    return NtStatus;
}

The entry point simply provides the device object for the device for which this request is being sent for. If you recall, a single driver can create multiple devices even though we have only created one. The other parameter is as was mentioned before which is an IRP!

The first thing we do is call “IoGetCurrentIrpStackLocation”, and this simply provides us with our IO_STACK_LOCATION. In our example, the only parameter we need from this is the length of the buffer provided to the driver, which is at Parameters.Write.Length.

The way buffered I/O works is that it provides you with a “MdlAddress” which is a “Memory Descriptor List”. This is a description of the user mode addresses and how they map to physical addresses. The function we call then is “MmGetSystemAddressForMdlSafe” and we use the Irp->MdlAddress to do this. This operation will then give us a system virtual address which we can then use to read the memory.

The reasoning behind this is that some drivers do not always process a user mode request in the context of the thread or even the process in which it was issued. If you process a request in a different thread which is running in another process context, you would not be able to read user mode memory across process boundaries. You should know this already, as you run two applications they can’t just read/write to each other without Operating System support.

So, this simply maps the physical pages used by the user mode process into system memory. We can then use the returned address to access the buffer passed down from user mode.

This method is generally used for larger buffers since it does not require memory to be copied. The user mode buffers are locked in memory until the IRP is completed which is the downside of using direct I/O. This is the only downfall and is why it’s generally more useful for larger buffers.

Buffered I/O

The first thing I will do is simply show you the code for handling buffered I/O.

NTSTATUS Example_WriteBufferedIO(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
    NTSTATUS NtStatus = STATUS_SUCCESS;
    PIO_STACK_LOCATION pIoStackIrp = NULL;
    PCHAR pWriteDataBuffer;

    DbgPrint("Example_WriteBufferedIO Called rn");
    
    
    pIoStackIrp = IoGetCurrentIrpStackLocation(Irp);
    
    if(pIoStackIrp)
    {
        pWriteDataBuffer = (PCHAR)Irp->AssociatedIrp.SystemBuffer;
    
        if(pWriteDataBuffer)
        {                             
            
           if(Example_IsStringTerminated(pWriteDataBuffer, 
                   pIoStackIrp->Parameters.Write.Length))
           {
                DbgPrint(pWriteDataBuffer);
           }
        }
    }

    return NtStatus;
}

As mentioned above, the idea is to pass data down to the driver that can be accessed from any context such as another thread in another process. The other reason would be to map the memory to be non-paged so the driver can also read it at raised IRQL levels.

The reason you may need to access memory outside the current process context is that some drivers create threads in the SYSTEM process. They then defer work to this process either asynchronously or synchronously. A driver at a higher level than your driver may do this or your driver itself may do it.

The downfall of using “Buffered I/O” is that it allocates non-paged memory and performs a copy. This is now overhead in processing every read and write into the driver. This is one of the reasons this is best used on smaller buffers. The whole user mode page doesn’t need to be locked in memory as with Direct I/O, which is the plus side of this. The other problem with using this for larger buffers is that since it allocates non-paged memory, it would need to allocate a large block of sequential non-paged memory.

Neither Buffered nor Direct

The first thing I will do is show you the code for handling neither Buffered nor Direct I/O.

NTSTATUS Example_WriteNeither(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
    NTSTATUS NtStatus = STATUS_SUCCESS;
    PIO_STACK_LOCATION pIoStackIrp = NULL;
    PCHAR pWriteDataBuffer;

    DbgPrint("Example_WriteNeither Called rn");
    
    
    pIoStackIrp = IoGetCurrentIrpStackLocation(Irp);
    
    if(pIoStackIrp)
    {
        
        __try {
        
                ProbeForRead(Irp->UserBuffer, 
                  pIoStackIrp->Parameters.Write.Length, 
                  TYPE_ALIGNMENT(char));
                pWriteDataBuffer = Irp->UserBuffer;
            
                if(pWriteDataBuffer)
                {                             
                    
                   if(Example_IsStringTerminated(pWriteDataBuffer, 
                          pIoStackIrp->Parameters.Write.Length))
                   {
                        DbgPrint(pWriteDataBuffer);
                   }
                }

        } __except( EXCEPTION_EXECUTE_HANDLER ) {

              NtStatus = GetExceptionCode();     
        }

    }

    return NtStatus;
}

In this method, the driver accesses the user mode address directly. The I/O manager does not copy the data, it does not lock the user mode pages in memory, it simply gives the driver the user mode address buffer.

The upside of this is that no data is copied, no memory is allocated, and no pages are locked into memory. The downside of this is that you must process this request in the context of the calling thread so you will be able to access the user mode address space of the correct process. The other downside of this is that the process itself can attempt to change access to the pages, free the memory, etc., on another thread. This is why you generally want to use “ProbeForRead” and “ProbeForWrite” functions and surround all the code in an exception handler. There’s no guarantee that at any time the pages could be invalid, you can simply attempt to make sure they are, before you attempt to read or write. This buffer is stored at Irp->UserBuffer.

What’s this #pragma stuff?

These directives you see simply let the linker know what segment to put the code and what options to set on the pages. The “DriverEntry”, for example, is set as “INIT” which is a discardable page. This is because you only need that function during initialization.

Homework!

Your homework is to create the Read routines for each type of I/O processing. You can use the Write routines as reference to figure out what you need to do.

Dynamically Loading and Unloading the Driver

A lot of tutorials will go and explain the registry, however, I have chosen not to at this time. There is a simple user mode API that you can use to load and unload the driver without having to do anything else. This is what we will use for now.

int _cdecl main(void)
{
    HANDLE hSCManager;
    HANDLE hService;
    SERVICE_STATUS ss;

    hSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_CREATE_SERVICE);
    
    printf("Load Drivern");

    if(hSCManager)
    {
        printf("Create Servicen");

        hService = CreateService(hSCManager, "Example", 
                                 "Example Driver", 
                                  SERVICE_START | DELETE | SERVICE_STOP, 
                                  SERVICE_KERNEL_DRIVER,
                                  SERVICE_DEMAND_START, 
                                  SERVICE_ERROR_IGNORE, 
                                  "C:\example.sys", 
                                  NULL, NULL, NULL, NULL, NULL);

        if(!hService)
        {
            hService = OpenService(hSCManager, "Example", 
                       SERVICE_START | DELETE | SERVICE_STOP);
        }

        if(hService)
        {
            printf("Start Servicen");

            StartService(hService, 0, NULL);
            printf("Press Enter to close servicern");
            getchar();
            ControlService(hService, SERVICE_CONTROL_STOP, &ss);

            DeleteService(hService);

            CloseServiceHandle(hService);
            
        }

        CloseServiceHandle(hSCManager);
    }
    
    return 0;
}

This code will load the driver and start it. We load the driver with “SERVICE_DEMAND_START” which means this driver must be physically started. It will not start automatically on boot, that way we can test it, and if we blue-screen, we can fix the issue without having to boot to safe mode.

This program will simply pause. You can then run the application that talks to the service, in another window. The code above should be pretty easy to understand that you need to copy the driver to C:example.sys in order to use it. If the service fails to create, it knows it has already been created and opens it. We then start the service and pause. Once you press Enter, we stop the service, delete it from the list of services, and exit. This is very simple code and you can modify it to serve your purposes.

Communicating to the Device Driver

The following is the code that communicates to the driver.

int _cdecl main(void)
{
    HANDLE hFile;
    DWORD dwReturn;

    hFile = CreateFile("\\.\Example", 
            GENERIC_READ | GENERIC_WRITE, 0, NULL, 
            OPEN_EXISTING, 0, NULL);

    if(hFile)
    {
        WriteFile(hFile, "Hello from user mode!", 
                  sizeof("Hello from user mode!"), &dwReturn, NULL); 
        CloseHandle(hFile);
    }
    
    return 0;
}

This is probably simpler than you thought. If you compile the driver three times using the three different methods of I/O, the message sent down from user mode should be printed in DBGVIEW. As you notice, you simply need to open the DOS Device Name using \.<DosName>. You could even open Device<Nt Device Name> using the same method. You will then create a handle to the device and you can call WriteFile, ReadFile, CloseHandle, DeviceIoControl! If you want to experiment, simply perform actions and use DbgPrint to show what code is being executed in your driver.

Conclusion

This article showed a simple example of how to create a driver, install it, and access it via a simple user mode application. You may use the associated source files to change and experiment. If you wish to write drivers, it’s best to read up on many of the basic concepts of drivers, especially, some of the ones linked to in this tutorial.

Ну вот мы и добрались до самого интересного — сейчас будем писать драйвер. Итак, приступим. Для удобства выполнения, всю последовательность действий по написанию драйвера я распишу по пунктам.

1. Создание директории проекта

Установив DDK, на Вашем компьютере в директории C:WINDDK2600.1106 должны появиться файлы DDK. В этой директории создадим папку, в которой будут храниться наши проекты. Назовем ее, например, MyDrivers.

В папке MyDrivers создадим папку FirstDriver — тут будет находится наш первый проект драйвера.

2. Подготовка файлов проекта

В папке FirstDriver создайте пустой текстовый файл и переименуйте его под именем FirstDriver.c

При попытке переименовки со сменой расширения файла, появляется следующее предупреждение:

Не обращаем внимания на это предупреждение, и нажимаем Да. При этом наш файл примет вид:

Если же никакого предупреждения не было и переименованный файл так и остался текстовым с именем FirstDriver.c и расширением .txt, то в настройках своийств папки, которые можно найти в Пуск-> Настройка-> Панель управления-> Свойства паки уберите галочку напротив пункта «Скрывать расширения для зарегестрированных типов файлов». Попробуйте еще раз и все должно быть в порядке.

Теперь нам надо добавить еще два очень важных файла в наш проект, без которых драйвер нам не сделать. Они называются makefile и sources (обратите внимание, у них нет расширения). Их можно создать самим, но мы сделаем проще: скопируем готовые из какого либо примера проекта драйвера из DDK. Например, возьмем их из C:WINDDK2600.1106srcgeneralcancelsys. Итак, копируем из указанной директории эти два файла и вставляем их в нашу папку проекта FirstDriver. Эти файлы управляют процессрм компиляции драйвера. Файл makefile оставляем без изменений, а вот sources надо подредактировать.

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

Первым параметром идет TARGETNAME, которому мы присвоили Port. Это значит, что когда DDK откомпилирует наш код и создаст драйвер, имя этого файла будет Port.sys Следующем параметром идет TARGETPATH, которому мы указали путь к папке нашего проекта. Если Вы устанавливали DDK в другое место, или создали пупку проекта в другой директории, здесь Вам надо это поправить на тот путь, который у Вас. Параметр TARGETTYPE пока оставлю без комментариев. В параметре SOURCES указываем, из каких файлов будет компилироваться драйвер. У нас это файл FirstDriver.c, вот мы его и указали.

Всю подготовительную работу мы сделали. Можно приступать к самой содержательной части — коду драйвера. Писать мы будем его на Си.

Еще раз напомню решаемую нами задачу: надо написать драйвер под Windows 2000, XP с помощью которого можно будет работать с портами компьютера (читать и писать данные) .

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

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

Вряд ли пользователь домашнего ПК заинтересуется тем, чтобы блокировать устройства на своем ПК. Но если дело касается корпоративной среды, то все становится иначе. Есть пользователи, которым можно доверять абсолютно во всем, есть такие, которым можно что-то делегировать, и есть те, кому доверять совсем нельзя. Например, вы заблокировали доступ к Интернету одному из пользователей, но не заблокировали устройства этого ПК. В таком случае пользователю достаточно просто принести USB-модем, и Интернет у него будет. Т.е. простым блокированием доступа к Интернету дело не ограничивается.

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

В этой статье я расскажу немного теоретическую часть, на основе которой все строится, и расскажу принцип самого решения.

Также полные исходные коды могут быть найдены в папке USBLock хранилища git по адресу: https://github.com/anatolymik/samples.git.

Структура DRIVER_OBJECT

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

Такая инициализация обычно выполняется при вызове системой точки входа драйвера, прототип которой изображен ниже.

Как видно из примера, сначала весь массив MajorFunction инициализируется одним и тем же обработчиком. В реальности типов запросов больше, чем в примере. Поэтому предварительно весь массив инициализируется так, чтобы запросы, которые не поддерживаются драйвером, обрабатывались корректно. Например, завершались с ошибкой. После инициализации массива обычно инициализируются обработчики для тех запросов, за которые драйвер отвечает.

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

Обратите внимание на то, что в поле DriverExtension->AddDevice устанавливается адрес обработчика, который вызывается всякий раз, когда система обнаруживает новое устройство, за работу которого драйвер отвечает. Данное поле может быть оставлено непроинициализированным, в таком случае драйвер не сможет обрабатывать это событие.

Структура DEVICE_OBJECT

Структура DEVICE_OBJECT представляет ту или иную функциональность драйвера. Т.е. эта структура может представлять физическое устройство, логическое устройство, виртуальное устройство или просто некий функционал, предоставляемый драйвером. Поэтому когда система будет посылать запросы, то в самом запросе она будет указывать адрес этой структуры. Таким образом, драйвер сможет определить, какой функционал от него запрашивается. Если не использовать такую модель, тогда драйвер может обрабатывать только какую-нибудь одну функциональность, а в современном мире это недопустимо. Прототип функции, которая обрабатывает конкретный запрос, приведена ниже.

Массив MajorFunction ранее упомянутой структуры DRIVER_OBJECT содержит адреса обработчиков именно с таким прототипом.

Сама структура DEVICE_OBJECT всегда создается драйвером при помощи функции IoCreateDevice. Если система посылает запрос драйверу, то она всегда направляет его какому-либо DEVICE_OBJECT, как это следует из вышепредставленного прототипа. Также, прототип принимает второй параметр, который содержит адрес IRP-структуры. Эта структура описывает сам запрос, и она существует в памяти до тех пор, пока драйвер не завершит его. Запрос отправляется драйверу на обработку при помощи функции IoCallDriver как системой, так и другими драйверами.

Также со структурой DEVICE_OBJECT может быть связано имя. Таким образом, этот DEVICE_OBJECT может быть найден в системе.

Фильтрация

Фильтрация являет собой механизм, который позволяет перехватывать все запросы, направленные к конкретному DEVICE_OBJECT. Чтобы установить такой фильтр, необходимо создать другой экземпляр DEVICE_OBJECT и прикрепить его к DEVICE_OBJECT, запросы которого необходимо перехватывать. Прикрепление фильтра выполняется посредством функции IoAttachDeviceToDeviceStack. Все DEVICE_OBJECT, прикрепленные к перехватываемому DEVICE_OBJECT, вместе с ним формируют так называемый стек устройства, как это изображено ниже.

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

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

PnP менеджер

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

Когда драйвер той или иной шины обнаруживает устройства на своих интерфейсах, то для каждого дочернего устройства он создает DEVICE_OBJECT. Этот DEVICE_OBJECT также называют Physical Device Object или PDO. Затем посредством функции IoInvalidateDeviceRelations он уведомляет PnP менеджер о том, что произошли изменения на шине. В ответ на это PnP менеджер посылает запрос с minor кодом IRP_MN_QUERY_DEVICE_RELATIONS с целью запросить список дочерних устройств. В ответ на этот запрос драйвер шины возвращает список PDO. Ниже изображен пример такой ситуации.

Как отражено на рисунке, в данном примере в качестве шины выступает драйвер USB-хаба. Конкретные DEVICE_OBJECT стека устройства этого хаба не изображены для краткости и с целью сохранения последовательности пояснений.

Как только PnP менеджер получит список всех PDO, он по отдельности соберет всю необходимую информацию об этих устройствах. Например, будет послан запрос с minor кодом IRP_MN_QUERY_ID. Посредством этого запроса PnP менеджер получит идентификаторы устройства, как аппаратные, так и совместимые. Также PnP менеджер соберет всю необходимую информацию о требуемых аппаратных ресурсах самим устройством. И так далее.

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

Следующая задача PnP — это запуск драйвера устройства. Если драйвер не был ранее установлен, тогда PnP будет ожидать установки. Иначе, при необходимости, PnP загрузит его и передаст ему управление. Ранее упоминалось, что поле DriverExtension->AddDevice структуры DRIVER_OBJECT содержит адрес обработчика, который вызывается всякий раз, когда система обнаруживает новое устройство. Прототип этого обработчика изображен ниже.

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

В задачу обработчика входит создание DEVICE_OBJECT и его прикрепление к PDO. Прикрепленный DEVICE_OBJECT также называют Functional Device Object или FDO. Именно этот FDO и будет отвечать за работу устройства и представление его интерфейсов в системе. Ниже представлен пример, когда PnP завершил вызов драйвера, отвечающего за работу устройства.

Как отражено на примере, кроме драйвера самого устройства также могут быть зарегистрированы нижние и верхние фильтры класса устройства. Следовательно, если таковые имеются, PnP также загрузит их драйвера и вызовет их AddDevice обработчики. Т.е. порядок вызова драйверов следующий: сначала загружаются и вызываются зарегистрированные нижние фильтры, затем загружается и вызывается драйвер самого устройства, и в завершении загружаются и вызываются верхние фильтры. Нижние и верхние фильтры являются обычным DEVICE_OBJECT, которые создают драйвера и прикрепляют их к PDO в своих обработчиках AddDevice. Количество нижних и верхних фильтров не ограничено.

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

Также, когда драйвер шины определяет, что произошли изменения на шине, он посредством функции IoInvalidateDeviceRelations уведомляет PnP о том, что следует заново собрать информацию о подключенных устройствах. В этот момент драйвер не удаляет ранее созданный PDO. Просто при получении запроса с minor кодом IRP_MN_QUERY_DEVICE_RELATIONS он не включит этот PDO в список. Затем PnP на основании полученного списка опознает новые устройства и устройства, которые были отключены от шины. PDO отключенных устройств драйвер удалит тогда, когда PnP пошлет запрос с minor кодом IRP_MN_REMOVE_DEVICE. Для драйвера этот запрос означает, что устройство более никем не используется, и оно может быть безопасно удалено.

Суть решения

Суть самого решения заключается в создании верхнего фильтра класса USB-шины. Зарезервированные классы можно найти по адресу: https://msdn.microsoft.com/en-us/library/windows/hardware/ff553419(v=vs.85).aspx. Нас интересует класс USB с GUID равным 36fc9e60-c465-11cf-8056-444553540000. Как гласит MSDN, этот класс используется для USB хост контроллеров и хабов. Однако практически это не так, этот же класс используется, например, flash-накопителями. Это немного добавляет нам работы. Код обработчика AddDevice представлен ниже.

Как следует из примера, мы создаем DEVICE_OBJECT и прикрепляем его к PDO. Таким образом, мы будем перехватывать все запросы, направленные к USB-шине.

В нашу задачу входит перехватывать запросы с minor кодом IRP_MN_START_DEVICE. Код обработчика этого запроса изображен ниже.

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

Обратите внимание: функция определяет, является ли устройство хабом или композитным устройством. Это необходимо потому, что, как уже было упомянуто, класс устройств, который используется для хабов и хост контроллеров, используется не только этими устройствами. А нам в первую очередь необходимо контролировать дочерние устройства только хабов, хост контроллеров и композитных устройств. Т.е. для хабов и композитных устройств дополнительно перехватывается запрос перечисления дочерних устройств, на этом этапе, важно также прикрепить ко всем дочерним устройствам фильтр, и этот фильтр будет нижним. В противном случае контроль над дочерними устройствами будет потерян.

Все упомянутые определения выполняются на основе идентификаторов устройств.

Заключение

Вы можете помочь и перевести немного средств на развитие сайта

Драйвер-это основа взаимодействия системы с устройством в ОС Windows.Это одновременно удобно и неудобно.
Про удобства я разъяснять не буду — это и так понятно,
а заострюсь я именно на неудобствах драйверов.
В сложившейся ситуации пользователь полностью подчинён воле производителя
— выпусти тот драйвер — хорошо, а не выпустит.
Только продвинутый пользователь, имеющий голову на плечах
(особенно, если он ешё и программер) не станет мириться с таким положением дел
— он просто возьмёт и сам напишет нужный драйвер.
Это нужно и взломщику: драйвер — это удобное окошко в ring0,
которое является раем для хакера. Но хоть написать драйвер и просто,
да не совсем — есть масса подводных камней. Да и документированность данного вопроса на русском языке оставляет желать лучшего.
Этот цикл статей поможет тебе во всём разобраться.
Приступим.

Хочу сразу же сделать несколько предупреждений.
Данная статья всё-таки подразумевает определённый уровень подготовки.
Драйвера-то ведь пишутся на C(++) с большим количеством ассемблерных вставок.
Поэтому хорошее знание обоих языков весьма желательно (если не сказать — обязательно).
Если же ты пока не можешь этим похвастаться,
но желание писать драйвера есть — что ж, так как эта статья вводная, в конце её будет приведён список полезной литературы,
ссылок и т.д. Но помни: учить тебя в этом цикле статей программированию как таковому я тебя не буду.
Может как-нибудь в другой раз. Согласен? Тогда поехали!


Скоро здесь, возможно, будет стоять твоё имя.

Практически в любом деле, как мне кажется, нужно начинать с теории.
Вот и начнём с неё. Для начала уясним себе поточнее основные понятия.
Первое: что есть драйвер? Драйвер — в сущности
кусок кода ОС, отвечающий за взаимодействие с аппаратурой.
Слово «аппаратура» в данном контексте следует понимать в самом широком смысле.
С момента своего появления как такого до сегодняшнего дня драйвер беспрерывно эволюционировал.
Вот, скажем, один из моментов его развития. Как отдельный и довольно независимый модуль драйвер сформировался не сразу.
Да и сейчас этот процесс до конца не завершён:
ты наверняка сталкивался с тем, что во многих
дистрибутивах никсов для установки/перестановки etc драйверов нужно перекомпилировать ядро,
т.е. фактически заново пересобирать систему.
Вот, кстати ещё один близкий моментец: разные принципы работы с драйверами в Windows 9x и NT.
В первом процесс установки/переустановки драйверов проходит практически без проблем,
во втором же случае это тяжёлое и неблагодарное дело,
для «благополучного» завершения которого нередко приходится прибегать к полной переустановке ОС.
А зато в Windows 9x. так,стоп,открывается широкая и волнующая тема,
которая уведёт меня далеко от темы нынешней статьи,
так что вернёмся к нашим баранам. ой,то есть к драйверам.
В порядке общего развития интересно сравнить особенности драйверов в Windows и *nix(xBSD) системах:

1) Способ работы с драйверами как файлами (подробнее см. ниже)
2) Драйвер, как легко заменяемая честь ОС (учитывая уже сказанные выше примечания)
3) Существование режима ядра

Теперь касательно первого пункта. Это значит,
что функции, используемые при взаимодействии с файлами,
как и с драйверами, практически идентичные (имеется в виду лексически):
open, close, read и т.д. И напоследок стоит отметить идентичность механизма
IOCTL (Input/Output Control Code-код управления вводом-выводом)
-запросов.

Драйвера под Windows делятся на два типа:
Legacy (устаревший) и WDM (PnP). Legacy драйверы (иначе называемые «драйверы в стиле
NT») чрезвычайно криво работают (если работают вообще)
под Windows 98, не работают с PnP устройствами, но зато могут пользоваться старыми функциями
HalGetBusData, HalGetInterruptVector etc, но при этом не имеют поддержки в лице шинных драйверов.
Как видишь, весьма средненький драйвер. То ли дело
WDM: главный плюс — поддержка PnP и приличненькая совместимость:
Windows 98, Me, 2000, XP, 2003, Server 2003 и т.д. с вариациями; но он тоже вынужден за это расплачиваться:
например, он не поддерживает некоторые устаревшие функции
(которые всё таки могут быть полезны). В любом случае,
не нужно ничего воспринимать как аксиому, везде бывают свои исключения.
В некоторых случаях лучше написания Legacy драйвера ничего не придумать.

Как ты наверняка знаешь, в Windows есть два мода работы:
User Mode и Kernel Mode — пользовательский режим и режим ядра соответственно.
Первый — непривилегированный, а второй — наоборот.
Вот во втором чаще всего и сидят драйвера (тем
более, что мы в данный момент говорим именно о драйверах режима ядра).
Главные различия между ними: это доступность всяких привилегированных команд процессора.
Программировать (а уж тем более качественно) в Kernel mode посложнее будет,
чем писать прикладные незамысловатые проги.
А драйвера писать без хорошего знания Kernel mode — никак.
Нужно попариться над назначением выполнения разнообразных работ отдельному подходящему уровню IRQL, желательно выучить новое API (так как в Kernel mode API отличается от прикладного).
в общем, предстоит много всяких радостей. Но тем не менее,
это очень интересно, познавательно, и даёт тебе совершенно иной уровень власти над компьютером.

А раз уж я упомянула про IRQL, разьясню и это понятие.
IRQL (Interrupt Request Level — уровень приоритета выполнения) — это приоритеты,
назначаемые специально для кода, работающего в режиме ядра.
Самый низкий уровень выполнения — PASSIVE_LEVEl. Работающий поток может быть прерван потоком только с более высоким
IRQL.

Ну и напоследок разъясним ещё несколько терминов:

1) ISR (Interrupt Service Routine) — процедура обслуживания прерываний.
Эта функция вызывается драйвером в тот момент,
когда обслуживаемая им аппаратура посылает сигнал прерывания.
Делает самые необходимые на первый момент вещи:
регистрирует callback — функцию и т.д.

2) DpcForISR (Deferred Procedure Call for ISR) — процедура отложенного вызова для обслуживания прерываний.
Эту функцию драйвер регистрирует в момент работы ISR для выполнения основной работы.

3) IRP (Input/Output Request Packet) — пакет запроса на ввод — вывод.
Пакет IRP состоит из фиксированной и изменяющейся частей.
Вторая носит название стека IRP или стека ввода — вывода (IO stack).

4) IO stack location — стек ввода — вывода в пакете IRP.

5) Dispatch Routines (Рабочие процедуры) — эти функции регистрируются в самой первой (по вызову) процедуре драйвера.

6) Major IRP Code — старший код IRP пакета.

7) Minor IRP Code — соответственно, младший код IRP пакета.

8) DriverEntry — эта функция драйвера будет вызвана первой при его загрузке.

9) Layering (Многослойность) — данной возможностью обладают только WDM — драйвера.
Она заключается в наличии реализации стекового соединения между драйверами.
Что такое стековое соединение? Для этого необходимо знать про Device
Stack (стек драйверов) — поэтому я обязательно вспомню про всё это чуточку ниже.

10) Device Stack, Driver Stack (стек устройств, стек драйверов) — всего лишь
объемное дерево устройств. Его, кстати, можно рассмотреть во всех подробностях с помощью программы
DeviceTree (из MS DDK), например.

11) Стековое соединение — как и обещала, объясняю. В стеке драйверов самый верхний драйвер — подключившийся позднее.
Он имеет возможность посылать/переадресовывать IRP запросы другим драйверам,
которые находятся ниже его. Воти всё. Правда,просто?

12) AddDevice — функция, которую обязательно должны поддерживать WDM драйверы.
Её название говорит само за себя.

13) Device Object, PDO, FDO (Объект устройства, физический,
функциональный) — при подключении устройства к шине она создаёт PDO.
А уже к PDO будут подключаться FDO объекты WDM драйверов.
Обьект FDO создаётся самим драйвером устройства при помощи функции IOCreateDevice.
Обьект FDO также может иметь свою символическую ссылку, от которой он будет получать запросы от драйвера.
Это что касается WDM драйверов. С драйверами «в стиле NT» ситуация несколько иная.
Если он не обслуживает реальных/PnP устройств,
то PDO не создаётся. Но для связи с внешним миром без FDO не обойтись.
Поэтому он присутствует и тут.

14) Device Extension (Расширение обьекта устройства) — «авторская» структура,
т.е. она полностью определяется разработчиком драйвера.
Правилом хорошего тона считается, например,
размещать в ней глобальные переменные.

15) Monolithic Driver (Монолитный драйвер) — это драйвер,
который самостоятельно обрабатывает все поступающие
IRP пакеты и сам работает с обслуживаемым им устройством
(в стеке драйверов он не состоит). Данный тип драйверов используется только если обслуживается не
PnР устройство или же всего лишь требуется окошко в ring0.

16) DIRQL (уровни аппаратных прерываний) —
прерывания, поступающие от реальных устройств, имеют наивысший приоритет IRQL,
поэтому для них решено было придумать специальное название
(Device IRQL).

17) Mini Driver (Мини — драйвер) — чуть меньше «полного» драйвера.
Обычно реализуется в виде DLL-ки и имеет оболочку в виде «полного» драйвера.

18) Class Driver (Классовый драйвер) — высокоуровневый драйвер,
который предоставляет поддержку класса устройств.

19) РnP Manager (PnP менеджер) — один из главных компонентов операционной системы.
Состоит из двух частей: PnP менеджера пользовательского и «ядерного» режимов.
Первый в основном взаимодействует с пользователем;
когда тому нужно, например, установить новые драйвера и т.д.
А второй управляет работой, загрузкой и т.д. драйверов.

20) Filter Driver (фильтр — драйвер) — драйверы, подключающиеся к основному драйверу либо сверху
(Upper), либо снизу (Lower). Фильтр драйверы (их может быть несколько) выполняют фильтрацию IRP пакетов.
Как правило, для основного драйвера Filter Drivers неощутимы.

21) Filter Device Object — объект устройства, создаваемый фильтр — драйвером.

22) HAL (Hardware Abstraction Layer) — слой аппаратных абстракций.
Данный слой позволяет абстрагироваться компонентам операционной системы от особенностей конкретной платформы.

23) Synchronization Objects (Обьекты синхронизации) — с помощью этих
объектов потоки корректируют и синхронизируют свою работу.

24) Device ID — идентификатор устройства.

25) DMA (Direct Memory Access) — метод обмена данными между устройством и памятью
(оперативной) в котором центральный процессор не принимает участия.

25) Polling — это особый метод программирования, при котором не устройство посылает сигналы прерывания драйверу,
а сам драйвер периодически опрашивает обслуживаемое им устройство.

26) Port Driver (Порт-драйвер) — низкоуровневый драйвер,
принимающий системные запросы. Изолирует классовые драйверы устройств от аппаратной специфики последних.

Ну вот, пожалуй, и хватит терминов. В будущем,
если нужны будут какие-нибудь уточнения по теме,
я обязательно их укажу. А теперь, раз уж эта статья
теоретическая, давай-ка взглянем на архитектуру Windows NT с высоты птичьего полёта.

Краткий экскурс в архитектуру Windows NT

Наш обзор архитектуры Windows NT мы начнём с разговора об уровнях разграничения привилегий. Я уже упоминала об user и kernel mode.
Эти два понятия тесно связаны с так называемыми кольцами (не толкиеновскими ).
Их ( колец) в виде всего четыре: Ring3,2,1 и 0. Ring3 — наименее привилегированное кольцо,
в котором есть множество ограничений по работе с устройствами,
памятью и т.д. Например, в третьем кольце нельзя видеть адресное пространство других приложений без особого на то разрешения. Естественно,
трояну вирусу etc эти разрешения получить будет трудновато, так что хакеру в третьем кольце жизни никакой. В третьем кольце находится user mode. Kernel mode сидит в нулевом кольце — наивысшем уровне привилегий. В этом кольце можно всё:
смотреть адресные пространства чужих приложений без каких — либо ограничений и разрешений, по своему усмотрению поступать с любыми сетевыми пакетами, проходящими через машину, на всю жизнь скрыть какой-нибудь свой процесс или файл и т.д. и т.п. Естественно,
просто так пролезть в нулевое кольцо не получиться:
для этого тоже нужны дополнительные телодвижения. У легального драйвера с этим проблем нет:
ему дадут все необходимые API — шки, доступ ко всем нужным системным таблицам и проч. Хакерской же нечисти опять приходиться туго:
все необходимые привилегии ему приходиться «выбивать»
незаконным путём. Но это уже тема отдельной статьи, и мы к ней как-нибудь ещё вернёмся. А пока продолжим.

У тебя наверняка возник законный вопрос:
а что же сидит в первом и втором кольцах ? В том то всё и дело,
что программисты из Microsoft почему — то обошли эти уровни своим вниманием. Пользовательское ПО сидит в user mode,а всё остальное (ядро,
драйвера. ) — в kernel mode. Почему они так сделали — загадка, но нам это только на руку. А теперь разберёмся с компонентами (или, иначе говоря, слоями ) операционной системы Windows
NT.

Посмотри на схему — по ней многое можно себе уяснить. Разберём её подробнее.
С пользовательским режимом всё понятно. В kernel mode самый низкий уровень аппаратный. Дальше идёт HAL, выше — диспетчер ввода — вывода и драйвера устройств в одной связке, а также ядрышко вместе с исполнительными компонентами. О HAL я уже говорила, поэтому поподробнее поговорим об исполнительных компонентах. Что они дают? Прежде всего они приносят пользу ядру. Как ты уже наверняка уяснил себе по схеме, ядро отделено от исполнительных компонентов. Возникает вопрос:
почему ? Просто на ядре оставили только одну задачу:
просто управление потоками, а все остальные задачи (управление доступом,
памятью для процессов и т.д.) берут на себя исполнительные компоненты (еxecutive). Они реализованы по модульной схеме, но несколько компонентов её (схему) не поддерживают . Такая концепция имеет свои преимущества:
таким образом облегчается расширяемость системы. Перечислю наиболее важные исполнительные компоненты:

1) System Service Interface (Интерфейс системных служб )
2) Configuration Manager (Менеджер конфигурирования)
3) I/O Manager (Диспетчер ввода-вывода,ДВВ)
4) Virtual Memory Manager,VMM (Менеджер виртуальной памяти)
5) Local Procedure Call,LPC (Локальный процедурный вызов )
6) Process Manager (Диспетчер процессов)
7) Object Manager (Менеджер объектов)

Так как эта статья — первая в цикле, обзорная, подробнее на этом пока останавливаться не будем. В процессе практического обучения написанию драйверов, я буду разъяснять все неясные термины и понятия. А пока перейдём к API.

API (Application Programming Interface) — это интерфейс прикладного программирования. Он позволяет обращаться прикладным программам к системным сервисам через их специальные абстракции. API-интерфейсов несколько, таким образом в Windows-системах присутствуют несколько подсистем. Перечислю:

1) Подсистема Win32.
2) Подсистема VDM (Virtual DOS Machine — виртуальная ДОС — машина)
3) Подсистема POSIX (обеспечивает совместимость UNIX — программ)
4) Подсистемиа WOW (Windows on Windows). WOW 16 обеспечивает совместимость 32-х разрядной системы с 16-битными приложениями. В 64-х разрядных системах есть подсистема WOW 32,
которая обеспечивает аналогичную поддержку 32 — битных приложений.
5) Подсистема OS/2. Обеспечивает совместимость с OS/2 приложениями.

Казалось бы, всё вышеперечисленное однозначно говорит в пользу WINDOWS NT систем!
Но не всё так хорошо. Основа WINDOWS NT (имеются ввиду 32-х разрядные версии) — подсистема Win32. Приложения, заточенные под одну подсистему не могут вызывать функции другой. Все остальные (не Win32) подсистемы существуют в винде только в эмуляции и реализуются функции этих подсистем только через соответствующие функции винды. Убогость и ограниченность приложений, разработанных, скажем, для подсистемы POSIX и запущенных под винду — очевидны.
Увы.

Подсистема Win32 отвечает за графический интерфейс пользователя, за обеспечение работоспособности Win32 API и за консольный ввод — вывод. Каждой реализуемой задаче
соответствуют и свои функции: функции, отвечающие за графический фейс,
за консольный ввод — вывод (GDI — функции) и функции управления потоками,
файлами и т.д. Типы драйверов, наличествующие в Windows, я уже упоминала в разделе терминов:
монолитный драйвер, фильтр — драйвер и т.д. А раз так, то пора закругляться. Наш краткий обзор архитектуры Windows NT можно считать завершённым. Этого тебе пока хватит для общего понимания концепций Windows NT, и концепций написания драйверов под эту ось — как следствие.

Описать и/или упомянуть обо всех утилитах, могущих понадобиться при разработке драйверов — немыслимо. Расскажу только об общих направлениях.

Без чего нельзя обойтись ни в коем случае — это Microsoft DDK (Driver Development Kit ). К этому грандиозному пакету прилагается и обширная документация. Её ценность — вопрос спорный. Но в любом случае, хотя бы ознакомиться с первоисточником информации по написанию драйверов для Windows — обязательно. В принципе, можно компилять драйвера и в Visual Studio, но это чревато долгим и нудным копанием в солюшенах и vcproj-ектах, дабы код твоего драйвера нормально откомпилировался. В любом случае, сорцы придётся набивать в визуальной студии, т.к. в DDK не входит
нормальная IDE. Есть пакеты разработки драйверов и от третьих фирм:
WinDriver или NuMega Driver Studio, например. Но у них есть отличия от майкрософтовского базиса функций (порой довольно большие ) и многие другие мелкие неудобства. Так что DDK — лучший вариант. Если же ты хочешь писать драйвера исключительно на ассемблере, тебе подойдёт KmdKit (KernelMode Driver DevelopmentKit) для MASM32. Правда, этот вариант только для Win2k/XP.

Теперь можно поговорить о сторонних утилитах. Некоторые уже включены в стандартную поставку
Windows: редактор реестра. Но их в любом случае не хватит. Надо будем инсталлить отдельно.
Множество наиполезнейших утилит создали патриархи системного кодинга в
Windows: Марк Руссинович, Гарри Нэббет, Свен Шрайбер. и т.д. Вот о них и поговорим.
Марк Руссинович создал много полезных утилит:
RegMon, FileMon (мониторы обращений к реестру и файлам соответственно), WinObj (средство просмотра директорий имен
объектов), DebugView,DebugPrint (программы просмотра, сохранения и т.д. отладочных сообщения) и проч. и проч. Все эти утилиты и огромное количество других можно найти на знаменитом сайте Руссиновича
http://www.sysinternals.com.

На диске, прилагающемся к знаменитой книге «Недокументированные возможности Windows 2000» Свена Шрайбера,
есть замечательные утилиты w2k_svc, -_sym, -_mem, позволяющие просматривать установленные драйвера, приложения и службы, работающие в режиме ядра, делать дамп памяти и т.д. Все эти утилиты, а также другие программы с диска можно скачать с
http://www.orgon.com/w2k_internals/cd.html.

Напоследок нельзя не упомянуть такие хорошие проги, как PE
Explorer, PE Browse Professional Explorer, и такие незаменимые, как дизассемблер IDA и лучший отладчик всех времён и народов SoftICE.

Ну вот и подошла к концу первая статья из цикла про написание драйверов под Windows. Теперь ты достаточно «подкован» по
теоретической части, так что в следующей статье мы перейдём к практике. Желаю тебе удачи в этом интереснейшем деле — написании драйверов! Да не облысеют твои пятки!

Лучший ответ Сообщение было отмечено R71MT как решение

Решение

Доступ к командам In/Out через драйвер режима ядра


Вообще то, здесь должна была быть статья с рабочим названием «Доступ к портам ввода/вывода под Windows XP». «Иллюстрациями» к статье должны были kernel-mode драйвера, но так как статья до сих пор не готова, а драйвера уже есть, начну с них. Все драйвера, которые будут приводится в этом топике, будут открывать порты 42h, 43h, 61h, чтобы сыграть системным динамиком «Марш клоунов» Нино Ротта.

Переделаем драйвер beeper из KmdTutor by Four-F.
Драйвер собирается при помощи следующих строк в bat-файле

Код

cls
set filename=%1
if exist %filename%.sys del if exist %filename%.sys
set masm_path=masm32
%masm_path%binml /c /Cp /Gz /I%masm_path%include /nologo /c /coff %filename%.asm  || exit
%masm_path%binlink /LIBPATH:%masm_path%lib /nologo /driver /base:0x10000 ^
/align:32 /out:%filename%.sys /subsystem:native %filename%.obj  || exit
if exist %filename%.obj del %filename%.obj

Теория


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

таймер порт назначение порта назначение канала
первый таймер 40h канал #0 отвечает за ход системных часов. Сигнал с этого канала вызывает прерывание времени. 18,2 раз в секунду выполняется процедура, на которую направлен вектор прерывания #8. Эта процедура производит изменения в области памяти, где хранится текущее время. В специальном регистре задвижки хранится число синхроимпульсов, по прошествии которых сигнал таймера должен вызвать прерывание времени. Уменьшая это число (через порт каналов), можно заставить идти системные часы быстрее. Канал #0 используется также для синхронизации некоторых дисковых операций, поэтому при при изменеии числа синхроимпульсов необходимо восстановить первоначальное значение перед использованием обращений к дискам.
  41h канал #1 отвечает за регенерацию памяти. Можно уменьшить число циклов регенерации памяти в секунду, однако это можно делать лишь в небольших пределах, так как при увеличении промежутка регенерации возрастает вероятность сбоя памяти
  42h канал #2 обычно задействуют для работы с динамиком, хотя можно использовать и для других целей, например для синхронизации какого-либо внешнего устройства.
  43h управляющий регистр  
второй таймер для компьютера с шиной Microchannel 44h канал #0  
  45h канал #1
  46h канал #2  
  47h управляющий регистр  
второй таймер для компьютера с шиной EISA 48h канал #0  
  49h канал #1  
  4Ah канал #2  
  4Bh управляющий регистр  

Таблица #1:Пространство портов ввода/вывода для таймера

Второй канал таймера управляет системным динамиком компьютера, генерируя прямоугольные импульсы с частотой 1193180/начальное значение счетчика герц. Начальное значение счетчика является 16-битным, и устанавливается через порт 42h. 1193180 Гц ― частота тактового генератора таймера. Динамик включается и выключается при выводе специального значения в порт управления динамиком с номером 61h, связанный с микросхемой программируемого контроллера периферийного интерфейса Intel 8255. Для включения динамика нужно прочитать байт из порта 61h, установить в единицу его два младших бита (0-ой бит разрешает работу канала таймера, а 1-ый бит включает динамик), а затем записать полученное значение в тот же порт. Чтобы отключить динамик, нужно сбросить значение двух младших битов порта 61h. Все управление таймером осуществляется путем вывода байта в порт 43h. Назначение битов порта 43h приведены в таблице #2.

Номер бита назначение
  Если 7-6 биты не равны 11b, значит байт, посылаемый в порт 43h ― это команда программирования канала[/B]
7-6 Номер канала:
  00b ― 0-ой канал
  01b ― 1-ый канал
  10b ― 2-ой канал
5-4 Индикатор считывания/записи
  00b ― зафиксировать текущее значение счетчика для чтения (в этом случае биты 3-0 не используются)
  01b ― чтение/запись только младшего байта
  10b ― чтение/запись только старшего байта
  11b ― чтение/запись сначала младшего, а потом старшего байта
3-1 Режим работы канала:
000b ― Прерывание IRQ0 при достижении нуля (концу счета). Сигнал GATE=1 разрешает счет, а GATE=0 запрещает счет, причем GATE не влияет на выход OUT. Содержимое CR передается в CE по первому импульсу CLK после того, как процессор осуществил запись в CR, независимо от сигнала на входе GATE. Импульс, который загружает CE, не учитывается при счете. На выходе OUT формируется низкий уровень при записи в регистр управления, который сохраняется до достижения счетчиком 0. Режим 0 предназначен в основном для счета событий.
  001b ― Аппаратно-перезапускаемый одновибратор (ждущий мультивибратор). После загрузки значения N в CR переход 0->1 на входе GATE вызывает загрузку CE, переход 1->0 на выходе OUT и запускает счет. Когда счетчик достигнет 0, на выходе OUT формируется высокий уровень; таким образом, результатом является отрицательный импульс на выходе OUT с продолжительностью N периодов синхронизации.
  010b ― Переодический интервальный таймер (генератор импульсов). После загрузки значения N в CR следующий импульс синхронизации осуществляет передачу из CR в CE. На выходе OUT возникает переход 1->0, когда счетчик достигает 0; низкий уровень сохраняется в течение одного импульса CLK. Затем на выходе OUT появляется высокий уровень, производится повторная загрузка CE из CR; в результате на выходе OUT появляется отрицательный импульс через N тактов синхронизации. Сигнал GATE=1 разрешает счет, а GATE=0 запрещает. Переход 0->1 на выходе GATE вызывает ренинициализацию счета следующим импульсом синхронизации. Данный режим применяется для реализации переодического интервального таймера.
  011b ― Генератор прямоугольного сигнала. Аналогичен режиму 2, но на входе OUT формируется низкий уровень при достижении половины начального счета; этот уровень сохраняется до достижения счетчиком 0. Как и прежде, сигнал GATE разрешает и запрещает счет, а его переход 0->1 реинициализирует счет. Этот режим применяется в генераторах, определяющих скорость передачи в бодах.
  100b ― Программно-запускаемый строб. Аналогичен режиму 0, но на выходе OUT в процессе счета действует высокий уровень, а при достижении счетчиком 0 появится отрицательный импульс с продолжительностью в один такт синхронизации.
  101b ― Аппаратно-запускаемый строб с перезапуском. После загрузки CR переход 0->1 на входе GATE вызывает передачу из CR в CE следующим импульсом CLK. В процессе счета на выходе OUT действует высокий уровень, а при достижении счетчиком 0 формируется отрицательный импульс с продолжительностью в один период CLK. Сигнал GATE может в любой момент времени реинициализировать счет.
0 Формат счетчика:
0 ― двоичное 16-разрядное число с диапазоном от 0 до 0FFFFh. Максимальное значение счетчика = 216- 1
  1 ― двоично-десятичное число с диапазоном от 0000 до 9999. Максимальное значение счетчика = 104- 1
  Если 7-6 биты равны 11b, значит байт посылаемый в порт 43h ― это команда чтения счетчиков
7-6 11b ― команда чтения счетчиков
5-4 00b ― сначала состояние канала, потом значение счетчиков
  01b ― значение счетчиков
  10b ― состояние канала
3-1 Команда относится к каналам 0-2

Таблица #2: Назначение битов порта 43h

Частота генерируемого звука задается с помощью микросхемы программируемого интервального таймера Intel 8253. Этот контроллер в числе прочего определяет, сколько импульсов в секунду следует послать на системный динамик. Таймер вырабатывает базовую частоту 1,193180 МГц. Первоначальное значение делителя частоты установлено в (1)0000h, что эквивалентно 65536. Для этого числа частота прерываний таймера равна 1193180/65536=18,20648193359375 Гц, что ниже граничной частоты восприятия звука человеком. Мы можем посылать Intel 8253 другое число для деления базовой частоты 1,19 МГц. Для установки таймера в правильный рабочий режим посылаем число 0B6h в порт 43h. После этого можно использовать порт 42h для передачи делителя. Если отправить в порт 42h число 5000, то получим частоту следования импульсов 1193180/5000=238,636 Гц, чуть ниже ноты си третьей октавы (для третьей октавы значение частоты из таблицы #3 делят на 2, а для пятой октавы ― умножают на 2). Так как делитель является 16-разрядным числом, поэтому передаем его двумя частями. Сначала отправляем младший байт, а затем старший байт.

нота частота (Гц) делитель =1193180/частота
до диез 277,23 10CFh
ре 293,66 0FDFh
ре диез 311,13 0EFAh
ми 329,63 0E23h
фа 349,23 0D58h
фа диез 370 0C98h
соль 392 0BE3h
соль диез 415,3 0B39h
ля 440 0A97h
ля диез 466,16 9FFh
си 493,88 96Fh
до 523,25 8E8h

Таблица #3: Ноты четвертой октавы

Практика


Выводим в порт 43h число 0B6h=10.11.011.0b, мы помещаем в управляющий регистр таймера значение, определяющее:

  • номер канала, которым мы будем управлять (10b=2-ой канал)
  • тип операции (11b = чтение/запись сначала младшего, а потом старшего байта)
  • режим работы канала (011b = генератор прямоугольных импульсов (основной режим))
  • формат счетчика (0 = 16-разрядное число от 0 до 0FFFFh)
Assembler
1
2
    mov al,0B6h
    out 43h,al

Затем, в порт 42h выводим 16-битное начальное значение счетчика. Сначала младший байт, затем старший.

Assembler
1
2
3
4
5
6
        mov esi,offset melody; данные
    mov ecx,size_melody  ; счетчик
    mov dx,42h      ;используем порт 42h для передачи делителя
a0: . . . ; создаем задержку между импульсами в 40 мкСек
    outsb
    loop a0

Команда OUTSB выводит данные в порт ввода- вывода, номер которого загружен в регистр DX, из ячейки памяти по адресу DS:ESI (массив melody), после выполнения команды OUTSB содержимое регистра ESI увеличивается на единицу, это будет происходить до тех пор, пока содержимое регистра ECX не станет равным нулю. Для получения задержки, чтобы не зависеть от быстродействия конкретного CPU, используем функцию KeStallExecutionProcessor. После проигрыша мелодии выключаем динамик, сбрасывая два младших бита. На время работы с регистрами таймера, запрещаем аппаратные прерывания (команда CLI). На этом работу нашего драйвера можно считать законченной. Драйвер возвращает системе STATUS_DEVICE_CONFIGURATION_ERROR ― код фиктивной ошибки и благополучно удаляется системой из памяти. Код ошибки возвращен только для того, чтобы система сама удалила драйвер и он понапрасну не «болтался» в памяти. Когда мы доберемся до полнофункциональных драйверов, то, естественно, будем возвращать STATUS_SUCCESS.

Assembler
1
2
    mov eax, STATUS_DEVICE_CONFIGURATION_ERROR;возвращаем код ошибки, для 
    ret     ;того, чтобы система удалила драйвер из памяти

Далее исходный текст драйвера scp00.sys

Assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
; masm windows native #
;написано на основе драйвера режима ядра beeper из «KmdTutor by Four-F»
.686P
.model flat
include ntstatus.inc
include ntddk.inc
includelib hal.lib
extern _imp__KeStallExecutionProcessor@4:dword
.code
DriverEntry proc pDriverObject:PDRIVER_OBJECT, pusRegistryPath:PUNICODE_STRING
    cli
    in al,61h   ;получаем текущий статус порта B
    or al,00000011b ;включаем системный динамик и таймер
    out 61h,al  ;заменяем байт
    mov al,0B6h     ;установка канала 2 микросхемы 8253 в правильный
    out 43h, al     ;рабочий режим для приема делителя
        mov esi,offset melody; данные
    mov ecx,size_melody  ; счетчик
    mov dx,42h      ;используем порт 42h для передачи делителя
a0: push ecx        ;запоминаем содержимое edx и ecx, так как функция
    push edx        ;KeStallExecutionProcessor их обязательно изменит
    mov ecx,800; 50 мкСек * 800 = 40 мСек
a1: push ecx
    push 50; максимальное количество мкСек для функции KeStallExecutionProcessor
    call _imp__KeStallExecutionProcessor@4
    pop ecx
    loop a1
    pop edx         ;восстанавливаем содержимое edx и ecx
    pop ecx         ;команда outs выводит данные в порт ввода/вывода
    outsb       ;номер, которого загружен в регистр DX, 
    loop a0     ;из ячейки памяти по адресу DS:ESI
    in al,61h   ;получаем текущий статус порта B
    and al,11111100b ;выключаем системный динамик и таймер
    out 61h,al      ;заменяем байт
    sti
    mov eax, STATUS_DEVICE_CONFIGURATION_ERROR;возвращаем код ошибки, для 
    ret     ;того, чтобы система удалила драйвер из памяти
melody  dw 2 dup(354h),2,2,2 dup(387h),2,2,2 dup(3BDh),2 dup(387h)
    dw 2 dup(3BDh),2 dup(3F5h),2 dup(432h),2,2,2 dup(472h),2,2
    dw 4 dup(4B5h),4 dup(472h),2 dup(3F5h),2,2,2 dup(432h),2,2
    dw 2 dup(472h),2 dup(432h),2 dup(472h),2 dup(4B5h),2 dup(4FDh) 
    dw 2,2,2 dup(549h),2,2,4 dup(599h),4 dup(549h),2 dup(472h)
    dw 2,2,5EEh,2,5EEh,2,4 dup(649h),4 dup(5EEh),2 dup(472h),2,2
    dw 5EEh,2,5EEh,2,4 dup(649h),4 dup(5EEh),2 dup(70Eh),2 dup(6A8h)
    dw 2 dup(649h),2 dup(5EEh),2 dup(599h),2 dup(549h),2 dup(4FDh)
    dw 2 dup(4B5h),2 dup(472h),2,2,2 dup(432h),2,2,2 dup(3F5h),2,2
    dw 2 dup(387h),2,2,8 dup(354h),2
size_melody = $ - melody

user-mode приложение может запустить драйвер следующими способами:

  1. использовать API-функции Service Control Manager’a;
  2. прописать драйвер в реестре «вручную» и загрузить его с помощью функции ZwLoadDriver. В реестре создается в минимум необходимых записей, после запуска драйвера его раздел удаляется из реестра;
  3. загрузить драйвер при помощи функции ZwSetSystemInformation;
  4. через динамический загрузчик Свена Шрайбера, прилагаемый к его книге «Недокументированные возможности Windows 2000» (сам загрузчик можно найти на WASM’е). (Ничего нового! Та же загрузка драйвера через API-функции Service Control Manager’a, единственный плюс ― ничего не нужно писать самому, всё, что от вас требуется ― это указать в коммандной строке динамическому загрузчику полный путь до вашего драйвера)

Первый вариант user-mode приложения, которое запускает драйвер scp00.sys

user-mode приложение, которое запускает scp00.sys драйвер, используя API-функции Service Control Manager’a: OpenSCManager, CreateService, StartService, CloseServiceHandle и DeleteService.
Этот способ очень прост и хорошо документирован, он подходит для постоянной установки драйверов.

Для запуска и управления драйвером необходимы следующие компоненты:

  • диспетчер управления службами (Service Control Menager ― SCM). Именно благодаря ему мы будем иметь возможность легко и просто загружать драйверы;
  • программа управления службами (Service Control Programm ― SCP). Это программа третьего кольца; она работает с диспетчером управления службами (вызывает функции, которые он предоставляет), чтобы установить, запустить драйвер и завершить его работу;
  • собственно сам драйвер;
  • для запуска драйвера его необходимо зарегистрировать. За этот процесс в системе отвечает функция CreateService. Также при работе с драйвером потребуются следующие функции: StartService, ControlService, DeleteService, CloseServiceHandle

Кроме того, для возможности работы с диспетчером управления службами необходимо получить к нему доступ при помощи функции OpenSCMenager. Функция CloseServiceHandle закрывает описатель, который возвращает функция OpenSCMenager. Все функции, которые предоставляет диспетчер управления службами, находятся в advapi32.dll.

На конечном этапе загрузки системы перед появлением диалога регистрации пользователя запускается SCM (%SystemRoot%System32Services.exe), который, просматривая раздел реестра HKEY_LOCAL_MASHINESYSTEMCurentControlSetService , создает свою внутреннюю базу данных (ServiceActive database или SCM database). Далее SCM находит в созданной базе все драйвера устройств и службы, помеченные для автоматического запуска, и загружает их.

Рассмотрим процесс запуска и управления драйвером более подробно.

Помещаем драйвер в сервисную базу. База должна быть предварительно открыта функцией OpenSCManager:

C
1
2
3
4
5
SC_HANDLE OpenSCManager(
  LPCTSTR lpMachineName,
  LPCTSTR lpDatabaseName,
  DWORD dwDesiredAccess
);

Параметры:

lpMachineName
Указатель на строку (завершающуюся нулём), содержащую имя компьютера. Этот параметр мы сразу устанавливаем в NULL, так как будем открывать канал связи с SCM только на локальном компьютере.
lpDatabaseName
Указатель на строку (завершающуюся нулём), которая содержит имя открываемой базы данных менеджера управления сервисами. Этот параметр должен быть равен SERVICES_ACTIVE_DATABASE. Если этот параметр приравнять NULL, то по умолчанию будет открыта база SERVICES_ACTIVE_DATABASE. Так как мы не собираемся открывать никакую другую базу данных SCM, кроме активной в данный момент, просто, установим этот параметр в NULL.
dwDesiredAccess
Права доступа к менеджеру управления сервисами. Сообщает SCM, что мы собственно намереваемся делать с его базой данных. Нам могут быть полезны три значения:

Значение Предназначение
SC_MANAGER_CONNECT доступ на установку канала связи с SCM. По умолчанию (то есть если просто передать в этом параметре 0), устанавливается именно это значение. Хотя в документации ничего не говорится, что конкретно мы можем делать с этим уровнем доступа. А делать можно многое: запускать драйвер, останавливать, и даже удалять сведения о нем из базы данных SCM и из реестра.
SC_MANAGER_CREATE_SERVICE доступ на занесение в базу данных SCM нового драйвера. Так как мы собираемся занести туда своего представителя, то именно это значение и используем. Можно подумать, что никаких других прав, кроме регистрации нового драйвера, этот флаг не дает. Это не так. Так как флаг SC_MANAGER_CONNECT считается установленным по умолчанию, то и соответствующие права нам тоже предоставляются. Что тоже совсем не очевидно;
SC_MANAGER_ALL_ACCESS позволяет получить максимальный доступ

Если канал связи с SCM успешно установлен, функция OpenSCManager вернет описатель (handle), предоставляющий доступ к активной базе данных SCM, который мы сохраняем в переменной hSCManager для дальнейшего использования.
Получив доступ к базе SCM, мы регистрируем в ней свой драйвер, с помощью функции CreateService. Функция CreateService создает объект службы и добавляет его в указанную базу данных диспетчера управления службами.

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
SC_HANDLE CreateService(
  SC_HANDLE hSCManager,
  LPCTSTR lpServiceName,
  LPCTSTR lpDisplayName,
  DWORD dwDesiredAccess,
  DWORD dwServiceType,
  DWORD dwStartType,
  DWORD dwErrorControl,
  LPCTSTR lpBinaryPathName,
  LPCTSTR lpLoadOrderGroup,
  LPDWORD lpdwTagId,
  LPCTSTR lpDependencies,
  LPCTSTR lpServiceStartName,
  LPCTSTR lpPassword
);

Параметры:
hSCManager
Дескриптор базы данных диспетчера управления службой. Этот дескриптор возвращается функцией OpenSCManager и должен иметь право доступа SC_MANAGER_CREATE_SERVICE. Определяет в какую именно базу мы добавляем сведения о новом драйвере.
lpServiceName
Указатель на строку с завершающим нулем, которая задает устанавливаемое имя службы. Максимальная длина строки ― 256 символов. База данных диспетчера управления службами сохраняет регистр символов, но при сравнении имени службы ― всегда без учета регистра. Прямой слэш (/) и обратный слэш () ― неприменяемые символы в имени службы.
lpDisplayName
Указатель на строку с завершающим нулем, которая имеет в своем составе отображаемое имя, которое используется пользовательскими программами интерфейса, чтобы идентифицировать службу. Эта строка имеет максимальную длину 256 символов. Имя сохраняется с учетом регистра в диспетчере управления службами. Сравнения отображаемого имени всегда не чувствительны к регистру.
dwDesiredAccess
Доступ к службе. Перед предоставлением требуемого доступа, система проверяет маркер доступа вызывающего процесса.

Тип Предназначение
SERVICE_ALL_ACCESS позволяет получить максимальный доступ
SERVICE_START доступ на запуск драйвера вызовом функции StartService
SERVICE_STOP доступ на останов драйвера вызовом функции ControlService с параметром SERVICE_CONTROL_STOP
DELETE доступ на удаление сведений о драйвере из базы данных SCM вызовом функции DeleteService

Нам потребуется выполнить всего два действия: запустить драйвер и удалить сведения о нем из базы данных SCM, а следовательно, и из реестра. Поэтому в этом параметре мы передаем комбинацию флагов SERVICE_START и DELETE. Останавливать запущенный драйвер нам не потребуется, так как его инициализация завершится ошибкой.
dwServiceType
Типы службы. Для драйвера может быть только SERVICE_KERNEL_DRIVER (Соответствует параметру Type в реестре)
dwStartType
Варианты запуска службы. Этот параметр может быть SERVICE_DEMAND_START Служба, запускается диспетчером управления службами, когда процесс вызывает функцию StartService. (Соответствует параметру Start в реестре)
dwErrorControl
Серьезность ошибки и предпринимаемое действие, если эта служба не в состоянии запуститься. Этот параметр может быть одним из следующих значений.

Значение Предназначение
SERVICE_ERROR_IGNORE Программа запуска регистрирует ошибку, но продолжает операцию запуска.
SERVICE_ERROR_NORMAL Программа запуска регистрирует ошибку и показывает всплывающее окно сообщения, но продолжает операцию запуска.
SERVICE_ERROR_SEVERE Программа запуска регистрирует ошибку. Если запускается последняя, заведомо без ошибок конфигурация, операция запуска продолжается. Иначе, система перезапускается с последней, заведомо без ошибок конфигурацией.
SERVICE_ERROR_CRITICAL Если возможно, программа запуска регистрирует ошибку . Если запускается последняя, заведомо без ошибок конфигурация, операция запуска завершается ошибкой. Иначе, система перезапускается с последней из известных конфигураций без ошибок.

lpBinaryPathName
Указатель на строку с завершающим нулем, которая имеет в своем составе полный путь доступа к двоичному файлу службы. Если путь имеет в своем составе пробел, он должен быть заключен в кавычки. Соответствует параметру ImagePath в реестре
lpLoadOrderGroup
Указатель на строку с завершающим нулем, именующую группу очередности загрузки, членом которой является эта служба. Задайте значение NULL или пустую строку, если служба не принадлежит группе. Программа запуска использует очередность загрузки групп, чтобы загрузить группы служб в указанном порядке по отношению к другим группам. Список очередности загрузки групп содержатся в следующем значении реестра:
HKEY_LOCAL_MACHINESystemCurrentControlSetContro lServiceGroupOrder
lpdwTagId
Указатель на переменную, которая получает значение признака, являющееся уникальным в группе, заданной в параметре lpLoadOrderGroup. Задайте значение ПУСТО (NULL), если Вы не изменяете существующий признак. Вы можете использовать признак для того, чтобы упорядочить запуск службы в пределах очередности загрузки группы, определяя вектор очередности признака в следующем значении реестра:
HKEY_LOCAL_MACHINESystemCurrentControlSetContro lGroupOrderList
Признаки вычисляются только для служб драйвера, которые имеют типы пуска SERVICE_BOOT_START или SERVICE_SYSTEM_START.
lpDependencies
Указатель на массив имен служб, разделенных нулем, с двойным символом нуля в конце или очередности загрузки групп, которую система должна запустить перед этой службой. Задайте значение NULL или пустую строку, если служба не имеет никаких зависимостей. Зависимость от группы означает, что эта служба может запуститься тогда, если по крайней мере один член группы запущен после попытки запустить все члены группы. Вы должны ставить в начале имен группы SC_GROUP_IDENTIFIER так, чтобы они могли отличаться от имени службы, потому что службы и группы служб совместно используют то же самое пространство имен. Если драйвер не зависит от других драйверов, то в этом параметре можно указать NULL или указатель на пустую строку.
lpServiceStartName
Указатель на строку с завершающим нулем, которая задает имя учетной записи, с правами которой будет запущен драйвер. Если тип службы SERVICE_KERNEL_DRIVER, то этот параметр должен содержать имя объекта драйвера, которое используется системой для загрузки. Если используется имя объекта «драйвер» созданное подсистемой ввода-вывода, то этот параметр устанавливается равным NULL.
lpPassword
Указатель на строку с завершающим нулем, которая имеет в своем составе пароль к имени учетной записи, заданному параметром lpServiceStartName. Для служб драйвера пароли игнорируются, поэтому NULL
Вызовом функции GetFullPathName, мы формируем строку с полным путем к файлу драйвера, состоящую из текущего каталога и имени файла драйвера, и передаем ее функции CreateService. CreateService регистрирует в базе данных SCM новый драйвер, и заполняет соответствующий подраздел реестра.
Запуск драйвера осуществляется функцией StartService

C++
1
2
3
4
5
BOOL StartService(
  SC_HANDLE hService,
  DWORD dwNumServiceArgs,
  LPCTSTR* lpServiceArgVectors
);

Параметры:
hService
Дескриптор службы. Этот дескриптор возвращается функцией OpenService или CreateService, и он должен иметь право доступа SERVICE_START.
dwNumServiceArgs
Число строк в массиве lpServiceArgVectors. Если lpServiceArgVectors имеет значение NULL, этот параметр может быть нулем. Для драйверов это всегда так.
lpServiceArgVectors
Указатель на массив указателей на строки с завершающим нулем, который передается службе как параметры. Службы драйвера не получают эти параметры, поэтому мы устанавливаем этот параметр в NULL.
Функция StartService заставляет систему произвести действия, очень сильно напоминающие загрузку обыкновенной DLL. Образ файла драйвера проецируется на системное адресное пространство. При этом, возможности управлять адресом загрузки нет никакой. Да это и не нужно. Предопределенный адрес загрузки (preferred base address) у всех наших драйверов будет равен 10000h, что значительно ниже начала системного диапазона адресов. Пытаться установить его в какое-то другое значение не имеет смысла, так как система, все равно, будет загружать драйвер по случайному (для нас) адресу. Поскольку фактический адрес загрузки не совпадает с предопределенным, система производит настройку адресов пользуясь таблицей перемещений (relocation table), находящейся в секции .reloc файла драйвера. Затем производится связывание (fix-up) импорта. Кстати, импорт в файле драйвера раскинут в секции INIT и .idata. В .idata находится таблица адресов импорта (import address table, IAT). В ней содержатся адреса функций во внешних модулях. Она нужна драйверу постоянно. А в секции INIT содержится остальная часть импорта, необходимая только на этапе загрузки (имена внешних модулей и имена импортируемых функций), после которой, память занимаемая этой секцией освобождается. Когда образ драйвера подготовлен, управление передается на точку входа (entry point), которая находится в процедуре DriverEntry. Принципиальная разница, не считая уровня привилегий, в том, что код процедуры DriverEntry всегда выполняется одним из потоков процесса System, и, естественно, в адресном контексте этого процесса.
Вызов StartService синхронный. Это значит, что она не вернет управление до тех пор, пока не отработает процедура DriverEntry в драйвере. Если инициализация драйвера прошла успешно, DriverEntry должна вернуть STATUS_SUCCESS, а функция StartService вернет значение отличное от нуля. И мы вновь окажемся в контексте потока вызвавшего StartService, то есть в контексте нашей SCP.
Вызов StartService может завершиться неудачей, если база данных SCM заблокирована. Последующий вызов функции GetLastError вернет ERROR_SERVICE_DATABASE_LOCKED. Как написано в документации, в этом случае, следует подождать несколько секунд, и повторить попытку, но мы этого делать не будем, так как такая ситуация крайне маловероятна. И вообще, в данном случае, нас не интересует возвращаемое функцией StartService значение, так как драйвер уже проиграл свою мелодию и вернул код ошибки. То есть мы заранее знаем, что вызов StartService даст ошибку.

Осталось привести систему в исходное состояние. Вызовом функции DeleteService мы удаляем сведения о драйвере из базы данных SCM и из реестра. Странно, но передавать описатель самой базы данных SCM в функцию DeleteService не нужно.
Функция DeleteService отмечает указанную службу для удаления из базы данных диспетчера управления службами.

C++
1
2
3
BOOL DeleteService(
  SC_HANDLE hService
);

Параметр:
hService
Дескриптор службы. Этот дескриптор возвращается функцией OpenService или CreateService и он должен иметь право доступа DELETE.
На самом деле, функция DeleteService ничего ниоткуда не удаляет. Она только сообщает системе, что это можно сделать, когда наступит благоприятный момент. А он наступит тогда, когда все описатели службы будут закрыты. Так как мы все еще держим описатель hService открытым, то удаления не происходит. Если попытаться вызвать DeleteService повторно, то он завершится неудачей, а последующий вызов функции GetLastError вернет ERROR_SERVICE_MARKED_FOR_DELETE.

Вызовом функции CloseServiceHandle мы закрываем описатель диспетчера управления службами hService.

C++
1
2
3
BOOL CloseServiceHandle( 
  SC_HANDLE hSCObject 
);

Параметр:
hSCObject
Дескриптор объекта диспетчера управления службами или объекта службы, который закрывается. Дескрипторы объектов менеджера управления службами возвращаются функцией OpenSCManager, а дескрипторы объектов служб возвращаются или функцией OpenService или функцией CreateService.
Поскольку больше открытых описателей службы нет, то именно в этот момент система приводит базу данных SCM в исходное состояние. Второй вызов CloseServiceHandle закрывает описатель самого SCM.

Assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
; masm windows gui #
.686P
.model flat
include windows.inc
 
includelib kernel32.lib
includelib user32.lib
includelib advapi32.lib
 
extern _imp__ExitProcess@4:dword 
extern _imp__GetFullPathNameA@16:dword
extern _imp__MessageBoxA@16:dword
extern _imp__CloseServiceHandle@4:dword
extern _imp__DeleteService@4:dword
extern _imp__StartServiceA@12:dword
extern _imp__CreateServiceA@52:dword
extern _imp__OpenSCManagerA@12:dword
.code
 
start proc
 
local hSCManager:HANDLE
local acDriverPath[MAX_PATH]:CHAR
 
        xchg eax,ebx
    xor esi,esi;тип ошибки равен 0
    mov edi,offset scp00_sys_name
    ; Open a handle to the SC Manager database
    push SC_MANAGER_CREATE_SERVICE;определяем нужный тип доступа
    push ebx;адрес имени базы данных сервисов. Для открытия базы 
;по умолчанию равно NULL 
    push ebx;имя (адрес имени) рабочей станции в сети, на которой хотят 
;открыть базу. Для локального компьютера равна NULL 
    call _imp__OpenSCManagerA@12;открываем базу сервисов 
    xchg eax,ecx
    jecxz err;if eax != NULL выводим сообщение об ошибке (тип ошибки равен 0)
    inc esi;тип ошибки равен 1
    mov hSCManager,ecx;при успешном выполнении возвращает дескриптор базы данных
    push eax
    push esp
    lea eax,acDriverPath
    push eax
    push MAX_PATH
    push edi;offset scp1_sys_name
    call _imp__GetFullPathNameA@16
        pop eax
    mov [edi+5],ebx
    ; Register driver in SCM active database
    push ebx;пароль учетной записи. NULL нет пароля
    push ebx;имя учетной записи, с которой должна запускаться служба. 
;NULL предполагает, что служба запускается под именем LocalSystem
    push ebx;сервисы и группы сервисов от которых зависит наш сервис NULL 
    push ebx;NULL 
    push ebx;порядок загрузки групп служб. В данном случае порядок не важен (NULL) 
    lea ecx,acDriverPath 
    push ecx;строка содержащая имя и полный путь к программе-сервису
    push ebx;SERVICE_ERROR_IGNORE=0 уровень реакции на ошибку
    push SERVICE_DEMAND_START;тип старта службы (здесь "запуск по требовнию")
    push SERVICE_KERNEL_DRIVER;тип сервиса "драйвер режима ядра"
    push SERVICE_START + DELETE;возможный тип доступа к сервису
    push edi;"scp00" параметр определяет отображаемое имя.
    push edi;"scp00" адрес строки, содержащей имя одной из логических служб, 
;по которому в дальнейшем будет возможно обращение к этой службе.
    push hSCManager;дескриптор сервисной базы данных
    call _imp__CreateServiceA@52;помещаем сервис в сервисную базу
    test eax,eax;if eax == NULL выводим сообщение об ошибке=1
    jne short a1
err:    push MB_ICONSTOP        
    push ebx;NULL
    push handle[esi*4]
    push ebx;NULL
    call _imp__MessageBoxA@16;выводим тип ошибки
    jmp short a2;выходим из программы
a1: push eax;hService для CloseServiceHandle
    push eax;hService для DeleteService
    push ebx;параметры передаваемые в службу, обычно NULL
    push ebx;параметры передаваемые в службу, обычно 0 
    push eax;hService дескриптор возвращенный функцией CreateService
    call _imp__StartServiceA@12;программный запуск зарегистрированного сервиса
    ; Here driver scp00.sys plays melody
    ;and reports error to be removed from memory
    ;Remove driver from SCM database
    call _imp__DeleteService@4;удаляем сервис
    call _imp__CloseServiceHandle@4;закрываем базу сервисов
    push hSCManager
    call _imp__CloseServiceHandle@4;закрываем базу сервисов
a2: push ebx;0
    call _imp__ExitProcess@4
 
start endp
scp00_sys_name db "scp00.sys",0
handle dd can_t_connect,can_t_register
can_t_connect db "Can't connect to Service Control Manager.",0
can_t_register db "Can't register driver.",0
end start

Второй вариант user-mode приложения, которое запускает драйвер scp00.sys
Предварительно прописываем драйвер scp00.sys в реестре «вручную» (используем функции RegOpenKey, RegCreateKey, RegSetValue) и загружаем драйвер scp00.sys с помощью функции ZwLoadDriver. В реестре создается минимум необходимых записей, запускаем драйвер, выгружаем драйвер функцией ZwUnloadDriver и удаляем его раздел из реестра (используем функции RegCloseKey, SHDeleteKey).
Этот способ позволяет запускать драйвер быстро и незаметно и подходит для маленьких программ не требующих установки, но требующих запуска своего драйвера.

Assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
; masm windows gui #
.686P
.model flat
include windows.inc
 
includelib kernel32.lib
includelib advapi32.lib
includelib ntdll.lib
includelib shlwapi.lib
 
extern _imp__ExitProcess@4:dword 
extern _imp__GetFullPathNameA@16:dword
extern _imp__RegOpenKeyA@12:dword
extern _imp__RegCreateKeyA@12:dword
extern _imp__RegSetValueExA@24:dword
extern _imp__RegCloseKey@4:dword
extern _imp__SHDeleteKeyA@8:dword
extern _imp__ZwLoadDriver@4:dword
extern _imp__ZwUnloadDriver@4:dword
;--------macros-------------------
du  macro string
    irpc c,<string>
    if '&c'gt 127
    db ('&c'- 0B0h),4
    else
    dw '&c'
    endif
    endm
    dw 0
    endm
.const
align 4
us: du <registrymachineSYSTEMCurrentControlSetServicesscp00>
len_us = $-us
align 4
cusDevice dw (len_us - 2)
          dw (len_us)
      dd us
.code
 
start proc
local Key2:HKEY
local Key:HKEY
local acDriverPath[MAX_PATH]:CHAR
 
        xchg eax,ebx
    mov esi,offset scp00_sys_name
    lea edi,acDriverPath
    mov eax,'??'
    stosd;путь к драйверу должен начаться с '??'
    push eax;резервирую место в стеке
    push esp;указатель на пустое место в стеке
    push edi
    sub edi,4
    push MAX_PATH
    push esi;offset scp1_sys_name
    call _imp__GetFullPathNameA@16
;GetFullPathName(scp00_sys_name, MAX_PATH, PChar(dword(@Image) + 4), Pth);
    add eax,4
    push eax;длина полного пути для RegSetValueEx(Key2,"ImagePath",0,
;REG_SZ,&acDriverPath,lenth(acDriverPath))
    lea eax,Key
    push eax
    push offset aSystem
    push HKEY_LOCAL_MACHINE
    call _imp__RegOpenKeyA@12
    mov [esi+5],ebx; из "scp00.sys" делаем "scp00"
    lea eax,Key2
    push eax
    push esi
    push Key
    call _imp__RegCreateKeyA@12
    push edi;&acDriverPath
    push REG_SZ
    push ebx;0
    push offset aImagePath
    push Key2
    call _imp__RegSetValueExA@24;RegSetValueEx(Key2,"ImagePath",0,
;REG_SZ,&acDriverPath,lenth(acDriverPath))
    mov eax,esp;указатель на пустое место в стеке
        push sizeof(dword)
    mov dword ptr [eax],1;в пустое место в стеке поместим переменную dType=1
    push eax;&dType
    push REG_DWORD;тип переменной dType
    push ebx;0
    push offset aType;название переменной dType
    push Key2
    call _imp__RegSetValueExA@24
;RegSetValueEx(Key2,"Type",0,REG_DWORD,&dType)
    push Key2
    call _imp__RegCloseKey@4
    mov [esp],offset cusDevice
    call _imp__ZwLoadDriver@4; выравниваем стек после GetFullPathNameA
    push offset cusDevice
    call _imp__ZwUnloadDriver@4
    push esi 
    push Key
    call _imp__SHDeleteKeyA@8;удаляем непустую строку из реестра 
    push Key
    call _imp__RegCloseKey@4
    push ebx;0
    call _imp__ExitProcess@4
start endp
aSystem db 'SYSTEMCurrentControlSetServices',0
scp00_sys_name db "scp00.sys",0
aType   db "Type",0
aImagePath db "ImagePath",0
end start

Третий вариант user-mode приложения, которое запускает драйвер scp00.sys

Собственно это модифицированный второй вариант. Произошла замена функций из advapi32.dll и других динамических библиотек на Zw-функции из ntdll.dll, а далее замена Zw-функций на вызов 2Eh прерывания, поэтому за исключением функции RtlGetFullPathName_U наша программа не использует импорт

DLL исходная функция функция из ntdll.dll номер функции в SDT для Windows XP Назначение Zw-функций
kernel32 GetFullPathName RtlGetFullPathName_U  
advapi32 RegOpenKey ZwOpenKey 119 открывает доступ к существующему подразделу реестра или создает новый. Возвращает дескриптор открытого объекта.
  RegCreateKey ZwCreateKey 41 открывает доступ к существующему подразделу реестра или создает новый. Возвращает дескриптор открытого объекта.
  RegSetValueEx ZwSetValueKey 247 создает или изменяет значение параметра в открытом подразделе реестра. Для возможности применения этой функции, дескриптор подраздела при открытии должен быть получен с применением маски DesiredAccess, содержащей флаг KEY_SET_VALUE
  RegCloseKey ZwClose 25 закрывает дескриптор открытого ранее подраздела реестра, фиксирует произведенные изменения на жестком диске
shlwapi SHDeleteKey ZwDeleteKey 63 удаляет открытый подраздел из реестра
ntdll ZwLoadDriver ZwLoadDriver 97
  ZwUnloadDriver ZwUnloadDriver 262

Таблица #7:

вызовом функции ZwOpenKey открываем существующий раздел, запрашивая необходимые в данный момент права доступа
вызовом функции ZwCreateKey, создаем новый подраздел «scp00» в ветке «HKEY_LOCAL_MACHINESYSTEMCurrentControlSetServi ces»

После успешного вызова ZwCreateKey по значению переменной dwDisposition можно определить, был ли создан новый подраздел (REG_CREATED_NEW_KEY) или такой подраздел уже существовал в реестре (REG_OPENED_EXISTING_KEY) и поэтому был открыт

При помощи ZwSetValueKey создадим в нашем подразделе:

  1. unicode-строковый параметр с именем «ImagePath». Строка содержит путь к исполняемому файлу драйвера или службы. В отличие от служб, для драйверов не обязательно указывать значение этого параметра, но тогда файл драйвера должен находиться в каталоге «%SystemRoot%System32Drivers». Полный путь к нашему драйверу, начинающийся с символов «??» содержится в edi. Длину полного пути нам вернула функция RtlGetFullPathName_U.
  2. также создаем параметр с именем «Type», определяющим тип службы, типом параметра REG_DWORD и значением SERVICE_KERNEL_DRIVER (=1).
Тип параметра Описание
REG_NONE Нетипизированный параметр
REG_SZ Unicode-строка фиксированной длины с нулём в конце
REG_EXPAND_SZ Unicode-строка переменной длины с нулём в конце; может включать переменные окружения
REG_BINARY Двоичные данные произвольной длины
REG_DWORD 32-битное число
REG_DWORD_LITTLE_ENDIAN 32-битное число, в котором первым является младший байт, эквивалентно REG_DWORD
REG_DWORD_BIG_ENDIAN 32-битное число, в котором первым является старший байт
REG_LINK Символьная строка в формате Unicode
REG_MULTI_SZ Массив Unicode-строк с завершающим нулём
REG_RESOURCE_LIST Описание аппаратного ресурса
REG_FULL_RESOURCE_DESCRIPTOR Описание аппаратного ресурса
REG_RESOURCE_REQUIREMENTS_LIST Список требований к ресурсам

Таблица #8: Типы параметров реестра

После проигрыша мелодии удаляем подраздел реестра вызовом ZwDeleteKey

при использовании sysenter вместо Zw-функций размер файла 789 байт, при использовании call ds:[7FFE0300h] ― размер файла 762 байта, при использовании int 2Eh ― размер 699 байт

Assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
.686P
.model tiny
include windows.inc
;for WinXP - 699 bytes
exebase     equ 400000h
OBJ_CASE_INSENSITIVE    equ 40h
SERVICE_KERNEL_DRIVER   equ 01h
du  macro string
    irpc c,<string>
    if '&c'gt 127
    db ('&c'- 0B0h),4
    else
    dw '&c'
    endif
    endm
    dw 0
    endm
 
UNICODE_STRING STRUCT
    _Length     WORD    ?; len of string in bytes (not chars)
    MaximumLength   WORD    ?; len of Buffer in bytes (not chars)
    Buffer      PWSTR   ?; pointer to string
UNICODE_STRING ENDS
 
PUNICODE_STRING typedef PTR UNICODE_STRING
 
OBJECT_ATTRIBUTES STRUCT        ; sizeof = 18h
    _Length             DWORD       ? ; original name Length
    RootDirectory           HANDLE      ?
    ObjectName          PUNICODE_STRING ?
    Attributes          DWORD       ?
    SecurityDescriptor      PVOID       ? ; Points to type SECURITY_DESCRIPTOR
    SecurityQualityOfService    PVOID       ? ; Points to type SECURITY_QUALITY_OF_SERVICE
OBJECT_ATTRIBUTES ENDS
 
.code
 
main:
include capito.asm
align 4
us: du <registrymachineSYSTEMCurrentControlSetServicesscp00.sys>
len_us = $-us
align 4
cusDevice dw len_us - 10
          dw len_us - 8
      dd us+exebase
pusSystem dw len_us-58; du <SYSTEMCurrentControlSetServices>
      dw len_us-56
      dd us+exebase+36
pusScp00  dw len_us-114; du <scp00>
      dw len_us-112
      dd us+exebase+104
align 4
aImagePath: du <ImagePath>
a2:
align 4
pusImagePath    dw a2-aImagePath-2
        dw a2-aImagePath
        dd aImagePath+exebase
align 4
aType:  du <Type>
a3:
align 4
pusType     dw a3-aType-2
        dw a3-aType
        dd aType+exebase 
start: 
Key2        equ dword ptr [ebp-4]
Key     equ dword ptr [ebp-8]
dwDisposition   equ dword ptr [ebp-12]
acDriverPath    equ dword ptr [ebp-MAX_PATH-12]
oa      equ dword ptr [ebp-MAX_PATH-12-sizeof(OBJECT_ATTRIBUTES)]
    enter MAX_PATH+12+sizeof(OBJECT_ATTRIBUTES),0
        xchg eax,ebx
    lea edi,acDriverPath
    mov eax,3F005Ch;unicode '?'
    stosd
    ror eax,10h
        stosd
    invoke RtlGetFullPathName_U,offset us+exebase+104,MAX_PATH,edi,esp,eax
    sub edi,8;путь к драйверу должен начаться с '??'
    add eax,10
    push eax;длина полного пути для ZwSetValueKey
;Для последующего вызова функции ZwOpenKey нам потребуется указатель на 
;заполненную структуру OBJECT_ATTRIBUTES
        lea ecx,oa
    assume ecx: ptr OBJECT_ATTRIBUTES 
;InitializeObjectAttributes
        mov [ecx]._Length,sizeof(OBJECT_ATTRIBUTES)
    mov [ecx].RootDirectory,ebx
    mov [ecx].ObjectName,offset pusSystem+exebase
    mov [ecx].Attributes,OBJ_CASE_INSENSITIVE
    mov [ecx].SecurityDescriptor,ebx
    mov [ecx].SecurityQualityOfService,ebx
        assume ecx: nothing
        lea eax,Key  ; Адрес Key
    push ecx;POBJECT_ATTRIBUTES  ObjectAttributes
    push KEY_READ;ACCESS_MASK  DesiredAccess
    push eax;PHANDLE  KeyHandle
    mov eax,119
    mov edx,esp
    int 2Eh;ZwOpenKey(&Key,2000000h,&oa)
    lea ecx,oa;ObjectAttributes
    mov (OBJECT_ATTRIBUTES ptr [ecx]).ObjectName,offset pusScp00+exebase
    lea eax,dwDisposition
    push eax;PULONG  Disposition  OPTIONAL
    push ebx;ULONG  CreateOptions=REG_OPTION_NON_VOLATILE
    push ebx;PUNICODE_STRING  Class  OPTIONAL
    push ebx;ULONG  TitleIndex
    push ecx;POBJECT_ATTRIBUTES  ObjectAttributes
    push KEY_READ;ACCESS_MASK  DesiredAccess
    lea eax,Key2
    push eax;PHANDLE  KeyHandle
    mov eax,41
    mov edx,esp
    int 2Eh;ZwCreateKey
    add esp,28+12
    push edi;PVOID  Data
    push REG_SZ;ULONG  Type
    push ebx;ULONG  TitleIndex  OPTIONAL
    push offset pusImagePath+exebase;PUNICODE_STRING  ValueName
    push Key2;HANDLE  KeyHandle
    mov eax,247
    mov edx,esp 
    int 2Eh;ZwSetValueKey(Key2,&pusImagePath,0,REG_SZ,edi)
    add esp,24
    mov eax,esp;указатель на пустое место в стеке
    mov dword ptr [eax],SERVICE_KERNEL_DRIVER;в пустое место в стеке поместим переменную dType=1
    push sizeof(dword);ULONG  DataSize
    push eax;PVOID  Data
    push REG_DWORD;ULONG  Type
    push ebx;ULONG  TitleIndex  OPTIONAL
    push offset pusType+exebase;PUNICODE_STRING  ValueName
    push Key2;HANDLE  KeyHandle
    mov eax,247
    mov edx,esp 
    int 2Eh;ZwSetValueKey(Key2,&pusType,0,REG_DWORD,eax,sizeof(dword))
    push Key2;Handle
    mov eax,25
    mov edx,esp 
    int 2Eh;ZwClose(Key2)
    mov edi,offset cusDevice+exebase
    push edi
        mov eax,97
    mov edx,esp 
    int 2Eh;ZwLoadDriver(&cusDevice)
    push edi
        mov eax,262
    mov edx,esp 
    int 2Eh;ZwUnloadDriver(&cusDevice)
    push Key2;Handle
    mov eax,63
    mov edx,esp 
    int 2Eh;ZwDeleteKey(Key2)
    push Key;Handle
    mov eax,25
    mov edx,esp 
    int 2Eh;ZwClose(Key)
    leave
    retn;ExitProcess
import:
dd 0,0,0,ntdll_dll,ntdll_table
dd 0,0
ntdll_table:
RtlGetFullPathName_U    dd _RtlGetFullPathName_U
            dw 0
_RtlGetFullPathName_U   db 0,0,"RtlGetFullPathName_U",0
ntdll_dll       db "ntdll"
end_import:
end main

Четвертый вариант user-mode приложения, которое запускает драйвер scp00.sys

Используя функцию ZwSetSystemInformation с параметром SystemLoadAndCalllmage можно за одно действие загрузить и запустить драйвер в системе Windows NT. При этом не требуется никакой регистрации драйвера. Однако возникнет «побочный эффект» ― после такой загрузки драйвер невозможно выгрузить «Обычный» (написанный по всем правилам драйвер) будет находиться в памяти до следующей перезагрузки компьютера. Но наш-то драйвер после выполнения возвратит системе STATUS_DEVICE_CONFIGURATION_ERROR (код фиктивной ошибки) и благополучно будет «самовыпилен» (удален системой из памяти) .

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

8-байтовая локальная переменная GregsImage представляет из себя структуру UNICODE_STRING используемую для работы с UNICODE-строками в нулевом кольце

Код

UNICODE_STRING STRUCT
	_Length		WORD	?; len of string in bytes (not chars)
	MaximumLength	WORD	?; len of Buffer in bytes (not chars)
	Buffer		PWSTR	?; pointer to string
UNICODE_STRING ENDS

Поле _Length содержит текущую длину строки в байтах, не считая двух завершающих нулей. Поле MaximumLength содержит максимальный размер буфера в байтах, в котором эта строка присутствует (MaximumLength = _Length + 2). Поле Buffer содержит указатель на буфер, где находится UNICODE-строка.

Assembler
1
2
3
4
5
    call _imp__RtlGetFullPathName_U@16
    imul eax,10001h
    add eax,2
        mov edi,esp;edi=&GregsImage
    mov [edi],eax

Для вычисления длины строки используем функцию RtlGetFullPathName умножив возвращенное значение на 10001h и добавив 2 мы получили значения для GregsImage._Length и GregsImage.MaximumLength

Assembler
1
2
        lea edi,daPath
    mov GregsImage.Buffer,edi

Помещаем в поле GregsImage.Buffer указатель на UNICODE-строку.

Assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
; masm windows gui #
.686P
.model flat
include windows.inc
 
includelib kernel32.lib
includelib ntdll.lib
 
extern _imp__ExitProcess@4:dword 
extern _imp__RtlGetFullPathName_U@16:dword
extern _imp__ZwSetSystemInformation@12:dword
;----------const------------------
SystemLoadAndCallImage equ 26h
;--------macros-------------------
du  macro string
    irpc c,<string>
    if '&c'gt 127
    db ('&c'- 0B0h),4
    else
    dw '&c'
    endif
    endm
    dw 0
    endm
;--------struct-------------------
UNICODE_STRING STRUCT
    _Length     WORD    ?; len of string in bytes (not chars)
    MaximumLength   WORD    ?; len of Buffer in bytes (not chars)
    Buffer      PWSTR   ?; pointer to string
UNICODE_STRING ENDS
;---------------------------------
.code
start proc
local daPath[MAX_PATH]:CHAR
local GregsImage:UNICODE_STRING
 
        lea edi,daPath
    mov GregsImage.Buffer,edi
    mov eax,3F005Ch;eax='',0,'?',0
    stosd;путь к драйверу должен начаться с '??'
    ror eax,16;eax='?',0,'',0
    stosd
    push eax;резервирую место в стеке
    push esp;указатель на пустое место в стеке
    push edi
    push MAX_PATH
    push offset scp00_sys_name
    call _imp__RtlGetFullPathName_U@16
;GetFullPathName(scp00_sys_name, MAX_PATH, PChar(dword(@Image) + 4), Pth);
    pop ecx
    add eax,8;учтем, что путь стал длиннее на '??'
    imul eax,10001h
    add eax,2
    mov edi,esp;edi=&GregsImage
    mov [edi],eax
    push sizeof UNICODE_STRING;8
    push edi;&GregsImage
    push SystemLoadAndCallImage;=26h
    call _imp__ZwSetSystemInformation@12
    push 0
    call _imp__ExitProcess@4
start endp
scp00_sys_name: du <scp00.sys>
end start

__________________________________________________ _______________

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

Программирование и устройство таймера:

  • Лю Ю-Чжен, Гибсон Г. Микропроцессоры семейства 8086/8088. Архитектура, программирование и проектирование микрокомпьютерных систем: Пер. с англ. ― М.: Радио и связь, 1987. ― 512 с.; ил.
    Глава 9. Интерфейсы ввода-вывода. Программируемый интервальный таймер.
  • Юров В.И. Assembler. Учебник для вузов 2-е изд. ― СПб.: Питер, 2007. ― 637 с.: ил.
    Глава 7. Команды обмена данными. Ввод из порта и вывод в порт.

Загрузка драйвера с помощью функций Service Control Manager’a и загрузка с помощью функции ZwLoadDriver:

  • KmdTutRu.chm «Драйверы режима ядра» by Four-F главы:
    1. Установка канала связи с SCM
    2. Регистрация драйвера
    3. Запуск драйвера

Загрузка драйвера через ZwSetSystemInformation

  • Драйвер m1gB0t by Greg Hoglund, 2004, исходный текст m1gloader
  • Грег Хогланд, Гари Мак-Гроу. Взлом программного обеспечения: анализ и использование кода.: Пер. с англ. ― М.: Издательский дом «Вильямс», 2005. ― 400 с.: ил. ― Парал. тит. англ.
    Глава 8. Наборы средств для взлома. Текст программы для загрузки драйвера c:_root_.sys

__________________________________________________ _______________
© Mikl___ 2011



23



Понравилась статья? Поделить с друзьями:
  • Как пишется яндекс латинскими буквами
  • Как пишут биографию образец
  • Как пишут докторскую диссертацию
  • Как пишется янгантау правильно
  • Как пишут бизнес план