📜 ⬆️ ⬇️

The simplest WDM driver

This article describes the process of writing the simplest driver that displays the scan codes of keystrokes.
This article also describes the process of setting up a workplace for writing drivers.
If you are interested, please under the cat.


Stand preparation


Install the necessary software for writing the simplest driver

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

I use two virtual machines, I write drivers on one, and I launch on another. If you also decide to do this for the machine on which you will run the drivers, 4 GB hard disk and 256 MB of RAM will suffice.

Setting up the workplace

DDK installation

Installation is extremely simple. The only thing you need to pay attention to is a dialogue in which you are invited to choose the components to be installed. I strongly recommend noting all the documentation and examples.
')
Installing and configuring Microsoft® Visual Studio 2005

Installing Microsoft® Visual Studio 2005 is no more difficult than installing the DDK. If you use it only for writing drivers, then when the installer asks which components to install, select only Visual C ++.
Next, you can install Visual Assist X. With this program (add-on), you can easily configure hints for writing drivers conveniently.
After installing Visual Assist X in Visual Studio 2005, a new VAssistX menu will appear. Further in this menu: Visual Assist X Options -> Projects -> C/C++ Directories -> Platform: Custom, Show Directories for: Stable include files . Click Ins or on the icon to add a new directory and in the appeared line, if you have Windows XP enter %WXPBASE%\inc\ddk\wxp .

Install and configure DDKWizard

In order for you to compile drivers in Visual Studio, you need to install DDKWizard. It can be downloaded from ddkwizard.assarbad.net . Also from this site download the ddkbuild.cmd script.
After the wizard is installed, you must perform the following steps:

Everything, the car on which we will start drivers, is ready.

Install the necessary software to run the drivers

Now we will configure the machine on which we will run the written drivers.
We will need the following programs:

Everything, the car is ready to start drivers.

Formulation of the problem


The task: to write a driver that will display scan codes of keystrokes and their combinations in debug.

A bit of theory

A driver is a set of functions that are called by the operating system upon the occurrence of certain events coming from the device or user mode.
There are many types of drivers, some of them are listed below:

Class drivers are drivers that Microsoft writes. These are common 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 related to the device.
Filter 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 the functions in your driver, but it must contain DriverEntry and AddDevice .

IRP is a structure that drivers use to exchange data.

So, in order to display scan codes ( what is this? ) In debug, we will use a filter driver.
There are two types of filter drivers:

What type your driver belongs to depends on where the driver is in the device driver stack. If your driver is above the functional driver, then it is called the upper filter driver, if lower, then the lower filter driver.

Differences between upper and lower filter drivers

All requests pass through the upper filter drivers, which means that they can change and / or filter information going to the functional driver, and then, perhaps, to the device.
An example of using top filter drivers:
A filter-hook driver, which sets its own hook function for the IpFilterDirver system driver, to monitor and filter traffic. Such drivers are used in firewalls.

Fewer requests pass through the lower filtering drivers because most requests execute and terminate a functional driver.

Sync issues

In the driver, which we will write, there are several "problem" sections. For our driver it is quite enough to use assembly inserts:

 __asm { lock dec «,     » } 

or
 __asm { lock inc «,     » } 

The lock prefix allows you to safely execute the command following it. It blocks the remaining processors while the command is executed.

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_EXTENSION
 typedef struct _DEVICE_EXTENSION{ PDEVICE_OBJECT pLowerDO; } DEVICE_EXTENSION, *PDEVICE_EXTENSION; 

The pLowerDO object is a device object that is below us on the stack. We need it in order to know who else to send IRP packets to.
Even for the operation of our driver, we need a variable in which the number of not completed requests will be stored.
 int gnRequests; 

Let's start with the function that is the main entry point of our driver.
 extern "C" NTSTATUS DriverEntry(IN PDRIVER_OBJECT theDriverObject, IN PUNICODE_STRING ustrRegistryPath) 

theDriverObject is a driver object that contains pointers to all functions necessary for the operating system, which we will need to initialize.
ustrRegistryPath is the name of the section in the registry where information about this driver is stored.
First you need to declare and reset the variables:
 gnRequests = 0; NTSTATUS status = {0}; 

Further, as I wrote above, it is necessary to initialize function pointers
 for (int i = 0; i<IRP_MJ_MAXIMUM_FUNCTION; ++i) { theDriverObject->MajorFunction[i] = DispatchThru; } theDriverObject->MajorFunction[IRP_MJ_READ] = DispatchRead; theDriverObject->DriverUnload = DriverUnload; 

