logo
Протоколирование обмена информацией между компьютером и внешним запоминающим USB-устройством

1.5 Пакет запроса ввода / вывода (IRP)

Пакеты ввода / вывода (IRP_пакеты) используются для передачи запросов к драйверу от его клиентов. Они являются структурами данных переменной длины, и состоят из стандартного заголовка, содержащего общую учетную информацию, и одного или нескольких блоков параметров, называемых ячейками стека ввода / вывода (I/O Stack Location).

Приведем структуру заголовка IRP_пакета:

Таблица 1.5.1. Структура заголовка IRP_пакета.

Поля

Описание

IO_STATUS_BLOCK IoStatus

Статус запроса

PVOID AssociatedIrp. SystemBuffer

Указатель на системный буфер для случая, если устройство поддерживает буферизованный ввод / вывод

PMDL MdlAddress

Указатель на MDL_список в случае, если устройство поддерживает прямой ввод / вывод

PVOID UserBuffer

Адрес пользовательского буфера для ввода / вывода

BOOLEAN Cancel

Индикатор того, что IRP_пакет должен быть аннулирован

Основное назначение ячеек стека ввода / вывода состоит в том, чтобы хранить функциональный код и параметры запроса на ввод / вывод. Ниже, в таблице 1.5.2 приводятся поля ячеек стека ввода / вывода, к которым драйвер может обращаться непосредственно по указателю (чего не рекомендуется делать для остальных полей):

Таблица 1.5.2. Структура ячейки стека ввода / вывода

Поля

Описание

UCHAR MajorFunction

Код IRP_MJ_XXX, описывающий назначение операции

UCHAR MinorFunction

Субкод операции

PDEVICE_OBJECT DeviceObject

Указатель на объект устройства, которому был адресован данный объект IRP

PFILE_OBJECT FileObject

Файловый объект для данного запроса, если он задан

union Parameters (трактовка определяется значением MajorFunction)

struct Read

Параметры для IRP типа IRP_MJ_READ:

ULONG Length

ULONG Key

LARGE_INTEGER ByteOffset

struct Write

Параметры для IRP типа IRP_MJ_WRITE:

ULONG Length

ULONG Key

LARGE_INTEGER ByteOffset

struct DeviceControl

Параметры для IRP типа IRP_MJ_DEVICE_CONTROL:

ULONG OutputBufferLength

ULONG InputBufferLength

ULONG IoControlCode

PVOID Type3InputBuffer

Приведем графическое представление структуры IRP_пакета:

Рис. 1.5.1 Структура IRP пакета

Общение с USB_накопителями в ОС Windows NT 5 на уровне драйверов, как уже было сказано в разделе 1.3.1, происходит посредством передачи URB_пакетов. Указатели на URB_пакеты содержат ячейки стека IRP_пакета, доступ к этим указателям осуществляется следующим образом:

PIO_STACK_LOCATION IrpSp = IoGetCurrentIrpStackLocation(Irp);

PURB Urb = IrpSp->Parameters. Others. Argument1;

Приведем частичное объявление структуры из справочной документации Microsoft. Отметим только поля, использование которых необходимо в рамках данной курсовой работы:

typedef struct _URB {

union {

struct _URB_HEADER UrbHeader;

struct _URB_SELECT_INTERFACE UrbSelectInterface;

struct _URB_SELECT_CONFIGURATION UrbSelectConfiguration;

struct _URB_BULK_OR_INTERRUPT_TRANSFER UrbBulkOrInterruptTransfer;

}

} URB, *PURB;

Поле UrbHeader хранит информацию о коде URB_пакета, по которому можно определить, какая операция запрашивается.

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

Поле UrbBulkOrInterruptTransfer несет наиболее важную в рамках данной курсовой работы информацию - указатели на блоки ввода / вывода USB_устройства. Приведем описание структуры _URB_BULK_OR_INTERRUPT_TRANSFER:

struct _URB_BULK_OR_INTERRUPT_TRANSFER {

struct _URB_HEADER Hdr;

USBD_PIPE_HANDLE PipeHandle;

ULONG TransferFlags;

ULONG TransferBufferLength;

PVOID TransferBuffer;

PMDL TransferBufferMDL;};

Поля этой структуры описаны в следующей таблице:

Таблица 1.5.3 Поля структуры URB_BULK_OR_INTERRUPT_TRANSFER

Поле

Описание

struct _URB_HEADER Hdr

Стандартный заголовок URB_пакета, содержащий код запроса

USBD_PIPE_HANDLE PipeHandle

Дескриптор канала, на который передаются данные

ULONG TransferFlags

Флаги, определяющие направление передачи данных и способ обработки ошибок

ULONG TransferBufferLength

Длина передаваемого блока данных в байтах

PVOID TransferBuffer

Указатель на передаваемый буфер. Буфер находится в нестраничной памяти

PMDL TransferBufferMDL

Указатель на MDL_список, несущий передаваемую информацию. Буфер находится в страничной памяти

Следует отметить, что один из указателей TransferBuffer или TransferBufferMDL равен NULL, то есть в пределах одного пакета передается только одна порция данных.

Задача протоколирования обмена информацией сводится к перехвату и сохранению буферов TransferBuffer и TransferBufferMDL.

1.6 Уровни запроса прерываний

В каждый момент времени центральный процессор находится на одном из уровней IRQL (Interrupt Request Level - уровень запросов прерываний). Уровни IRQL располагаются в порядке убывания от HIGHEST_LEVEL до PASSIVE_LEVEL. Каждому из прерываний (прерывания от внешних устройств, системные часы, и т.д.) соответствует свой уровень IRQL. Специальным действиям операционной системы также назначены IRQL. Они отмечены в нижней части приведённой таблицы:

Таблица 1.6.1. Уровни запросов прерываний.

Уровень

Назначение

HIGHEST_LEVEL

Наивысший уровень. Все прерывания заблокированы

POWER_LEVEL

Прерывания по отказу питания

IPI_LEVEL

Межпроцессорное взаимодействие

CLOCK2_LEVEL

Прерывание по системному таймеру 2

СLOCK1_LEVEL

Прерывание по системному таймеру 1

PROFILE_LEVEL

Прерывание по таймеру замера производительности

уровни DRQL

Обычные прерывания устройств

DISPATCH_LEVEL

Диспетчеризация потоков и выполнение отложенных процедур

APC_LEVEL

Выполнение асинхронного вызова процедуры

PASSIVE_LEVEL

Обычное исполнение кода потока

Общее правило обработки уровней запросов прерываний гласит, что прерывания с IRQL, меньшим, чем у выполняемого в данный момент кода, маскируются. Во время исполнения кода потока (пользовательского или системного) устанавливается наименьший IRQL = 0 (PASSIVE_LEVEL). Работа драйвера чаще всего выполняется на уровне IRQL = 2 (DISPATCH_LEVEL). Уровни, лежащие над ним, называются DIRQL (Device IRQL) и выставляются для обработчиков прерываний от внешних устройств (ISR - interrupt service routine). Даже во время выполнения ISR драйвера может произойти прерывание с большим IRQL, например, принадлежащее другому драйверу.

Чем выше текущий уровень IRQL исполняемого кода, тем меньше функций ему доступно. Так, например, диспетчер потоков работает на уровне
DISPATCH_LEVEL, и, следовательно, не будет вызываться, пока на процессоре с уровнем большим или равным DISPATCH_LEVEL исполняется другой код. Таким образом, на уровнях DISPATCH_LEVEL и выше отключается переключение потоков. Функции ожидания диспетчерских объектов (события, мьютексы, семафоры) с отличным от нуля временем, обращение к файлам, подкачка отсутствующих в физической памяти страниц - всё это также становится недоступным. Для корректного сохранения запросов в файле фильтр в таких случаях должен применять специальную методику.

