Userland/Kernel communication – DeviceIoControl method
Introduction
It is sometime very useful to use userland’s APIs to handle different tasks such as networking or to interact with the driver from a graphical interface. In a short serie of posts, we’ll explain the basic technics to achieve communication between the kernel driver and a userland application.
The Windows Driver Development Kit provides a data structure called I/O Request Packet also known as IRP. This post will cover only the needed part of an IRP structure and will not study all the details of it, for more detailled informations visit the MSDN page for IRP. To communicate, a userland application must first open a handle to a device using the CreateFile function like if it was a file. Depending on the driver’s implementation, you can use DeviceIOControl, ReadFile or WriteFile to send and receive messages to and from the kernel driver. As with file, you must close the handle with the CloseHandle function. This article will cover the use of the DeviceIOControl function and show both, kernel driver and userland application implementation. The use of ReadFile and WriteFile function will be covered in the next article.
A very important concept to understand is the MajorFunction array found in the kernel driver object. It can be seen as event callback used to handle the device status and all IRPs created. As an example, the IRP_MJ_CREATE MajorFunction will be called when a userland application used the CreateFile function so we can do whatever it takes to properly set a device handle to the CreateFile function. Maybe it’s not clear, but it will become as we will talk enough about it later. In this post, we’ll take a look at IRP_MJ_CREATE, IRP_MJ_CLOSE and IRP_MJ_DEVICE_CONTROL.
Device and symbolic link creation
In order to enable communication between the driver and the application, a device must be created to let the application having a handle to it with the CreateFile function. First we’ll declare three global variables which represent the device name, his symbolic link and a pointer to the device object. Even if there’s cleaner ways to do it by using C++ encapsulation, we’ll keep using C to keep it simple. As we create a device, we have to delete it properly in our OnUnload function.
const WCHAR deviceNameBuffer[] = L"\\Device\\MYDEVICE"; const WCHAR deviceSymLinkBuffer[] = L"\\DosDevices\\MyDevice"; PDEVICE_OBJECT g_MyDevice; // Global pointer to our device object
Then let’s create the device in the « DriverEntry » function as follow:
NTSTATUS DriverEntry( IN PDRIVER_OBJECT pDriverObject, IN PUNICODE_STRING pRegistryPath ) { NTSTATUS ntStatus = 0; UNICODE_STRING deviceNameUnicodeString, deviceSymLinkUnicodeString; // Normalize name and symbolic link. RtlInitUnicodeString (&deviceNameUnicodeString, deviceNameBuffer); RtlInitUnicodeString (&deviceSymLinkUnicodeString, deviceSymLinkBuffer); // Create the device. ntStatus = IoCreateDevice ( pDriverObject, 0, // For driver extension &deviceNameUnicodeString, FILE_DEVICE_UNKNOWN, FILE_DEVICE_UNKNOWN, FALSE, &g_MyDevice); // Create the symbolic link ntStatus = IoCreateSymbolicLink(&deviceSymLinkUnicodeString, &deviceNameUnicodeString); pDriverObject->DriverUnload = OnUnload; pDriverObject->MajorFunction[IRP_MJ_CREATE] = Function_IRP_MJ_CREATE; pDriverObject->MajorFunction[IRP_MJ_CLOSE] = Function_IRP_MJ_CLOSE; pDriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = Function_IRP_DEVICE_CONTROL; DbgPrint("Loading driver"); return STATUS_SUCCESS; }
Notice the three functions mapped to their respective IRPs MajorFunction. Here we simply tell our driver which function to call if an IRP event occurs. We’ll discuss the role and the content of these functions later.
VOID OnUnload( IN PDRIVER_OBJECT pDriverObject ) { UNICODE_STRING symLink; RtlInitUnicodeString(&symLink, deviceSymLinkBuffer); IoDeleteSymbolicLink(&symLink); IoDeleteDevice(pDriverObject->DeviceObject); DbgPrint("OnUnload called!"); }
Communication protocol setup
With the DeviceIoControl method, we can build a communication protocol using the Device Input and Output Control, IOCTL. Each IOCTL will define the device type, the function code, the method telling how to use buffers and the access rights. Some device types are already define but we have defined our own code which is 40000. In case of the function code, it is up to your implementation and imagination as long as it’s no larger then 4095, this is where you’ll create your protocol logic. In our example, we use the method METHOD_BUFFERED, it is very important to understand these methods (refer to IRP’s documentation for more details) otherwise you’re just about to create your first BSOD. For the access rights, we use FILE_READ_DATA|FILE_WRITE_DATA. Microsoft provides a macro to create IOCTL code and we’ll use it to define all IOCTLs. Note that both, driver and application, must share the IOCTL definition and a good practice is to use a header file included by both.
#define SIOCTL_TYPE 40000 #define IOCTL_HELLO\ CTL_CODE( SIOCTL_TYPE, 0x800, METHOD_BUFFERED, FILE_READ_DATA|FILE_WRITE_DATA)
MajorFunction and driver-side communication
Here we’ll detail the role of the three MajorFunction and their content. Let’s begin with Function_IRP_MJ_CREATE. This function will be called every time an application will try to open a handle to the device with the CreateFile function. Actually, our function is empty because we don’t have anything special like network or database connection.
NTSTATUS Function_IRP_MJ_CREATE(PDEVICE_OBJECT pDeviceObject, PIRP Irp) { DbgPrint("IRP MJ CREATE received."); return STATUS_SUCCESS; }
Same for the Function_IRP_MJ_CLOSE, it is empty but if we do have some connections or memory clean up, there is the place to do it. This function is called by the CloseHandle function is used by an application.
NTSTATUS Function_IRP_MJ_CLOSE(PDEVICE_OBJECT pDeviceObject, PIRP Irp) { DbgPrint("IRP MJ CLOSE received."); return STATUS_SUCCESS; }
Now let’s take a look at the core function where our communication logic or protocol takes place: Function_IRP_DEVICE_CONTROL. This function is use when the DeviceIoControl function is called. Every MajorFunction calls come with the Device and the Irp pointers. We’ll need the Irp pointer to retrieve the pointer to the buffer and the pointer of the current IRP stack. At this point, for those who wants to go farther, I strongly suggest to visit the MSDN Irp documentation for a better understanding. To keep it simple, the Irp contains the IOCTL, the message from the application as well as a place to put the driver answer. We can switch the IOCTL and then use it as control command or whatever you like. So let’s take a look at this function:
NTSTATUS Function_IRP_DEVICE_CONTROL(PDEVICE_OBJECT pDeviceObject, PIRP Irp) { PIO_STACK_LOCATION pIoStackLocation; PCHAR welcome = "Hello from kerneland."; PVOID pBuf = Irp->AssociatedIrp.SystemBuffer; pIoStackLocation = IoGetCurrentIrpStackLocation(Irp); switch(pIoStackLocation->Parameters.DeviceIoControl.IoControlCode) { case IOCTL_HELLO : DbgPrint("IOCTL HELLO."); DbgPrint("Message received : %s", pBuf); RtlZeroMemory(pBuf,pIoStackLocation->Parameters.DeviceIoControl.InputBufferLength); RtlCopyMemory( pBuf, welcome, strlen(welcome) ); break; } // Finish the I/O operation by simply completing the packet and returning // the same status as in the packet itself. Irp->IoStatus.Status = STATUS_SUCCESS; Irp->IoStatus.Information = strlen(welcome); IoCompleteRequest(Irp,IO_NO_INCREMENT); return STATUS_SUCCESS; }
Userland application example
Here is a simple command-line program to test our communication protocol:
#include "stdafx.h" #include <windows.h> // Device type #define SIOCTL_TYPE 40000 // The IOCTL function codes from 0x800 to 0xFFF are for customer use. #define IOCTL_HELLO\ CTL_CODE( SIOCTL_TYPE, 0x800, METHOD_BUFFERED, FILE_READ_DATA|FILE_WRITE_DATA) int __cdecl main(int argc, char* argv[]) { HANDLE hDevice; char *welcome = "Hello from userland."; DWORD dwBytesRead = 0; char ReadBuffer[50] = {0}; hDevice = CreateFile(L"\\\\.\\MyDevice", GENERIC_WRITE|GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); printf("Handle : %p\n",hDevice); DeviceIoControl(hDevice, IOCTL_HELLO, welcome, strlen(welcome), ReadBuffer, sizeof(ReadBuffer), &dwBytesRead, NULL); printf("Message received from kerneland : %s\n",ReadBuffer); printf("Bytes read : %d\n", dwBytesRead); CloseHandle(hDevice); return 0; }