Click here to Skip to main content
Click here to Skip to main content

Keystroke Monitoring

, , 19 Oct 2009 CPOL
Rate this:
Please Sign up or sign in to vote.
In this article, we will consider the methods of hooking keyboard data in the kernel mode.

Table of Contents

  1. Introduction
  2. Devices and Drivers
  3. 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
  4. Method 2 (universal): kbdclass.sys Driver Patch
  5. About WDM Keyboard Filter
  6. Demo Project Class Architecture
  7. Supported Microsoft Windows Versions
  8. Recommended Reading
  9. History

1. Introduction

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.

2. 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 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.

image002_0002.gif

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 the possibility not only to hook keystrokes but also replace them by our own or block.

3. 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.

3.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.

3.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));

3.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);
}  

3.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 asynchronously. 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.

4. 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 got from the structure DEVICE_EXTENSION from the project kbdclass.sys in DDK.

5. 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.

6. Demo Project Class Architecture

Demo project is based on the KBHookLib library. It contains all described methods of the keystroke hooking and also the necessary interfaces for further integration.

Class diagram of KBHookLib:

image004_0004.jpg

7. Supported Microsoft Windows Versions

  • Microsoft Windows XP – SP1, SP2, SP3 – x86/x64
  • Microsoft Windows 2003 Server – all versions  – x86/x64
  • Microsoft Windows Vista – all version – x86

8. Recommended Reading

  • Russinovich, Mark; Solomon, David – Microsoft Windows Internals
  • Oney, Walter – Programming The Microsoft Windows Driver Model
  • Hoglund, Greg – Rootkits, Subverting the Windows Kernel

Research more Network Security Cases in the Apriorit Case Study section at www.apriorit.com.

9. History

  • 19th October, 2009: Initial post

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Authors

Apriorit Inc
Apriorit Inc.
Ukraine Ukraine
ApriorIT is a Software Research and Development company that works in advanced knowledge-intensive scopes.
 
Company offers integrated research&development services for the software projects in such directions as Corporate Security, Remote Control, Mobile Development, Embedded Systems, Virtualization, Drivers and others.
 
Official site http://www.apriorit.com
Group type: Organisation

31 members

Follow on   LinkedIn

Vladimir S. Sabanov
Software Developer (Senior) AptiorIT
Ukraine Ukraine
No Biography provided

Comments and Discussions

 
QuestionUser Output in USB keyboard (HidP_SetData) PinmemberMember 1026391510-Sep-13 0:19 
Questionkbdclass.sys Driver Patch Pinmemberneelabhmam16-Jan-13 20:06 
Questionusb keyboard code is not working in Windows 7 PinmemberCodzer6-Oct-11 19:29 
AnswerRe: usb keyboard code is not working in Windows 7 PinmemberVladimir S. Sabanov7-Oct-11 0:43 
GeneralRe: usb keyboard code is not working in Windows 7 PinmemberCodzer7-Oct-11 1:08 
I had patch IRP_MJ_READ of \Driver\kbdclass.But no success. It gives exception when "ObReferenceObjectByName" is called. its only in case of windows 7. why ObReferenceObjectByName didn't worked out in win 7?
GeneralProblem with ObReferenceObjectByName PinmemberMr.Arsalan30-Jul-10 2:33 
Generalhello winddk 7600.16385.1 build Error Pingroupdnybz6-Jun-10 22:54 
GeneralRe: hello winddk 7600.16385.1 build Error Pingroupdnybz10-Jun-10 1:00 
GeneralRe: hello winddk 7600.16385.1 build Error Pingroupdnybz10-Jun-10 2:47 
GeneralRe: hello winddk 7600.16385.1 build Error Pinmembergndnet28-Jun-12 5:57 
GeneralQuestion regarding IoGetDeviceObjectPointer PinmemberMr.Arsalan14-May-10 22:21 
GeneralRe: Question regarding IoGetDeviceObjectPointer PinmemberVladimir S. Sabanov17-May-10 11:00 
GeneralObReferenceObjectByName - USB PinmemberMember 197257522-Dec-09 15:32 
Generalhow?! Pinmember__erfan__19-Oct-09 22:07 
GeneralRe: how?! PinmemberVladimir S. Sabanov20-Oct-09 0:55 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web02 | 2.8.141220.1 | Last Updated 19 Oct 2009
Article Copyright 2009 by Apriorit Inc, Vladimir S. Sabanov
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid