Драйверы режима ядра: Часть 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 у клавиатуры (и у мыши тоже) имеется более одного (определяется количеством терминальных сессий) стека. На "обычной" машине клавиатурный и "мышиный" стеки выглядят примерно так:
Код (Text):
kd> !drvobj <font color="blue">i8042prt</font> 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. "Мышиный" стек ведет себя подобным же образом.
Код (Text):
kd> !devobj 8181be30 Device object (8181be30) is for: KeyboardClass0 \Driver\Kbdclass DriverObject 818372b0 Current Irp <font color="blue">815a2e68</font> 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 <font color="blue">815a2e68</font> 1 Irp is active with 6 stacks 6 is current (= 0x815a2f8c) No Mdl System buffer = <font color="blue">81664b48</font> Thread <font color="blue">8171bca0</font>: 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 = <font color="blue">f3ec82e0</font> 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: <font color="blue">00000078</font> 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.
Код (Text):
kd> !thread <font color="blue">8171bca0</font> THREAD 8171bca0 Cid a4.bc Teb: 00000000 Win32Thread: e28ae5a8 WAIT: (WrUserRequest) KernelMode Alertable 8171bf20 SynchronizationEvent 8171bc08 SynchronizationEvent 8171bbc8 NotificationTimer 8171bc48 SynchronizationEvent IRP List: <font color="blue">815a2e68</font>: (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!<font color="blue">RawInputThread</font> (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).
Код (Text):
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.
Код (Text):
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 является "пустой" функцией.
Код (Text):
KeReleaseSpinLockFromDpcLevel proc retn 4 KeReleaseSpinLockFromDpcLevel endpНечто подобное (я имею в виду оптимизацию) можно сделать и для макроса LOCK_ACQUIRE. Потребуется только узнать текущий IRQL и если он равен DISPATCH_LEVEL, то вызвать KeAcquireSpinLockAtDpcLevel, которая (на однопроцессорной машине) тоже выполняет инструкцию ret.
Код (Text):
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 драйвер. Кода довольно много и я не буду приводить его полностью (см. архив к статье).
Код (Text):
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.
Код (Text):
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.
Код (Text):
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.
Код (Text):
InitializeListHead addr g_KeyDataListHeadГлобальная переменная g_KeyDataListHead является головой двусвязного списка структур KEY_DATA_ENTRY.
Код (Text):
invoke KeInitializeSpinLock, addr g_KeyDataSpinLockСпин-блокировка нам потребуется для организации монопольного доступа к списку структур KEY_DATA_ENTRY. Использовать объекты синхронизации, например, мьютекс, мы не можем, т.к. будем обращаться к списку на IRQL = DISPATCH_LEVEL.
Код (Text):
invoke KeInitializeSpinLock, addr g_EventSpinLockЭта спин-блокировка поможет нам организовать монопольный доступ к переменной g_pEventObject, в которой будет храниться указатель на объект событие. Этот объект будет использоваться для уведомления программы управления, о новых данных (Подробнее см. часть 14 "Базовая техника. Синхронизация: Использование объекта "событие" для взаимодействия драйвера с программой управления").
Код (Text):
MUTEX_INIT g_mtxCDO_StateС помощью этого мьютекса мы сможем монопольно выполнять некоторые участки кода.
Код (Text):
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.
Код (Text):
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
Код (Text):
.while TRUE invoke RemoveEntry, addr KeyData .break .if eax == 0 .endwДрайвер и программа управления построены таким образом, что программу управления можно выгрузить и загрузить повторно при уже запущенном драйвере и подключенном фильтре. Может так случиться, что список g_KeyDataListHead не пуст. Если вы внимательно проанализируете ход возможных событий после прочтения всей статьи, то станет ясно, что в списке может находиться одна структура KEY_DATA_ENTRY, соответствующая коду клавиши, нажатой сразу после некорректного завершения работы программы управления. Вышеприведенный цикл опустошает, возможно, непустой список g_KeyDataListHead.
Код (Text):
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
Код (Text):
and g_fSpy, FALSEЕсли клиент отключается, то и незачем следить за клавиатурой - FiDO_DispatchRead не должна больше устанавливать процедуру завершения.
Код (Text):
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, находящейся в теле драйвера, то можно разрешить его выгрузить. Если фильтр всё ещё существует, драйвер остается невыгружаемым. Перед завершением работы программа управления просит драйвер отключить и удалить фильтр. Но возможны ситуации, когда драйвер не сможет этого сделать. Например, если кто-то подключен к стеку поверх нас, отключение фильтра "разорвет стек". Программа управления может просто забыть отключить фильтр или в ней может произойти исключение и описатель устройства автоматически закрывается системой. Речь, разумеется, не идет о нашей программе управления, в которой (я надеюсь) все сделано правильно. Имеется в виду программа управления вообще, т.е. общий принцип. Наконец, пользователь может завершать сеанс работы с системой, и все пользовательские процессы принудительно завершаются. В любом случае, как я уже сказал, драйвер и программа управления построены таким образом, что программу управления можно запустить повторно.
Код (Text):
and g_fCDO_Opened, FALSE MUTEX_RELEASE g_mtxCDO_StateТ.к. единственный наш клиент только что "ушел", сбрасываем флаг g_fCDO_Opened.
Процедура CDO_DispatchDeviceControl
Код (Text):
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). Если это действительно объект "событие", то у нас есть два варианта: либо мы должны создать фильтр и подключить его к стеку клавиатуры, либо фильтр уже существует и подключён.
Код (Text):
.if !g_fFiDO_Attached invoke KeyboardAttach mov [esi].IoStatus.Status, eaxЕсли фильтр не подключен, будем считать, что он ещё не создан. Процедура KeyboardAttach сделает всё необходимое, вернув соответствующий код.
Код (Text):
.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 кроме нас никто трогать не будет.
Код (Text):
.else LOCK_ACQUIRE g_EventSpinLock mov bl, alЕсли фильтр уже подключен, необходимо блокировать доступ к переменной g_pEventObject, т.к. к ней может обращаться наша процедура завершения ReadComplete. Спин-блокировка требуется из-за того, что ReadComplete работает на IRQL = DISPATCH_LEVEL.
Код (Text):
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 избавит нас от множества потенциальных проблем.
Код (Text):
MUTEX_ACQUIRE g_mtxCDO_StateПри получении от программы управления управляющего кода IOCTL_KEYBOARD_DETACH, пытаемся отключить фильтр, также под защитой мьютекса.
Код (Text):
.if g_fFiDO_Attached and g_fSpy, FALSE invoke KeyboardDetach mov [esi].IoStatus.Status, eaxЕсли фильтр подключен, сбрасываем флаг g_fSpy, чтобы FiDO_DispatchRead не устанавливала больше процедуру завершения, и пытаемся отключить и удалить фильтр.
Код (Text):
.if eax == STATUS_SUCCESS mov g_fFiDO_Attached, FALSE .endifЕсли фильтр успешно отключен, сбрасываем соответствующий флаг. Если отключить фильтр не удалось, этот флаг останется во взведенном состоянии, что даст нам возможность при следующем (возможном) получении IOCTL_KEYBOARD_ATTACH, всё сделать корректно.
Код (Text):
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Под защитой спин-блокировки, удаляем ссылку на объект "событие".
Код (Text):
invoke FillKeyData, [esi].AssociatedIrp.SystemBuffer, \ [edi].Parameters.DeviceIoControl.OutputBufferLengthПри получении от программы управления управляющего кода IOCTL_GET_KEY_DATA, копируем в пользовательский буфер имеющиеся у нас к настоящему моменту структуры KEY_DATA. Процедуру FillKeyData, а также AddEntry и RemoveEntry я разбирать не буду, т.к. если вы читали седьмую часть цикла "Использование ассоциативных списков", их содержимое не должно представлять сложность, а подробности относительно структуры KEYBOARD_INPUT_DATA смотрите в DDK.
Процедура KeyboardAttach
Код (Text):
.if ( g_pFilterDeviceObject != NULL ) mov status, STATUS_SUCCESSЕсли переменная g_pFilterDeviceObject не равна нулю, очевидно, она содержит указатель на объект "устройство-фильтр" и, наверное, он уже подключен к стеку.
Код (Text):
.elseЕсли фильтра нет, создадим его.
Код (Text):
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 позволяет драйверу создать сколь угодно много объектов "устройство" и хранить все относящиеся к ним данные в самих этих объектах.
Код (Text):
invoke IoGetDeviceObjectPointer, addr g_usTargetDeviceName, FILE_READ_DATA, \ addr pTargetFileObject, addr pTargetDeviceObject .if eax == STATUS_SUCCESSНадеюсь, вы помните, что функция IoGetAttachedDevice всегда возвращает указатель на объект "устройство", находящийся на вершине стека. Мы используем функцию IoGetDeviceObjectPointer для получения указателя на вершину стека по заранее известному нам имени одного из объектов "устройство", принадлежащего стеку. PnP драйверам, в этом смысле, проще, т.к. диспетчер PnP предоставляет им указатель на корневой объект стека - объект "физическое устройство". Т.е. я хочу сказать, что для подключения к стеку вам нужен указатель на любой объект стека. Как вы его получите, не имеет значения.
Код (Text):
mov eax, g_pDriverObject and (DRIVER_OBJECT PTR [eax]).DriverUnload, NULLТ.к. сейчас мы подключимся к стеку, то через нас могут пойти IRP. После установки флага g_fSpy мы будем устанавливать в них процедуру завершения. Когда эти IRP завершаться, мы не знаем, но знаем, что до этих пор драйвер нельзя выгружать, т.к. процедура завершения находится в теле драйвера. Поэтому проще всего просто сделать драйвер невыгружаемым.
Код (Text):
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.
Код (Text):
NTSTATUS ZwReadFile( IN HANDLE hFile, . . . ) { PFILE_OBJECT pFileObject PDEVICE_OBJECT pDeviceObject ObReferenceObjectByHandle( hFile, ... &pFileObject ... ) pDeviceObject = IoGetRelatedDeviceObject( pFileObject ) . . . }Функция IoGetRelatedDeviceObject (исходный код см. в предыдущей статье) возвращает указатель на самый верхний объект "устройство" в стеке. Если же IRP формируется драйвером, так сказать вручную (см. исходный код процедуры QueryPnpDeviceState в предыдущей статье), то он будет послан напрямую целевому устройству и перехватить такой запрос с помощью фильтра невозможно, если конечно фильтр не находится ниже по стеку.
Код (Text):
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 по отношению к этому объекту "файл".
Код (Text):
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, мы используем более общий и универсальный механизм.
Код (Text):
and [ecx].Flags, not DO_DEVICE_INITIALIZINGФункция IoCreateDevice создает объект "устройство" с установленным флагом DO_DEVICE_INITIALIZING. До сих пор мы не касались этого момента потому, что создавали устройства только в процедуре DriverEntry. Дело в том, что по выходу из DriverEntry диспетчер ввода/вывода (в функции IopReadyDeviceObjects) сам сбрасывает этот флаг во всех объектах "устройство", созданных драйвером. Если же мы создаем устройство не в DriverEntry, придется сбросить флаг DO_DEVICE_INITIALIZING самостоятельно, иначе никто не сможет подключиться к неинициализированному объекту, как вы только что видели в коде IoAttachDeviceToDeviceStack. Также этот флаг проверяется при некоторых других операциях.
Процедура KeyboardDetach
Код (Text):
.if g_pFilterDeviceObject != NULL mov eax, g_pFilterDeviceObject or (DEVICE_OBJECT ptr [eax]).Flags, DO_DEVICE_INITIALIZINGПеред тем как отключаться от стека, посмотрим, находимся ли мы на самой его вершине или к нам тоже уже кто-то подключился. Блокировать базу данных диспетчера ввода/вывода мы не можем, но зато можем предотвратить новые подключения, до тех пор, пока не проверим, есть ли кто-нибудь над нами.
Код (Text):
PDEVICE_OBJECT IoGetAttachedDeviceReference( IN PDEVICE_OBJECT pDeviceObject ) { ExAcquireSpinLock( &IopDatabaseLock, ... ) pDeviceObject = IoGetAttachedDevice( pDeviceObject ) ObReferenceObject( pDeviceObject ) ExReleaseSpinLock( &IopDatabaseLock, ... ) return pDeviceObject }Здесь должно быть всё понятно.
Код (Text):
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 не делает каких бы то ни было проверок. Отключение объекта "устройство" от стека состоит в простом обнулении соответствующих указателей в связанных объектах.
Код (Text):
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 проверяет, не ожидает ли оно удаления, а его драйвер выгрузки. И если это так и счетчик ссылок на объект равен нулю, инициирует отложенные операции.
Код (Text):
.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 не имеет смысла, т.к. сейчас мы удалим фильтр.
Код (Text):
mov eax, g_pFilterDeviceObject and g_pFilterDeviceObject, NULL invoke IoDeleteDevice, eaxУдаляем объект "устройство-фильтр", но драйвер всё ещё остается невыгружаемым, т.к. мы можем иметь ожидающий завершения IRP, содержащий указатель на нашу процедуру завершения.
Код (Text):
.endif invoke ObDereferenceObject, pTopmostDeviceObjectФункция IoGetAttachedDeviceReference, в отличие от функции IoGetAttachedDevice, увеличивает счетчик ссылок в объекте, указатель на который возвращает. Это гарантирует, что объект не будет удален. Если мы были на вершине стека, то увеличили счетчик ссылок в нашем же объекте "устройство-фильтр" и IoDeleteDevice не сможет его удалить.
Код (Text):
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
Код (Text):
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, pIrpIRP типа IRP_MJ_POWER обрабатываются отличным от всех остальных типов IRP способом.
Макрос IoCopyCurrentIrpStackLocationToNext мы подробно разобрали в прошлой статье (его мы будем использовать в процедуре FiDO_DispatchRead). Макрос IoSkipCurrentIrpStackLocation намного проще.
Код (Text):
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, перед тем как вызвать процедуру диспетчеризации драйвера, смещает текущий блок стека на одну позицию вниз.
Код (Text):
Irp->CurrentLocation-- pIrp->Tail.Overlay.CurrentStackLocation -= sizeof(IO_STACK_LOCATION)Если перед этим использовать макрос IoSkipCurrentIrpStackLocation, то получиться, что указатель блока стека вообще не изменится и нижестоящий драйвер получить тот же самый блок стека, что и драйвер вызвавший IoCallDriver (PoCallDriver). Вызов макроса IoSkipCurrentIrpStackLocation - это просто оптимизация. Действительно, если нам не нужно устанавливать процедуру завершения, то вызов макроса IoCopyCurrentIrpStackLocationToNext скопирует наш блок стека в блок стека нижестоящего драйвера (поля Control, CompletionRoutine и Context, как вы помните, не копируются). Т.о. нижестоящий драйвер всё равно получит те же самые параметры. Используя макрос IoSkipCurrentIrpStackLocation вместо IoCopyCurrentIrpStackLocationToNext, мы избегаем ненужной операции копирования блоков стека. Но, повторяю, это можно делать, только если не требуется устанавливать процедуру завершения, что должно быть и так понятно.
Процедура FiDO_DispatchPassThrough
Код (Text):
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
Код (Text):
.if g_fSpyПолучив запрос типа IRP_MJ_READ адресованный фильтру, смотрим, взведен ли флаг g_fSpy. Если да, то мы должны установить процедуру завершения.
Код (Text):
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-функций, реализующих атомарный доступ, но мы можем использовать средства ассемблера.
Код (Text):
IoCopyCurrentIrpStackLocationToNext pIrp IoSetCompletionRoutine pIrp, ReadComplete, NULL, TRUE, TRUE, TRUEУстанавливаем процедуру завершения (детали см. в предыдущей статье). Когда IRP будет завершаться, мы сможем узнать код клавиши.
Код (Text):
.else IoSkipCurrentIrpStackLocation pIrpЕсли флаг g_fSpy сброшен, процедура FiDO_DispatchRead ведет себя аналогично процедуре FiDO_DispatchPassThrough.
Код (Text):
.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.
Код (Text):
.if [esi].IoStatus.Status == STATUS_SUCCESS mov edi, [esi].AssociatedIrp.SystemBuffer assume edi:ptr KEYBOARD_INPUT_DATAЕсли IRP завершается с кодом успеха, то его буфер содержит, по крайней мере, одну структуру KEYBOARD_INPUT_DATA, несущую с собой вожделенный код клавиши.
Код (Text):
mov ebx, [esi].IoStatus.InformationПоле Information содержит размер действительной части буфера и должно быть кратно размеру структуры KEYBOARD_INPUT_DATA.
Код (Text):
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 для блокировки.
Код (Text):
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 (мне так больше нравится ;) ).
Код (Text):
.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 всё ещё содержит действительный указатель.
Код (Text):
.if [esi].PendingReturned IoMarkIrpPending esi .endifПоскольку мы возвращаем из процедуры завершения код отличный от STATUS_MORE_PROCESSING_REQUIRED, то должны следовать правилу №6 (см. часть 15).
Код (Text):
lock dec g_dwPendingRequestsЗапрос обработан - атомарно уменьшаем счетчик g_dwPendingRequests.
Код (Text):
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.
Код (Text):
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 в действии.
Исходный код драйвера в архиве.
© Four-F
Драйверы режима ядра: Часть 16 : Драйвер-фильтр (не PnP)
Дата публикации 12 янв 2005