Kernel Mode Drivers

Part 16: Driver Filter (not PnP)

This article is a practical complement to the previous one, where we covered the lifecycle of an IRP. Without reading the previous article, it will be difficult to fully understand the material in this one. If you are using a USB mouse or keyboard, unfortunately, you may have some difficulties (see the end of the article).

Keyboard stack

To begin with, just a little bit of theory on how the keyboard stack works.

The physical connection of the keyboard to the bus is carried out by the Intel 8042 keyboard microcontroller (or compatible with it). On modern computers, it is integrated into the motherboard chipset. This controller can operate in two modes: AT-compatible and PS / 2-compatible. An AT keyboard is probably hard to find now. All keyboards have long been PS / 2 compatible or USB keyboards. In PS / 2-compatible mode, the keyboard microcontroller also communicates with the bus and a PS / 2-compatible mouse. All this farm is managed by the i8042prt functional driver (Intel 8042 Port Driver), the full source code of which can be found in the DDK (DDK \ src \ input \ pnpi8042). The i8042prt driver creates two unnamed device objects and connects one to the keyboard stack and the other to the mouse stack. In the last article in Figure 15-4, you saw that on a Terminal Server machine, the keyboard (and the mouse too) has more than one (determined by the number of terminal sessions) stack. On a "normal" machine, the keyboard and mouse stacks look something like this:

 kd> !drvobj i8042prt
 Driver object (818377d0) is for:
  \Driver\i8042prt
 Driver Extension List: (id , addr)

 Device Object list:
 8181a020  8181b020


 kd> !devstack 8181a020
   !DevObj   !DrvObj            !DevExt   ObjectName
   8181ae30  \Driver\Mouclass   8181aee8  PointerClass0
 > 8181a020  \Driver\i8042prt   8181a0d8
   81890df0  \Driver\ACPI       8186e008  00000017
 !DevNode 8188fe48 :
   DeviceInst is "ACPI\PNP0F13\4&2658d0a0&0"
   ServiceName is "i8042prt"


 kd> !devstack 8181b020
   !DevObj   !DrvObj            !DevExt   ObjectName
   8181be30  \Driver\Kbdclass   8181bee8  KeyboardClass0
 > 8181b020  \Driver\i8042prt   8181b0d8
   81890f10  \Driver\ACPI       8189d228  00000016
 !DevNode 8188ff28 :
   DeviceInst is "ACPI\PNP0303\4&2658d0a0&0"
   ServiceName is "i8042prt"

On top of the i8042prt driver, more precisely on top of its devices, there are named "device" objects of the Kbdclass and Mouclass drivers. The name "KeyboardClass" is base name and indices (0, 1, etc.) are appended to it. The base name is stored in the registry parameter HKLM \ SYSTEM \ CurrentControlSet \ Services \ Kbdclass \ Parameters \ KeyboardDeviceBaseName and can also be "KeyboardPort" if the keyboard uses legacy drivers, although I did not understand this in detail (see the source code of the driver Kbdclass). We will use the "device" object named "KeyboardClass0" as the target device for connecting the filter.

The Kbdclass and Mouclass drivers are so-called class drivers and provide common functionality for all types of keyboards and mice, i.e. for the entire class of these devices. Both of these drivers are installed as high-level filter drivers and their complete source code can also be found in the DDK (DDK \ src \ input \ kbdclass and DDK \ src \ input \ mouclass, respectively). The archive for this article in the SetKeyboardLeds directory contains a simple driver that lights up all three keyboard indicators. Something like this (ie through the I / O ports) the i8042prt functional driver controls the "keyboard" device. Of course, you will not find port calls in the source code of the Kbdclass and Mouclass drivers.

The keyboard stack handles several types of requests (see the DDK section "Kbdclass Major I / O Requests" for a complete list). We will only be interested in IRPs of type IRP_MJ_READ, which carry key codes with them. The generator of these IRPs is the raw input stream RawInputThread of the csrcc.exe system process. This thread opens the device object of the keyboard class driver for exclusive use and uses the ZwReadFile function to send an IRP of type IRP_MJ_READ to it. After receiving the IRP, the Kbdclass driver, using the IoMarkIrpPending macro, marks it as pending, enqueues it, and returns STATUS_PENDING. The raw input stream will have to wait for the IRP to complete (more specifically, RawInputThread receives keyboard events as an Asynchronous Procedure Call (APC)). When connecting to the stack, the Kbdclass driver registers the KeyboardClassServiceCallback callback procedure with the i8042prt driver, sending it the IOCTL_INTERNAL_KEYBOARD_CONNECT IRP. The i8042prt driver also registers its I8042KeyboardInterruptService Interrupt Service Routine (ISR) with the system by calling the IoConnectInterrupt function. When a key is pressed or released, the keyboard controller will generate a hardware interrupt. Its handler will call I8042KeyboardInterruptService, which will read the necessary data from the internal queue of the keyboard controller. Because the hardware interrupt is handled at a higher IRQL, the ISR does only the most urgent work and queues up a Deferred Procedure Call (DPC). DPC works with IRQL = DISPATCH_LEVEL. When the IRQL drops to DISPATCH_LEVEL, the system will call the I8042KeyboardIsrDpc procedure, which will call the KeyboardClassServiceCallback callback procedure registered by the Kbdclass driver (also executed at IRQL = DISPATCH_LEVEL). KeyboardClassServiceCallback will retrieve the pending IRP from its queue, fill in the KEYBOARD_INPUT_DATA structure (in fact, i8042prt tries to empty the entire keyboard controller queue, and Kbdclass accordingly fill as many KEYBOARD_INPUT_DATA structures as it can fit into the IRP buffer) and fill / release all the necessary information about keys IRP. The raw input stream wakes up, processes the received information, and again sends an IRP of type IRP_MJ_READ to the class driver, which is again enqueued until the next key press / release. Thus, the keyboard stack always has at least one pending IRP in the queue of the Kbdclass driver. The mouse stack behaves the same way.

 kd> !devobj 8181be30
 Device object (8181be30) is for:
  KeyboardClass0 \Driver\Kbdclass DriverObject 818372b0
 Current Irp 815a2e68 RefCount 0 Type 0000000b Flags 00002044
 DevExt 8181bee8 DevObjExt 8181bfd8
 ExtensionFlags (0000000000)
 AttachedTo (Lower) 8181b020 \Driver\i8042prt
 Device queue is busy -- Queue empty.

 kd> !irp 815a2e68 1
 Irp is active with 6 stacks 6 is current (= 0x815a2f8c)
  No Mdl System buffer = 81664b48 Thread 8171bca0:  Irp stack trace.
 Flags = 00000970
 ThreadListEntry.Flink = 8171beac
 ThreadListEntry.Blink = 8171beac
 IoStatus.Status = 00000103
 IoStatus.Information = 00000000
 RequestorMode = 00000000
 Cancel = 00
 CancelIrql = 0
 ApcEnvironment = 00
 UserIosb = e28b1028
 UserEvent = 00000000
 Overlay.AsynchronousParameters.UserApcRoutine = a0063334
 Overlay.AsynchronousParameters.UserApcContext = e28b1008
 Overlay.AllocationSize = e28b1008 - a0063334
 CancelRoutine = f3ec82e0
 UserBuffer = e28b1068
 &Tail.Overlay.DeviceQueueEntry = 0006da94
 Tail.Overlay.Thread = 8171bca0
 Tail.Overlay.AuxiliaryBuffer = 00000000
 Tail.Overlay.ListEntry.Flink = 00000000
 Tail.Overlay.ListEntry.Blink = 00000000
 Tail.Overlay.CurrentStackLocation = 815a2f8c
 Tail.Overlay.OriginalFileObject = 8171aa08
 Tail.Apc = 00000000
 Tail.CompletionKey = 00000000
      cmd  flg cl Device   File     Completion-Context
  [  0, 0]   0  0 00000000 00000000 00000000-00000000
 
                         Args: 00000000 00000000 00000000 00000000
  [  0, 0]   0  0 00000000 00000000 00000000-00000000
 
                         Args: 00000000 00000000 00000000 00000000
  [  0, 0]   0  0 00000000 00000000 00000000-00000000
 
                         Args: 00000000 00000000 00000000 00000000
  [  0, 0]   0  0 00000000 00000000 00000000-00000000
 
                         Args: 00000000 00000000 00000000 00000000
  [  0, 0]   0  0 00000000 00000000 00000000-00000000
 
                         Args: 00000000 00000000 00000000 00000000
 >[  3, 0]   0  1 8181be30 8171aa08 00000000-00000000    pending
                \Driver\Kbdclass
                         Args: 00000078 00000000 00000000 00000000