1.7 Уведомление о завершении запроса нижестоящим драйвером

При отслеживании обмена данными драйвер-фильтр может получать уведомления о том, что некоторый переданный запрос был завершён нижестоящим драйвером. Механизм уведомления заключается в том, что вызовом специальной функции IoSetCompletionRoutine фильтр обращается к стеку в пакете IRP. В позиции стека, следующей за текущей позицией, он устанавливает в специальном поле адрес функции завершения (completion routine). Затем при передаче пакета по цепочке позиция стека увеличивается.

Когда нижестоящий драйвер отправляет пакет запроса на завершение (вызовом IoCompleteRequest), подсистема ввода / вывода начинает просматривать стек внутри этого пакета от конца к началу. Если в какой-то позиции стека определена функция завершения, управление передаётся ей. Отработав, функция возвращает результат, сигнализирующий об успехе, ошибке или необходимости дальнейшей обработки запроса.

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

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

1.8 Работа с файлами в режиме ядра

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

Для открытия файла из драйвера режима ядра используется универсальная функция ZwCreateFile. Универсальность этой функции состоит в том, что с ее помощью производится и открытие существующих файлов, и создание новых.

Специфика системной функции ZwCreateFile состоит в том, что она имеет протокольных параметров даже больше, чем пользовательский вызов CreateFile. Существенная часть входной информации об открываемом объекте поступает внутри структуры OBJECT_ATTRIBURTES, которую следует предварительно создать и заполнить соответствующими конкретными данными. Для ведения учетной информации открытого объекта используется структура данных IO_STATUS_BLOCK, которую следует предоставить при вызове (инициализировать ее не следует).

Представим основные параметры функции ZwCreateFile в следующей таблице:

Таблица 1.8.1. Параметры функции ZwCreateFile

Тип параметра

Описание параметра

OUT PHANDLE pHandle

Указатель на переменную, куда следует поместить дескриптор открытого объекта

IN ACCESS_MASK DesiredAccess

Характеристика доступа к объекту. Для фалов чаще всего используются значения

GENERIC_READ или GENERIC_WRITE

IN POBJECT_ATTRIBUTES

pObjAttributes

Указатель на заполненную вызывающим кодом структуру данных, которая описывает имя, местоположение и некоторые другие характеристики открываемого объекта

OUT PIO_STATUS_BLOCK pIOStatus

Указатель на буфер, в котором будет размещена информация об открытом объекте в формате структуры IO_STATUS_BLOCK

IN PLARGE_INTEGER AllocationSize

Начальный размер файла в байтах. Ненулевое значение принимается во внимание только при создании и перезаписи файла

IN ULONG FileAttributes

Атрибуты открываемого файла. Типовым является значение FILE_ATTRIBUTE_NORMAL

IN ULONG SharedAccessFlags

Описывает, разрешен ли совместный доступ, например, FILE_SHARE_READ - для чтения

IN ULONG CreateDispositionFlags

Способ открытия файла, например, FILE_OPEN_IF - если не существует, создать

IN ULONG CreateOptions

Комбинация флагов создания, например, FILE_SYNCHONOUS_IO_NONALERT - все операции над файлом выполняются как синхронные (DesiredAccess должен включать флаг SYNCHRONIZE)

IN PVOID EaBuffer

Для драйверов устройств следует указывать NULL

IN ULONG EaLength

Для драйверов устройств следует указывать 0

Для заполнения структуры атрибутов объекта используется функция
InitializeObjectAttributes. Опишем ее параметры в следующей таблице:

Таблица 1.8.2. Параметры функции InitializeObjectAttributes

Тип параметра

Описание параметра

OUT POBJECT_ATTRIBUTES pObjAttributes

Указатель на переменную, куда следует поместить атрибуты объекта

