The simplest WDM driver
This article describes the process of writing a simple driver that displays scan codes of pressed keys.
This article also describes the process of setting up a workplace for writing drivers.
If you are interested, please, under cat.
Required Software:
I use two virtual machines, write drivers on one, and run on the other. If you also decide to do this, then for the machine on which you will run the drivers, 4 GB hard drive and 256 MB RAM are enough.
Installation is extremely simple. The only thing you need to pay attention to is the dialogue in which you are asked to select the components that will be installed. I strongly recommend that all documentation and examples be noted.
Installing Microsoft® Visual Studio 2005 is no more difficult than installing DDK. If you will use it only for writing drivers, then when the installer asks which components should be installed, select only Visual C ++.
Next, you can install Visual Assist X. Using this program (add-on), you can easily customize the tips for convenient writing drivers.
After installing Visual Assist X in Visual Studio 2005, the new VAssistX menu appears. Further in this menu
In order for Visual Studio to compile drivers, you need to install DDKWizard. It can be downloaded from ddkwizard.assarbad.net . Also download the ddkbuild.cmd script from this site.
After the wizard is installed, you must perform the following steps:
Everything, the machine on which we will run the drivers, is ready.
Now configure the machine on which we will run the written drivers.
We will need the following programs:
Everything, the car is ready to run drivers.
Task: write a driver that will output scan codes of pressed keys and their combinations to debug.
A driver is a set of functions that are called by the operating system when certain events occur from a device or user mode.
There are many types of drivers, some of which are listed below:
Class drivers are drivers that Microsoft writes. These are generic drivers for a certain class of (really!) Devices.
Minidrivers are drivers that use a class driver to control a device.
Functional drivers are drivers that work independently and determine everything that is connected with the device.
Filtering drivers are drivers that are used to monitor or change the logic of another driver by changing the data that goes to it.
It is not necessary to define all possible functions in your driver, but it must contain
So, in order to display scan codes ( what is it?) in debug, we will use the filter driver.
There are two types of filtering drivers:
The type of driver you have depends on where the driver is located in the device driver stack. If your driver is above the functional driver, then it is called the upper filtering driver, if lower, then the lower filtering driver.
All requests go through the upper filtering drivers, which means that they can change and / or filter the information that goes to the functional driver, and then, possibly, to the device.
An example of using the upper filtering drivers:
A filter hook driver that sets its hook function for the IpFilterDirver system driver to track and filter traffic. Such drivers are used in firewalls.
Less requests go through the lower filtering drivers because most requests execute and complete a functional driver.
In the driver that we will write, there are several "problem" sections. Using our assembly inserts is sufficient for our driver:
or
The prefix
First you need to include the header files "ntddk.h", "ntddkbd.h"
It is also necessary to describe the structure
An object
For our driver to work, we need a variable in which the number of incomplete requests will be stored.
Let's start with the function, which is the main entry point of our driver.
First you need to declare and nullify the variables:
Next, as I wrote above, you need to initialize function pointers
The function
The function
A function
Next, we call our function to create and install our device on the device stack:
I will describe this function below.
We return
We pass to function
This function creates a device object, configures it, and includes it on the device stack over
Declare variables:
Call
Let's analyze the parameters in more detail:
Next, set the device flags.
The flags that we set for our device must be equivalent to the flags of the device on top of which we are pushed onto the stack.
Next, we must perform the conversion of the name of the device that we are putting on the stack.
The function
Freeing up resources:
Next, we will analyze the function
This function will be called by the operating system when you press or release the keyboard key. We
increase the counter of incomplete requests.
Before passing the request to the next driver, we must configure the stack pointer for the driver.
where is
We pass to the
Now we will analyze the function that will be called each time upon completion
We get
The structure is
Check if the request completed successfully or not
To get the structure,
Find out the number of keys
And print each key:
This article also describes the process of setting up a workplace for writing drivers.
If you are interested, please, under cat.
Stand preparation
Installing the necessary software to write a simple driver
Required Software:
- Windows DDK (Driver Development Kit);
- VMware Workstation or Virtual Box;
- Windows XP
- Visual Studio 2005;
- DDKWizard;
- KmdManager
- DebugView;
I use two virtual machines, write drivers on one, and run on the other. If you also decide to do this, then for the machine on which you will run the drivers, 4 GB hard drive and 256 MB RAM are enough.
Workplace setup
DDK installation
Installation is extremely simple. The only thing you need to pay attention to is the dialogue in which you are asked to select the components that will be installed. I strongly recommend that all documentation and examples be noted.
Install and configure Microsoft® Visual Studio 2005
Installing Microsoft® Visual Studio 2005 is no more difficult than installing DDK. If you will use it only for writing drivers, then when the installer asks which components should be installed, select only Visual C ++.
Next, you can install Visual Assist X. Using this program (add-on), you can easily customize the tips for convenient writing drivers.
After installing Visual Assist X in Visual Studio 2005, the new VAssistX menu appears. Further in this menu
Visual Assist X Options -> Projects -> C/C++ Directories -> Platform: Custom, Show Directories for: Stable include files. Click Inson the icon or add a new directory and in the line that appears, if you enter Windows XP %WXPBASE%\inc\ddk\wxp.Install and configure DDKWizard
In order for Visual Studio to compile drivers, you need to install DDKWizard. It can be downloaded from ddkwizard.assarbad.net . Also download the ddkbuild.cmd script from this site.
After the wizard is installed, you must perform the following steps:
- Create system (recommended) or user variables with the following names and a value that matches the path to the DDK
DDK version Variable name Default path Windows XP DDK Wxpbase C: \ WINDDK \ 2600 Windows 2003 Server DDK Wnetbase C: \ WINDDK \ 3790.1830 Windows Vista / Windows 2008 Server WDK Wlhbase Windows 7 / Windows 2008 Server R2 WDK W7base
For example, if I use Windows XP DDK, then I have to create a WXPBASE variable with a value that matches the path to the DDK. Since I did not change the installation path, the value I have will be C: \ WINDDK \ 2600. - Copy the downloaded ddkbuild.cmd script, for example, to the folder with DDK. I have this C: \ WINDDK \.
- Add the path to the ddkbuild.cmd script to the end of the Path system variable.
Everything, the machine on which we will run the drivers, is ready.
Installing the necessary software to run the drivers
Now configure the machine on which we will run the written drivers.
We will need the following programs:
- DebugView ( link ) is a utility that allows you to view the debug output of both user mode and kernel mode.
- KmdManager ( link ) - a utility for dynamically loading / unloading drivers
Everything, the car is ready to run drivers.
Formulation of the problem
Task: write a driver that will output scan codes of pressed keys and their combinations to debug.
Bit of theory
A driver is a set of functions that are called by the operating system when certain events occur from a device or user mode.
There are many types of drivers, some of which are listed below:
- class drivers
- mini-drivers;
- functional drivers;
- filtering drivers.
Class drivers are drivers that Microsoft writes. These are generic drivers for a certain class of (really!) Devices.
Minidrivers are drivers that use a class driver to control a device.
Functional drivers are drivers that work independently and determine everything that is connected with the device.
Filtering drivers are drivers that are used to monitor or change the logic of another driver by changing the data that goes to it.
It is not necessary to define all possible functions in your driver, but it must contain
DriverEntryand AddDevice. IRPIs a structure that drivers use to exchange data. So, in order to display scan codes ( what is it?) in debug, we will use the filter driver.
There are two types of filtering drivers:
- top filtering drivers;
- bottom filter drivers.
The type of driver you have depends on where the driver is located in the device driver stack. If your driver is above the functional driver, then it is called the upper filtering driver, if lower, then the lower filtering driver.
Differences between Upper and Lower Filter Drivers
All requests go through the upper filtering drivers, which means that they can change and / or filter the information that goes to the functional driver, and then, possibly, to the device.
An example of using the upper filtering drivers:
A filter hook driver that sets its hook function for the IpFilterDirver system driver to track and filter traffic. Such drivers are used in firewalls.
Less requests go through the lower filtering drivers because most requests execute and complete a functional driver.
Sync issues
In the driver that we will write, there are several "problem" sections. Using our assembly inserts is sufficient for our driver:
__asm {
lock dec «переменная, которую нужно уменьшить на единицу»
}
or
__asm {
lock inc «переменная, которую нужно увеличить на единицу»
}
The prefix
lockallows you to safely execute the command following it. It blocks the rest of the processors while the command is running.Action
First you need to include the header files "ntddk.h", "ntddkbd.h"
extern "C"
{
#include "ntddk.h"
}
#include "ntddkbd.h"
It is also necessary to describe the structure
DEVICE_EXTENSIONtypedef struct _DEVICE_EXTENSION{
PDEVICE_OBJECT pLowerDO;
} DEVICE_EXTENSION, *PDEVICE_EXTENSION;
An object
pLowerDOis a device object that is below us on the stack. We need it in order to know who to send IRP packets to. For our driver to work, we need a variable in which the number of incomplete requests will be stored.
int gnRequests;
Let's start with the function, which is the main entry point of our driver.
extern "C" NTSTATUS DriverEntry(IN PDRIVER_OBJECT theDriverObject, IN PUNICODE_STRING ustrRegistryPath)
theDriverObject- the driver object, contains pointers to all the functions necessary for the operating system, which we will need to initialize. ustrRegistryPath- the name of the section in the registry where information about this driver is stored. First you need to declare and nullify the variables:
gnRequests = 0;
NTSTATUS status = {0};
Next, as I wrote above, you need to initialize function pointers
for (int i = 0; iMajorFunction[i] = DispatchThru;
}
theDriverObject->MajorFunction[IRP_MJ_READ] = DispatchRead;
theDriverObject->DriverUnload = DriverUnload;
The function
DispatchReadwill process read requests. It will be called when the keyboard key is pressed or released. The function
DriverUnloadis called when the driver is no longer needed and can be unloaded from memory, or when the user unloads the driver. In this function, “stripping” should be performed, i.e. resources that were used by the driver are freed, all pending requests are completed, etc. A function
DispatchThruis a stub function. All she does is pass the IRP packet to the next driver (the driver that is under ours in the stack, i.e. pLowerDOfrom DEVICE_EXTENSION). Next, we call our function to create and install our device on the device stack:
status = InstallFilter(theDriverObject);
I will describe this function below.
We return
statusin which, if the function InstallFiltersucceeds, the value is stored STATUS_SUCCESS. We pass to function
InstallFilter. Here is her prototype:NTSTATUS InstallFilter(IN PDRIVER_OBJECT theDO);
This function creates a device object, configures it, and includes it on the device stack over
\\Device\\KeyboardClass0Declare variables:
PDEVICE_OBJECT pKeyboardDevice;
NTSTATUS status = {0};
pKeyboardDeviceIs a device object that we must create. Call
IoCreateDeviceto create a new devicestatus = IoCreateDevice(theDO, sizeof(DEVICE_EXTENSION), NULL, FILE_DEVICE_KEYBOARD, 0, FALSE, &pKeyboardDevice);
Let's analyze the parameters in more detail:
- The first argument is the driver object that we received as the parameter to the InstallFilter function. It is passed to IoCreateDevice in order to establish a connection between our driver and the new device.
- The third parameter is the device name
- The fourth parameter is the type of device
- The fifth parameter is the flags that are usually set for mass storage devices.
- The sixth parameter describes whether it is possible to open the device's manipulators in an amount of more than one. If FALSE, you can open only one manipulator. Otherwise, you can open any number of manipulators.
- The seventh parameter is the memory in which the created device object will be saved.
Next, set the device flags.
pKeyboardDevice->Flags = pKeyboardDevice->Flags | (DO_BUFFERED_IO | DO_POWER_PAGABLE);
pKeyboardDevice->Flags = pKeyboardDevice->Flags & ~DO_DEVICE_INITIALIZING;
The flags that we set for our device must be equivalent to the flags of the device on top of which we are pushed onto the stack.
Next, we must perform the conversion of the name of the device that we are putting on the stack.
CCHAR cName[40] = "\\Device\\KeyboardClass0";
STRING strName;
UNICODE_STRING ustrDeviceName;
RtlInitAnsiString(&strName, cName);
RtlAnsiStringToUnicodeString(&ustrDeviceName, &strName, TRUE);
The function
IoAttachDevicepushes our device onto the stack. The pdx->pLowerDOobject of the next (lower) device will be stored in.IoAttachDevice(pKeyboardDevice, &ustrDeviceName, &pdx->pLowerDO);
Freeing up resources:
RtlFreeUnicodeString(&ustrDeviceName);
Next, we will analyze the function
DispatchReadwith the prototype:NTSTATUS DispatchRead(IN PDEVICE_OBJECT pDeviceObject, IN PIRP pIrp);
This function will be called by the operating system when you press or release the keyboard key. We
increase the counter of incomplete requests.
__asm {
lock inc gnRequests
}
Before passing the request to the next driver, we must configure the stack pointer for the driver.
IoCopyCurrentIrpStackLocationToNextcopies the portion of memory that belongs to the current driver to the memory area of the next driver.IoCopyCurrentIrpStackLocationToNext(theIrp);
When a request goes down the stack, it does not yet have the data we need, so we need to set a function that will be called when the request goes up the stack with the data we need.IoSetCompletionRoutine(theIrp, ReadCompletionRoutine, pDeviceObject, TRUE, TRUE, TRUE)
where is
ReadCompletionRoutineour function. We pass to the
IRPfollowing driver:return IoCallDriver(((PDEVICE_EXTENSION) pDeviceObject->DeviceExtension)->pLowerDO ,theIrp);
Now we will analyze the function that will be called each time upon completion
IRP. Prototype:NTSTATUS ReadCompletionRoutine(IN PDEVICE_OBJECT pDeviceObject, IN PIRP theIrp, IN PVOID Context);
We get
DEVICE_EXTENSION:PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)pDeviceObject->DeviceExtension;
The structure is
PKEYBOARD_INPUT_DATAused to describe the pressed key.PKEYBOARD_INPUT_DATA kidData;
Check if the request completed successfully or not
if (NT_SUCCESS(theIrp->IoStatus.Status))
To get the structure,
KEYBOARD_INPUT_DATAyou need to access the system IRPpacket buffer .kidData = (PKEYBOARD_INPUT_DATA)theIrp->AssociatedIrp.SystemBuffer;
Find out the number of keys
int n = theIrp->IoStatus.Information / sizeof(KEYBOARD_INPUT_DATA);
And print each key:
for(int i = 0; i
И не забываем уменьшать количество не обработанных запросов
__asm {
lock dec gnRequests
}
Возвращаем статус запроса
return theIrp->IoStatus.Status;
Разберем функцию завершения работы. Прототип:
VOID DriverUnload(IN PDRIVER_OBJECT theDO);
Получаем DEVICE_EXTENSION:
PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)theDO->DeviceObject->DeviceExtension;
Извлекаем устройство из стека:
IoDetachDevice(pdx->pLowerDO);
Удаляем устройство:
IoDeleteDevice(theDO->DeviceObject);
Проверяем есть незавершенные запросы или нет. Если мы выгрузим драйвер без этой проверки, при первом нажатии на клавишу после выгрузки будет БСоД.
if (gnRequests != 0)
{
KTIMER ktTimer;
LARGE_INTEGER liTimeout;
liTimeout.QuadPart = 1000000;
KeInitializeTimer(&ktTimer);
Задаем таймер и пока не завершены все запросы, крутим цикл
while(gnRequests > 0)
{
KeSetTimer(&ktTimer, liTimeout, NULL); // Устанавливаем таймер
KeWaitForSingleObject(&ktTimer, Executive, KernelMode, FALSE, NULL); // Ждем пока истечет время
}
}
Код драйвера:
extern "C"
{
#include "ntddk.h"
}
#include "ntddkbd.h"
typedef struct _DEVICE_EXTENSION{
PDEVICE_OBJECT pLowerDO;
} DEVICE_EXTENSION, *PDEVICE_EXTENSION;
int gnRequests;
NTSTATUS DispatchThru(PDEVICE_OBJECT theDeviceObject, PIRP theIrp)
{
IoSkipCurrentIrpStackLocation(theIrp);
return IoCallDriver(((PDEVICE_EXTENSION) theDeviceObject->DeviceExtension)->pLowerDO ,theIrp);
}
NTSTATUS InstallFilter(IN PDRIVER_OBJECT theDO)
{
PDEVICE_OBJECT pKeyboardDevice;
NTSTATUS status = {0};
status = IoCreateDevice(theDO, sizeof(DEVICE_EXTENSION), NULL, FILE_DEVICE_KEYBOARD, 0, FALSE, &pKeyboardDevice);
if (!NT_SUCCESS(status))
{
DbgPrint("IoCreateDevice error..");
return status;
}
pKeyboardDevice->Flags = pKeyboardDevice->Flags | (DO_BUFFERED_IO | DO_POWER_PAGABLE);
pKeyboardDevice->Flags = pKeyboardDevice->Flags & ~DO_DEVICE_INITIALIZING;
RtlZeroMemory(pKeyboardDevice->DeviceExtension, sizeof(DEVICE_EXTENSION));
PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)pKeyboardDevice->DeviceExtension;
CCHAR cName[40] = "\\Device\\KeyboardClass0";
STRING strName;
UNICODE_STRING ustrDeviceName;
RtlInitAnsiString(&strName, cName);
RtlAnsiStringToUnicodeString(&ustrDeviceName, &strName, TRUE);
IoAttachDevice(pKeyboardDevice, &ustrDeviceName, &pdx->pLowerDO);
//DbgPrint("After IoAttachDevice");
RtlFreeUnicodeString(&ustrDeviceName);
return status;
}
VOID DriverUnload(IN PDRIVER_OBJECT theDO)
{
PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)theDO->DeviceObject->DeviceExtension;
IoDetachDevice(pdx->pLowerDO);
IoDeleteDevice(theDO->DeviceObject);
if (gnRequests != 0)
{
KTIMER ktTimer;
LARGE_INTEGER liTimeout;
liTimeout.QuadPart = 1000000;
KeInitializeTimer(&ktTimer);
while(gnRequests > 0)
{
KeSetTimer(&ktTimer, liTimeout, NULL);
KeWaitForSingleObject(&ktTimer, Executive, KernelMode, FALSE, NULL);
}
}
}
NTSTATUS ReadCompletionRoutine(IN PDEVICE_OBJECT pDeviceObject, IN PIRP theIrp, IN PVOID Context)
{
PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)pDeviceObject->DeviceExtension;
PKEYBOARD_INPUT_DATA kidData;
if (NT_SUCCESS(theIrp->IoStatus.Status))
{
kidData = (PKEYBOARD_INPUT_DATA)theIrp->AssociatedIrp.SystemBuffer;
int n = theIrp->IoStatus.Information / sizeof(KEYBOARD_INPUT_DATA);
for(int i = 0; iPendingReturned)
IoMarkIrpPending(theIrp);
__asm{
lock dec gnRequests
}
return theIrp->IoStatus.Status;
}
NTSTATUS DispatchRead(IN PDEVICE_OBJECT pDeviceObject, IN PIRP theIrp)
{
__asm{
lock inc gnRequests
}
IoCopyCurrentIrpStackLocationToNext(theIrp);
IoSetCompletionRoutine(theIrp, ReadCompletionRoutine, pDeviceObject, TRUE, TRUE, TRUE);
return IoCallDriver(((PDEVICE_EXTENSION) pDeviceObject->DeviceExtension)->pLowerDO ,theIrp);
}
extern "C" NTSTATUS DriverEntry(IN PDRIVER_OBJECT theDriverObject, IN PUNICODE_STRING RegistryPath)
{
NTSTATUS status = {0};
gnRequests = 0;
for (int i = 0; iMajorFunction[i] = DispatchThru;
}
theDriverObject->MajorFunction[IRP_MJ_READ] = DispatchRead;
status = InstallFilter(theDriverObject);
theDriverObject->DriverUnload = DriverUnload;
return status;
}
MAKEFILE:
#
# DO NOT EDIT THIS FILE!!! Edit .\sources. if you want to add a new source
# file to this component. This file merely indirects to the real make file
# that is shared by all the driver components of the Windows NT DDK
#
!INCLUDE $(NTMAKEENV)\makefile.def
SOURCES:
TARGETNAME=sysfile
TARGETPATH=BIN
TARGETTYPE=DRIVER
SOURCES = DriverMain.cpp
Как запустить драйвер и просмотреть отладочную информацию
Для запуска драйвера я использовал утилиту KmdManager. Для просмотра отладочной информации использовалась утилита DbgView.
P. S. Статью писал давно, ещё на третьем курсе, сейчас уже почти ничего не помню. Но если есть вопросы, постараюсь ответить.
P. P. S. Прошу обратить внимание на комментарии, в частности на этот
UPD: Проект на GitHub: https://github.com/pbespechnyi/simple-wdm-driver