|
|
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 ;-------------------------- |