Драйверы режима ядра: Часть 16 : Драйвер-фильтр (не PnP) — Архив WASM.RU

Все статьи

Драйверы режима ядра: Часть 16 : Драйвер-фильтр (не PnP) — Архив WASM.RU



Эта статья является практическим дополнением предыдущей, где мы рассмотрели жизненный цикл IRP. Без прочтения предыдущей статьи будет трудно до конца понять материал этой. Если вы используете USB-мышь или клавиатуру у вас, к сожалению, могут возникнуть некоторые трудности (см. конец статьи).



Стек клавиатуры

Для начала совсем чуть-чуть теории о том, как функционирует стек клавиатуры.

Физическую связь клавиатуры с шиной осуществляет микроконтроллер клавиатуры Intel 8042 (или совместимый с ним). На современных компьютерах он интегрирован в чипсет материнской платы. Этот контроллер может работать в двух режимах: AT-совместимом и PS/2-совместимом. AT-клавиатуру, сейчас, наверное, уже сложно найти. Все клавиатуры уже давно являются PS/2-совместимыми или клавиатурами, подключаемыми через интерфейс USB. В PS/2-совместимом режиме микроконтроллер клавиатуры также связывает с шиной и PS/2-совместимую мышь. Всем этим хозяйством управляет функциональный драйвер i8042prt (Intel 8042 Port Driver), полный исходный код которого, можно найти в DDK (DDK\src\input\pnpi8042). Драйвер i8042prt создает два безымянных объекта "устройство" и подключает один к стеку клавиатуры, а другой к стеку мыши. В прошлой статье на рисунке 15-4 вы видели, что на машине с системой Terminal Server у клавиатуры (и у мыши тоже) имеется более одного (определяется количеством терминальных сессий) стека. На "обычной" машине клавиатурный и "мышиный" стеки выглядят примерно так:


 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"

Поверх драйвера i8042prt, точнее поверх его устройств, располагаются именованные объекты "устройство" драйверов Kbdclass и Mouclass. Имя "KeyboardClass" является базовым, и к нему добавляются индексы (0, 1 и т.д.). Базовое имя хранится в параметре реестра HKLM\SYSTEM\CurrentControlSet\Services\Kbdclass\Parameters\KeyboardDeviceBaseName и может быть также "KeyboardPort" в случае если клавиатура использует унаследованные (legacy) драйверы, хотя подробно я в этом не разбирался (см. исходный код драйвера Kbdclass). Мы будем использовать в качестве устройства-цели для подключения фильтра объект "устройство" под именем "KeyboardClass0".

Драйверы Kbdclass и Mouclass являются так называемыми драйверами класса (class drivers) и реализуют общую функциональность для всех типов клавиатур и мышей, т.е. для всего класса этих устройств. Оба эти драйвера устанавливаются как высокоуровневые драйверы фильтры и их полный исходный код также можно найти в DDK (DDK\src\input\kbdclass и DDK\src\input\mouclass, соответственно). В архиве к этой статье в каталоге SetKeyboardLeds находится простейший драйвер, зажигающий все три индикатора клавиатуры. Примерно так (т.е. через порты ввода/вывода) функциональный драйвер i8042prt управляет устройством "клавиатура". В исходном коде драйверов Kbdclass и Mouclass обращения к портам вы конечно не найдете.

Стек клавиатуры обрабатывает несколько типов запросов (полный список см. в разделе DDK "Kbdclass Major I/O Requests"). Нас будут интересовать только IRP типа IRP_MJ_READ, которые несут с собой коды клавиш. Генератором этих IRP является поток необработанного ввода RawInputThread системного процесса csrcc.exe. Этот поток открывает объект "устройство" драйвера класса клавиатуры для эксклюзивного использования и с помощью функции ZwReadFile направляет ему IRP типа IRP_MJ_READ. Получив IRP, драйвер Kbdclass, используя макрос IoMarkIrpPending, отмечает его как ожидающий завершения (pending), ставит в очередь и возвращает STATUS_PENDING. Потоку необработанного ввода придется ждать завершения IRP (если точнее, то RawInputThread получает клавиатурные события как вызов асинхронной процедуры (Asynchronous Procedure Call, APC)). Подключаясь к стеку, драйвер Kbdclass регистрирует у драйвера i8042prt процедуру обратного вызова KeyboardClassServiceCallback, направляя ему IRP IOCTL_INTERNAL_KEYBOARD_CONNECT. Драйвер i8042prt тоже регистрирует у системы свою процедуру обработки прерывания (Interrupt Service Routine, ISR) I8042KeyboardInterruptService, вызовом функции IoConnectInterrupt. Когда будет нажата или отпущена клавиша, контроллер клавиатуры выработает аппаратное прерывание. Его обработчик вызовет I8042KeyboardInterruptService, которая прочитает из внутренней очереди контроллера клавиатуры необходимые данные. Т.к. обработка аппаратного прерывания происходит на повышенном IRQL, ISR делает только самую неотложную работу и ставит в очередь вызов отложенной процедуры (Deferred Procedure Call, DPC). DPC работает при IRQL = DISPATCH_LEVEL. Когда IRQL понизится до DISPATCH_LEVEL, система вызовет процедуру I8042KeyboardIsrDpc, которая вызовет зарегистрированную драйвером Kbdclass процедуру обратного вызова KeyboardClassServiceCallback (также выполняется на IRQL = DISPATCH_LEVEL). KeyboardClassServiceCallback извлечет из своей очереди ожидающий завершения IRP, заполнит структуру KEYBOARD_INPUT_DATA (на самом деле i8042prt старается опустошить всю очередь контроллера клавиатуры, а Kbdclass соответственно заполнить столько структур KEYBOARD_INPUT_DATA, сколько влезет в буфер IRP), несущую всю необходимую информацию о нажатиях/отпусканиях клавиш и завершит IRP. Поток необработанного ввода пробуждается, обрабатывает полученную информацию и вновь посылает IRP типа IRP_MJ_READ драйверу класса, который опять ставится в очередь до следующего нажатия/отпускания клавиши. Таким образом, у стека клавиатуры всегда есть, по крайней мере, один, ожидающий завершения IRP и находится он в очереди драйвера Kbdclass. "Мышиный" стек ведет себя подобным же образом.


 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