As you can see, the Kbdclass driver has an incomplete IRP of type IRP_MJ_READ (number 3 in square brackets). The cl column shows the contents of the IO_STACK_LOCATION.Control field. In this case, this is the SL_PENDING_RETURNED flag set by calling the IoMarkIrpPending macro. The Args string is the contents of the nested IO_STACK_LOCATION.Read structure. The buffer size (78h) is sufficient for exactly 10 KEYBOARD_INPUT_DATA structures. The buffer itself is located at System buffer = 81664b48. Also notice the line CancelRoutine = f3ec82e0. This is the address of the cancel routine owned by the Kbdclass driver. IRP cancellation is another large and relatively complex topic (which I have no plans to cover). We'll talk about this a little later.

This pending IRP naturally belongs to the RawInputThread.

 kd> !thread 8171bca0
 THREAD 8171bca0  Cid a4.bc  Teb: 00000000  Win32Thread: e28ae5a8 WAIT: (WrUserRequest) KernelMode Alertable
     8171bf20  SynchronizationEvent
     8171bc08  SynchronizationEvent
     8171bbc8  NotificationTimer
     8171bc48  SynchronizationEvent
 IRP List:
     815a2e68: (0006,0148) Flags: 00000970  Mdl: 00000000
 Not impersonating
 Owning Process 81736160
 Wait Start TickCount    57881
 Context Switch Count    18896
 UserTime                  0:00:00.0000
 KernelTime                0:00:00.0070
 Start Address win32k!RawInputThread (0xa00ad1aa)
 Stack Init bfd1d000 Current bfd1caf0 Base bfd1d000 Limit bfd1a000 Call 0
 Priority 19 BasePriority 13 PriorityDecrement 0 DecrementCount 0

When we install the filter, the IRPs will be sent to us first. Because An IRP of type IRP_MJ_READ is actually a request to read data, then when it goes down the stack, its buffer is naturally empty. The read data will be contained in the buffer only after the completion of the IRP. In order for them (data) to be seen, the filter must install a completion routine in each IRP (more precisely, in its own stack block). Because the IRP in the queue of the Kbdclass driver was sent before we install the filter, then it does not contain our completion procedure, which means that we will not be able to see the code of the key that will be pressed immediately after the filter is installed. When the key is pressed, the pending IRP will complete and RawInputThread will send a new IRP of type IRP_MJ_READ. We have already intercepted this and all subsequent IRPs and set the completion procedure. When the key is released, we will read its code in the completion routine. That is why the filter does not see the moment when the first key is pressed, and in the monitor you will always see for the first pressed key only its break code (release of the key). Then the filter intercepts all keystrokes and releases of any keys.

Before we move on to the driver code, there are a few more things to deal with.



Spin lock

Since IRP processing is inherently asynchronous, often "the right hand does not know what the left is doing." Therefore, in every more or less serious driver, you have to use a synchronization mechanism called "mutually exclusive access". In the thirteenth part of the series - "Basic Technique. Synchronization: Mutual Access", we have already seen how to organize mutually exclusive access using a mutex. Therefore, I am silent about the macros MUTEX_INIT, MUTEX_ACQUIRE and MUTEX_RELEASE, which we will use again.

Synchronization using a mutex is convenient, but, unfortunately, it has one drawback: as you know, you cannot expect objects at IRQL equal to DISPATCH_LEVEL or higher. To organize mutually exclusive access to IRQL up to DISPATCH_LEVEL, there is a mechanism called "spin lock".

 KfAcquireSpinLock proc                   ; Single processor spin lock capture HAL
     xor     eax, eax
     mov     al, ds:0FFDFF024h            ; al = KPCR.Irql
     mov     byte ptr ds:0FFDFF024h, 2    ; KPCR.Irql = DISPATCH_LEVEL
     ret
 KfAcquireSpinLock endp
 
 KfAcquireSpinLock proc                   ; Multiprocessor spin locking HAL
     mov     edx, ds:0FFFE0080h           ; edx = APIC[TASK PRIORITY REGISTER]
     mov     dword ptr ds:0FFFE0080h, 41h ; APIC[TASK PRIORITY REGISTER] = DPC VECTOR
     shr     edx, 4
     movzx   eax, ds:HalpVectorToIRQL[edx]; OldIrql
 
 trytoacquire:
     lock bts dword ptr [ecx], 0          ; Let's try atomic locking.
     jb      spin                         ; If the lock is occupied, we will rotate the loop.
     ret                                  ; We captured a lock and returned to IRQL,
                                          ; CPU locked
     align 4
 
 spin:                                    ; Lock.Let's rotate the loop.
     test    dword ptr [ecx], 1           ; Check locking.
     jz      trytoacquire                 ; If the lock is empty, let's try again.
     pause                                ; If not, we continue the cycle.Special note
                                          ; For spin locking cycles.
                                          ; For details, see the "PAUSE-Spin Loop Hint"
                                          ; IA-32 Intel Architecture Software Developer's Manual
                                          ; Volume 2 : Instruction Set Reference.
     jmp     spin
 KfAcquireSpinLock endp

On a uniprocessor machine, capturing a spinlock is simply raising the IRQL to DISPATCH_LEVEL. As you know, there is no thread scheduling at this IRQL, and the thread that owns the processor will be executed until the IRQL is lowered. Since IRQL is an attribute of the processor, on a multiprocessor machine, simply raising the IRQL is not enough. it does not block threads running on other processors. Therefore, in multiprocessor HALs, the spin-lock is implemented somewhat more complicatedly and is a spin-lock in the true sense of the word (spin - to twist, twirl; spin the top). Those. if the lock is busy, the thread loops endlessly trying to acquire it. Moreover, since the loop is executed at IRQL = DISPATCH_LEVEL, there is no thread scheduling on this processor and the processor cannot do useful work. This is why spinlocks are much more time critical. Those. you need to release the spinlock as quickly as possible. DDK even defines a maximum time interval of 25 microseconds during which a spinlock can be held. In this sense, mutexes and other wait objects are less demanding because a thread waiting for a busy object is simply excluded from scheduling, and the processor gets another thread ready to run.

And since we had to touch on spin-blocking, remember a few classic rules. First, capturing a spinlock, as we just found out, raises the IRQL to DISPATCH_LEVEL, which means that only non-swap memory is available to us and the code itself must also be in non-swap memory. Second, recapturing the same spinlock will naturally result in a complete deadlock. Thirdly, you cannot capture a spinlock at IRQL higher than DISPATCH_LEVEL, because this actually means a clear lowering of the IRQL, which will inevitably lead to BSOD. Fourth, if you need to acquire two (or more) spinlocks, then all threads must do it in the same order. Otherwise, mutual blocking is possible. For example, two threads need to acquire locks A and B. If they do this in a different order, then perhaps

 LOCK_ACQUIRE MACRO lck:REQ
     mov ecx, lck
     fastcall KfAcquireSpinLock, ecx
 ENDM
 
 LOCK_RELEASE MACRO lck:REQ, NewIrql:REQ
 
     mov ecx, lck
     mov dl, NewIrql
 
     .if dl == DISPATCH_LEVEL
         fastcall KefReleaseSpinLockFromDpcLevel, ecx
     .else
         and edx, 0FFh
         fastcall KfReleaseSpinLock, ecx, edx
     .endif
 ENDM