The DispatchRead function will handle read requests. It will be called when the keyboard key is pressed or released.
The DriverUnload function DriverUnload 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. the resources used by the driver are released, all pending requests are completed, etc.
The DispatchThru function is a stub function. All it does is transfer the IRP packet to the next driver (the driver that is under our stack, i.e. pLowerDO from DEVICE_EXTENSION ).
Next, we call our function to create and install our device to the device stack:
 status = InstallFilter(theDriverObject); 

I will describe this function below.
Returning status , in which, if the InstallFilter function was InstallFilter , the value STATUS_SUCCESS is stored.
Moving on to the InstallFilter function. Here is its prototype:
 NTSTATUS InstallFilter(IN PDRIVER_OBJECT theDO); 

This function creates a device object, sets it up and puts it on the stack of devices on top of \\Device\\KeyboardClass0

We declare variables:
 PDEVICE_OBJECT pKeyboardDevice; NTSTATUS status = {0}; 

pKeyboardDevice is the device object we need to create.
Call IoCreateDevice to create a new device.
 status = IoCreateDevice(theDO, sizeof(DEVICE_EXTENSION), NULL, FILE_DEVICE_KEYBOARD, 0, FALSE, &pKeyboardDevice); 

Let us consider the parameters in more detail:

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 over which we turn on the stack.
Next we have to perform the transformation of the name of the device that we include in the stack.
 CCHAR cName[40] = "\\Device\\KeyboardClass0"; STRING strName; UNICODE_STRING ustrDeviceName; RtlInitAnsiString(&strName, cName); RtlAnsiStringToUnicodeString(&ustrDeviceName, &strName, TRUE); 

The IoAttachDevice function injects our device onto the stack. The object of the next (lower) device will be stored in pdx->pLowerDO .
 IoAttachDevice(pKeyboardDevice, &ustrDeviceName, &pdx->pLowerDO); 

Free up resources:
 RtlFreeUnicodeString(&ustrDeviceName); 

Next we analyze the function DispatchRead with 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 a keyboard key.
Increase the count of incomplete requests
 __asm { lock inc gnRequests } 

Before passing the request to the next driver, we must configure the stack pointer for the driver. IoCopyCurrentIrpStackLocationToNext copies the memory location that belongs to the current driver to the memory area of ​​the next driver.
 IoCopyCurrentIrpStackLocationToNext(theIrp); 
When a request goes down the stack, it still does not have the data we need, so we need to specify 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 ReadCompletionRoutine our function.
Pass the IRP following driver:
 return IoCallDriver(((PDEVICE_EXTENSION) pDeviceObject->DeviceExtension)->pLowerDO ,theIrp); 

Now we will analyze the function that will be called every time the IRP completed. Prototype:
 NTSTATUS ReadCompletionRoutine(IN PDEVICE_OBJECT pDeviceObject, IN PIRP theIrp, IN PVOID Context); 

DEVICE_EXTENSION :
 PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)pDeviceObject->DeviceExtension; 

The PKEYBOARD_INPUT_DATA structure PKEYBOARD_INPUT_DATA used to describe the key pressed.
 PKEYBOARD_INPUT_DATA kidData; 

Check if the request is successfully completed or not.
 if (NT_SUCCESS(theIrp->IoStatus.Status)) 

To get the KEYBOARD_INPUT_DATA structure, you need to refer to the system IRP packet buffer.
 kidData = (PKEYBOARD_INPUT_DATA)theIrp->AssociatedIrp.SystemBuffer; 

Find out the number of keys
 int n = theIrp->IoStatus.Information / sizeof(KEYBOARD_INPUT_DATA); 

And display each key:
 for(int i = 0; i<n; ++i) DbgPrint("Code: %x\n", kidData[i].MakeCode); 

And do not forget to reduce the number of requests not processed
 __asm { lock dec gnRequests } 

We return the status of the request
 return theIrp->IoStatus.Status; 

Let us analyze the completion function. Prototype:
 VOID DriverUnload(IN PDRIVER_OBJECT theDO); 

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

Removing the device from the stack:
 IoDetachDevice(pdx->pLowerDO); 

Remove the device:
 IoDeleteDevice(theDO->DeviceObject); 

We check there are incomplete requests or not. If we unload the driver without this check, the first time the key is pressed after unloading will be the BSOD.
 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); //     } } 