Как видно, драйвер Kbdclass имеет незавершенный IRP типа IRP_MJ_READ (цифра 3 в квадратных скобках). Колонка cl показывает содержимое поля IO_STACK_LOCATION.Control. В данном случае это флаг SL_PENDING_RETURNED, установленный вызовом макроса IoMarkIrpPending. Строка Args представляет собой содержимое вложенной структуры IO_STACK_LOCATION.Read. Размер буфера (78h) достаточен ровно для 10 структур KEYBOARD_INPUT_DATA. Сам буфер находится по адресу System buffer = 81664b48. Обратите также внимание на строку CancelRoutine = f3ec82e0. Это адрес процедуры отмены IRP (cancel routine), принадлежащей драйверу Kbdclass. Отмена IRP - это ещё одна большая и относительно сложная тема (которую я не планирую освещать). Позже мы немного поговорим об этом.

Этот, ожидающий завершения, IRP, естественно, принадлежит потоку 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

Когда мы установим фильтр, то IRP сначала будут попадать к нам. Т.к. IRP типа IRP_MJ_READ является фактически запросом на чтение данных, то когда он идет вниз по стеку его буфер, естественно пуст. Прочитанный данные буфер будет содержать только после завершения IRP. Для того чтобы их (данные) увидеть фильтр должен установить в каждый IRP (точнее в свой блок стека) процедуру завершения. Т.к. находящийся в очереди драйвера Kbdclass IRP был послан до того, как мы установим фильтр, то в нем нет нашей процедуры завершения, а значит, мы никак не сможем увидеть код той клавиши, которая будет нажата сразу после установки фильтра. Когда клавиша будет нажата, ожидающий завершения IRP будет завершен и RawInputThread пошлет новый IRP типа IRP_MJ_READ. Этот и все последующие IRP мы уже перехватим и поставим процедуру завершения. Когда клавиша будет отпущена, мы прочитаем в процедуре завершения её код. Именно поэтому фильтр не видит момента нажатия на первую клавишу, и в мониторе вы всегда будете видеть для первой нажатой клавиши только её код break (отпускание клавиши). Дальше фильтр перехватывает все нажатия и все отпускания любых клавиш.

Прежде чем мы перейдем к коду драйвера, разберемся ещё с несколькими моментами.



Спин-блокировка

Поскольку обработка IRP по своей природе асинхронна, то зачастую "правая рука не знает, что делает левая". Поэтому в каждом более-менее серьезном драйвере приходится использовать механизм синхронизации под названием "взаимоисключающий доступ". В тринадцатой части цикла - "Базовая техника. Синхронизация: Взаимоисключающий доступ" мы уже познакомились с тем, как организовать взаимоисключающий доступ с помощью мьютекса. Поэтому про макросы MUTEX_INIT, MUTEX_ACQUIRE и MUTEX_RELEASE, которые мы вновь будем использовать, я молчу.

Синхронизация с помощью мьютекса удобна, но, к сожалению, у нее есть один недостаток: как известно, ожидать объекты на IRQL равном DISPATCH_LEVEL и выше нельзя. Для организации взаимоисключающего доступа на IRQL до DISPATCH_LEVEL включительно существует механизм под названием "спин-блокировка" (spin lock).


 KfAcquireSpinLock proc                   ; Захват спин-блокировки в однопроцессорном 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                   ; Захват спин-блокировки в многопроцессорном 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          ; Попробуем атомарно захватить блокировку.
     jb      spin                         ; Если блокировка занята будем крутить цикл.
     ret                                  ; Мы захватили блокировку, возвращаем IRQL,
                                          ; на котором процессор находился до блокировки
     align 4
 
 spin:                                    ; Блокировка занята. Будем крутить цикл.
     test    dword ptr [ecx], 1           ; Проверим блокировку.
     jz      trytoacquire                 ; Если блокировка освободилась, попробуем захватить её ещё раз.
     pause                                ; Если нет, крутим цикл дальше.  Инструкция pause специально
                                          ; предназначена для использования в цикле спин-блокировки.
                                          ; Подробности можно почитать в разделе "PAUSE-Spin Loop Hint"
                                          ; IA-32 Intel Architecture Software Developer's Manual
                                          ; Volume 2 : Instruction Set Reference.
     jmp     spin
 KfAcquireSpinLock endp

На однопроцессорной машине захват спин-блокировки заключается в простом повышении IRQL до DISPATCH_LEVEL. Как известно, на этом IRQL не происходит планирования потоков, и поток, владеющий процессором, будет выполняться до тех пор, пока IRQL не понизится. Поскольку IRQL является атрибутом процессора, на многопроцессорной машине простого повышения IRQL не достаточно, т.к. это не блокирует потоки, выполняющиеся другими процессорами. Поэтому в многопроцессорных HAL спин-блокировка реализована несколько сложнее и является спин-блокировкой в истинном смысле этого слова (spin - крутить, вертеть; пускать волчок). Т.е. если блокировка занята, поток крутит бесконечный цикл, пытаясь её захватить. При этом т.к. цикл выполняется на IRQL = DISPATCH_LEVEL, планирования потоков на этом процессоре не происходит и процессор не может заняться полезной работой. Именно поэтому спин-блокировки гораздо более критичны ко времени. Т.е. освободить спин-блокировку нужно как можно быстрее. DDK даже определяет максимальный временной интервал в 25 микросекунд, в течении которого можно держать спин-блокировку. В этом смысле мьютексы и другие объекты ожидания менее требовательны, т.к. поток, ожидающий занятый объект, просто исключается из планирования, и процессор получает другой, готовый к выполнению поток.

И раз уж нам пришлось коснуться спин-блокировки, то запомните несколько классических правил. Во-первых, захват спин-блокировки, как мы только что выяснили, повышает IRQL до DISPATCH_LEVEL, а значит, нам доступна только неподкачиваемая память и сам код также должен находиться в неподкачиваемой памяти. Во-вторых, повторный захват той же самой спин-блокировки, естественно, приведет к полной блокировке (deadlock). В третьих, захватывать спин-блокировку на IRQL выше DISPATCH_LEVEL нельзя, т.к. это фактически означает явное понижение IRQL, что неминуемо приведет к BSOD. В-четвертых, если требуется захватить две (или больше) спин-блокировки, то все потоки должны это делать в одном и том же порядке. Иначе возможна взаимная блокировка. Например, двум потокам требуется захватить блокировки A и B. Если они будут делать это в разном порядке, то, возможно, что в одно и то же время первый поток захватит блокировку A, а второй блокировку B. После этого оба потоку будут бесконечно ждать: первый - когда освободится блокировка B, а второй - когда освободиться блокировка A.


 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

Для захвата спин-блокировки будем использовать макросы. Это упрощенные версии. В макросе LOCK_RELEASE мы используем небольшую оптимизацию: если мы были до захвата спин-блокировки на IRQL = DISPATCH_LEVEL, то выгоднее вызвать KefReleaseSpinLockFromDpcLevel вместо KfReleaseSpinLock, т.к. изменять IRQL не требуется и на однопроцессорной машине KefReleaseSpinLockFromDpcLevel является "пустой" функцией.


 KeReleaseSpinLockFromDpcLevel proc
     retn 4
 KeReleaseSpinLockFromDpcLevel endp

Нечто подобное (я имею в виду оптимизацию) можно сделать и для макроса LOCK_ACQUIRE. Потребуется только узнать текущий IRQL и если он равен DISPATCH_LEVEL, то вызвать KeAcquireSpinLockAtDpcLevel, которая (на однопроцессорной машине) тоже выполняет инструкцию ret.


 KeAcquireSpinLockAtDpcLevel proc
     retn 4
 KeAcquireSpinLockAtDpcLevel endp

Я не стал оптимизировать макрос LOCK_ACQUIRE, т.к. написал эти макросы давно и несколько раз успешно использовал, к тому же не ясно, что быстрее: просто вызвать KfAcquireSpinLock или выяснять IRQL и в зависимости от его значения вызывать KeAcquireSpinLockAtDpcLevel. Поэтому я не стал ничего мудрить и оставил всё как есть. Если есть неуёмное желание оптимизировать, изучайте hal.dll/halmps.dll и ntoskrnl.exe/ntkrnlmp.exe и оптимизируйте на здоровье.

Для полноты картины, надо ещё добавить, что есть функция KeAcquireSpinLockRaiseToSynch, повышающая IRQL при захвате блокировки до CLOCK2_LEVEL (28).



Процедура DriverEntry

Теперь займемся собственно нашим фильтром. Как я уже говорил, это не-Pnp драйвер. Кода довольно много и я не буду приводить его полностью (см. архив к статье).


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

На этот раз наш драйвер будет управлять уже двумя объектами: объектом "устройство-фильтр" (filter device object) и объектом "устройство управления" (control device object). Объект "устройство-фильтр" будет подключен к стеку клавиатуры, и через него будут проходить все IRP управляющие клавиатурой. Посредством объекта "устройство управления" программа управления будет отдавать драйверу необходимые команды: "подключить фильтр", "отключить фильтр", "передать перехваченные данные". На данный момент нам нужно только устройство управления. Этот объект будет именованным, для того чтобы программа управления могла получить к нему доступ. Мы не хотим работать одновременно с несколькими клиентами. Поэтому создадим эксклюзивный объект, определив TRUE в параметре Exclusive. В этом случае диспетчер объектов позволит создать только один описатель объекта. К сожалению, этот простой способ не очень надёжен, и открыть объект все же можно по относительному пути, т.е. открыв каталог "\Device" и передав его описатель в параметре RootDirectory макроса InitializeObjectAttributes. DDK вообще говорит, что параметр Exclusive зарезервирован. Поэтому мы добавим кое-какую дополнительную обработку запросов IRP_MJ_CREATE и IRP_MJ_CLOSE.


             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

Выделяем память под ассоциативный список и инициализируем его. Из этого списка мы будем выделять память под экземпляры нами же определенной структуры KEY_DATA_ENTRY.


 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

Экземпляры этой структуры будут хранить данные о перехваченных нажатиях/отпусканиях клавиш и их (экземпляры структуры) мы будем хранить в двусвязном списке. В седьмой части цикла - "Базовая техника: Работа с памятью. Использование ассоциативных списков" мы достаточно подробно разобрали как ассоциативный список (look-aside list), так и двусвязный список (doubly linked list). Уверен, что многие просто пропустили эту статью ;) Если это так, то придется её прочитать сейчас, т.к. я не буду повторяться, а без этого материала кое-что может быть не понятно. Единственная разница в том, что сейчас мы будем использовать неподкачиваемый ассоциативный список. Функции ExAllocateFromNPagedLookasideList и ExFreeToNPagedLookasideList для работы с неподкачиваемым ассоциативным списком реализованы в DDK как макросы, в отличие от именно функций для подкачиваемого ассоциативного списка. К сожалению, ввиду ограниченности макроязыка masm, мне пришлось реализовать их в виде функций _ExAllocateFromNPagedLookasideList и _ExFreeToNPagedLookasideList. Неподкачиваемый ассоциативный список нам потребовался, как вы догадываетесь, потому, что мы будем работать с ним на IRQL = DISPATCH_LEVEL.


                 InitializeListHead addr g_KeyDataListHead

Глобальная переменная g_KeyDataListHead является головой двусвязного списка структур KEY_DATA_ENTRY.


                 invoke KeInitializeSpinLock, addr g_KeyDataSpinLock

Спин-блокировка нам потребуется для организации монопольного доступа к списку структур KEY_DATA_ENTRY. Использовать объекты синхронизации, например, мьютекс, мы не можем, т.к. будем обращаться к списку на IRQL = DISPATCH_LEVEL.


                 invoke KeInitializeSpinLock, addr g_EventSpinLock

Эта спин-блокировка поможет нам организовать монопольный доступ к переменной g_pEventObject, в которой будет храниться указатель на объект событие. Этот объект будет использоваться для уведомления программы управления, о новых данных (Подробнее см. часть 14 "Базовая техника. Синхронизация: Использование объекта "событие" для взаимодействия драйвера с программой управления").


                 MUTEX_INIT g_mtxCDO_State

С помощью этого мьютекса мы сможем монопольно выполнять некоторые участки кода.


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

Заполняем все элементы массива указателей на процедуры диспетчеризации драйвера, адресом единственной процедуры DriverDispatch. Эта процедура будет распределять запросы между фильтром и устройством управления. Устройство управления будет получать от программы управления всего три запроса: IRP_MJ_CREATE, IRP_MJ_CLOSE и IRP_MJ_DEVICE_CONTROL. А вот устройство-фильтр может получить любой запрос, т.к. оно подключается в уже существующий стек, по которому могут циркулировать IRP любого типа. Зачастую, весь спектр IRP, проходящий по фильтруемому стеку вообще не известен. Фильтровать приходится только некоторые типы IRP, но если фильтр получит запрос, который его не интересует, он обязан направить его ниже по стеку. Именно поэтому мы должны заполнить весь массив MajorFunction. Иначе в незаполненных элементах останется указатель на системную функцию IopInvalidDeviceRequest, которая будет завершать IRP с кодом STATUS_INVALID_DEVICE_REQUEST, и мы блокируем продвижение таких запросов.



Процедура DriverDispatch

Через наш драйвер идут запросы к двум объектам: устройству управления и фильтру (если он подключен). Все IRP попадают в общую процедуру диспетчеризации 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

Используя глобальные указатели g_pFilterDeviceObject и g_pControlDeviceObject, определяем, к какому объекту пришел запрос и, в зависимости от типа запроса вызываем соответствующую процедуру. Наше устройство управления обрабатывает только три типа запросов: IRP_MJ_CREATE, IRP_MJ_CLOSE и IRP_MJ_DEVICE_CONTROL. Но мы обязаны обработать все запросы к фильтру. Обработка будет заключаться в простой передаче IRP нижестоящему драйверу в процедуре FiDO_DispatchPassThrough. Запросы типа IRP_MJ_READ несут в себе коды клавиш, поэтому для этого типа запросов обработка будет особой. IRP типа IRP_MJ_POWER просто требует специфической обработки, поэтому и выделен в отдельную процедуру. Если мы, вдруг (чего быть не может), получили запрос для неизвестного нам устройства, завершаем его с кодом ошибки, т.к. не понятно, что ещё с этим IRP можно сделать.

Сначала разберем обработку запросов к устройству управления.



Процедура CDO_DispatchCreate


     .while TRUE

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

     .endw

Драйвер и программа управления построены таким образом, что программу управления можно выгрузить и загрузить повторно при уже запущенном драйвере и подключенном фильтре. Может так случиться, что список g_KeyDataListHead не пуст. Если вы внимательно проанализируете ход возможных событий после прочтения всей статьи, то станет ясно, что в списке может находиться одна структура KEY_DATA_ENTRY, соответствующая коду клавиши, нажатой сразу после некорректного завершения работы программы управления. Вышеприведенный цикл опустошает, возможно, непустой список g_KeyDataListHead.


     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

Если описатель объекта "устройство управления" уже открыт, не разрешаем повторное открытие. Это гарантирует нам наличие только одного клиента (остальные получат код STATUS_DEVICE_BUSY), а захват мьютекса гарантирует, что процедура CDO_DispatchClose в то же самое время не закроет описатель и не обнулит флаг g_fCDO_Opened.



Процедура CDO_DispatchClose


     and g_fSpy, FALSE

Если клиент отключается, то и незачем следить за клавиатурой - FiDO_DispatchRead не должна больше устанавливать процедуру завершения.


     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

Если переменная g_pFilterDeviceObject пуста, то, очевидно, что нет и фильтра. Если к тому же у нас нет незавершенных IRP, завершение которых привело бы к вызову нашей процедуры завершения ReadComplete, находящейся в теле драйвера, то можно разрешить его выгрузить. Если фильтр всё ещё существует, драйвер остается невыгружаемым. Перед завершением работы программа управления просит драйвер отключить и удалить фильтр. Но возможны ситуации, когда драйвер не сможет этого сделать. Например, если кто-то подключен к стеку поверх нас, отключение фильтра "разорвет стек". Программа управления может просто забыть отключить фильтр или в ней может произойти исключение и описатель устройства автоматически закрывается системой. Речь, разумеется, не идет о нашей программе управления, в которой (я надеюсь) все сделано правильно. Имеется в виду программа управления вообще, т.е. общий принцип. Наконец, пользователь может завершать сеанс работы с системой, и все пользовательские процессы принудительно завершаются. В любом случае, как я уже сказал, драйвер и программа управления построены таким образом, что программу управления можно запустить повторно.


     and g_fCDO_Opened, FALSE    

     MUTEX_RELEASE g_mtxCDO_State

Т.к. единственный наш клиент только что "ушел", сбрасываем флаг g_fCDO_Opened.



Процедура CDO_DispatchDeviceControl


             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

При получении от программы управления управляющего кода IOCTL_KEYBOARD_ATTACH, захватываем мьютекс и проверяем переданный нам описатель объекта "событие". Это мы уже делали в Process Monitor (см. часть 14). Если это действительно объект "событие", то у нас есть два варианта: либо мы должны создать фильтр и подключить его к стеку клавиатуры, либо фильтр уже существует и подключён.


                 .if !g_fFiDO_Attached

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

Если фильтр не подключен, будем считать, что он ещё не создан. Процедура KeyboardAttach сделает всё необходимое, вернув соответствующий код.


                     .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

Если подключение прошло успешно, запоминаем указатель на объект "событие" в глобальной переменной g_pEventObject и взводим флаги g_fFiDO_Attached и g_fSpy. Хотя фильтр уже подключен, блокировать доступ к переменной g_pEventObject, в данном случае, не требуется, т.к. флаг g_fSpy взводится после инициализации переменной g_pEventObject, а до тех пор процедура FiDO_DispatchRead не будет устанавливать процедуру завершения, а значит ReadComplete вообще не будет вызываться и g_pEventObject кроме нас никто трогать не будет.


                 .else

                     LOCK_ACQUIRE g_EventSpinLock
                     mov bl, al

Если фильтр уже подключен, необходимо блокировать доступ к переменной g_pEventObject, т.к. к ней может обращаться наша процедура завершения ReadComplete. Спин-блокировка требуется из-за того, что ReadComplete работает на 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

На всякий случай, если g_pEventObject содержит указатель на объект событие, уменьшаем счетчик ссылок и заносим туда указатель на новый объект событие. Тут требуется небольшое пояснение. На первый взгляд, этот код может показаться бессмысленным. Дело в том, что в предыдущих примерах, мы, для простоты, предполагали корректное поведение программы управления драйвером. Но, в идеале, драйвер должен быть непотопляем, даже если его собственная программа управления или кто-то другой выполнит какие-то непредсказуемые действия. В состав инструментов DDK для тестирования драйверов даже входит специальная утилита Device Path Exerciser (dc2.exe), которая, среди других тестов, шлет драйверу огромное количество управляющих кодов с заведомо неверными параметрами. Если программа управления дважды пошлет драйверу IOCTL_KEYBOARD_ATTACH, то, благодаря вышеприведенному коду, мы сможем корректно разобраться с двумя объектами "событие", а мьютекс g_mtxCDO_State избавит нас от множества потенциальных проблем.


         MUTEX_ACQUIRE g_mtxCDO_State

При получении от программы управления управляющего кода IOCTL_KEYBOARD_DETACH, пытаемся отключить фильтр, также под защитой мьютекса.


         .if g_fFiDO_Attached

             and g_fSpy, FALSE

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

Если фильтр подключен, сбрасываем флаг g_fSpy, чтобы FiDO_DispatchRead не устанавливала больше процедуру завершения, и пытаемся отключить и удалить фильтр.


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

Если фильтр успешно отключен, сбрасываем соответствующий флаг. Если отключить фильтр не удалось, этот флаг останется во взведенном состоянии, что даст нам возможность при следующем (возможном) получении 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

Под защитой спин-блокировки, удаляем ссылку на объект "событие".


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

При получении от программы управления управляющего кода IOCTL_GET_KEY_DATA, копируем в пользовательский буфер имеющиеся у нас к настоящему моменту структуры KEY_DATA. Процедуру FillKeyData, а также AddEntry и RemoveEntry я разбирать не буду, т.к. если вы читали седьмую часть цикла "Использование ассоциативных списков", их содержимое не должно представлять сложность, а подробности относительно структуры KEYBOARD_INPUT_DATA смотрите в DDK.



Процедура KeyboardAttach


     .if ( g_pFilterDeviceObject != NULL )

         mov status, STATUS_SUCCESS

Если переменная g_pFilterDeviceObject не равна нулю, очевидно, она содержит указатель на объект "устройство-фильтр" и, наверное, он уже подключен к стеку.


     .else

Если фильтра нет, создадим его.


         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

Объект "устройство-фильтр" должен быть безымянным, для того чтобы его нельзя было открыть напрямую по имени. Т.к. фильтр принадлежит стеку, но является привнесенным объектом, то явно не ему решать, разрешать ли открытие описателя или нет. Пусть с этим разбираются нижестоящие драйверы. На самом деле это не всегда верно. В случае со стеком клавиатуры, высокоуровневый драйвер фильтра Kbdclass имеет именованный объект "устройство-фильтр" KeyboardClassX и именно он обрабатывает запрос IRP_MJ_CREATE. Второй параметр функции IoCreateDevice определяет размер дополнительной области памяти объекта "устройство" (device extension), которая описывается гипотетической структурой DEVICE_EXTENSION. Гипотетической в том смысле, что такой структуры нет. Вы сами определяете, что необходимо поместить в дополнительную область памяти объекта "устройство" и сами определяете структуру. Device extension следует сразу за структурой DEVICE_OBJECT и инициализируется нулями. В нашем случае это структура FiDO_DEVICE_EXTENSION. Использование device extension позволяет драйверу создать сколь угодно много объектов "устройство" и хранить все относящиеся к ним данные в самих этих объектах.


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

Надеюсь, вы помните, что функция IoGetAttachedDevice всегда возвращает указатель на объект "устройство", находящийся на вершине стека. Мы используем функцию IoGetDeviceObjectPointer для получения указателя на вершину стека по заранее известному нам имени одного из объектов "устройство", принадлежащего стеку. PnP драйверам, в этом смысле, проще, т.к. диспетчер PnP предоставляет им указатель на корневой объект стека - объект "физическое устройство". Т.е. я хочу сказать, что для подключения к стеку вам нужен указатель на любой объект стека. Как вы его получите, не имеет значения.


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

Т.к. сейчас мы подключимся к стеку, то через нас могут пойти IRP. После установки флага g_fSpy мы будем устанавливать в них процедуру завершения. Когда эти IRP завершаться, мы не знаем, но знаем, что до этих пор драйвер нельзя выгружать, т.к. процедура завершения находится в теле драйвера. Поэтому проще всего просто сделать драйвер невыгружаемым.


 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
 }

