Kernel Mode Driver Tutorial: Part I: “The Skeleton KMD”

by Clandestiny

                    

 

 

 

I. Introduction

 

Greetings!  Here is the first of hopefully several KMD (kernel mode driver) tutorials.  KMD’s are the accepted driver format for the Windows NT/2000/XP operating systems, but by and large they are still a mystery.  There are hundreds of obscure structures and obtuse functions documented in the mess that calls itself the MSDN.  There are a handful of books on the subject which are useful references if we ignore the fact that most of them exceed 500 pages and require a PhD in computer science to get past the first couple pages of the “introduction”.  There are even a few snippets of example KMD code lying around the internet if you know where to look.  Of course, they too assume an advanced background and are hardly suited to serve as examples to the programmer taking his/her first baby steps into kernel mode programming.  When I first became interested in writing a driver, I was using Windows 98 and I started by learning about VxD’s from Iczelion’s fantastic win32 asm series tutorials.  Eventually, when I migrated to Windows 2000, I was forced to abandon my VxD knowledge and embark on a new search for the KMD equivalent to Iczelion’s famous tutorials.  I’m sorry to report that I never found it.  After much searching, I cobbled up enough information to understand and create a successful skeleton KMD. What you are about to read is the result of that effort.  It is my hope that this tutorial serves as a simple introduction to those programmers, like me, who are taking their first baby steps into KMD development.  I am by no means an expert on the subject and readily welcome corrections or suggestions for improvement from those more knowlegable in the field. You can contact me at clandestiny(at)despammed.com.  If this tutorial generates some positive feedback, ultimately I hope to write KMD tutorials on other low-level programming tasks like interrupt hooking and memory access / management.

 

 

 

IIa. Getting Started

 

For the skeleton KMD you will need at least the following include files / libraries... I used the include files / libraries kindly provided by Four-F in his KMD kit.

 

§    ntoskrnl.lib

§    ntddk.inc

§    ntoskrnl.inc

§    ntstatus.inc

§    Strings.mac – not strictly required but very helpful nonetheless

 

NOTE: The code presented in this tutorial was written and assembled using MASM version 7.

 

I would also like to add that DriverStudio by Numega contains a very handy utility called DriverMonitor that can be used to start, stop, load, and unload drivers while testing and debugging.  I recommend obtaining it both for DriverMonitor and for SoftICE (IMO, the BEST ring 0 debugger). There is also some KMD example code included with DriverStudio if you know C++.

 

IIb. Driver Structure

 

Unlike the VxDs of Win9x which followed the LE (linear executable) format, KMDs structurally follow the standard PE (portable executable format) of .exe’s and .dll’s and compile to a .sys file extension. 

 

On the programmatic level, the skeleton kernel mode driver can be broken down into 3 basic functions, DriverEntry, DriverDispatcher, and DriverUnload.  The purpose of Driver Entry is to perform initialization tasks.  Unless your driver is intended to be loaded upon bootup, it’s driver entry function will be called from within a ring 3 loader that you write.  The second primary function of your KMD will be the DriverDispatcher routine.  As its name implies, the DriverDispatcher routine handles messages dispatched to the driver by either the OS or another ring 3 application via the DeviceIoControl interface.  As such, it can act as a communication conduit between ring 0 and ring 3 where the ring 0 driver provides privaleged “services” to a ring 3 program for tasks it would be unable to accomplish under ring 3 restrictions (ie. like accessing the IDT,GDT, and LDT system descriptors or low level hooking and spying on I/O functions).  Used in this manner, a KMD functions much like a ring 0 dll. Finally, like the DriverEntry function, DriverUnload will be called from a ring 3 application.  It performs standard deinitialization and cleanup (freeing memory, closing handles, removing hooks, ect).

 

    

 

Ø                           The Driver Entry Routine

 