We will use macros to capture a spinlock. These are simplified versions. In the LOCK_RELEASE macro, we use a small optimization: if we were before capturing the spinlock on IRQL = DISPATCH_LEVEL, then it is more profitable to call KefReleaseSpinLockFromDpcLevel instead of KfReleaseSpinLock, because there is no need to change IRQL and on a uniprocessor machine KefReleaseSpinLockFromDpcLevel is an "empty" function.

 KeReleaseSpinLockFromDpcLevel proc
     retn 4
 KeReleaseSpinLockFromDpcLevel endp

Something similar (I mean optimization) can be done for the LOCK_ACQUIRE macro. You only need to find out the current IRQL and if it is equal to DISPATCH_LEVEL, then call KeAcquireSpinLockAtDpcLevel, which (on a uniprocessor machine) also executes the ret instruction.

 KeAcquireSpinLockAtDpcLevel proc
     retn 4
 KeAcquireSpinLockAtDpcLevel endp

I did not optimize the LOCK_ACQUIRE macro, because wrote these macros a long time ago and used it several times, besides it is not clear which is faster: just call KfAcquireSpinLock or find out the IRQL and, depending on its value, call KeAcquireSpinLockAtDpcLevel. Therefore, I didn抰 think anything over and left everything as it is. If you have an irrepressible desire to optimize, study hal.dll / halmps.dll and ntoskrnl.exe / ntkrnlmp.exe and optimize for health.

For completeness, we must also add that there is a KeAcquireSpinLockRaiseToSynch function that raises the IRQL when capturing a lock to CLOCK2_LEVEL (28).



DriverEntry procedure

Now let's get down to our actual filter. As I said, this is a non-Pnp driver. There is a lot of code and I will not give it in full (see the archive for the article).

     invoke IoCreateDevice, pDriverObject, 0, addr g_usControlDeviceName, \
                             FILE_DEVICE_UNKNOWN, 0, TRUE, addr g_pControlDeviceObject

This time our driver will manage two objects: a filter device object and a control device object. The filter device object will be attached to the keyboard stack, and all keyboard IRPs will pass through it. By means of the "control device" object, the control program will send the necessary commands to the driver: "connect the filter", "disable the filter", "transmit the intercepted data". For now, we only need a control device. This object will be named so that the control program can access it. We do not want to work with several clients at the same time. Therefore, we will create an exclusive object by specifying TRUE in the Exclusive parameter. In this case, the object manager will only allow one object handle to be created. Unfortunately, this simple method is not very reliable, and you can still open an object by a relative path, i.e. by opening the "\ Device" directory and passing its handle in the RootDirectory parameter of the InitializeObjectAttributes macro. DDK generally says that the Exclusive parameter is reserved. So we'll add some extra handling for the IRP_MJ_CREATE and IRP_MJ_CLOSE requests.

             invoke ExAllocatePool, NonPagedPool, sizeof NPAGED_LOOKASIDE_LIST
             .if eax != NULL

                 mov g_pKeyDataLookaside, eax

                 invoke ExInitializeNPagedLookasideList, g_pKeyDataLookaside, \
                                         NULL, NULL, 0, sizeof KEY_DATA_ENTRY, 'ypSK', 0

Allocate memory for the associative list and initialize it. From this list, we will allocate memory for instances of our own defined KEY_DATA_ENTRY structure.

 KEY_DATA STRUCT
     dwScanCode  DWORD   ?
     Flags       DWORD   ?
 KEY_DATA ENDS
 PKEY_DATA typedef ptr KEY_DATA
 
 KEY_DATA_ENTRY STRUCT
     ListEntry   LIST_ENTRY  <>
     KeyData     KEY_DATA    <>
 KEY_DATA_ENTRY ENDS

Instances of this structure will store data on intercepted keystrokes / releases and we will store them (structure instances) in a doubly linked list. In the seventh part of the cycle - "Basic Technique: Working with Memory. Using Associative Lists", we examined in some detail both the look-aside list and the doubly linked list. I am sure that many people just missed this article;) If so, then you will have to read it now, because I will not repeat myself, and without this material, something may not be clear. The only difference is that we are going to use a non-paged associative list for now. The functions ExAllocateFromNPagedLookasideList and ExFreeToNPagedLookasideList for working with an unpumped associative list are implemented in the DDK as macros, as opposed to just the functions for a swapped associative list. Unfortunately, due to the limitations of the masm macro language, I had to implement them as _ExAllocateFromNPagedLookasideList and _ExFreeToNPagedLookasideList functions. As you might guess, we needed an unpumpable associative list because we will work with it at IRQL = DISPATCH_LEVEL.

                 InitializeListHead addr g_KeyDataListHead

The global variable g_KeyDataListHead is the head of the doubly linked list of KEY_DATA_ENTRY structures.

                 invoke KeInitializeSpinLock, addr g_KeyDataSpinLock

We need a spin lock to organize exclusive access to the list of KEY_DATA_ENTRY structures. We cannot use synchronization objects, for example, a mutex, because we will refer to the list at IRQL = DISPATCH_LEVEL.

                 invoke KeInitializeSpinLock, addr g_EventSpinLock

This spinlock will help us organize exclusive access to the g_pEventObject variable, which will store the pointer to the event object. This object will be used to notify the control program about new data (For more details, see part 14 "Basic technique. Synchronization: Using the" event "object for interaction between the driver and the control program").

                 MUTEX_INIT g_mtxCDO_State

With the help of this mutex, we will be able to exclusively execute some parts of the code.

                 mov ecx, IRP_MJ_MAXIMUM_FUNCTION + 1
                 .while ecx
                     dec ecx
                     mov [eax].MajorFunction[ecx*(sizeof PVOID)], offset DriverDispatch
                 .endw

We fill all the elements of the array of pointers to the driver dispatching procedures with the address of the only DriverDispatch procedure. This procedure will distribute requests between the filter and the controller. The control unit will receive only three requests from the control program: IRP_MJ_CREATE, IRP_MJ_CLOSE, and IRP_MJ_DEVICE_CONTROL. But the filter device can receive any request. it plugs into a pre-existing stack through which any type of IRP can circulate. Often, the entire IRP spectrum passing through the filtered stack is not known at all. Only certain types of IRPs have to be filtered, but if a filter receives a request that it does not care about, it must route it down the stack. This is why we have to fill the entire MajorFunction array.



DriverDispatch Procedure

Through our driver, requests go to two objects: the control device and the filter (if it is connected). All IRPs fall into the general dispatch procedure DriverDispatch.

     IoGetCurrentIrpStackLocation pIrp

     movzx eax, (IO_STACK_LOCATION PTR [eax]).MajorFunction
     mov dwMajorFunction, eax

     mov eax, pDeviceObject
     .if eax == g_pFilterDeviceObject

         mov eax, dwMajorFunction
         .if eax == IRP_MJ_READ
             invoke FiDO_DispatchRead, pDeviceObject, pIrp
             mov status, eax
         .elseif eax == IRP_MJ_POWER
             invoke FiDO_DispatchPower, pDeviceObject, pIrp
             mov status, eax
         .else
             invoke FiDO_DispatchPassThrough, pDeviceObject, pIrp
             mov status, eax
         .endif

     .elseif eax == g_pControlDeviceObject

         mov eax, dwMajorFunction
         .if eax == IRP_MJ_CREATE
             invoke CDO_DispatchCreate, pDeviceObject, pIrp
             mov status, eax
         .elseif eax == IRP_MJ_CLOSE
             invoke CDO_DispatchClose, pDeviceObject, pIrp
             mov status, eax
         .elseif eax == IRP_MJ_DEVICE_CONTROL
             invoke CDO_DispatchDeviceControl, pDeviceObject, pIrp
             mov status, eax
         .else

             mov ecx, pIrp
             mov (_IRP PTR [ecx]).IoStatus.Status, STATUS_INVALID_DEVICE_REQUEST
             and (_IRP PTR [ecx]).IoStatus.Information, 0

             fastcall IofCompleteRequest, ecx, IO_NO_INCREMENT

             mov status, STATUS_INVALID_DEVICE_REQUEST
    
         .endif
    
     .else

         mov ecx, pIrp
         mov (_IRP PTR [ecx]).IoStatus.Status, STATUS_INVALID_DEVICE_REQUEST
         and (_IRP PTR [ecx]).IoStatus.Information, 0

         fastcall IofCompleteRequest, ecx, IO_NO_INCREMENT

         mov status, STATUS_INVALID_DEVICE_REQUEST

     .endif

     mov eax, status
     ret