Сначала функция IoAttachDeviceToDeviceStack получает указатель на структуру DEVOBJ_EXTENSION (не путать с необязательной структурой DEVICE_EXTENSION). В поле ExtensionFlags этой структуры имеются кое-какие интересующие функцию IoAttachDeviceToDeviceStack флаги. Затем блокируется база данных диспетчера ввода/вывода и в pTopMostDeviceObject заносится указатель на объект "устройство", находящийся на вершине стека. Т.к. база данных диспетчера ввода/вывода блокирована, то состояние стека не изменится до снятия блокировки. Если объект "устройство" ещё не инициализирован или устройство или его драйвер отмечены для удаления или уже находится в состоянии удаления, функция IoAttachDeviceToDeviceStack отказывается прикреплять к стеку новый объект и возвращает NULL. В противном случае в поля AttachedDevice и AttachedTo объектов "устройство" заносятся соответствующие указатели (в этом и заключается процесс подключения к стеку нового объекта) и в подключенном объекте обновляются поля AlignmentRequirement, SectorSize и StackSize. AlignmentRequirement и SectorSize важны для устройств хранения, а StackSize необходимо увеличить на единицу в любом случае, т.к. глубина стека увеличилась на один объект (подробности см. в предыдущей статье). Обратите внимание на то, что подключение происходит не к объекту, указатель на который передан в параметре pTargetDevice, а к вершине стека кто бы на ней не находился. Если в промежутке между получением указателя на pTargetDevice и вызовом IoAttachDeviceToDeviceStack кто-то успеет подключить к стеку свой объект, pTargetDevice и возвращаемый pTopMostDeviceObject будут отличаться. В любом случае, возвращаемое из IoAttachDeviceToDeviceStack значение, в случае успеха, является указателем на объект "устройство", к которому был подключен фильтр. А фильтр теперь является вершиной стека и первым получает все IRP для этого стека предназначенные. В начале статьи мы выяснили, что поток необработанного ввода открывает один из объектов стека клавиатуры и, используя его описатель (точнее описатель объекта "файл", соответствующий объекту "устройство"), направляет ему запросы на чтение. Если IRP предназначаются какому-то устройству ниже по стеку, то каким образом они попадают в фильтр? Подсистема ввода/вывода ведет себя аналогично функциям IoGetAttachedDevice и IoAttachDeviceToDeviceStack, в том смысле, что в качестве адресата для IRP использует указатель на вершину стека. Вот, например, что делает функция ZwReadFile.


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

Функция IoGetRelatedDeviceObject (исходный код см. в предыдущей статье) возвращает указатель на самый верхний объект "устройство" в стеке. Если же IRP формируется драйвером, так сказать вручную (см. исходный код процедуры QueryPnpDeviceState в предыдущей статье), то он будет послан напрямую целевому устройству и перехватить такой запрос с помощью фильтра невозможно, если конечно фильтр не находится ниже по стеку.


                 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

Если IoAttachDeviceToDeviceStack подключила нас к стеку, заполняем структуру FiDO_DEVICE_EXTENSION. Туда мы помещаем указатель на объект, к которому мы подключились и указатель на объект "файл" ассоциированный с целевым объектом устройство (подробности см. в предыдущей статье). При отключении мы должны будем вызвать ObDereferenceObject по отношению к этому объекту "файл".


                     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

Функция IoCreateDevice создает объект "устройство" с установленным флагом DO_DEVICE_INITIALIZING. До сих пор мы не касались этого момента потому, что создавали устройства только в процедуре DriverEntry. Дело в том, что по выходу из DriverEntry диспетчер ввода/вывода (в функции IopReadyDeviceObjects) сам сбрасывает этот флаг во всех объектах "устройство", созданных драйвером. Если же мы создаем устройство не в DriverEntry, придется сбросить флаг DO_DEVICE_INITIALIZING самостоятельно, иначе никто не сможет подключиться к неинициализированному объекту, как вы только что видели в коде IoAttachDeviceToDeviceStack. Также этот флаг проверяется при некоторых других операциях.



Процедура KeyboardDetach


     .if g_pFilterDeviceObject != NULL

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

Перед тем как отключаться от стека, посмотрим, находимся ли мы на самой его вершине или к нам тоже уже кто-то подключился. Блокировать базу данных диспетчера ввода/вывода мы не можем, но зато можем предотвратить новые подключения, до тех пор, пока не проверим, есть ли кто-нибудь над нами.


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

     pDeviceObject = IoGetAttachedDevice( pDeviceObject )
     ObReferenceObject( pDeviceObject )

     ExReleaseSpinLock( &IopDatabaseLock, ... )

     return pDeviceObject
 }

Здесь должно быть всё понятно.


         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

Если, возвращенный функцией IoGetAttachedDeviceReference, указатель не является указателем на наш фильтр, значит, к нам кто-то подключен. В этом случае отключаться от стека мы не будем и сбросим флаг DO_DEVICE_INITIALIZING. Если мы вызовем IoDetachDevice, то просто "разорвем стек", т.к. IoDetachDevice не делает каких бы то ни было проверок. Отключение объекта "устройство" от стека состоит в простом обнулении соответствующих указателей в связанных объектах.


 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, ... )
 
 }

Отключив устройство от стека, IoDetachDevice проверяет, не ожидает ли оно удаления, а его драйвер выгрузки. И если это так и счетчик ссылок на объект равен нулю, инициирует отложенные операции.


         .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

Если мы на вершине стека, уменьшаем счетчик ссылок на файловый объект, ассоциированный с объектом устройство, и отключаемся от стека. Восстанавливать флаг DO_DEVICE_INITIALIZING не имеет смысла, т.к. сейчас мы удалим фильтр.


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

Удаляем объект "устройство-фильтр", но драйвер всё ещё остается невыгружаемым, т.к. мы можем иметь ожидающий завершения IRP, содержащий указатель на нашу процедуру завершения.


         .endif

         invoke ObDereferenceObject, pTopmostDeviceObject