This is the one function common to all KMD drivers and as its name implies, serves as the driver entry point. The first function of the DriverEntry is to create a "device object" using the IoCreateDevice call.  Assuming this is successful, it will secondarily set up a "symbolic link" using the IoCreateSymbolicLink function.  The symbolic link is optional for the functioning of the driver, BUT is required if you need a ring 3 application to be able to communicate with your driver.  Indeed, the symbolic link basically creates a name for your driver which will be used to access it in ring 3 using the CreateFile call.  The CreateFile call takes this name as its path parameter.  Lastly, DriverEntry must set up pointers to functions for any requests that it will handle (these requests are of the form IRP_MJ_XXX).  The minimum standard requests that must be handled for the driver to work are IRP_MJ_CREATE, IRP_MJ_CLOSE, AND IRP_MJ_CLEANUP.  The IRP_MJ_DEVICE_CONTROL request must be handled if the driver is to be able to communicate with ring 3 apps via the DeviceIoControl interface. The driver object maintains an array of pointers to the dispatch procedures that will be called when these requests are recieved.  It is the responsiblity of DriverEntry to set the function pointers in this array.  A successful DriverEntry function returns the value STATUS_SUCCESS (0X00).  I should also mention that KMDs typically only use strings in the UNICODE format. Four-f provides some convienient macros for dealing with them in Strings.mac found in his KMD Kit.

 

We can summarize the DriverEntry routine’s responsiblites as follows…

 

1. Create the device using IoCreateDevice

2. Set up a SymbolicLink using IoCreateSymbolicLink if your driver needs to be able to communicate with ring 3 applications

3. Set up the entry points for the IRP_MJ requests your driver services

4. Set the DriverUnload entry point

 

 

 

DriverEntry     PROC    pDriverObject:PDRIVER_OBJECT, pRegistryPath:PUNICODE_STRING   

LOCAL status:DWORD     

pushad

mov esi, pDriverObject

ASSUME esi:PTR DRIVER_OBJECT

mov edi, OFFSET gDeviceObject

ASSUME edi:PTR DEVICE_OBJECT

 

;------------------------------------

;Create the device & Set up the symbolic

;link so ring 3 apps can communicate w/

;our ring 0 driver

;------------------------------------

invoke IoCreateDevice,esi,0,$CCOUNTED_UNICODE_STRING("\\Device\\skeleton", uDeviceName, 4),FILE_DEVICE_UNKNOWN,0,FALSE,edi

.IF eax == STATUS_SUCCESS

        invoke IoCreateSymbolicLink,$CCOUNTED_UNICODE_STRING("\\DosDevices\\skeleton", uSymbolicLinkName, 4),OFFSET uDeviceName

        .IF eax == STATUS_SUCCESS

                ;------------------------------------

                ;Set up dispatch routine entry points

                ;for IRP_MJ_Xxx requests

                ;------------------------------------

                mov [esi].MajorFunction[IRP_MJ_DEVICE_CONTROL*(sizeof PVOID)],OFFSET DriverDispatcher

                mov [esi].MajorFunction[IRP_MJ_CREATE*(sizeof PVOID)],OFFSET DriverDispatcher

                mov [esi].MajorFunction[IRP_MJ_CLOSE*(sizeof PVOID)],OFFSET DriverDispatcher

                mov [esi].MajorFunction[IRP_MJ_CLEANUP*(sizeof PVOID)],OFFSET DriverDispatcher

               

                ;----------------------------------

                ;Set DriverUnload entry point

                ;----------------------------------

                mov [esi].DriverUnload, OFFSET DriverUnload

        .ENDIF 

.ENDIF

mov status,eax

popad

mov eax,status

ret

DriverEntry     endp

 

 

 

 

Ø                              The Driver Dispatcher Routine

 

The DriverDispatcher routine functions more or less as the WndProc does to a standard win32 application.  It processes messages / requests.  A request is defined by an IRP also known I/O request packet. Every driver is assigned a unique location in the IRP stack which contains information about its requests.  When the driver recieves a request, it looks up the request in the IRP stack using IoGetCurrentStackLocation.  These requests can fall into 3 categories:

 

