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. На этом уровне подкачка страниц невозможна, что при обращении к выгруженной функции приведет к краху системы.