IN PUNICODE_STRING ObjectName

Имя объекта, HANDLE которого создается

IN ULONG Attributes

Флаги атрибутов объекта, при открытии файла как правило используются флаги OBJ_CASE_INSENSITIVE и OBJ_KERNEL_HANDLE

IN HANDLE RootDirectory

Дескриптор корневой директории для объекта, описатель атрибутов которого создается. Если ObjectName полностью описывает путь к объекту, то значению RootDirectory присваивается NULL

IN PSECURITY_DESCRIPTOR SecurityDescriptor

Дескриптор безопасности. Если указано NULL, то применяется стандартный дескриптор

Запись в файл выполняется системной функцией ZwWriteFile:

Таблица 1.8.3. Параметры функции ZwWriteFile

Тип параметра

Описание параметра

IN HANDLE FileHandle

Дескриптор открытого или модифицированного файлового объекта

IN HANDLE Event

Для драйверов устройств следует указывать NULL

IN PIO_APC_ROUTINE

Для драйверов устройств следует указывать NULL

IN PVOID ApcContext

Для драйверов устройств следует указывать NULL

OUT PIO_STATUS_BLOCK pioStatusBlock

В поле pIoStatusBlock->Information по завершении вызова находится число реально записанных байт

IN PVOID Buffer

Буфер с данными для записи

IN ULONG Length

Размер записываемой порции данных

IN PLARGE_INTEGER pByteOffset

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

IN PULONG Key

Для драйверов устройств следует указывать NULL

Для закрытия дескриптора объекта следует применять функцию ZwCloseKey.

Следует отметить, что функции работы с файлами могут работать только на уровне IRQL, равном PASSIVE_LEVEL. Это приводит к необходимости применения специальной методики при протоколировании обмена данными с USB_накопителем.

1.9 Работа с реестром в режиме ядра

Работа с реестром из драйвера уровня ядра необходима, так как именно в системном реестре хранится информация о настройках протоколирования. Информация о настройках хранится в ключе реестра, связанном с устройством, к которому подключается драйвер-фильтр. Имя этого устройства соответствует шаблону HKEY_LOCAL_MACHINESystemCurrentControlSetEnumUSBXXXXXXDeviceParameters.

Доступ ключу устройства в реестре в драйвере предоставляется функцией IoOpenDeviceRegistryKey. Перечислим ее параметры:

Таблица 1.9.1. Параметры функции IoOpenDeviceRegistry

Тип параметра

Описание параметра

IN PDEVICE_OBJECT DeviceObject

Указатель на объект физического устройства, ключ которого должен быть открыт

IN ULONG DevInstKeyType

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

IN ACCESS_MASK DesiredAccess

Этот параметр определяет права доступа к ключу

OUT PHANDLE DevInstRegKey

Указатель на переменную, куда следует поместить дескриптор открытого ключа

Открыв основной ключ, следует получить доступ к вложенному в него ключу с параметрами протоколирования. Для этого используется функция ZwOpenKey. Перечислим ее параметры:

Таблица 1.9.2. Параметры функции ZwOpenKey

Тип параметра

Описание параметра

OUT PHANDLE KeyHandle

Указатель на переменную, куда следует поместить дескриптор открытого ключа

IN ACCESS_MASK DesiredAccess

Этот параметр определяет права доступа к ключу

IN POBJECT_ATTRIBUTES pObjectAttributes

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

Открыв ключ собственных параметров драйверу необходимо считать настройки протоколирования. Для чтения значения параметров ключа реестра используется функция ZwQueryValueKey. Перечислим ее параметры:

Таблица 1.9.3. Параметры функции ZwQueryValueKey

Тип параметра

Описание параметра

IN HANDLE KeyHandle

Дескриптор ключа, которому принадлежит считываемый параметр

IN PUNICODE_STRING ValueName

Строка юникод-символов, содержащая имя параметра ключа

IN KEY_VALUE_INFORMATION_CLASS