1. Standard Requests necessary for the driver to function like  IRP_MJ_CREATE, IRP_MJ_CLOSE, and IRP_MJ_CLEANUP.  The programmer's responsiblity at a bare minimum is to return STATUS_SUCCESS in response to these requests.

 

 

2. Programmer Defined Requests which fall under IRP_MJ_DEVICE_CONTROL are those custom services provided to ring 3 applications via the DeviceIoControl interface. As in the VxD, you will look up the IOCTL code and branch off to the procedure that needs to be handled.  In order for the DeviceIoControl  interface to work with a KMD, it is necessary to write an IOCTL header file.  The IOCTL header file defines the device type, service control code value, buffer transfer type, and access type for both the driver and the requestor of its services.  All together these parameters define an IOCTL value.  IOCTL values are obtained by calling the macro CTL_CODE in your IOCTL header file.  The header file will be included both in the driver build and the ring 3 application which desires to communicate with the driver. The IOCTL parameters are as follows:

 

CTL_CODE( DeviceType, ControlCode, TransferType, RequiredAccess)

 

 

       Parameter                                                                 Description            

DeviceType

File_Device_XXX values supplied to IoCreateDevice

§         00h to 7FFh – reserved for Microsoft

§         8000h to FFFh – customer defined

 

Control Code

 

Driver defined IOCTL code

§         000h to 7FFh – reserved for Microsoft

§         800h to FFFh – customer defined

Transfer Type

Buffer passing mechanism for this control code

§         METHOD_INDIRECT

§         METHOD_BUFFERED

§         METHOD_OUT_DIRECT

§         METHOD_NEITHER

Required Access

 

Requestor access requirement

§         FILE_ANY_ACCESS

§         FILE_READ_DATA

§         FILE_WRITE_DATA

§         FILE_READ_DATA | FILE_WRITE_DATA

 

 

In the skeleton driver I have defined 1 IOCTL value to serve as an example for how the DeviceIoControl Interface operates.  I declared it in ctrl_codes.inc using the CTL_CODE macro as follows:

            

SERVICE_SAY_HELLO equ CTL_CODE (FILE_DEVICE_UNKNOWN,801H,METHOD_NEITHER,FILE_ANY_ACCESS)

 

For the device type I delcared , FILE_DEVICE_UNKNOWN, the same device type as I declared when I created it using IoCreateDevice. I set the control code at 801, but it could have been any number within the customer defined range of 800h to FFFh.  The important point when choosing control codes is that all of the control codes for your device are unique values within that range. I set the transfer type as METHOD_NEITHER for this service.  Basically this means that the I/O manager directly passes you the address to the input or output buffer sent by the DeviceIoControl call.  In other words, there is no error checking done on this address by the I/O manager and consequently, it is the least safe method of passing your data to and from the driver.  The METHOD_BUFFERED, METHOD_INDIRECT, and METHOD_OUT_DIRECT all have checks performed on them by the I/O Manager and are therefore safer to use.

 

Passing data via the DeviceIoControl interface differs depending on the transfer type you’ve specified…

 

For METHOD_BUFFERED, the I/O manager allocates a single buffer which is accessed through pIRP.AssoicatedIrp.SystemBuffer.  Because this buffer is used for both input and output, the driver must be careful to extract all input information before writing output back to the buffer.

 

For METHOD_IN_DIRECT, the I/O manager checks the input buffer and builds an MDL for it.  It then stores a pointer to the MDL in pIRP.MdlAddress. The output buffer is accessed through pIRP.AssociatedIrp.SystemBuffer.

 

For METHOD_OUT_DIRECT, the I/O manager checks the output buffer and builds an MDL for it.  It then stores a pointer to the MDL in pIRP.MdlAddress. The input buffer is accessed through pIRP.AssociatedIrp.SystemBuffer.

 

For METHOD_NEITHER, the addresses specified by the user in the DeviceIoControl call are provided directly to the driver with no error checking or verification. The input buffer can be accessed though pIRP.Parameters.DeviceIoControl.Type3InputBuffer and the output buffer can be accessed though pIrp.UserBuffer.

 