Функция IoGetAttachedDeviceReference, в отличие от функции IoGetAttachedDevice, увеличивает счетчик ссылок в объекте, указатель на который возвращает. Это гарантирует, что объект не будет удален. Если мы были на вершине стека, то увеличили счетчик ссылок в нашем же объекте "устройство-фильтр" и IoDeleteDevice не сможет его удалить.


 VOID
   IoDeleteDevice(
     IN PDEVICE_OBJECT pDeviceObject
     )
 {
     ...

     ExAcquireSpinLock( &IopDatabaseLock, ... )

     pDeviceObject->DeviceObjectExtension->ExtensionFlags |= DOE_DELETE_PENDING

     if  pDeviceObject->ReferenceCount == 0  {

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

Но IoDeleteDevice добавит флаг DOE_DELETE_PENDING, отметив тем самым, что объект "устройство" ожидает удаления. Когда мы вызовем ObDereferenceObject, счетчик ссылок станет равен 0, диспетчер объектов увидит, что объект должен быть удален и предпримет соответствующие шаги.

Теперь разберем процедуры обработки запросов к фильтру.



Процедура FiDO_DispatchPower


     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

IRP типа IRP_MJ_POWER обрабатываются отличным от всех остальных типов IRP способом.

Макрос IoCopyCurrentIrpStackLocationToNext мы подробно разобрали в прошлой статье (его мы будем использовать в процедуре FiDO_DispatchRead). Макрос IoSkipCurrentIrpStackLocation намного проще.


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

Из прошлой статьи вы должны помнить, что функция IoCallDriver, перед тем как вызвать процедуру диспетчеризации драйвера, смещает текущий блок стека на одну позицию вниз.


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

Если перед этим использовать макрос IoSkipCurrentIrpStackLocation, то получиться, что указатель блока стека вообще не изменится и нижестоящий драйвер получить тот же самый блок стека, что и драйвер вызвавший IoCallDriver (PoCallDriver). Вызов макроса IoSkipCurrentIrpStackLocation - это просто оптимизация. Действительно, если нам не нужно устанавливать процедуру завершения, то вызов макроса IoCopyCurrentIrpStackLocationToNext скопирует наш блок стека в блок стека нижестоящего драйвера (поля Control, CompletionRoutine и Context, как вы помните, не копируются). Т.о. нижестоящий драйвер всё равно получит те же самые параметры. Используя макрос IoSkipCurrentIrpStackLocation вместо IoCopyCurrentIrpStackLocationToNext, мы избегаем ненужной операции копирования блоков стека. Но, повторяю, это можно делать, только если не требуется устанавливать процедуру завершения, что должно быть и так понятно.



Процедура FiDO_DispatchPassThrough


     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

Здесь просто передаем IRP нижестоящему драйверу.



Процедура FiDO_DispatchRead


     .if g_fSpy

Получив запрос типа IRP_MJ_READ адресованный фильтру, смотрим, взведен ли флаг g_fSpy. Если да, то мы должны установить процедуру завершения.


         lock inc g_dwPendingRequests

Атомарно увеличиваем значение счетчика незавершенных запросов g_dwPendingRequests на единицу. Когда IRP будет завершаться, система вызовет нашу процедуру завершения ReadComplete, она прочитает код клавиши и уменьшит счетчик g_dwPendingRequests. "Атомарно" - означает, что только один поток даже на многопроцессорной машине сможет изменить значение переменной, а IRQL, на котором он выполняется, вообще не имеет значения. Даже если поток, выполняющийся на другом процессоре, попытается в то же самое время (на MP-машине в буквальном смысле) выполнить такой же код, он получит уже обновленное первым потоком значение. Это достигается за счет использования префикса lock. Увидев этот префикс, процессор блокирует шину данных на время выполнения инструкции. Другие процессоры не смогут в этот момент обратиться к этой области памяти и изменить её. Даже если эта область памяти кэшируется несколькими процессорами, в действие вступит механизм обеспечения когерентности кэша (processor's cache coherency mechanism) и кэши других процессоров будут объявлены недействительными, в результате чего процессоры должны будут повторно загрузить кеши уже обновленным содержимым памяти. Префикс lock может использоваться не со всеми инструкциями, а некоторые (например, xchg) всегда выполняются с этим префиксом. Подробнее см. "Intel Architecture Software Developer's Manual". Система (как ядро, так и режим пользователя) экспортирует целый набор Interlocked-функций, реализующих атомарный доступ, но мы можем использовать средства ассемблера.


         IoCopyCurrentIrpStackLocationToNext pIrp

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

Устанавливаем процедуру завершения (детали см. в предыдущей статье). Когда IRP будет завершаться, мы сможем узнать код клавиши.


     .else

         IoSkipCurrentIrpStackLocation pIrp

Если флаг g_fSpy сброшен, процедура FiDO_DispatchRead ведет себя аналогично процедуре FiDO_DispatchPassThrough.


     .endif

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

     invoke IoCallDriver, eax, pIrp

     ret

Обратите внимание на то, что из всех процедур FiDO_XXX мы возвращаем код, который вернула функция IoCallDriver (PoCallDriver). Соответственно, DriverDispatch возвращает его в систему.



Процедура ReadComplete

Ну, и наконец, процедура ReadComplete, где собственно и происходят главные события, а именно получение кодов клавиш. Мы установили адрес этой процедуры в наш блок стека вызовом макроса IoSetCompletionRoutine. Когда IRP завершается, функция IoCompleteRequest последовательно вызывает все процедуры завершения. Этому и была посвящена практически вся предыдущая статья. IRP завершается в результате пост-обработки аппаратного прерывания (в нашем случае прерывания от контроллера клавиатуры), а значит в контексте случайного потока и на повышенном IRQL.


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

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

Если IRP завершается с кодом успеха, то его буфер содержит, по крайней мере, одну структуру KEYBOARD_INPUT_DATA, несущую с собой вожделенный код клавиши.

       
         mov ebx, [esi].IoStatus.Information

Поле Information содержит размер действительной части буфера и должно быть кратно размеру структуры KEYBOARD_INPUT_DATA.


         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

Перекладываем интересующие нас поля структуры KEYBOARD_INPUT_DATA в нашу структуру KEY_DATA_ENTRY и привязываем её к двусвязному списку g_KeyDataListHead. Это мы делаем в функции AddEntry и под защитой спин-блокировки g_KeyDataSpinLock. Блокировка списка нужна, как вы понимаете, для монопольного доступа к списку, а спин-блокировкой она должна быть потому, что процедура ReadComplete выполняется на IRQL = DISPATCH_LEVEL. DDK утверждает, что процедуры завершения могут быть вызваны на IRQL <= DISPATCH_LEVEL, но в данном случае мы всегда будем строго на IRQL = DISPATCH_LEVEL. Функция KeyboardClassServiceCallback драйвера Kbdclass, которая собственно и завершает IRP, использует KeAcquireSpinLockAtDpcLevel и KeReleaseSpinLockFromDpcLevel для блокировки.


 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 );
 
     . . .
 }

Но я всё равно использую макросы LOCK_ACQUIRE и LOCK_RELEASE (мне так больше нравится ;) ).


         .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