KeyValueInformationClass

Этот параметр принимает одно их трех значений в зависимости от полноты информации о параметре:

KeyValueBasicInformation

KeyValueFullInformation

KeyValuePartialInformation

OUT PVOID KeyInformation

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

IN ULONG Length

Длина предоставленного буфера

OUT PULONG ResultLength

Указатель на переменную, содержащую число реально записанных в KeyInformation байт

После того, как работа с ключом реестра закончена, его дескриптор следует освободить вызовом функции ZwClose.

1.10 MDL_списки

MDL_список - это структура, хранящая отображение блока виртуальной памяти на физическую память. MDL_список используется в разрабатываемом драйвере для хранения информации из URB_пакетов, связанных с вводом / выводом USB_устройства. Кроме того, обмен информацией с USB_устройством в режиме прямого доступа к памяти ведется именно посредством MDL_списков.

Перед использовании MDL_списка в драйвере необходимо провести ряд подготовительных действий:

· выделить область в страничной памяти с помощью вызова функции
ExAllocatePool;

· вызвать функцию MmCreateMdl, создающую и инициализирующую MDL_список;

· выполнить фиксацию страниц, описанных в MDL_списке, в физической памяти с помощью вызова функции MmProbeAndLockPages.

После завершения использования MDL_списка его следует освободить:

· отменить фиксацию страниц страничной памяти в оперативной памяти вызовом функции MmUnlockPages;

· очистить MDL_список, вызвав функцию IoFreeMdl;

· освободить выделенную под список страничную память вызовом
ExFreePool.

2. Конструкторский раздел

2.1 Точки входа разрабатываемого драйвера

Разрабатываемый драйвер является драйвером нижнего уровня. В стеке драйверов USB_накопителя он находится непосредственно под драйвером устройства, если после его загрузки не произойдет установки какого-либо другого драйвера-фильтра нижнего уровня.

Разрабатываемый драйвер включает в себя следующие точки входа:

· DriverEntry;

· AddDevice;

· DriverUnload;

· Функции обработки IRP_пакетов:

· обработка IRP_пакетов с кодами IRP_MJ_INTERNAL_DEVICE_CONTROL - функция DispatchInternalDeviceControl;

· обработка IRP пакетов с прочими кодами - функция DispatchRoutine.

Рассмотрим каждую из них более подробно.

2.1.1 Функция DriverEntry

В этой функции происходит регистрация всех стандартных точек входа драйвера и обработчиков IRP_пакетов. В разрабатываемом драйвере пакеты IRP c кодами, не равными IRP_MJ_INTERNAL_DEVICE_CONTROL обрабатываются функцией DispatchRoutine.

2.1.2 Функция AddDevice

Управление этой функции передается диспетчером ввода / вывода после того, как завершает свою работу DriverEntry. AddDevice создает функциональный объект устройства с помощью вызова IoCreateDevice и подключает его к стеку драйверов выбранного устройства (вызовом IoAttachDeviceToDeviceStack). Кроме того, в этой функции производятся действия по подготовке к протоколированию: считываются настройки из системного реестра, выделяется буфер для сбора протоколируемой информации, создается лог-файл.

2.1.3 Функция DriverUnload

Функция DriverUnload необходима для того, чтобы сделать драйвер выгружаемым. В унаследованных драйверах на эту функцию возложен весь процесс выгрузки драйвера: удаление символьных ссылок, объектов устройств драйвера, отключение прерываний от объектов, освобождение выделенной памяти. В WDM_драйверах все эти действия возложены на функцию-обработчик пакетов с кодом IRP_MJ_PNP.

2.1.4 Функция DispatchRoutine

