In this article we will consider the methods of hooking keyboard data in the kernel mode. The described approaches can be used for solving the tasks of keystroke analysis, blocking and also redefining some combinations.
Table of Content
- Devices and drivers
- Method 1 (the simplest): IRP and driver stack
- Attaching the unknown keyboard device
- I/O completion routine
- Log information storage
- APC routine patch and example of active window detection
- Method 2 (universal): kbdclass.sys driver patch
- About WDM keyboard filter
- Demo project class architecture
- Supported MS Windows Versions
- Recommended reading
1. Devices and drivers
Before starting to implement hooking it's necessary to understand how the interaction between devices and drivers is performed.
Drivers frequently have multilevel architecture and represent stack based on the driver that works directly with the device. The task of the underlying driver is to read data from the device and transmit them upwards by the stack for the further processing.
The scheme beneath represents the relations between drivers and devices for PS/2 and USB keyboards, but this model is the same for any other device.
The task of the port driver (i8042prt and usbhid) is to get all data stored in the keyboard buffer and transmit them upwards by the chain of drivers. Data exchange between drives is performed by means of IRP, that are moving in the stack in both directions. After reaching the top of the stack data from IRP are copied to the user space in the context of
csrss service, and then are transmitted to the active application as the window message. Thus placing our own driver in this chain we get possibility not only to hook keystrokes but also replace them by our own or block.
2. Method 1 (the simplest): IRP and driver stack
IRP is created in the moment when I/O Manager sends its request. The first to accept IRP is the highest driver in the stack, and correspondingly the last one to get it is the driver responsible for the interaction with the real device. By the moment of IRP creation the number of drivers in the stack is known. I/O Manager allocates some space in IRP for
IO_STACK_LOCATION structure for each driver. Also the index and pointer of the current
IO_STACK_LOCATION structure are stored in the IRP header.
As it was mentioned before the drivers form the chain with IRP as the data medium. Correspondingly the simplest way to hook data from the device driver (and keyboard driver in particular) is to attach own specially developed driver to the stack with the existing ones.
2.1. Attaching the unknown keyboard device
To attach the device to the existing chain we should create it first:
To attach the device to the stack it is recommended to use the call of
IoAttachDeviceToDeviceStack. But first we should get the pointer of the device class:
You should pay attention that we get the pointer to the device
\Device\KeyboardClass0, that is PS/2 keyboard. It’s the only class, pointer to which can be obtained directly (how to hook the packages sent by USB keyboard will be described in the section 4).
Thus the current IRP handlers registered for our driver will get the packages containing the information about the keyboard controller events.
2.2 I/O completion routine
To read data from the keyboard controller (i8042prt or usbhid) the driver of the class (
IRP_MJ_READ request to the port driver.
Kbdclass is also the filter and is absolutely “transparent”. It’s naturally to assume that we should hook the needed IRP when scan codes are already written and the package is going upwards by the stack. For this purpose the functions of I/O completion exist (
I/O completion routine).
I/O completion routine is called after the current I/O request is completed (
The registration of
I/O completion routine is performed as follows:
And at the end it’s necessary to transmit IRP down by the stack:
2.3 Log information store
In the demo project all information about keystrokes is saved to the file, but for the better code flexibility the handler of keyboard events implements the interface of
IKBExternalObserver and basically can perform any actions with the hooked data.
The function of the completion and processing of the hooked data:
2.4 APC Routine patch
Besides the documented method of IRP completion using
I/O completion routine, there exists also more flexible however undocumented way – APC routine patch.
When completing IRP, besides the call of the registered
I/O completion routine,
pIRP->Overlay.AsynchronousParameters.UserApcRoutine is called in the
csrss context anisochronously. Correspondingly the replacing of this function is as follows:
The handler is almost the same to the I/O completion dispatch:
APC routine there is a possibility to detect the current active window where the keystroke was performed. It can be performed by calling
NtUserGetForegroundWindow, that is located in
SSDT Shadow is not exported by the graphical subsystem (win32k.sys), but it can be called in the
csrss context by means of
SYSENTER. For Windows XP it will be like this:
To make the process of getting the active window universal it’s necessary to implement the search for
NtUserGetForegroundWindow function in
SSDT Shadow or get its number from Ntdll.dll.
3. Method 2 (universal): kbdclass.sys driver patch
Direct utilizing of the previously described methods without any additional implementations is possible only for PS/2 keyboards since only pointer to
\Device\KeyboardClass0 can be obtained directly. Unfortunately it’s impossible for USB keyboards. But after research of this question I came to the rather simple and natural solution: if the driver of the class kbdclass.sys gets all data from the port drivers (usbhid, i8042prt etc.), then we can hook its handlers
It’s easy to do it:
Thus the handler
IRP_MJ_READ for kbdclass.sys is our function, pointer to which is stored in
In the case when the information about the lowest driver in the stack is important, it can be get from the structure
DEVICE_EXTENSION from the project kbdclass.sys in DDK.
4. About WDM keyboard filter
Demo project is the legacy driver. But all methods described in this article are applicable for the WDM drivers too. The only essential difference is that in WDM driver the hooking method described in section 3 will work for all connection interfaces (USB and PS/2). Naturally to do this the calling of device creation and attaching it to the stack should be placed in the
AddDevice function of the driver.
5. Demo project Class architecture
Demo project is based on the KBHookLib library. It contains all described methods of the keystroke hooking and also necessary interfaces for the further integration.
Class diagram of KBHookLib:
6. Supported MS Windows Versions
- MS Windows XP – SP1, SP2, SP3 – x86/x64
- MS Windows 2003 Server – all versions – x86/x64
- MS Windows Vista – all version – x86
7. Recommended reading
- Russinovich, Mark; Solomon, David – Microsoft Windows Internals
- Oney, Walter – Programming The Microsoft Windows Driver Model
- Hoglund, Greg – Rootkits, Subverting the Windows Kernel
Download the source files of demo project.