Если у нас есть новые данные, сообщаем об этом программе управления, сигнализируя объект "событие". Здесь я также использую блокировку, для уверенности в том, что g_pEventObject всё ещё содержит действительный указатель.


     .if [esi].PendingReturned
         IoMarkIrpPending esi
     .endif

Поскольку мы возвращаем из процедуры завершения код отличный от STATUS_MORE_PROCESSING_REQUIRED, то должны следовать правилу №6 (см. часть 15).


     lock dec g_dwPendingRequests

Запрос обработан - атомарно уменьшаем счетчик g_dwPendingRequests.


     mov eax, STATUS_SUCCESS

Завершение IRP должно быть продолжено. Из материала предыдущей статьи вы должны помнить, что из процедур завершения можно возвращать либо STATUS_MORE_PROCESSING_REQUIRED, либо любой другой код. Код STATUS_SUCCESS использован для наглядности.



Программа управления

Код программы управления разберите сами. Ничего принципиально нового там нет. Поясню только несколько моментов. Во-первых, как известно, скорость набора текста примерно в 10 знаков в секунду является хорошим показателем. Так что максимально мы можем получать около 20 структур KEY_DATA в секунду, и в то же самое время до клавиатуры могут очень долго не дотрагиваться. Поэтому для исключения лишних запросов к драйверу мы забираем накопившуюся информацию не чаше чем раз в секунду. А если забирать нечего, то и вовсе ничего не запрашиваем. Такая логика работы достигается за счет усыпления потока на некоторое время и ожидания события, которое сигнализирует драйвер. Во-вторых, т.к. 20 структур KEY_DATA - это намного меньше, чем одна страница, мы используем буферизованный ввод/вывод и забираем информацию через DeviceIoControl. Если требуется обмен бОльшими объемами (скажем, ориентировочно, несколько страниц), то лучше использовать метод METHOD_NEITHER, а вместо DeviceIoControl - ReadFile. В-третьих, пользователь может закрыть программу управления либо с помощью мыши, либо с помощью клавиатуры. Если он использует мышь, то драйвер не будет выгружен, т.к. последний прошедший через драйвер IRP содержит указатель на нашу процедуру завершения и находится сейчас в очереди драйвера Kbdclass. Чтобы программа управления имела возможность выгрузить драйвер, пользователь должен нажать на клавишу. Поэтому, выводя соответствующее сообщение, мы даем пользователю необходимые инструкции. И наконец, про обещанную отмену IRP. Если бы можно было отменить этот злосчастный ожидающий завершения IRP, то нам не пришлось бы пугать пользователя странными сообщениями. Мы бы просто отменили этот запрос и выгрузили бы драйвер. И такой механизм существует. Драйвер Kbdclass поддерживает отмену только одного типа IRP и это как раз IRP_MJ_READ. Проблема в том, что отменить IRP, находящийся в очереди другого драйвера не так то просто. В своей книге "Programming The Windows Driver Model" 2nd Edition Вальтер Они приводит пару способов отмены чужих IRP. Первый их них точно не подойдет, а вот второй… Если не подойдет и второй, то остается только организовать свою собственную очередь и помещать туда все пришедшие IRP типа IRP_MJ_READ, а нижестоящему драйверу слать их дубликаты. Завершения дублированных IRP перехватывать, извлекать из очереди их оригиналы и перекладывать в них необходимые данные. Если у фильтра будет своя очередь, то отмена IRP становится делом техники. Насколько этот сценарий возможен практически, я не знаю, т.к. при его реализации могут возникнуть непредвиденные сложности.

Ну, и самое последнее. В архиве вы обнаружите сразу два фильтра. Второй - MouSpy, получен путем замены слов "keyboard", "kbd" и т.п. в прародителе на их "мышиные" аналоги. Ну и конечно я не смог удержаться и добавил ещё кое-что. Поэтому этот фильтр не просто пассивно следит за "мышиными" событиями, но может вносить в них некоторые коррективы. Но, если у вас клавиатура/мышь USB, то подключить фильтры, скорее всего, не удастся. Во всяком случае, подключить фильтр к стеку для USB-мыши мне не удалось, а USB-клавиатуры у меня нет. Причина в том, что внутренне функция IoGetDeviceObjectPointer вызывает функцию ZwOpenFile, а она, в свою очередь, формирует запрос IRP_MJ_CREATE и шлет его в стек (см. предыдущую статью). Вот три стека мыши на одной из моих машин: первый для классической PS/2 мыши, второй для терминальной сессии и третий для мыши USB.


 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"

Видимо, один из драйверов в стеке (скорее всего mouhid) отказывается обработать запрос IRP_MJ_CREATE, возвращая код STATUS_SHARING_VIOLATION, т.е. совместный доступ к файлу запрещен (имеется в виду объект "файл" ассоциированный с объектом "устройство"). Как бы там ни было, в дальнейшие детали я не вдавался. Не удаётся подключиться к USB стеку… и слава богу, ибо "со свиным рылом, да в калашный ряд?"… Как мы будем обрабатывать IRP_MN_QUERY_REMOVE_DEVICE, IRP_MN_REMOVE_DEVICE и IRP_MN_SURPRISE_REMOVAL, в случае если пользователь отключит/выдернет клавиатуру/мышку из USB-порта? Так что, доставайте из чулана своих старых боевых подруг или ждите следующую статью (если я её когда-нибудь напишу ;) ).

Рис. 16-1. KbdSpy и MouSpy в действии.

Исходный код драйвера в архиве.

2002-2013 (c) wasm.ru