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.
Contents
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:
PDEVICE_OBJECT pKeyboardDeviceObject = NULL;
NTSTATUS lStatus = IoCreateDevice(pDriverObject,
0,
NULL,
FILE_DEVICE_KEYBOARD,
0,
FALSE,
&pKeyboardDeviceObject);
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:
UNICODE_STRING usClassName;
RtlInitUnicodeString(&usClassName, L"\\Device\\KeyboardClass0");
PDEVICE_OBJECT pClassDeviceObject = NULL;
PFILE_OBJECT pClassFileObject = NULL;
//Get pointer for \\Device\\KeyboardClass0
lStatus = IoGetDeviceObjectPointer(&usClassName, FILE_READ_DATA, &pClassFileObject, &pClassDeviceObject);
if (!NT_SUCCESS(lStatus)){
throw(std::runtime_error("[KBHookDriver]Cannot get device object of \\Device\\KeyboardClass0."));
}
g_pFilterManager = new CFilterManager();
g_pSimpleHookObserver = new CKeyLoggerObserver(L"\\DosDevices\\c:\\KeyboardClass0.log");
g_pFilterManager->RegisterFilter(pKeyboardDeviceObject, pClassDeviceObject, g_pSimpleHookObserver);
g_pFilterManager->GetFilter(pKeyboardDeviceObject)->AttachFilter();
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).
And then:
void CKBFilterObject::AttachFilter(void){
m_pNextDevice = IoAttachDeviceToDeviceStack(m_pKBFilterDevice, m_pNextDevice);
if (m_pNextDevice == NULL){
throw(std::runtime_error("[KBHookDriver]Cannot attach filter."));
}
m_bIsAttached = true;
return;
}
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 (kbdclass
) sends 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 (IoCompleteRequest
).
The registration of I/O completion routine
is performed as follows:
void IOCompletionRoutine(IIRPProcessor *pContext, PIRP pIRP){
//Copy parameters to low level driver
IoCopyCurrentIrpStackLocationToNext(pIRP);
//Set I/O completion routine
IoSetCompletionRoutine(pIRP, OnReadCompletion, pContext, TRUE, TRUE, TRUE);
//Increment pending IRPs count
pContext->AddPendingPacket(pIRP);
return;
}
And at the end itโs necessary to transmit IRP down by the stack:
return(IofCallDriver(m_pNextDevice, pIRP));
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:
static NTSTATUS OnReadCompletion(PDEVICE_OBJECT pDeviceObject, PIRP pIRP, PVOID pContext){
IIRPProcessor *pIRPProcessor = (IIRPProcessor*)pContext;
//Checks completion status success
if (pIRP->IoStatus.Status == STATUS_SUCCESS){
PKEYBOARD_INPUT_DATA keys = (PKEYBOARD_INPUT_DATA)pIRP->AssociatedIrp.SystemBuffer;
//Get data count
unsigned int iKeysCount = pIRP->IoStatus.Information / sizeof(KEYBOARD_INPUT_DATA);
for (unsigned int iCounter = 0; iCounter < iKeysCount; ++iCounter){
KEY_STATE_DATA keyData;
keyData.pusScanCode = &keys[iCounter].MakeCode;
//If key have been pressed up, itโs marked with flag KEY_BREAK
if (keys[iCounter].Flags & KEY_BREAK){
keyData.bPressed = false;
}
else{
keyData.bPressed = true;
}
try{
//OnProcessEvent is a method of IKBExternalObserver.
pIRPProcessor->GetDeviceObserver()->OnProcessEvent(keyData);
keys[iCounter].Flags = keyData.bPressed ? KEY_MAKE : KEY_BREAK;
}
catch(std::exception& ex){
DbgPrint("[KBHookLib]%s\n", ex.what());
}
}
}
if(pIRP->PendingReturned){
IoMarkIrpPending(pIRP);
}
pIRPProcessor->RemovePendingPacket(pIRP);
return(pIRP->IoStatus.Status);
}
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:
void APCRoutinePatch(IIRPProcessor *pIRPProcessor, PIRP pIRP){
CAPCContext *pContext =
new CAPCContext(pIRP->Overlay.AsynchronousParameters.UserApcContext,
pIRP->Overlay.AsynchronousParameters.UserApcRoutine,
pIRP->UserBuffer,
pIRPProcessor->GetDeviceObserver(),
pIRP);
pIRP->Overlay.AsynchronousParameters.UserApcRoutine = Patch_APCRoutine;
pIRP->Overlay.AsynchronousParameters.UserApcContext = pContext;
return;
}
The handler is almost the same to the I/O completion dispatch:
void NTAPI Patch_APCRoutine(PVOID pAPCContext, PIO_STATUS_BLOCK pIoStatusBlock, ULONG ulReserved){
std::auto_ptr<CAPCContext> pContext((CAPCContext*)pAPCContext);
PKEYBOARD_INPUT_DATA pKeyData = (PKEYBOARD_INPUT_DATA)pContext->GetUserBuffer();
KEY_STATE_DATA keyData;
keyData.pusScanCode = &pKeyData->MakeCode;
if (pKeyData->Flags == KEY_MAKE){
keyData.bPressed = true;
}
else{
if (pKeyData->Flags == KEY_BREAK){
keyData.bPressed = false;
}
else{
pContext->GetOriginalAPCRoutine()(pContext->GetOriginalAPCContext(),
pIoStatusBlock,
ulReserved);
return;
}
}
try{
pContext->GetObserver()->OnProcessEvent(keyData);
pKeyData->Flags = keyData.bPressed ? KEY_MAKE : KEY_BREAK;
}
catch(std::exception& ex){
DbgPrint("[KBHookLib]%s\n", ex.what());
}
pContext->GetOriginalAPCRoutine()(pContext->GetOriginalAPCContext(),
pIoStatusBlock,
ulReserved);
return;
}
In 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
. 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:
__declspec(naked) HANDLE NTAPI NtUserGetForegroundWindow(void){
__asm{
mov eax, 0x1194; //NtUserGetForegroundWindows number in SSDT Shadow for Windows XP
int 2eh; //Call SYSENTER gate
retn;
}
}
โฆโฆโฆ
PEPROCESS pProcess = PsGetCurrentProcess();
KAPC_STATE ApcState;
KeStackAttachProcess(pProcess, &ApcState);
HANDLE hForeground = NtUserGetForegroundWindow(); //returns HWND of current window
KeUnstackDetachProcess(&ApcState);
โฆโฆโฆ
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 IRP_MJ_READ
.
Itโs easy to do it:
void CKbdclassHook::Hook(void){
UNICODE_STRING usKbdClassDriverName;
RtlInitUnicodeString(&usKbdClassDriverName, m_wsClassDrvName.c_str());
//Get pointer to class driver object
NTSTATUS lStatus = ObReferenceObjectByName(&usKbdClassDriverName,
OBJ_CASE_INSENSITIVE,
NULL,
0,
(POBJECT_TYPE)IoDriverObjectType,
KernelMode,
NULL,
(PVOID*)&m_pClassDriver);
if (!NT_SUCCESS(lStatus)){
throw(std::exception("[KBHookLib]Cannot get driver object by name."));
}
KIRQL oldIRQL;
KeRaiseIrql(HIGH_LEVEL, &oldIRQL);
//IRP_MJ_READ patching
m_pOriginalDispatchRead = m_pClassDriver->MajorFunction[IRP_MJ_READ];
m_pClassDriver->MajorFunction[IRP_MJ_READ] = m_pHookCallback;
m_bEnabled = true;
KeLowerIrql(oldIRQL);
return;
}
Thus the handler IRP_MJ_READ
for kbdclass.sys is our function, pointer to which is stored in m_pHookCallback
.
Handler:
NTSTATUS CKbdclassHook::Call_DispatchRead(PDEVICE_OBJECT pDeviceObject, PIRP pIRP){
//KBDCLASS_DEVICE_EXTENSION is equal DEVICE_EXTENSION for kbdclass from DDK
PKBDCLASS_DEVICE_EXTENSION pDevExt = (PKBDCLASS_DEVICE_EXTENSION)pDeviceObject->DeviceExtension;
if (pIRP->IoStatus.Status == STATUS_SUCCESS){
PKEYBOARD_INPUT_DATA key = (PKEYBOARD_INPUT_DATA)pIRP->UserBuffer;
KEY_STATE_DATA keyData;
keyData.pusScanCode = &key->MakeCode;
if (key->Flags & KEY_BREAK){
keyData.bPressed = false;
}
else{
keyData.bPressed = true;
}
m_pObserver->OnProcessEvent(pDevExt->TopPort, keyData);
}
//Original function calling for data translation to user space.
return(m_pOriginalDispatchRead(pDeviceObject, pIRP));
}
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
Downloads
Download the source files of demo project.