A Keyboard Sniffer

 < Day Day Up > 

Layering a driver requires some firsthand knowledge about how the Windows kernel handles drivers. This is best learned by example. In this chapter, we will walk you through creating a "hello layers" keyboard-sniffer rootkit. The keyboard sniffer will use a layered filter driver to intercept keystrokes.

The layered keyboard sniffer operates at a much higher level than that of the keyboard hardware. As it turns out, even working with hardware as simple as a keyboard controller can be very problematic. (See Chapter 8, Hardware Manipulation, for an example that directly accesses the keyboard hardware.)

With a layered driver, at the point at which we intercept keystrokes the hardware device drivers have already converted the keystrokes into I/O request packets (IRPs). These IRPs are passed up and down a "chain" of drivers. To intercept keystrokes, our rootkit simply needs to insert itself into this chain.

A driver adds itself to the chain of drivers by first creating a device, and then inserting the device into the group of devices. The distinction between device and driver is important, and is illustrated in Figure 6-1.

Figure 6-1. Illustration of the relationship between a driver and a device.


Many devices can attach to the device chain for legitimate purposes. As an example, Figure 6-2 shows a computer having two encryption packages, BestCrypt and PGP, both of which use filter drivers to intercept keystrokes and mouse activity.

Figure 6-2. DeviceTree utility[1] showing multiple filter devices attached to the keyboard and mouse.


[1] Available from www.osronline.com.

To better understand how the device chain processes information, one must follow the IRP through its lifetime. First, a read request is made to read a keystroke. This causes an IRP to be constructed. This IRP travels down the device chain, with an ultimate destination of the 8042 controller. Each device in the chain has a chance to modify or respond to the IRP. Once the 8042 driver has retrieved the keystroke from the keyboard buffer, the scancode is placed in the IRP and the IRP travels back up the chain. (A scancode is a number that corresponds to the key that was pressed on the keyboard.) On the IRP's way back up the chain, the drivers again have a chance to modify or respond to it.

I/O Request Packet (IRP) and Stack Locations

The IRP is a partially documented structure. It is allocated by the I/O manager within the Windows kernel, and is used to pass operation-specific data between drivers. When drivers are layered, they are registered in a chain. When an I/O request is made for chained drivers, an IRP is created and passed to all drivers in the chain. The "topmost" driver, the first one in the chain, is the first driver to receive the IRP. The last driver in the chain is the "lowest," and the one responsible for talking directly to the hardware.

When a new request is made, the I/O manager must create a new IRP. At the time of IRP creation, the I/O manager knows exactly how many drivers are registered in the chain. For each driver in the chain, the I/O manager adds extra space to the IRP being allocated, called an IO_STACK_LOCATION. Thus, while the IRP is a single large structure in memory, it will vary in size depending on the number of drivers in the chain. The entire IRP will reside in memory, looking something like Figure 6-3.

Figure 6-3. An IRP with three IO_STACK_LOCATIONs.


The IRP header stores an array index for the current IO_STACK_LOCATION. It also stores a pointer to the current IO_STACK_LOCATION. The index starts at 1; there is no member #0. In the example shown in Figure 6-3, the IRP would be initialized with a current stack index of 3, and the current IO_STACK_LOCATION pointer would point to the third member of the array. The first driver in the chain would be called with a current stack location of 3.

When a driver passes an IRP to the next-lowest driver, it uses the IoCallDriver routine (see Figure 6-4). One of the first actions of the IoCallDriver routine is to decrement the current stack location index. So, when the topmost driver in the Figure 6-3 example calls IoCallDriver, the current stack location is decremented to 2 before the next driver is called. Finally, when the lowest driver is called, the current stack location is set to 1. Note that if the current stack location is ever set to 0, the machine will crash.

Figure 6-4. IRP traversing a chain of drivers, each with its own stack location.


A filter driver must support the same major functions as the driver beneath it. A simple "hello world" filter driver would simply pass all IRPs to the underlying driver. Setting up a pass-through function is easy:

 ... for(int i = 0; i < IRP_MJ_MAXIMUM_FUNCTION; i++)    pDriverObject->MajorFunction[i] = MyPassThru; ... 

In this example, MyPassThru is a function similar to the following:

 NTSTATUS MyPassThru(PDEVICE_OBJECT theCurrentDeviceObject, PIRP theIRP) {   IoSkipCurrentIrpStackLocation(theIRP);   Return IoCallDriver(gNextDevice, theIRP); } 

The call to IoSkipCurrentStackLocation sets up the IRP so that when we call IoCallDriver, the next-lowest driver will use our current IO_STACK_LOCATION. In other words, the current IO_STACK_LOCATION pointer will not be changed.[2] This trick allows the lower-level driver to use any arguments or completion routines that have been supplied by the driver above us. (This suits us because we are lazy, so we don't want to initialize the next-lowest driver stack location.)

[2] For those who must know the nitty-gritty details, IoSkipCurrentIrpStackLocation actually increments the stack location pointer, only to have it decremented back when IoCallDriver is used thus rendering a net change of 0 in the pointer.

It's important to note that because IoSkipCurrentIrpStackLocation() may be implemented as a macro, you need to be sure that you always use curly braces in a conditional expression:

 if(something) {   IoSkipCurrentStackLocation() } 

This will not work:

 // This may cause a crash: if(something) IoSkipCurrentStackLocation(); 

Of course, this example is contrived and does nothing useful. To get somewhere with this technique, we would want to examine the contents of the IRPs after they have been completed. For example, IRPs are used to get keystrokes from the keyboard. Such IRPs will contain the scancodes for the keys that have been pressed.

To get some experience with this, take a walk through the KLOG rootkit in the next section.

     < Day Day Up > 


    Rootkits(c) Subverting the Windows Kernel
    Rootkits: Subverting the Windows Kernel
    ISBN: 0321294319
    EAN: 2147483647
    Year: 2006
    Pages: 111

    flylib.com © 2008-2017.
    If you may any questions please contact us: flylib@qtcs.net