Logo
blank Skip to main content

Keystroke Monitoring

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.

 

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.

pic1

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:

C
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:

C
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:

C
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:

C
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:

C
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:

C
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:

C
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:

C
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:

C
__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:

C
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:

C
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:

pic2

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

Have a question?

Ask our expert!

Tell us about
your project

...And our team will:

  • Process your request within 1-2 business days.
  • Get back to you with an offer based on your project's scope and requirements.
  • Set a call to discuss your future project in detail and finalize the offer.
  • Sign a contract with you to start working on your project.

Do not have any specific task for us in mind but our skills seem interesting? Get a quick Apriorit intro to better understand our team capabilities.