Case Studies
The preceding theory lays out a framework for writing any sort of WDM filter driver. To write a filter driver for any particular purpose, you will undoubtedly encounter many pitfalls based on peculiarities in device stacks and your position in the device stack. In this section, I ll present some additional examples of filter drivers to help you get started.
Lower Filter for Traffic Monitoring
I ll tell you a secret. I have a hard time getting power management to work right in my drivers. It s such a recurring theme that I finally wrote the POWTRACE filter driver. You install POWTRACE as a lower filter for any driver you re trying to debug. It then logs all the power-related IRPs that your function driver generates. There s actually nothing at all remarkable about how POWTRACE works on the inside, so I ll commend you to the companion content.
Let s say you want to investigate how Microsoft s HIDUSB and HIDCLASS drivers handle wake-up for a USB mouse. After making sure that the POWTRACE service entries are defined, you can modify the hardware key for the mouse in question (Figure 16-7). Then unplug and replug the mouse, and start watching the debug trace with DbgView. Figure 16-8 is the trace I generated by enabling wake-up in the Device Manager (line 58), putting the machine in standby (lines 60 through 68), and then waking the system with a mouse click (lines 70 through 89).
Figure 16-7. Preparing to use POWTRACE.
Figure 16-8. POWTRACE log of wake-up/resume cycle.
Named Filters
Sometimes you want to have an application talk directly to a filter driver. The straightforward way to arrange this sort of communication is for the application to open a handle to the device and then use some private IOCTL operations that the filter consumes. Unfortunately, this approach isn t always feasible:
Some devices won t let your application open a handle. A mouse or a keyboard, for example, will already be open for the raw input thread, and you can t open a second handle. A serial port is usually an exclusive device and likewise won t let you open a second handle if someone else is already using the port.
You re at the mercy of all the drivers above you as to whether you ll see any IOCTL traffic. MOUCLASS and KBDCLASS, for example, block private IOCTLs. Even if your application could open a handle, therefore, it still couldn t talk to a filter in the mouse or keyboard stack.
The standard solution to these kinds of problems is to create an Extra Device Object (EDO) that shadows the FiDO you put in the PnP stack. Figure 16-9 illustrates the concept.
Figure 16-9. An Extra Device Object for a filter driver.
To bring this off, you need to define two different device extension structures that have at least one initial member in common. For example:
typedef struct _COMMON_DEVICE_EXTENSION { ULONG flag; } COMMON_DEVICE_EXTENSION, *PCOMMON_DEVICE_EXTENSION; typedef struct _DEVICE_EXTENSION : public _COMMON_DEVICE_EXTENSION { struct _EXTRA_DEVICE_EXTENSION* edx; } DEVICE_EXTENSION, *PDEVICE_EXTENSION; typedef struct _EXTRA_DEVICE_EXTENSION : public _COMMON_DEVICE_EXTENSION { PDEVICE_EXTENSION pdx; } EXTRA_DEVICE_EXTENSION, *PEXTRA_DEVICE_EXTENSION; #define FIDO_EXTENSION 0 #define EDO_EXTENSION 1
In your AddDevice function, you create two device objects: a FiDO that you link into the PnP stack and an EDO that you don t. You give the EDO a unique name too. For example:
NTSTATUS AddDevice(PDRIVER_OBJECT Driver Object, PDEVICE_OBJECT pdo) { PDEVICE_OBJECT fido; IoCreateDevice(...); PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) fido->DeviceExtension; <etc. -- all the stuff shown earlier in the chapter> pdx->flag = FIDO_EXTENSION; WCHAR namebuf[64]; static LONG numextra = -1; _snwprintf(namebuf, arraysize(namebuf), L"\\Device\\MyExtra%d", InterlockedIncrement(&numextra)); UNICODE_STRING edoname; RtlInitUnicodeString(&ednoname, namebuf); IoCreateDevice(DriverObject, sizeof(EXTRA_DEVICE_EXTENSION), &edoname, FILE_DEVICE_UNKNOWN, 0, FALSE, &edo); PEXTRA_DEVICE_EXTENSION edx = (PEXTRA_DEVICE_EXTENSION) edo->DeviceExtension; edx->flag = EDO_EXTENSION; edx->pdx = pdx; pdx->edx = edx; }
You ll also want to create a uniquely named symbolic link to point to the EDO. That s the name your application will use when it wants to open a handle to your filter.
In each dispatch routine, you first cast the device object s DeviceExtension pointer to PCOMMON_DEVICE_EXTENSION, and you inspect the flag member to see whether the IRP is aimed at the FiDO or the EDO. You handle FiDO IRPs as shown earlier for a generic filter driver. You handle EDO IRPs however you please. At a minimum, you ll succeed IRP_MJ_CREATE and IRP_MJ_CLOSE requests for the EDO, and you ll respond to whichever set of IOCTLs you define.
The foregoing are the basics for making your filter driver accessible no matter where it is in the driver stack and no matter which policies the drivers above you may be enforcing on file opens and private IOCTLs. There are a few other details to attend to if you want to put the idea to real use:
Pay attention to the security attributes on the EDO. This would be a good time to use IoCreateDeviceSecure when it becomes available in the DDK. (This function, which was brand-new at press time, allows you to specify a security descriptor for a new device object. This is one of the situations it was invented to handle.)
Use instructions you receive over the EDO pathway to modulate how you handle IRPs in the FiDO pathway.
You need to delete the EDO at the same time you delete the FiDO. Don t forget to drain IRPs through the EDO too.
The EDO is not part of the PnP stack. Consequently, it doesn t receive PnP, power, or WMI requests. Moreover, you can t use IoRegisterDeviceInterface to create a symbolic link to it you have to use the older method of naming the EDO and creating a symbol link that points to it.
Bus Filters
A bus filter is a special kind of upper filter that attaches just above a bus driver. Recall that bus drivers wear two hats: a function device object (FDO) hat and a PDO hat. Creating an upper filter for the FDO personality of a bus driver is completely trivial: just make yourself a device UpperFilter in the hardware key for the bus driver. The tricky part about a bus filter is inserting yourself into each of the child device stacks right above the PDOs that the bus driver creates. Figure 16-10 illustrates the topology you re aiming for.
Figure 16-10. Bus filter topology.
Managing the FiDOs in each child device stack isn t too hard. In your parent stack filter role (FDO hat, in other words), be on the lookout for IRP_MN_QUERY_DEVICE_RELATIONS asking for BusRelations. This query is how the PnP Manager asks the bus driver for a list of all the child devices. Review Chapter 11 if you re a bit unclear on how this protocol works. You ll want to pass this query down synchronously (the ForwardAndWait scenario from the end of Chapter 5) and then inspect the list of PDOs that the bus driver returns. Each time you see a child PDO for the first time, you ll create a child-stack FiDO and attach it to the PDO. Each time you fail to see a PDO that you had seen earlier, you ll detach and delete the now-obsolete child-stack FiDO.
Creating and destroying child-stack FiDOs may not be all you need to do to create a working bus filter, though. The USB driver stack, for example, uses a back door to let USBHUB communicate efficiently with the host controller driver without sending IRPs through all the intermediate hub drivers that might be present. A USB bus filter uses USBD_RegisterHcFilter in the parent device stack for the host controller to make sure that it sees this backdoor traffic. Other buses might have similar registration requirements.
Keyboard and Mouse Filters
A common use of filtering is for keyboard and mouse input. Among the applications I ve seen for such filters are
Computer-aided training, especially when it involves raw motion reports or keystrokes that can t be hooked in user mode.
Accessibility applications.
Automated testing.
Earlier in this chapter, I showed example driver stacks for keyboards and mice. I think it s easiest to plan on building a keyboard or mouse filter as a class upper filter that sits just below KBDCLASS or MOUCLASS, as the case may be. Your DriverEntry and AddDevice routines will be as shown earlier for a standard WDM filter driver. You ll probably want to create an EDO for out-of-band communication with your own user-mode application code too.
NOTE
The DDK samples KBFILTR and MOUFILTR illustrate the basics of writing a keyboard or mouse filter.
KBDCLASS and MOUCLASS use a direct-call interface to receive keyboard and mouse reports from whichever port driver is actually handling the hardware. (These two drivers are samples in the DDK, so you can see for yourself exactly what s going on.) To effectively filter the reports, you need to hook into this direct-call mechanism by handling either IOCTL_INTERNAL_KEYBOARD_CONNECT or IOCTL_INTERNAL_MOUSE_CONNECT, which are internal device control requests. These IOCTLs use a parameter structure declared in KBDMOU.H:
typedef struct _CONNECT_DATA { IN PDEVICE_OBJECT ClassDeviceObject; IN PVOID ClassService; } CONNECT_DATA, *PCONNECT_DATA;
Here ClassDeviceObject is the address of a device object belonging to KBDCLASS or MOUCLASS, and ClassService is the address of a function with the following abstract prototype:
typedef VOID (*PSERVICE_CALLBACK_ROUTINE) (PVOID NormalContext, PVOID SystemArgument1, PVOID SystemArgument2, PVOID SystemArgument3);
Your filter driver s dispatch routine for IRP_MJ_INTERNAL_DEVICE_CONTROL would process the CONNECT request by saving the ClassDeviceObject and ClassService values in your own device extension and then substituting your own device object and callback routine addresses before passing the IRP down the stack to the port driver.
Once the direct-call connection is made, the port driver calls the ClassService callback routine each time an input event occurs. The callback routine is expected to consume a certain number of reports from any array provided by the port driver and return with an indication of how many reports were consumed.
In the case of a keyboard filter, the callback routine has this actual prototype:
VOID KeyboardCallback(PDEVICE_OBJECT fido, PKEYBOARD_INPUT_DATA start, PKEYBOARD_INPUT_DATA end, PULONG consumed);
The start and end parameters delimit an array of KEYBOARD_INPUT_DATA structures, each of which relates to a single key press or release event (see NTDDKKB.H):
typedef struct _KEYBOARD_INPUT_DATA { USHORT UnitId; USHORT MakeCode; USHORT Flags; USHORT Reserved; ULONG ExtraInformation; } KEYBOARD_INPUT_DATA, *PKEYBOARD_INPUT_DATA;
The so-called MakeCode is a raw scan code generated by the keyboard. This scan code describes the physical position of the key on the keyboard and has no necessary relationship to the graphic on the keycap. The Flags member indicates whether the key has been pressed (KEY_MAKE) or released (KEY_BREAK). There are also flag bits (KEY_E0 and KEY_E1) to indicate extended shift states that modulate certain special keys like SysRq and Pause.
In the case of a mouse filter, the callback has this actual prototype:
VOID MouseCallback(PDEVICE_OBJECT fido, PMOUSE_INPUT_DATA start, PMOUSE_INPUT_DATA end, PULONG consumed);
In this function, start and end refer to instances of the following structure (see NTDDMOU.H):
typedef struct _MOUSE_INPUT_DATA { USHORT UnitId; USHORT Flags; union { ULONG Buttons; struct { USHORT ButtonFlags; USHORT ButtonData; }; }; ULONG RawButtons; LONG LastX; LONG LastY; ULONG ExtraInformation; } MOUSE_INPUT_DATA, *PMOUSE_INPUT_DATA;
The mouse report information is in LastX and LastY (distances moved or absolute position) and ButtonFlags (bits such as MOUSE_LEFT_BUTTON_DOWN to indicate button events).
In either case, you return (in *consumed) the number of events you ve actually removed from the array bounded by start and end. The function driver will report any unconsumed events in a subsequent callback. When you re testing a filter, you can easily be misled into thinking that you never get more than one report at a time, but you really do have to program the driver to accept an array of reports.
Within your callback routine, you can do any of the following:
Forward events by passing them to the higher-level driver s callback routine. Don t forget to take into account that the higher-level driver might not consume all the events that you try to forward.
Remove events by not passing them up. You can, for example, make several calls to the higher-level driver s callback routines for portions of the array not including the events you want to elide.
You can also insert events into the stream by making your own call to the higher-level driver s callback routine.
Filtering Other HID Devices
It s very difficult to provide a useful filter driver for a HID device. To understand why, we need to look closely at the device stack for a HID device such as a joystick. (See Figure 16-11.) The function driver for the physical device is HIDUSB, a HIDCLASS minidriver. It does you no particular good to attach an upper filter to this device (which you could easily do using techniques already discussed in this chapter) because HIDCLASS will fail any IRP_MJ_CREATE directed to the FDO. There is, in fact, no simple way for a filter driver in the parent driver stack to receive any IRPs except by creating an Extra Device Object.
Figure 16-11. Driver stack for a joystick.
Often, you re not interested in filtering the real device anyway. Instead, you want to filter the reports flowing upward in the child driver stack that HIDCLASS creates for each top-level collection exported by the real device. The problem is, you can t know in advance which device identifier HIDCLASS will use for the collection PDOs, and you can t modify the Microsoft-provided INF files for the collection device class without breaking Microsoft s digital signature for its own drivers. If you can get by with a class filter, as you can for keyboards and mice, well and good just install your filter as a class filter. But if you need a device-specific filter, it looks as though you re out of luck.
This is where the concept of a bus filter might be useful. Go ahead and install your filter as an upper filter in the parent stack. Then have it monitor the BusRelations queries as described earlier for bus filter drivers. At the appropriate time, you can instantiate a FiDO in the collection driver stack. If you can get by with having your filter be the bottommost in the collection stack, you re done. Otherwise, you may need to do something relatively heroic such as alter the device identifiers reported by IRP_MN_QUERY_ID in order to force use of your own INF file.