Using the global pointers g_pFilterDeviceObject and g_pControlDeviceObject, we determine which object the request came to and, depending on the type of request, call the appropriate procedure. Our controller only handles three types of requests: IRP_MJ_CREATE, IRP_MJ_CLOSE, and IRP_MJ_DEVICE_CONTROL. But we are obliged to process all requests to the filter. The processing would be to simply pass the IRP to the downstream driver in the FiDO_DispatchPassThrough procedure. Requests of the IRP_MJ_READ type contain key codes, therefore, processing will be special for this type of requests. The IRP of the IRP_MJ_POWER type simply requires specific processing, and therefore is allocated in a separate procedure. If we, suddenly (which cannot be), received a request for a device unknown to us, we complete it with an error code, because it is not clear what else can be done with this IRP.

First, let's look at the processing of requests to the control device.



CDO_DispatchCreate Procedure

     .while TRUE

         invoke RemoveEntry, addr KeyData
         .break .if eax == 0

     .endw

The driver and the control program are built in such a way that the control program can be unloaded and reloaded when the driver is already running and the filter is connected. It may happen that the list g_KeyDataListHead is not empty. If you carefully analyze the course of possible events after reading the entire article, it will become clear that the list may contain one KEY_DATA_ENTRY structure corresponding to the key code pressed immediately after the control program terminated incorrectly. The above loop empties the possibly non-empty g_KeyDataListHead list.

     MUTEX_ACQUIRE g_mtxCDO_State

     .if g_fCDO_Opened

         mov status, STATUS_DEVICE_BUSY

     .else

         mov g_fCDO_Opened, TRUE
        
         mov status, STATUS_SUCCESS

     .endif

     MUTEX_RELEASE g_mtxCDO_State

If the handle to the "control device" object is already open, do not allow reopening. This ensures we have only one client (the rest will receive the STATUS_DEVICE_BUSY code), and capturing the mutex ensures that the CDO_DispatchClose procedure does not close the handle at the same time and does not reset the g_fCDO_Opened flag.



CDO_DispatchClose Procedure

     and g_fSpy, FALSE

If the client disconnects, then there is no need to watch the keyboard - FiDO_DispatchRead should no longer set the termination routine.

     MUTEX_ACQUIRE g_mtxCDO_State
                
     .if ( g_pFilterDeviceObject == NULL )

         .if g_dwPendingRequests == 0

             mov eax, g_pDriverObject
             mov (DRIVER_OBJECT PTR [eax]).DriverUnload, offset DriverUnload

         .endif

     .endif

If the g_pFilterDeviceObject variable is empty, then obviously there is no filter either. If, in addition, we do not have any incomplete IRPs, the completion of which would lead to a call to our ReadComplete completion routine, which is in the driver's body, then we can allow it to be unloaded. If the filter still exists, the driver remains unloadable. Before exiting, the control program asks the driver to disable and remove the filter. But there are situations when the driver cannot do this. For example, if someone is connected to the stack on top of us, disabling the filter will "break the stack". The control program may simply forget to turn off the filter, or an exception may occur in it and the device handle is automatically closed by the system. This, of course, is not about our management program, which (I hope) did everything right. This refers to the control program in general, i.e. general principle. Finally, the user can log off the system session, and all user processes are forcibly terminated. In any case, as I said, the driver and the control program are built in such a way that the control program can be re-run.

     and g_fCDO_Opened, FALSE    

     MUTEX_RELEASE g_mtxCDO_State

Because our only client has just "left", we reset the g_fCDO_Opened flag.



CDO_DispatchDeviceControl Procedure

             MUTEX_ACQUIRE g_mtxCDO_State

             mov edx, [esi].AssociatedIrp.SystemBuffer
             mov edx, [edx]

             mov ecx, ExEventObjectType
             mov ecx, [ecx]
             mov ecx, [ecx]
    
             invoke ObReferenceObjectByHandle, edx, EVENT_MODIFY_STATE, ecx, \
                                         UserMode, addr pEventObject, NULL
             .if eax == STATUS_SUCCESS

Upon receipt of the control code IOCTL_KEYBOARD_ATTACH from the control program, we capture the mutex and check the handle of the "event" object passed to us. We have already done this in Process Monitor (see part 14). If this is really an "event" object, then we have two options: either we have to create a filter and connect it to the keyboard stack, or the filter already exists and is connected.

                 .if !g_fFiDO_Attached

                     invoke KeyboardAttach
                     mov [esi].IoStatus.Status, eax

If the filter is not connected, we will assume that it has not been created yet. The KeyboardAttach procedure will do whatever is necessary by returning the appropriate code.

                     .if eax == STATUS_SUCCESS

                         mov eax, pEventObject
                         mov g_pEventObject, eax

                         mov g_fFiDO_Attached, TRUE
                         mov g_fSpy, TRUE
        
                     .else
                         invoke ObDereferenceObject, pEventObject
                     .endif

If the connection was successful, store the pointer to the "event" object in the global variable g_pEventObject and set the g_fFiDO_Attached and g_fSpy flags. Although the filter is already connected, blocking access to the g_pEventObject variable is not required in this case, since the g_fSpy flag is set after the g_pEventObject variable is initialized, and until then the FiDO_DispatchRead procedure will not set the completion procedure, which means that ReadComplete will not be called at all and no one will touch g_pEventObject except us.

                 .else

                     LOCK_ACQUIRE g_EventSpinLock
                     mov bl, al

If the filter is already connected, it is necessary to block access to the g_pEventObject variable, since it can be accessed by our ReadComplete completion routine. Spin lock is required due to ReadComplete running at IRQL = DISPATCH_LEVEL.

                     mov eax, g_pEventObject
                     .if eax != NULL
                         and g_pEventObject, NULL
                         invoke ObDereferenceObject, eax
                     .endif

                     mov eax, pEventObject
                     mov g_pEventObject, eax

                     LOCK_RELEASE g_EventSpinLock, bl

Just in case, if g_pEventObject contains a pointer to an event object, decrease the reference count and put a pointer to a new event object there. A little explanation is required here. At first glance, this code may seem meaningless. The fact is that in the previous examples, for simplicity, we assumed the correct behavior of the driver control program. But, ideally, the driver should be unsinkable, even if its own control program or someone else does some unpredictable action. The DDK tools for testing drivers even include a special utility Device Path Exerciser (dc2.exe), which, among other tests, sends a huge number of control codes with deliberately incorrect parameters to the driver. If the control program sends IOCTL_KEYBOARD_ATTACH twice to the driver, then

         MUTEX_ACQUIRE g_mtxCDO_State

When we receive the control code IOCTL_KEYBOARD_DETACH from the control program, we try to disable the filter, also under the protection of a mutex.

         .if g_fFiDO_Attached

             and g_fSpy, FALSE

             invoke KeyboardDetach
             mov [esi].IoStatus.Status, eax

If the filter is connected, we reset the g_fSpy flag so that FiDO_DispatchRead no longer installs the termination routine, and we try to disable and remove the filter.

             .if eax == STATUS_SUCCESS
                 mov g_fFiDO_Attached, FALSE
             .endif

If the filter is successfully disabled, clear the corresponding flag. If it was not possible to disable the filter, this flag will remain in the cocked state, which will give us the opportunity to do everything correctly the next (possible) receipt of IOCTL_KEYBOARD_ATTACH.

             LOCK_ACQUIRE g_EventSpinLock
             mov bl, al

             mov eax, g_pEventObject
             .if eax != NULL
                 and g_pEventObject, NULL
                 invoke ObDereferenceObject, eax
             .endif

             LOCK_RELEASE g_EventSpinLock, bl

Under the protection of a spinlock, remove the reference to the "event" object.

             invoke FillKeyData, [esi].AssociatedIrp.SystemBuffer, \
                         [edi].Parameters.DeviceIoControl.OutputBufferLength

When we receive the control code IOCTL_GET_KEY_DATA from the control program, we copy the KEY_DATA structures we currently have to the user buffer. I will not analyze the FillKeyData procedure, as well as AddEntry and RemoveEntry, because If you've read Part 7 of the "Using Associative Lists" series, their contents shouldn't be too complex, and see the DDK for details on the KEYBOARD_INPUT_DATA structure.



KeyboardAttach Procedure

     .if ( g_pFilterDeviceObject != NULL )

         mov status, STATUS_SUCCESS

If the variable g_pFilterDeviceObject is nonzero, obviously it contains a pointer to a filter device object and it is probably already attached to the stack.

     .else

If there is no filter, let's create it.

         mov eax, g_pControlDeviceObject
         mov ecx, (DEVICE_OBJECT PTR [eax]).DriverObject

         invoke IoCreateDevice, ecx, sizeof FiDO_DEVICE_EXTENSION, NULL, \
                     FILE_DEVICE_UNKNOWN, 0, FALSE, addr g_pFilterDeviceObject
         .if eax == STATUS_SUCCESS

The filter device object must be unnamed so that it cannot be opened directly by name. Because the filter belongs to the stack, but is an imported object, it is clearly not for it to decide whether to allow the handle to be opened or not. Let the downstream drivers deal with this. In fact, this is not always true. In the case of the keyboard stack, the high-level filter driver Kbdclass has a named filter device object KeyboardClassX and it is this that handles the IRP_MJ_CREATE request. The second parameter to the IoCreateDevice function defines the size of the device extension's additional memory, which is described by the hypothetical DEVICE_EXTENSION structure. Hypothetical in the sense that there is no such structure. You decide for yourself what you need to place in the additional memory area of ​​the "device" object and define the structure yourself. The device extension immediately follows the DEVICE_OBJECT structure and is zero-initialized. In our case, this is the FiDO_DEVICE_EXTENSION structure. Using the device extension allows a driver to create as many device objects as desired and store all data related to them in those objects themselves.

             invoke IoGetDeviceObjectPointer, addr g_usTargetDeviceName, FILE_READ_DATA, \
                                        addr pTargetFileObject, addr pTargetDeviceObject
             .if eax == STATUS_SUCCESS

I hope you remember that the IoGetAttachedDevice function always returns a pointer to the device object at the top of the stack. We use the IoGetDeviceObjectPointer function to get a pointer to the top of the stack using the previously known name of one of the device objects belonging to the stack. PnP drivers, in this sense, are easier, because the PnP manager provides them with a pointer to the root of the stack ?the physical device object. Those. what i mean is that in order to connect to the stack you need a pointer to any stack object. How you get it doesn't matter.

                 mov eax, g_pDriverObject
                 and (DRIVER_OBJECT PTR [eax]).DriverUnload, NULL

Because now we connect to the stack, then an IRP can go through us. After setting the g_fSpy flag, we will set the completion routine in them. We do not know when these IRPs will complete, but we do know that until then the driver cannot be unloaded, because the completion routine is in the body of the driver. Therefore, the easiest way is to simply make the driver unloadable.

 PDEVICE_OBJECT
   IoGetAttachedDevice(
     IN PDEVICE_OBJECT pDeviceObject
     )
 {

     while  pDeviceObject->AttachedDevice  {

         pDeviceObject = pDeviceObject->AttachedDevice
     }
 
     return pDeviceObject
 }


 PDEVICE_OBJECT
   IoAttachDeviceToDeviceStack(
     IN PDEVICE_OBJECT pSourceDevice,
     IN PDEVICE_OBJECT pTargetDevice
     )
 {
     PDEVICE_OBJECT     pTopMostDeviceObject
     PDEVOBJ_EXTENSION  pSourceDeviceExtension

     pSourceDeviceExtension = pSourceDevice->DeviceObjectExtension

     ExAcquireSpinLock( &IopDatabaseLock, ... )

     pTopMostDeviceObject = IoGetAttachedDevice( pTargetDevice )

     if  pTopMostDeviceObject->Flags & DO_DEVICE_INITIALIZING
            ||
         pTopMostDeviceObject->DeviceObjectExtension->ExtensionFlags &
         (DOE_UNLOAD_PENDING | DOE_DELETE_PENDING | DOE_REMOVE_PENDING | DOE_REMOVE_PROCESSED)  {

         pTopMostDeviceObject = NULL

     } else {

         pTopMostDeviceObject ->AttachedDevice = pSourceDevice

         pSourceDevice->AlignmentRequirement = pTopMostDeviceObject->AlignmentRequirement
         pSourceDevice->SectorSize           = pTopMostDeviceObject->SectorSize
         pSourceDevice->StackSize            = pTopMostDeviceObject->StackSize + 1


         if  pTopMostDeviceObject ->DeviceObjectExtension->ExtensionFlags & DOE_START_PENDING  {

             pSourceDevice->DeviceObjectExtension->ExtensionFlags |= DOE_START_PENDING
         }

         pSourceDeviceExtension->AttachedTo = pTopMostDeviceObject
     }
 
     ExReleaseSpinLock( &IopDatabaseLock, ... )
 
     return pTopMostDeviceObject
 }

First, the IoAttachDeviceToDeviceStack function receives a pointer to a DEVOBJ_EXTENSION structure (not to be confused with the optional DEVICE_EXTENSION structure). The ExtensionFlags field of this structure contains some flags of interest to the IoAttachDeviceToDeviceStack function. The I / O manager database is then locked and a pointer to the device object at the top of the stack is pushed into pTopMostDeviceObject. Because the I / O manager database is locked, the stack state will not change until the lock is released. If the device object has not yet been initialized, or the device or its driver is marked for deletion, or is already in a deletion state, the IoAttachDeviceToDeviceStack function refuses to attach a new object to the stack and returns NULL. Otherwise, in the fields of AttachedDevice and AttachedTo objects " the value returned from IoAttachDeviceToDeviceStack, if successful, is a pointer to the device object to which the filter was attached. And the filter is now the top of the stack and is the first to receive all the IRPs intended for this stack. At the beginning of the article, we found out that the raw input stream opens one of the keyboard stack objects and, using its handle (more precisely, the file object descriptor corresponding to the device object), sends it read requests. If the IRPs are destined for a device down the stack, how do they get into the filter? The I / O subsystem behaves similarly to the IoGetAttachedDevice and IoAttachDeviceToDeviceStack functions, in the sense that it uses the top-of-stack pointer as the destination for the IRP. For example, here's what the ZwReadFile function does.

 NTSTATUS
    ZwReadFile(
     IN HANDLE hFile,
     . . .
     )
 {
 
     PFILE_OBJECT    pFileObject
     PDEVICE_OBJECT  pDeviceObject
 
     ObReferenceObjectByHandle( hFile, ... &pFileObject ... )
 
     pDeviceObject = IoGetRelatedDeviceObject( pFileObject )
 
     . . .
 }

The IoGetRelatedDeviceObject function (see the source code in the previous article) returns a pointer to the topmost device object on the stack. If the IRP is generated by the driver, so to speak, manually (see the source code of the QueryPnpDeviceState procedure in the previous article), then it will be sent directly to the target device and it is impossible to intercept such a request using a filter, unless of course the filter is below the stack.

                 invoke IoAttachDeviceToDeviceStack, g_pFilterDeviceObject, pTargetDeviceObject
                 .if eax != NULL

                     mov edx, eax

                     mov ecx, g_pFilterDeviceObject
                     mov eax, (DEVICE_OBJECT ptr [ecx]).DeviceExtension
                     assume eax:ptr FiDO_DEVICE_EXTENSION
                     mov [eax].pNextLowerDeviceObject, edx
                     push pTargetFileObject
                     pop [eax].pTargetFileObject
                     assume eax:nothing

If IoAttachDeviceToDeviceStack connected us to the stack, fill in the FiDO_DEVICE_EXTENSION structure. There we put a pointer to the object we connected to and a pointer to the "file" object associated with the target device object (see the previous article for details). When disabled, we will have to call ObDereferenceObject on this file object.

                     assume edx:ptr DEVICE_OBJECT
                     assume ecx:ptr DEVICE_OBJECT

                     mov eax, [edx].DeviceType
                     mov [ecx].DeviceType, eax

                     mov eax, [edx].Flags
                     and eax, DO_DIRECT_IO + DO_BUFFERED_IO + DO_POWER_PAGABLE
                     or [ecx].Flags, eax

湾耜铍?綦嚆钼 ?磬?钺牝?翳朦蝠?镳桎弪? 钺眍忤螯 襦祛耱?蝈朦眍. 腻腩 ?蝾? 黩?潆 滂耧弪麇疣 忖钿?恹忸溧 磬?钺牝 漕腈屙 恹汶溴螯 蜞赕?赅?钺牝, ?觐蝾痤祗 禧 镱潢膻麒腓顸. 袜镳桁屦, 綦嚆 DO_BUFFERED_IO 泐忸痂?滂耧弪麇痼 忖钿?恹忸溧 ?蝾? 黩?镳?铒屦圉??黩屙?/玎镨耔 铐 漕腈屙 觐镨痤忄螯 镱朦珙忄蝈朦耜桢 狍翦瘥 ?耔耱屐眍?噤疱耥铄 镳铖蝠囗耱忸, ?? 桉镱朦珙忄螯 戾蝾?忖钿?恹忸溧 METHOD_BUFFERED. 噪嚆?DO_DIRECT_IO ?DO_BUFFERED_IO, 羼蝈耱忮眄? 忡噼祛桉觌帼. 疹? 磬?玎疣礤?桤忮耱睇 綦嚆? 觐蝾瘥?桉镱朦珞弪 篑蝠铋耱忸 KeyboardClass0, ?桁屙眍 DO_BUFFERED_IO ?DO_POWER_PAGABLE, 禧 桉镱朦珞屐 犷脲?钺??箜桠屦襦朦睇?戾踵龛珈.

                     and [ecx].Flags, not DO_DEVICE_INITIALIZING

The IoCreateDevice function creates a device object with the DO_DEVICE_INITIALIZING flag set. Until now, we have not touched on this point because we created devices only in the DriverEntry procedure. The fact is that upon exiting DriverEntry, the I / O dispatcher (in the IopReadyDeviceObjects function) itself clears this flag in all device objects created by the driver. If we create a device not in DriverEntry, we will have to clear the DO_DEVICE_INITIALIZING flag ourselves, otherwise no one will be able to connect to an uninitialized object, as you just saw in the IoAttachDeviceToDeviceStack code. Also this flag is checked for some other operations.



KeyboardDetach procedure

     .if g_pFilterDeviceObject != NULL

         mov eax, g_pFilterDeviceObject
         or (DEVICE_OBJECT ptr [eax]).Flags, DO_DEVICE_INITIALIZING

Before disconnecting from the stack, let's see if we are at the very top of it or if someone has already connected to us. We cannot block the I / O manager database, but we can prevent new connections until we check if there is anyone above us.

 PDEVICE_OBJECT
   IoGetAttachedDeviceReference(
     IN PDEVICE_OBJECT pDeviceObject
     )
 {
     ExAcquireSpinLock( &IopDatabaseLock, ... )

     pDeviceObject = IoGetAttachedDevice( pDeviceObject )
     ObReferenceObject( pDeviceObject )

     ExReleaseSpinLock( &IopDatabaseLock, ... )

     return pDeviceObject
 }

Everything should be clear here.

         invoke IoGetAttachedDeviceReference, g_pFilterDeviceObject
         mov pTopmostDeviceObject, eax

         .if eax != g_pFilterDeviceObject

             mov eax, g_pFilterDeviceObject
             and (DEVICE_OBJECT ptr [eax]).Flags, not DO_DEVICE_INITIALIZING

If the pointer returned by the IoGetAttachedDeviceReference function is not a pointer to our filter, then someone is connected to us. In this case, we will not disconnect from the stack and we will clear the DO_DEVICE_INITIALIZING flag. If we call IoDetachDevice, we will simply "break the stack", since IoDetachDevice does not do any checks. Disconnecting a device object from the stack consists of simply clearing the corresponding pointers in the associated objects.

 VOID
   IoDetachDevice(
     IN OUT PDEVICE_OBJECT pTargetDeviceObject
     )
 {
     PDEVICE_OBJECT    pDeviceToDetach
     PDEVOBJ_EXTENSION pDeviceToDetachExtension

     ExAcquireSpinLock( &IopDatabaseLock, ... )

     pDeviceToDetach          = pTargetDeviceObject->AttachedDevice
     pDeviceToDetachExtension = pDeviceToDetach->DeviceObjectExtension

     pDeviceToDetachExtension->AttachedTo = NULL
     pTargetDeviceObject->AttachedDevice  = NULL

     if  pTargetDeviceObject->DeviceObjectExtension->ExtensionFlags &
         (DOE_UNLOAD_PENDING | DOE_DELETE_PENDING | DOE_REMOVE_PENDING)
             &&
         pTargetDeviceObject->ReferenceCount == 0
     {
 
         // Complete Unload Or Delete
     }
 
     ExReleaseSpinLock( &IopDatabaseLock, ... )
 
 }

By unmounting the device from the stack, the IoDetachDevice checks to see if it is pending deletion, and its driver is unloading. And if so and the object reference count is zero, it initiates deferred operations.

         .else           

             mov eax, g_pFilterDeviceObject
             mov eax, (DEVICE_OBJECT ptr [eax]).DeviceExtension
             mov ecx, (FiDO_DEVICE_EXTENSION ptr [eax]).pTargetFileObject

             fastcall ObfDereferenceObject, ecx

             mov eax, g_pFilterDeviceObject
             mov eax, (DEVICE_OBJECT ptr [eax]).DeviceExtension
             mov eax, (FiDO_DEVICE_EXTENSION ptr [eax]).pNextLowerDeviceObject

             invoke IoDetachDevice, eax
            
             mov status, STATUS_SUCCESS

If we are at the top of the stack, decrement the reference count of the file object associated with the device object and detach from the stack. It makes no sense to restore the DO_DEVICE_INITIALIZING flag, since now we will remove the filter.

             mov eax, g_pFilterDeviceObject
             and g_pFilterDeviceObject, NULL
             invoke IoDeleteDevice, eax

We delete the "device-filter" object, but the driver is still unloadable because we can have a pending IRP containing a pointer to our completion routine.

         .endif

         invoke ObDereferenceObject, pTopmostDeviceObject

The IoGetAttachedDeviceReference function, unlike the IoGetAttachedDevice function, increments the reference count in the object that the pointer returns to. This will ensure that the object is not deleted. If we were on the top of the stack, then we increased the reference count in our filter device object and IoDeleteDevice will not be able to delete it.

 VOID
   IoDeleteDevice(
     IN PDEVICE_OBJECT pDeviceObject
     )
 {
     ...

     ExAcquireSpinLock( &IopDatabaseLock, ... )

     pDeviceObject->DeviceObjectExtension->ExtensionFlags |= DOE_DELETE_PENDING

     if  pDeviceObject->ReferenceCount == 0  {

         // Complete Unload Or Delete
     }
 
     ExReleaseSpinLock( &IopDatabaseLock, ... )
 }

But IoDeleteDevice will add the DOE_DELETE_PENDING flag to indicate that the device object is pending deletion. When we call ObDereferenceObject, the reference count is 0, the object manager will see that the object should be deleted and take the appropriate steps.

Now let's look at the procedures for processing requests to the filter.



FiDO_DispatchPower Procedure

     invoke PoStartNextPowerIrp, pIrp

     IoSkipCurrentIrpStackLocation pIrp
	
     mov eax, pDeviceObject
     mov eax, (DEVICE_OBJECT ptr [eax]).DeviceExtension
     mov eax, (FiDO_DEVICE_EXTENSION ptr [eax]).pNextLowerDeviceObject

     invoke PoCallDriver, eax, pIrp

IRPs of type IRP_MJ_POWER are handled differently from all other IRP types.

We examined the IoCopyCurrentIrpStackLocationToNext macro in detail in the last article (we will use it in the FiDO_DispatchRead procedure). The IoSkipCurrentIrpStackLocation macro is much simpler.

 IoSkipCurrentIrpStackLocation MACRO pIrp:REQ
     mov eax, pIrp
     inc (_IRP PTR [eax]).CurrentLocation
     add (_IRP PTR [eax]).Tail.Overlay.CurrentStackLocation, sizeof IO_STACK_LOCATION
 ENDM

From the last article, you should remember that the IoCallDriver function shifts the current stack block one position down before calling the driver dispatch routine.

     Irp->CurrentLocation--
     pIrp->Tail.Overlay.CurrentStackLocation -= sizeof(IO_STACK_LOCATION)

If you use the IoSkipCurrentIrpStackLocation macro first, it turns out that the stack block pointer will not change at all and the downstream driver will receive the same stack block as the driver that called IoCallDriver (PoCallDriver). Calling the IoSkipCurrentIrpStackLocation macro is just an optimization. Indeed, if we do not need to set up a completion routine, then calling the IoCopyCurrentIrpStackLocationToNext macro will copy our stack block into the downstream driver's stack block (the Control, CompletionRoutine and Context fields, as you remember, are not copied). That. the downstream driver will still receive the same parameters. By using the IoSkipCurrentIrpStackLocation macro instead of IoCopyCurrentIrpStackLocationToNext, we avoid unnecessary copying of stack blocks. But, I repeat, this can be done,



FiDO_DispatchPassThrough Procedure

     IoSkipCurrentIrpStackLocation pIrp

     mov eax, pDeviceObject
     mov eax, (DEVICE_OBJECT ptr [eax]).DeviceExtension
     mov eax, (FiDO_DEVICE_EXTENSION ptr [eax]).pNextLowerDeviceObject

     invoke IoCallDriver, eax, pIrp
     ret

Here we simply pass the IRP to the downstream driver.



FiDO_DispatchRead Procedure

     .if g_fSpy

Having received a request of the IRP_MJ_READ type addressed to the filter, we look to see if the g_fSpy flag is set. If so, then we need to establish a termination procedure.

         lock inc g_dwPendingRequests

Atomically increment the g_dwPendingRequests pending counter by one. When the IRP completes, the system will call our ReadComplete completion routine, it will read the keycode and decrement the g_dwPendingRequests counter. "Atomically" means that only one thread, even on a multiprocessor machine, can change the value of a variable, and the IRQL on which it is executed does not matter at all. Even if a thread running on a different processor tries at the same time (literally on an MP machine) to execute the same code, it will receive the value already updated by the first thread. This is accomplished by using the lock prefix. Upon seeing this prefix, the processor blocks the data bus while the instruction is being executed. Other processors will not be able to access this memory area at this moment and change it. Even if this region of memory is cached by multiple processors, the processor's cache coherency mechanism will come into play and the caches of other processors will be invalidated, causing the processors to reload the caches with already updated memory contents. The lock prefix may not be used with all instructions, and some (for example, xchg) are always executed with this prefix. See "Intel Architecture Software Developer's Manual" for details. The system (both kernel and user mode) exports a whole set of Interlocked functions that implement atomic access, but we can use assembler tools. s cache coherency mechanism) and caches of other processors will be invalidated, causing the processors to reload the caches with already updated memory contents. The lock prefix may not be used with all instructions, and some (for example, xchg) are always executed with this prefix. See "Intel Architecture Software Developer's Manual" for details. The system (both kernel and user mode) exports a whole set of Interlocked functions that implement atomic access, but we can use assembler tools. s cache coherency mechanism) and caches of other processors will be invalidated, causing the processors to reload the caches with already updated memory contents. The lock prefix may not be used with all instructions, and some (for example, xchg) are always executed with this prefix. See "Intel Architecture Software Developer's Manual" for details. The system (both kernel and user mode) exports a whole set of Interlocked functions that implement atomic access, but we can use assembler tools. See "Intel Architecture Software Developer's Manual" for details. The system (both kernel and user mode) exports a whole set of Interlocked functions that implement atomic access, but we can use assembler tools. See "Intel Architecture Software Developer's Manual" for details. The system (both kernel and user mode) exports a whole set of Interlocked functions that implement atomic access, but we can use assembler tools.

         IoCopyCurrentIrpStackLocationToNext pIrp

         IoSetCompletionRoutine pIrp, ReadComplete, NULL, TRUE, TRUE, TRUE

Establish a completion routine (see the previous article for details). When the IRP completes, we can find out the keycode.

     .else

         IoSkipCurrentIrpStackLocation pIrp

If the g_fSpy flag is cleared, the FiDO_DispatchRead procedure behaves similarly to the FiDO_DispatchPassThrough procedure.

     .endif

     mov eax, pDeviceObject
     mov eax, (DEVICE_OBJECT ptr [eax]).DeviceExtension
     mov eax, (FiDO_DEVICE_EXTENSION ptr [eax]).pNextLowerDeviceObject

     invoke IoCallDriver, eax, pIrp

     ret

Note that from all FiDO_XXX procedures we return the code that was returned by the IoCallDriver (PoCallDriver) function. Accordingly, DriverDispatch returns it to the system.



ReadComplete Procedure

Well, and finally, the ReadComplete procedure, where the main events actually take place, namely, getting the key codes. We have set the address of this procedure to our stack block by calling the IoSetCompletionRoutine macro. When the IRP completes, the IoCompleteRequest function calls all completion routines sequentially. Almost the entire previous article was devoted to this. The IRP is completed as a result of post-processing a hardware interrupt (in our case, an interrupt from the keyboard controller), which means in the context of a random thread and at a high IRQL.

     .if [esi].IoStatus.Status == STATUS_SUCCESS

         mov edi, [esi].AssociatedIrp.SystemBuffer
         assume edi:ptr KEYBOARD_INPUT_DATA

If the IRP completes with a success code, then its buffer contains at least one KEYBOARD_INPUT_DATA structure, which carries the desired key code.

       
         mov ebx, [esi].IoStatus.Information

The Information field contains the size of the real part of the buffer and must be a multiple of the size of the KEYBOARD_INPUT_DATA structure.

         and cEntriesLogged, 0
         .while sdword ptr ebx >= sizeof KEYBOARD_INPUT_DATA
            
             movzx eax, [edi].MakeCode
             mov KeyData.dwScanCode, eax

             movzx eax, [edi].Flags
             mov KeyData.Flags, eax

             invoke AddEntry, addr KeyData
                
             inc cEntriesLogged

             add edi, sizeof KEYBOARD_INPUT_DATA
             sub ebx, sizeof KEYBOARD_INPUT_DATA
         .endw

         assume edi:nothing