Driver Code:
 extern "C" { #include "ntddk.h" } #include "ntddkbd.h" typedef struct _DEVICE_EXTENSION{ PDEVICE_OBJECT pLowerDO; } DEVICE_EXTENSION, *PDEVICE_EXTENSION; int gnRequests; NTSTATUS DispatchThru(PDEVICE_OBJECT theDeviceObject, PIRP theIrp) { IoSkipCurrentIrpStackLocation(theIrp); return IoCallDriver(((PDEVICE_EXTENSION) theDeviceObject->DeviceExtension)->pLowerDO ,theIrp); } NTSTATUS InstallFilter(IN PDRIVER_OBJECT theDO) { PDEVICE_OBJECT pKeyboardDevice; NTSTATUS status = {0}; status = IoCreateDevice(theDO, sizeof(DEVICE_EXTENSION), NULL, FILE_DEVICE_KEYBOARD, 0, FALSE, &pKeyboardDevice); if (!NT_SUCCESS(status)) { DbgPrint("IoCreateDevice error.."); return status; } pKeyboardDevice->Flags = pKeyboardDevice->Flags | (DO_BUFFERED_IO | DO_POWER_PAGABLE); pKeyboardDevice->Flags = pKeyboardDevice->Flags & ~DO_DEVICE_INITIALIZING; RtlZeroMemory(pKeyboardDevice->DeviceExtension, sizeof(DEVICE_EXTENSION)); PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)pKeyboardDevice->DeviceExtension; CCHAR cName[40] = "\\Device\\KeyboardClass0"; STRING strName; UNICODE_STRING ustrDeviceName; RtlInitAnsiString(&strName, cName); RtlAnsiStringToUnicodeString(&ustrDeviceName, &strName, TRUE); IoAttachDevice(pKeyboardDevice, &ustrDeviceName, &pdx->pLowerDO); //DbgPrint("After IoAttachDevice"); RtlFreeUnicodeString(&ustrDeviceName); return status; } VOID DriverUnload(IN PDRIVER_OBJECT theDO) { PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)theDO->DeviceObject->DeviceExtension; IoDetachDevice(pdx->pLowerDO); IoDeleteDevice(theDO->DeviceObject); if (gnRequests != 0) { KTIMER ktTimer; LARGE_INTEGER liTimeout; liTimeout.QuadPart = 1000000; KeInitializeTimer(&ktTimer); while(gnRequests > 0) { KeSetTimer(&ktTimer, liTimeout, NULL); KeWaitForSingleObject(&ktTimer, Executive, KernelMode, FALSE, NULL); } } } NTSTATUS ReadCompletionRoutine(IN PDEVICE_OBJECT pDeviceObject, IN PIRP theIrp, IN PVOID Context) { PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)pDeviceObject->DeviceExtension; PKEYBOARD_INPUT_DATA kidData; if (NT_SUCCESS(theIrp->IoStatus.Status)) { kidData = (PKEYBOARD_INPUT_DATA)theIrp->AssociatedIrp.SystemBuffer; int n = theIrp->IoStatus.Information / sizeof(KEYBOARD_INPUT_DATA); for(int i = 0; i<n; ++i) { DbgPrint("Code: %x\n", kidData[i].MakeCode); } } if(theIrp->PendingReturned) IoMarkIrpPending(theIrp); __asm{ lock dec gnRequests } return theIrp->IoStatus.Status; } NTSTATUS DispatchRead(IN PDEVICE_OBJECT pDeviceObject, IN PIRP theIrp) { __asm{ lock inc gnRequests } IoCopyCurrentIrpStackLocationToNext(theIrp); IoSetCompletionRoutine(theIrp, ReadCompletionRoutine, pDeviceObject, TRUE, TRUE, TRUE); return IoCallDriver(((PDEVICE_EXTENSION) pDeviceObject->DeviceExtension)->pLowerDO ,theIrp); } extern "C" NTSTATUS DriverEntry(IN PDRIVER_OBJECT theDriverObject, IN PUNICODE_STRING RegistryPath) { NTSTATUS status = {0}; gnRequests = 0; for (int i = 0; i<IRP_MJ_MAXIMUM_FUNCTION; ++i) { theDriverObject->MajorFunction[i] = DispatchThru; } theDriverObject->MajorFunction[IRP_MJ_READ] = DispatchRead; status = InstallFilter(theDriverObject); theDriverObject->DriverUnload = DriverUnload; return status; } 


MAKEFILE:
 # # DO NOT EDIT THIS FILE!!! Edit .\sources. if you want to add a new source # file to this component. This file merely indirects to the real make file # that is shared by all the driver components of the Windows NT DDK # !INCLUDE $(NTMAKEENV)\makefile.def 

SOURCES:
 TARGETNAME=sysfile TARGETPATH=BIN TARGETTYPE=DRIVER SOURCES = DriverMain.cpp 

How to run the driver and view debug information

To run the driver, I used the KmdManager utility. To view debug information, the DbgView utility was used.

PS I wrote the article a long time ago, when I was still in my third year, now I don’t remember almost anything. But if you have questions, I will try to answer.
PPS Please pay attention to the comments, in particular to this

UPD: Project on GitHub: https://github.com/pbespechnyi/simple-wdm-driver

Source: https://habr.com/ru/post/146071/


All Articles