Keylogger Mechanics
Kernel Driver Architecture
Mastering Keyboard Input at the Kernel Level
To truly understand how Windows handles raw data, we must dive below the user interface and into the kernel itself. In this chapter, we will build a filter driver that attaches directly to the keyboard stack to intercept IRP_MJ_READ packets. By the end of this lesson, you will be able to capture raw input directly from the hardware, bypassing standard OS hooks and establishing a powerful, low-level foundation for your driver development skills.
The Anatomy of the Filter Device Object
To intercept keyboard traffic, we must instantiate a Filter Device Object (FiDO). This object acts as a proxy, sitting quietly in the device stack to observe or modify data as it passes between the hardware and the class driver. The process begins within the DriverEntry routine, where the driver's framework is initialized. Unlike a standalone hardware driver, our filter's primary goal is to coexist within an existing stack, requiring us to be surgical in our configuration.
The FiDO must mirror the characteristics of the device it is filtering. If the target device uses buffered I/O, your filter must also set the DO_BUFFERED_IO flag to ensure the memory manager handles IRP buffers correctly.
Attaching to the Keyboard Stack
Once the FiDO is created, the next critical step is the attachment. We use to insert our filter into the chain. This function places our device object at the top of the current stack, ensuring that any IRPs sent to the keyboard first land in our dispatch routines. Timing is everything here; in multi-processor environments, a race condition can occur if an IRP is dispatched while the stack is being modified. Modern development favors the Windows Driver Framework (WDF) approach using WdfFdoInitFilter, which abstracts much of the complexity of PnP and Power management transitions that were notoriously difficult to handle in the older WDM model.
Intercepting the Read Request
The heart of a keyboard filter is the IRP_MJ_READ dispatch routine. When a user presses a key, the system generates a read request. To actually see the keystroke, we cannot simply look at the IRP as it goes down; the buffer is empty at that point. Instead, we must use IoSetCompletionRoutine. This tells the kernel: "Let the lower drivers fill this buffer with hardware data, but call me back before you finish." This callback allows us to examine the input buffer, which contains structures, before the data moves back up to the OS.
WDM vs WDF Approaches
While traditional Windows Driver Model (WDM) requires manual management of the device stack and complex state machines for Power and PnP, the Windows Driver Framework (WDF) has become the modern standard. By using WdfFdoInitFilter, the framework handles the heavy lifting of 'plug and play' events. This allows developers to focus on the logic of the IRP dispatch rather than the boilerplate code required to prevent system crashes during a sleep or wake cycle. Regardless of the framework, the underlying goal remains the same: precise, low-overhead interception of data packets.
Building a FiDO is a lesson in kernel etiquette. You are a guest in the device stack, and your implementation must be as efficient as possible. By correctly setting your I/O flags and utilizing completion routines, you gain unprecedented access to the system's input stream, turning raw electronic signals into actionable data for your driver.