We transfer the fields of the KEYBOARD_INPUT_DATA structure of interest to our KEY_DATA_ENTRY structure and bind it to the doubly linked list g_KeyDataListHead. We do this in the AddEntry function and protected by the g_KeyDataSpinLock spinlock. A list lock is needed, as you understand, for exclusive access to the list, and it should be a spinlock because the ReadComplete procedure is executed at IRQL = DISPATCH_LEVEL. The DDK states that completion routines can be called at IRQL <= DISPATCH_LEVEL, but in this case we will always be strictly at IRQL = DISPATCH_LEVEL. The KeyboardClassServiceCallback function of the Kbdclass driver, which actually completes the IRP, uses KeAcquireSpinLockAtDpcLevel and KeReleaseSpinLockFromDpcLevel to lock.

 VOID
   KeyboardClassServiceCallback(
     . . .
     )
 {

     . . .

     //
     // N.B. We can use KeAcquireSpinLockAtDpcLevel, instead of
     //      KeAcquireSpinLock, because this routine is already running
     //      at DISPATCH_IRQL.
     //

     KeAcquireSpinLockAtDpcLevel( &deviceExtension->SpinLock );

     . . .

     //
     // Release the class input data queue spinlock.
     //

     KeReleaseSpinLockFromDpcLevel( &deviceExtension->SpinLock );

     . . .
 
     IoCompleteRequest( irp, IO_KEYBOARD_INCREMENT );
 
     . . .
 }

But I still use the LOCK_ACQUIRE and LOCK_RELEASE macros (I like it better;)).

         .if cEntriesLogged != 0

             LOCK_ACQUIRE g_EventSpinLock
             mov bl, al

             .if g_pEventObject != NULL
                 invoke KeSetEvent, g_pEventObject, 0, FALSE
             .endif
            
             LOCK_RELEASE g_EventSpinLock, bl
                        
         .endif

If we have new data, we inform the control program about it, signaling the "event" object. I also use locking here to ensure that g_pEventObject still contains a valid pointer.

     .if [esi].PendingReturned
         IoMarkIrpPending esi
     .endif

Since we return a code other than STATUS_MORE_PROCESSING_REQUIRED from the completion routine, we must follow rule # 6 (see part 15).

     lock dec g_dwPendingRequests

Request processed - atomically decrement the g_dwPendingRequests counter.

     mov eax, STATUS_SUCCESS

Completion of the IRP should continue. Remember from the previous article that you can return either STATUS_MORE_PROCESSING_REQUIRED or any other code from completion routines. The STATUS_SUCCESS code is used for clarity.



Management program

Parse the code of the control program yourself. There is nothing fundamentally new there. I will explain only a few points. First, as you know, a typing speed of about 10 characters per second is a good indicator. So the maximum we can get about 20 KEY_DATA structures per second, and at the same time the keyboard may not be touched for a very long time. Therefore, to eliminate unnecessary requests to the driver, we collect the accumulated information no more than once a second. And if there is nothing to take, then we do not ask for anything at all. This logic of work is achieved by lulling the thread for a while and waiting for an event that is signaled by the driver. Secondly, because 20 KEY_DATA structures is much less than one page, we use buffered I / O and retrieve information through DeviceIoControl. If you need to exchange large volumes (say, roughly, several pages), then it is better to use the METHOD_NEITHER method, and instead of DeviceIoControl - ReadFile. Third, the user can close the control program either with the mouse or with the keyboard. If he is using a mouse, the driver will not be unloaded. the last IRP passed through the driver contains a pointer to our completion routine and is now in the Kbdclass driver queue. For the control program to be able to unload the driver, the user must press a key. Therefore, by displaying the appropriate message, we give the user the necessary instructions. And finally, about the promised cancellation of the IRP. If we could cancel this ill-fated pending IRP, then we wouldn't have to scare the user with strange messages. We would simply cancel this request and unload the driver. And such a mechanism exists. The Kbdclass driver only supports cancellation of one type of IRP, and that is IRP_MJ_READ. The problem is that it is not easy to undo an IRP that is in the queue of another driver. In his book "Programming The Windows Driver Model" 2nd Edition, Walter Ony provides a couple of ways to cancel someone else's IRP. The first of them will definitely not work, but the second ... If the second does not work either, then all you have to do is organize your own queue and put all incoming IRPs of the IRP_MJ_READ type there, and send their duplicates to the downstream driver. Completion of duplicate IRPs to intercept, extract their originals from the queue and transfer the necessary data into them. If the filter has its own queue, then canceling the IRP becomes a matter of technique. To what extent this scenario is practically possible, I do not know, tk. unforeseen difficulties may arise in its implementation.

Well, and the last thing. In the archive you will find two filters at once. The second is MouSpy, obtained by replacing the words "keyboard", "kbd", etc. in the progenitor to their "mouse" counterparts. And of course I could not resist and added something else. Therefore, this filter not only passively monitors mouse events, but can make some adjustments to them. But, if you have a USB keyboard / mouse, then you will most likely fail to connect filters. In any case, I failed to connect the filter to the stack for a USB mouse, and I don't have a USB keyboard. The reason is that internally the IoGetDeviceObjectPointer function calls the ZwOpenFile function, which, in turn, forms an IRP_MJ_CREATE request and sends it onto the stack (see the previous article). Here are three stacks of mouse on one of my machines: the first for a classic PS / 2 mouse.

 kd> !drvobj mouclass
 Driver object (816a68e8) is for:
  \Driver\Mouclass
 Driver Extension List: (id , addr)
 
 Device Object list:
 812b5a20  8169e820  816a3030


 kd> !devstack 816a3030
   !DevObj   !DrvObj            !DevExt   ObjectName
 > 816a3030  \Driver\Mouclass   816a30e8  PointerClass0
   816a63a8  \Driver\nmfilter   816a6460  0000006c
   816a6530  \Driver\i8042prt   816a65e8
   8192f3e8  \Driver\ACPI       81969008  00000051
 !DevNode 818685e8 :
   DeviceInst is "ACPI\PNP0F13\3&13c0b0c5&0"
   ServiceName is "i8042prt"


 kd> !devstack 8169e820
   !DevObj   !DrvObj            !DevExt   ObjectName
 > 8169e820  \Driver\Mouclass   8169e8d8  PointerClass1
   8169ea08  \Driver\TermDD     8169eac0  RDP_CONSOLE1
   8197f970  \Driver\PnpManager 8197fa28  00000038
 !DevNode 8197f828 :
   DeviceInst is "Root\RDP_MOU\0000"
   ServiceName is "TermDD"


 kd> !devstack 812b5a20
   !DevObj   !DrvObj            !DevExt   ObjectName
 > 812b5a20  \Driver\Mouclass   812b5ad8  PointerClass2
   813c1e20  \Driver\mouhid     813c1ed8
   815f2a90  \Driver\HidUsb     815f2b48  00000074
 !DevNode 81361008 :
   DeviceInst is "HID\Vid_09da&Pid_000a\6&3a964113&0&0000"
   ServiceName is "mouhid"

Apparently, one of the drivers on the stack (most likely mouhid) refuses to process the IRP_MJ_CREATE request, returning the STATUS_SHARING_VIOLATION code, i.e. file sharing is denied (meaning the file object associated with the device object). Be that as it may, I did not go into further details. I can't connect to the USB stack ... and thank God, because "with a pig's snout, but in a kalashny row?" ... How will we handle IRP_MN_QUERY_REMOVE_DEVICE, IRP_MN_REMOVE_DEVICE and IRP_MN_SURPRISE_REMOVAL if the user disconnects the mouse / USB / REMOVAL port from the port? So, get your old fighting friends out of the closet or wait for the next article (if I ever write one;)).

Rice. 16-1. KbdSpy and MouSpy in action.

The source code of the driver in the archive .