На эту функцию возложены обязанности по обработке IRP_пакетов с различными кодами, хотя в разрабатываемом драйвере существует необходимость в обработке только двух типов запросов. Все запросы с кодом, отличным от IRP_MJ_PNP передаются по стеку драйверов без изменений. Запросы же IRP_MJ_PNP диспетчеризуются по суб-кодам в функции PnP_Dispatch. Необходимость диспетчеризации по суб-кодам запросов IRP_MJ_PNP вызвана тем, что драйвер не должен нарушать порядка работы операционной системы и обязан подчиняться PnP_менеджеру, то есть в драйвере должны корректно обрабатываться события старта и удаления устройства.

2.1.5 Функция DispatchInternalDeviceControl

Запросы ввода / вывода к USB_накопителю передаются в составе IRP_пакетов с кодом IRP_MN_INTERNAL_DEVICE_CONTROL. Этот пакет содержит полную информацию о направлении и характере передаваемых данных. То есть для протоколирования обмена информацией с USB_носителем следует перехватывать пакеты именно этого типа.

Для того чтобы перехватывать информацию, передаваемую в обоих направлениях, следует установить функцию обратного вызова диспетчера ввода / вывода. Методика установки этой функции была описана в разделе 1.7. При наличии этой функции разрабатываемый драйвер-фильтр получит возможность перехвата данных, передаваемых от устройства к хосту.

Для сохранения протоколируемой информации используется, как уже было сказано в разделе 1.10, MDL_список. Этот MDL_список создается в функции AddDevice. Объем памяти, выделяемой под список, совпадает с максимальным размером лог-файла, задаваемым в пользовательском приложении. После создания список фиксируется в страничной памяти, что предотвращает его выгрузку на жесткий диск во время работы драйвера. После этих подготовительных действий список используется в функции DispatchInternalDeviceControl - он заполняется перехватываемой информацией.

Запись накопленного буфера в лог-файл происходит при удалении устройства.

Такая методика выбрана из-за того, что функция DispatchInternalDeviceControl работает на уровне запроса прерываний, равном DISPATCH_LEVEL, что сильно затрудняет использование механизмов синхронизации, которые могли бы позволить перейти на уровень запроса прерываний, равный PASSIVE_LEVEL, где становятся доступными функции работы с файлами. Если бы это было достигнуто в разрабатываемом драйвере, то отпала бы необходимость выделения больших объемов нестраничной памяти для хранения протокола.

Запись файла на диск в момент удаления устройства возможна, так как это событие инициализируется PnP_менеджером, запросы которого всегда происходят на уровне IRQL, равном PASSIVE_LEVEL.

2.2 Размещение кода драйвера в памяти

Некоторые функции драйвера, например те, которые выполняют инициализацию, выгодно выполнить и освободить память, занимаемую ими. В языке C есть специальная директива #pragma_alloc_text (<тип секции>, <имя размещаемой функции>), позволяющая управлять размещением кода. В качестве типа секции могут указываться значения «INIT» или «PAGE».

Функции с размещением в секции «INIT» выгружаются, и память, занимаемая ими, освобождается сразу по завершении их работы. В разрабатываемом драйвере в секции «INIT» размещена точка входа DriverEntry, поскольку она выполняется единожды при загрузке драйвера.

Точки входа AddDevice и DriverUnload располагаются в секции «PAGE», то есть в страничной памяти, поскольку они гарантированно вызываются на уровне привилегий, равном PASSIVE_LEVEL и, даже оказавшись выгруженными на диск, будут немедленно загружены в физическую память менеджером страничной памяти (который способен работать только на уровне PASSIVE_LEVEL).

Остальные же точки входа (DispatchRoutine и DispatchInternalDeviceControl) располагаются по умолчанию, в нестраничной памяти, поскольку их работа зависит от клиентского драйвера USB_устройства, под которым в стеке драйверов располагается разрабатываемый драйвер-фильтр. Уровень привилегий его запросов слабо предсказуем и может быть равен DISPATCH_LEVEL. На этом уровне подкачка страниц невозможна, что при обращении к выгруженной функции приведет к краху системы.