As I mentioned previously, I handled 1 programmer defined request in the skeleton driver.  It’s function was simply return a string from ring 0 to ring 3 which is printed out by the ring 3 applciation in a messagebox.  When handling a user defined request, the first step is to verify that it is an IRP_MJ_DEVICE_CONTROL request.  If it is, then you will go on to check the CTRL code to determine what service is being requested.  There could be several which you would have to check, but in this case the only request is defined as SERVICE_SAY_HELLO. The function of SERVICE_SAY_HELLO is to copy a string into the output buffer and return STATUS_SUCCESS.  Remember, I am using METHOD_NEITHER for the data transfer so the data will be copied into the location addressed by pIrp.UserBuffer.

 

.ELSEIF [eax].MajorFunction == IRP_MJ_DEVICE_CONTROL      ;check for DeviceIoControl request from user

.IF [eax].Parameters.DeviceIoControl.IoControlCode == SERVICE_SAY_HELLO  ;determine what service is being requested

         invoke CopyString,[edi].UserBuffer,OFFSET msgHello,[eax].Parameters.DeviceIoControl.OutputBufferLength ;copy string into output buffer

         mov[edi].IoStatus.Status,STATUS_SUCCESS 

.ELSE

         mov[edi].IoStatus.Status,STATUS_NOT_IMPLEMENTED

.ENDIF

 

 

3. Undefined Requests, or requests which are not handled directly by the given driver. For these, it is the programmers responsiblity to return the  STATUS_NOT_IMPLEMENTED value.

 

 

DriverDispatcher         PROC    pDeviceObject:PDEVICE_OBJECT, pIrp:PIRP

LOCAL status:DWORD

pushad

mov esi, pDeviceObject

ASSUME esi: PTR DEVICE_OBJECT

mov edi, pIrp

ASSUME edi:PTR _IRP

 

;----------------------------------

;Determine what driver service is

;being requested

;----------------------------------

IoGetCurrentIrpStackLocation edi

assume eax:PTR IO_STACK_LOCATION

 

;---------------------------------

;Standard Requests

;---------------------------------

.IF [eax].MajorFunction == IRP_MJ_CREATE || [eax].MajorFunction == IRP_MJ_CLOSE || [eax].MajorFunction == IRP_MJ_CLEANUP

mov [edi].IoStatus.Status, STATUS_SUCCESS

 

;--------------------------------

;Programmer Defined Requests

;--------------------------------

.ELSEIF [eax].MajorFunction == IRP_MJ_DEVICE_CONTROL

.IF [eax].Parameters.DeviceIoControl.IoControlCode == SERVICE_SAY_HELLO

         invoke CopyString,[edi].UserBuffer,OFFSET msgHello,[eax].Parameters.DeviceIoControl.OutputBufferLength

         mov[edi].IoStatus.Status,STATUS_SUCCESS

.ELSE

         mov[edi].IoStatus.Status,STATUS_NOT_IMPLEMENTED

.ENDIF

 

;--------------------------------

;Undefined Requests

;--------------------------------

.ELSE

         mov[edi].IoStatus.Status,STATUS_NOT_IMPLEMENTED

.ENDIF

 

push [edi].IoStatus.Status

pop status

mov[edi].IoStatus.Information,0

invoke IoCompleteRequest,edi,IO_NO_INCREMENT

popad

mov eax,status

ret

DriverDispatcher endp

 

 

 

Ø                              The Driver Unload Routine

 

The DriverUnload routine is pretty self explanatory.  It will be called when the loader invokes the ControlService API with a SERVICE_CONTROL_STOP message. DriverUnload basically unwinds the actions taken by the DriverEntry function.  It will perform any driver specific cleanup and is at a minimum required to delete the device object (IoDeleteDevice) and the symbolic link (IoDeleteSymbolicLink).  Failure to perform either of these actions will corrupt the internal service database entry for the driver preventing it from being loaded again (until you reboot L).

 

 

DriverUnload             PROC    pDriverObject:PDRIVER_OBJECT

pushad

mov esi, pDriverObject

ASSUME esi: PTR DRIVER_OBJECT

invoke   IoDeleteSymbolicLink, OFFSET uSymbolicLinkName

invoke IoDeleteDevice,[esi].DeviceObject                         

popad

ret

DriverUnload     endp

 

 

 

 

IIc. The Loader

 

If you have Driver Studio, you can use the handy little utitlity “Driver Monitor” to load, start, stop, and unload drivers for testing purposes. However, if your driver must provide services to a ring 3 application will, the ring 3 application will will manually have to install the driver as a service in order to be able to communicate with it.  The procedure for loading a driver is as follows:

 

1. Obtain a handle to the service manager using OpenSCManager.

 

2. Define the driver using CreateService. This creates an entry in the service database for the driver.  Note, that this information is kept in the registry under...HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\"My Driver's Name".  Realize, that CreateService does not *load* the driver.  It simply creates an entry in the service database so that it can be recognized.

 

3. Open the driver using OpenService.  This call loads the driver, but realize that this call does not *start* the driver.

 

4. Finally start the driver using the StartService function.  Note that this function is what finally calls the DriverEntry routine.  If there are errors in your DriverEntry routine, expect StartService to fail.

 

5. Now you can open a handle to the driver in a ring 3 application using CreateFile and send requests via the DeviceIoControl.

 

6. Stop the driver by sending a SERVICE_CONTROL_STOP via ControlService.  Note that this function calls the DriverUnload routine.  If there are errors in your DriverUnload routine, expect this function to fail and expect to be unable to load the driver again unless you reboot.  This is because if this call fails, or the driver crashes before executing this call, the service database entry for the driver will be corrupted until you reboot.

 

7. Finally delete the service using DeleteService and release the handle to the service control database using CloseServiceHandle.  DeleteService is the function responsible for deleting the actual entry in the service database and the corresponding key in the registry.

 

;THE LOADER

 

.code

start:

TestDriver PROC

;--------------------------------------------------

;Obtain a handle to the service control manager

;--------------------------------------------------

invoke OpenSCManager, 0, 0, SC_MANAGER_ALL_ACCESS

mov hScManager,eax

;-------------------------------------------------

;Add the service to the system

;-------------------------------------------------

invoke GetFullPathName,ADDR DriverName,256,ADDR FilePath,ADDR pFilePart

;------------------------------------------------

;Register the service w/ the system

;------------------------------------------------

invoke CreateService,hScManager,ADDR ServiceName,ADDR ServiceName,SERVICE_ALL_ACCESS,SERVICE_KERNEL_DRIVER, \

SERVICE_DEMAND_START,SERVICE_ERROR_NORMAL,ADDR FilePath,0,0,0,0,0

xchg eax,ebx

invoke GetLastError

.IF (ebx != 0) || (eax == ERROR_SERVICE_EXISTS) ;service database entry already exists for driver

         mov hService,eax

        

         ;---------------------------------------------------------------

         ;Load the service /driver

         ;---------------------------------------------------------------

         invoke OpenService,hScManager,ADDR ServiceName,SERVICE_ALL_ACCESS

         .IF eax != 0

                 mov hService,eax

 

                 ;--------------------------------------------------

                 ;Start the service to set the service to the running

                 ;state. StartService will call the DriverEntry procedure

                 ;--------------------------------------------------

                 invoke StartService,hService,0,0

                 .IF eax != 0

 

                         ;---------------------------------------------------

                         ;Obtain a handle to the loaded driver for DeviceIoControl

                         ;interface communcation

                         ;---------------------------------------------------

                         invoke CreateFile,ADDR DriverPath ,GENERIC_READ or GENERIC_WRITE,0,0,OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,0

                        .IF eax != INVALID_HANDLE_VALUE

                              mov       hDriver, eax

                         .ELSE

                               invoke  MessageBoxA,NULL,ADDR lpMsgFileErrorText,ADDR lpMsgErrorTitle,MB_OK

                               jmp file_error             ;CreateFile error

                         .ENDIF

                         ;--------------------------