Драйверы режима ядра: Часть 4: Подсистема ввода-вывода — Архив WASM.RU
В первой части я сказал, что разрабатываемые нами драйверы можно считать DLL режима ядра. С определенной долей условности это действительно так. Зачем еще нужен драйвер, который не управляет каким-либо реальным устройством? Только для того, чтобы служить проводником в режим ядра. При этом код драйвера, по сути, является набором функций, позволяющих решать задачи недоступные коду режима пользователя. Когда нужно решить одну из таких задач, вызывается соответствующая функция. Принципиальная разница (не считая уровня привилегий) в том, что, в случае обычной DLL, мы получаем (явно или неявно) адрес интересующей нас функции и передаем туда управление. В случае драйвера режима ядра, такой сценарий был бы крайне опасен с точки зрения безопасности системы. Поэтому, система предоставляет посредника в лице диспетчера ввода-вывода (I/O manager), который является одним из компонентов подсистемы ввода-вывода (I/O subsystem). Диспетчер ввода-вывода отвечает за формирование пакета запроса ввода-вывода (I/O request packet, IRP) и посылку его драйверу для дальнейшей обработки. Весьма упрощенная схема взаимодействия диспетчера ввода-вывода с приложениями пользовательского режима и драйверами устройств приведена на рис 4-1.
Рис. 4-1. Упрощенная схема ввода-вывода
Из этой схемы следует, что абсолютно все обращения пользовательских приложений к устройствам и, следовательно, к драйверам устройств выполняются под управлением диспетчера ввода-вывода. Возвращаемые результаты также проходят через этот компонент системы ввода-вывода. Не стоит думать, что драйвер так уж сильно ущемлен в своих правах. Просто механизм, предоставляемый диспетчером ввода-вывода, работает именно так как показано на схеме. На самом деле, драйвер, конечно же, может обратиться к режиму пользователя и напрямую, а вот у кода режима пользователя нет никаких документированных возможностей напрямую обращаться к режиму ядра. Система не может позволить пользовательским приложениям безконтрольно переходить границу между режимом пользователя и режимом ядра. И это правильно.
Код режима пользователя вынужден заказывать операцию ввода-вывода на устройство. Именно на устройство. Дело в том, что в процедуре DriverEntry драйвер должен создать устройство (или несколько), которым он будет управлять. В нашем случае это устройство виртуальное. Создание устройства, конечно же, не следует понимать буквально, даже если драйвер призван управлять реальным устройством. Это означает создание в памяти соответствующих структур данных, олицетворяющих для системы само устройство. Создавая устройство, драйвер как бы говорит диспетчеру ввода-вывода: "Вот устройство, которым я буду управлять. Если к тебе придет запрос ввода-вывода на это устройство, ты отправляй его ко мне, а я уж разберусь, что с ним делать." Только драйвер знает как работать со своим устройством. Диспетчер ввода-вывода просто занимается формированием пакетов запроса ввода-вывода и направлением их нужным драйверам. А код режима пользователя вообще не знает, и не должен знать, какой драйвер или драйверы какое устройство обслуживают.
Программа управления драйвером VirtToPhys.sys
Называть код, который мы сейчас рассмотрим, программой управления драйвером, вообще говоря, не совсем верно. Он (этот код) совмещает в себе как программу управления, ответственную за регистрацию и запуск драйвера, так и программу-клиент устройства, призванную осуществлять с устройством операции ввод-вывода.
Вот этот исходный код.
Код (Text):
;::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: ; ; VirtToPhys.asm - Программа управления драйвером VirtToPhys ; ;::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: .386 .model flat, stdcall option casemap:none ;::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: ; В К Л Ю Ч А Е М Ы Е Ф А Й Л Ы ;::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: include \masm32\include\windows.inc include \masm32\include\kernel32.inc include \masm32\include\user32.inc include \masm32\include\advapi32.inc includelib \masm32\lib\kernel32.lib includelib \masm32\lib\user32.lib includelib \masm32\lib\advapi32.lib include \masm32\include\winioctl.inc include \masm32\Macros\Strings.mac include common.inc ;::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: ; К О Д ;::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: .code ;::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: ; BigNumToString ;::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: BigNumToString proc uNum:UINT, pacBuf:LPSTR ; переводит число в строку разделяя разряды пробелами local acNum[32]:CHAR local nf:NUMBERFMT invoke wsprintf, addr acNum, $CTA0("%u"), uNum and nf.NumDigits, 0 and nf.LeadingZero, FALSE mov nf.Grouping, 3 mov nf.lpDecimalSep, $CTA0(".") mov nf.lpThousandSep, $CTA0(" ") and nf.NegativeOrder, 0 invoke GetNumberFormat, LOCALE_USER_DEFAULT, 0, addr acNum, addr nf, pacBuf, 32 ret BigNumToString endp ;::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: ; start ;::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: start proc uses esi edi local hSCManager:HANDLE local hService:HANDLE local acModulePath[MAX_PATH]:CHAR local _ss:SERVICE_STATUS local hDevice:HANDLE local adwInBuffer[NUM_DATA_ENTRY]:DWORD local adwOutBuffer[NUM_DATA_ENTRY]:DWORD local dwBytesReturned:DWORD local acBuffer[256+64]:CHAR local acThis[64]:CHAR local acKernel[64]:CHAR local acUser[64]:CHAR local acAdvapi[64]:CHAR local acNumber[32]:CHAR invoke OpenSCManager, NULL, NULL, SC_MANAGER_ALL_ACCESS .if eax != NULL mov hSCManager, eax push eax invoke GetFullPathName, $CTA0("VirtToPhys.sys"), \ sizeof acModulePath, addr acModulePath, esp pop eax invoke CreateService, hSCManager, $CTA0("VirtToPhys"), \ $CTA0("Virtual To Physical Address Converter"), \ SERVICE_START + SERVICE_STOP + DELETE, SERVICE_KERNEL_DRIVER, \ SERVICE_DEMAND_START, SERVICE_ERROR_IGNORE, addr acModulePath, \ NULL, NULL, NULL, NULL, NULL .if eax != NULL mov hService, eax ; в драйвере будет вызвана функция DriverEntry invoke StartService, hService, 0, NULL .if eax != 0 ; драйверу будет направлен IRP типа IRP_MJ_CREATE invoke CreateFile, $CTA0("\\\\.\\slVirtToPhys"), GENERIC_READ + GENERIC_WRITE, \ 0, NULL, OPEN_EXISTING, 0, NULL .if eax != INVALID_HANDLE_VALUE mov hDevice, eax lea esi, adwInBuffer assume esi:ptr DWORD invoke GetModuleHandle, NULL mov [esi][0*(sizeof DWORD)], eax invoke GetModuleHandle, $CTA0("kernel32.dll", szKernel32) mov [esi][1*(sizeof DWORD)], eax invoke GetModuleHandle, $CTA0("user32.dll", szUser32) mov [esi][2*(sizeof DWORD)], eax invoke GetModuleHandle, $CTA0("advapi32.dll", szAdvapi32) mov [esi][3*(sizeof DWORD)], eax lea edi, adwOutBuffer assume edi:ptr DWORD ; драйверу будет направлен IRP типа IRP_MJ_DEVICE_CONTROL invoke DeviceIoControl, hDevice, IOCTL_GET_PHYS_ADDRESS, \ esi, sizeof adwInBuffer, \ edi, sizeof adwOutBuffer, \ addr dwBytesReturned, NULL .if ( eax != 0 ) && ( dwBytesReturned != 0 ) invoke GetModuleFileName, [esi][0*(sizeof DWORD)], \ addr acModulePath, sizeof acModulePath lea ecx, acModulePath[eax-5] .repeat dec ecx mov al, [ecx] .until al == '\' inc ecx push ecx CTA0 "%s \t%08Xh\t%08Xh ( %s )\n", szFmtMod invoke BigNumToString, [edi][0*(sizeof DWORD)], addr acNumber pop ecx invoke wsprintf, addr acThis, addr szFmtMod, ecx, \ [esi][0*(sizeof DWORD)], \ [edi][0*(sizeof DWORD)], addr acNumber invoke BigNumToString, [edi][1*(sizeof DWORD)], addr acNumber invoke wsprintf, addr acKernel, addr szFmtMod, addr szKernel32, \ [esi][1*(sizeof DWORD)], \ [edi][1*(sizeof DWORD)], addr acNumber invoke BigNumToString, [edi][2*(sizeof DWORD)], addr acNumber invoke wsprintf, addr acUser, addr szFmtMod, addr szUser32, \ [esi][2*(sizeof DWORD)], \ [edi][2*(sizeof DWORD)], addr acNumber invoke BigNumToString, [edi][3*(sizeof DWORD)], addr acNumber invoke wsprintf, addr acAdvapi, addr szFmtMod, addr szAdvapi32, \ [esi][3*(sizeof DWORD)], \ [edi][3*(sizeof DWORD)], addr acNumber invoke wsprintf, addr acBuffer, \ $CTA0("Module:\t\tVirtual:\t\tPhysical:\n\n%s\n%s%s%s"), \ addr acThis, addr acKernel, addr acUser, addr acAdvapi assume esi:nothing assume edi:nothing invoke MessageBox, NULL, addr acBuffer, $CTA0("Modules Base Address"), \ MB_OK + MB_ICONINFORMATION .else invoke MessageBox, NULL, $CTA0("Can't send control code to device."), NULL, \ MB_OK + MB_ICONSTOP .endif ; драйверу будет направлен IRP типа IRP_MJ_CLOSE invoke CloseHandle, hDevice .else invoke MessageBox, NULL, $CTA0("Device is not present."), NULL, MB_ICONSTOP .endif ; в драйвере будет вызвана функция DriverUnload invoke ControlService, hService, SERVICE_CONTROL_STOP, addr _ss .else invoke MessageBox, NULL, $CTA0("Can't start driver."), NULL, MB_OK + MB_ICONSTOP .endif invoke DeleteService, hService invoke CloseServiceHandle, hService .else invoke MessageBox, NULL, $CTA0("Can't register driver."), NULL, MB_OK + MB_ICONSTOP .endif invoke CloseServiceHandle, hSCManager .else invoke MessageBox, NULL, $CTA0("Can't connect to Service Control Manager."), NULL, \ MB_OK + MB_ICONSTOP .endif invoke ExitProcess, 0 start endp ;::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: ; ;::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: end startНе считая кода подготавливающего информацию для ввода в устройство и кода ответственного за форматирование и вывод на экран полученных данных, принципиально нового здесь немного - всего три функции: CreateFile, DeviceIoControl и CloseHandle. Эти три функции являются ключевыми. Причем, ни в одном параметре передающимся в эти функции вы не найдете и намека, как я уже сказал, на какой-либо драйвер. Все операции ввода-вывода здесь происходят с виртуальным устройством.
Создает устройство и присваивает ему имя драйвер. Для драйверов создающих виртуальные устройства это происходит обычно в процедуре инициализации DriverEntry. Драйвер VirtToPhys создает устройство под именем "devVirtToPhys" (префикс "dev" я добавил сознательно - зачем, скажу ниже). К сожалению, вы пока не можете посмотреть на исходный код драйвера - в следующей статье у вас появится такая возможность.
Итак, драйвер регистрируется и запускается обычным образом. Обращение к функции StartService приводит к тому, что в драйвере вызывается процедура DriverEntry, в которой и создается виртуальное устройство под именем "devVirtToPhys". Именуется устройство для того, чтобы его можно было найти (по имени) и получить к нему доступ.
Раз создается какой-то новый объект, да еще имеющий имя, то на сцену выходит еще один ключевой компонент системы, которому мы еще не уделили должного внимания - диспетчер объектов (object manager). Он отвечает за создание, защиту и управление объектами.
Имена объектов попадают в пространство имен диспетчера объектов. Пространство имен имеет иерархическую структуру аналогичную структуре каталогов файловой системы. Для просмотра базы данных диспетчера объектов можно использовать разные утилиты, самой удобной из которых, на мой взгляд, является WinObj уже знакомого вам Марка Руссиновича http://www.sysinternals.com/.
Для просмотра объектов созданных драйвером VirtToPhys на своем компьютере, просто запустите VirtToPhys.exe, но не закрывайте диалоговое окно
В каталог "\Device", по соглашению, помещаются объекты "устройство". Наше устройство devVirtToPhys помещается драйвером именно в этот каталог.
Рис. 4-2. Объект "устройство" devVirtToPhys в пространстве имен диспетчера объектов
Рис. 4-3. Свойства объекта "устройство" devVirtToPhys
В каталог "\Driver" попадают объекты "драйвер". В этот каталог система и помещает наш драйвер VirtToPhys (никаких префиксов в имени я не использовал).
Рис. 4-4. Объект "драйвер" VirtToPhys в пространстве имен диспетчера объектов
Все каталоги в пространстве имен диспетчера объектов, кроме двух - "\BaseNamedObjects" и "\??", невидимы для кода режима пользователя. Поэтому, обратиться ни к одному объекту, кроме находящихся в этих двух каталогах, код режима пользователя не может. Это сделано все по тем же соображениям безопасности. Таким образом, обратиться ни к объекту "драйвер" VirtToPhys в каталоге "\Driver", ни к объекту "устройство" devVirtToPhys в каталоге "\Device", код режима пользователя не может.
Для того чтобы объект "устройство" стал доступен коду режима пользователя, драйвер должен создать в доступном ему (коду режима пользователя) каталоге "\??" еще один объект - символьную ссылку (symbolic link). Да, да именно символьную, а не символическую, как пишут иногда в иных изданиях. И не надо говорить: "А вот в моем большом - на 386 тысяч слов - словаре...". ;-)
Кстати, создать символьную ссылку можно и из режима пользователя функцией DefineDosDevice.
Открыв каталог "\??" вы увидите, что он буквально кишит символьными ссылками. Такое странное имя этот каталог получил потому, что при сортировке по алфавиту он будет первым, что увеличивает скорость поиска объектов. До Windows NT4 этот каталог назывался "\DosDevices" и являлся наиболее часто используемым при поиске объектов. Поэтому и был переименован в "\??".
Для совместимости с драйверами предыдущих версий Windows, в корневом каталоге пространства имен диспетчера объектов имеется символьная ссылка "\DosDevices", значением которой является строка "\??"
Драйвер VirtToPhys создает символьную ссылку "slVirtToPhys" на свое устройство "devVirtToPhys" в каталоге "\??", значением которой является строка "\Device\devVirtToPhys" (Что, совсем запутались? ;-) ). В имени символьной ссылки я также специально использовал префикс "sl".
Рис. 4-5. Объект "символьная ссылка" slVirtToPhys в пространстве имен диспетчера объектов
Рис. 4-6. Свойства объекта "символьная ссылка" slVirtToPhys
Префиксы в именах устройства и символьной ссылки я добавил для наглядности. Только чтобы показать, что имя устройства и имя символьной ссылки совсем не обязательно, хотя и могут (и обычно так и бывает), должны совпадать с именем драйвера. Важно чтобы символьная ссылка указывала на действительное имя устройства. И еще один важный момент, который вы и без меня несомненно понимаете - в одном и том же каталоге не может быть двух объектов с одинаковыми именами, точно также, как в одном и том же каталоге файловой системы нет двух файлов с одинаковыми именами.
Таким образом, на момент выхода из функции StartService мы имеем три объекта: драйвер "\Driver\VirtToPhys", устройство "\Device\devVirtToPhys" и символьную ссылку на устройство "\??\slVirtToPhys".
Когда будете просматривать эти объекты на своем компьютере, имейте ввиду, что WinObj сортирует имена с учетом регистра.
Если вы еще помните, во второй части статьи, где мы разбирали параметр реестра ImagePath, я обещал рассказать о том, что такое "\??" в самом начале пути "\??\C:\masm32\..." к файлу драйвера. Ну, что такое "\??" вы уже узнали, а вот "\??\C:" это не что иное, как символьная ссылка на устройство "Device\Harddisk\Volume1" или, в моем случае, на первый том первого жесткого диска. При отображении системой образа файла драйвера в память, именно это устройство получит запрос ввода-вывода.
Перейдем теперь к разбору исходного кода. При успешном завершении функции StartService мы вызываем стандартную Win32-функцию ввода-вывода CreateFile. Батюшки-святы! А файлы то тут при чем?!... Согласен, пока все кажется довольно запутанным. Надеюсь, к концу статьи кое-что прояснится.
Описание функции CreateFile занимает в документации довольно много места. Из всей этой информации к устройствам относится процентов 20. Поэтому, я немного облегчу вам жизнь, выделив только самое необходимое.
Код (Text):
CreateFile proto stdcall lpFileName:LPCSTR, dwDesiredAccess:DWORD, \ dwShareMode:DWORD, lpSecurityAttributes:LPVOID, \ dwCreationDistribution:DWORD, dwFlagsAndAttributes:DWORD, \ hTemplateFile:HANDLEВопреки своему названию, эта функция создает или открывает существующий (многие Create* функции работают таким образом) объект, а не только файл. Microsoft следовало бы назвать ее CreateObject. В качестве объекта может выступать и устройство.
lpFileName
- указатель на завершающуюся нулем строку, с именем открываемого устройства, точнее, символьной ссылки указывающей на устройство.
На эту тему мы поговорим достаточно подробно ниже после описания всех параметров.
dwDesiredAccess
- определяет права доступа к открываемому устройству.
Нам потребуются два значения:GENERIC_READ
- доступ на чтение данных с устройства;
GENERIC_WRITE
- доступ на запись данных в устройство.
Можно использовать и комбинацию этих двух значений.
dwShareMode
- определяет, может ли устройство быть совместо используемым с другим кодом. Т.е. можно ли еще раз получить описатель устройства вызовом функции CreateFile.
Нам могут пригодиться три значения:0
- в большинстве случаев нужно именно это значение, т.к. обычно устройства выполняющие такие специфические задачи, как наши, открываются для монопольного использования.
Если нужно использовать устройство совместно с каким-либо кодом, то используем:
FILE_SHARE_READ
- другой код тоже может получить описатель устройства с доступом на чтение;
FILE_SHARE_WRITE
- другой код тоже может получить описатель устройства с доступом на запись.
Хотя в документации написано именно так, но сколько я не экспериментировал с этим параметром, мне так и не удалось получить монопольный доступ к устройству. Какое бы значения я не указывал, последующий вызов CreateFile все равно возвращал описатель устройства. Не смотря на это, в дальнейшем, мы все равно будем считать, что этот параметр работает так, как написано в документации.
lpSecurityAttributes
- указатель на структуру SECURITY_ATTRIBUTES.
Т.к. никакой особой защиты нам не нужно, а также мы не собираемся наследовать описатель устройства каким-то другим процессам, то просто устанавливаем этот параметр в NULL.
dwCreationDistribution
- определяет действия в случае если устройство с заданным именем не найдено или уже существует.
Для устройств используется только значение OPEN_EXISTING. Т.е. на момент вызова CreateFile устройство должно уже существовать.
dwFlagsAndAttributes
- определяет атрибуты и специальные флаги.
У нас этот параметр всегда будет равен 0.
hTemplateFile
- описатель файла-прототипа.
И этот параметр у нас тоже всегда будет равен NULL.
В случае успеха функция CreateFile вернет описатель устройства. Подчеркиваю - именно устройства, которое создал драйвер, а не описатель самого драйвера. Такая неверная трактовка иногда встречается.
Если вызов функции CreateFile потерпит неудачу, то, в отличие от многих других функций, возвращающих NULL в подобных случаях, функция CreateFile вернет INVALID_HANDLE_VALUE (-1).
С учетом всего вышесказанного, мы вызываем CreateFile следующим образом.
Код (Text):
invoke CreateFile, $CTA0("\\\\.\\slVirtToPhys"), GENERIC_READ + GENERIC_WRITE, \ 0, NULL, OPEN_EXISTING, 0, NULLС последними пятью параметрами все, вроде более-менее, ясно. Во втором параметре мы передаем комбинацию флагов GENERIC_READ + GENERIC_WRITE, т.к. собираемся, как отправлять данные на устройство, так и получать результаты его работы.
А вот с первым параметром, в качестве которого мы передаем указатель на строку "\\.\slVirtToPhys", надо разобраться. Префикс "\\.\" является псевдонимом локального компьютера. Функция CreateFile является оболочкой вокруг другой функции NtCreateFile (реализованной в \%SystemRoot%\System32\ntdll.dll), которая, в свою очередь, обращается к одноименному системному сервису (не путать со службами).
Системный сервис - это функция, вызов которой осуществляется через прерывание int 2Eh, что влечет за собой переход в режим ядра. Все адреса таких функций сведены в единый массив KiServiceTable (недокументирован и адрес не экспортируется, префикс Ki означает Kernel Internal), доступ к которому можно получить через переменную ядра KeServiceDescriptorTable.
Системные сервисы не имеют никакого прямого отношения к нашей сегодняшней теме. Может быть, в будущем, мы к этому еще и вернемся. Недокументированую и не экспортируемую информацию (имена и адреса функций и переменных, структуры и идентификаторы и т.п.) можно получить при наличии пакета отладочных символов (symbol package), который пока еще - слава Microsoft! - можно скачать бесплатно.
NtCreateFile заменяет псевдоним локального компьютера "\\.\" на имя каталога "\??" в пространстве имен диспетчера объектов (т.о. "\\.\slVirtToPhys" превращается в "\??\slVirtToPhys") и вызывает функцию ядра ObOpenObjectByName. Через символьную ссылку ObOpenObjectByName находит объект "\Device\devVirtToPhys" и возвращает указатель на него (таким образом символьная ссылка, видимая коду режима пользователя, используется диспетчером объектов для трансляции во внутреннее имя устройства). Используя этот указатель NtCreateFile создает новый объект "файл" представляющий устройство и возвращает его описатель.
Операционная система абстрагирует запросы ввода-вывода, обрабатывая их так, будто они адресованы файлам. Драйвер преобразует запросы к виртуальному файлу в запросы, специфичные для устройства, скрывая тот факт, что конечное устройство может и не быть устройством с файловой структурой. Что мы и имеем в нашем случае. Такая запутанная схема позволяет обобщить интерфейс между приложениями и устройствами. Т.е. все считываемые или записываемые данные представляются потоками байтов, направляемыми в виртуальные файлы.
Прежде чем функция CreateFile вернет управление, в драйвере будет вызвана функция которую определяет сам драйвер (точнее, диспетчер ввода-вывода сформирует IRP типа IRP_MJ_CREATE и направит его драйверу обслуживающему наше устройство). При этом, эта процедура будет выполнена в контексте потока вызвавшего CreateFile при IRQL = PASSIVE_LEVEL (помните еще что это такое? ;-) ). Если она вернет код успеха, то новый описатель будет создан и возвращен коду вызвавшему CreateFile. Если драйвер вернет код ошибки, то соответственно никаких новых описателей не возникнет.
Созданый CreateFile новый объект "файл" является объектом исполнительной системы (executive) и не попадает в пространство имен диспетчера объектов. Просмотреть описатели принадлежащие процессу можно с помощью утилиты Process Explorer. Марк Руссинович (http://www.sysinternals.com/) и тут приложил свою руку, за что ему большое спасибо.
Рис. 4-7. Объект "файл"
Рис. 4-8. Свойства объекта "файл"
Чтобы охватить одним взглядом, только что описанный и весьма непростой процесс, позволю себе подытожить. Итак, "\\.\slVirtToPhys" превращается в символьную ссылку "\??\slVirtToPhys", через которую, уже на стороне режима ядра, осуществляется доступ к устройству "\Device\devVirtToPhys". Из структуры объекта "устройство" DEVICE_OBJECT (см. \include\w2k\ntddk.inc) извлекаются сведения об обслуживающем его драйвере. Диспетчер ввода-вывода формирует пакет запроса ввода-вывода и направляет его драйверу. Так драйвер узнает о том, что код режима пользователя пытается получить доступ к его устройству. Если драйвер не имеет ничего против, то он возвращает код успеха, что является сигналом диспетчеру объектов о создании виртуального файла. При этом в таблице описателей (handle table) процесса создается новый элемент с указателем на объект "файл", и коду режима пользователя возвращается новый описатель. Таким образом, символьная ссылка и описатель служат косвенными указателями на системные ресурсы, что позволяет системе оградить прикладные программы от прямого взаимодействия с системными структурами данных.
Я был неприятно удивлен, когда сравнил значения счетчиков ссылок и открытых описателей, которые показывает Process Explorer v5.25 в свойствах описателя, с реальными значениями. Мы уже немного касались темы указателей и описателей объекта в прошлой статье. Описатель является косвенной ссылкой на объект, предоставляемой ядром коду режима пользователя, а указателями пользуется ядро. Оказалось, что значение счетчика ссылок References постоянно завышается на единицу. Поставив прерывание на область памяти содержащую счетчик ссылок я обнаружил, что при просмотре свойств описателя, значение счетчика динамически повышается на несколько единиц и тут же понижается до прежнего значения, но на вкладке свойств все равно отображается неверное - на единицу больше - значение. Так что в действительности значение References на рис. 4-8 равно 1, а не 2. И на старуху бывает проруха. Реальное значение счетчиков можно посмотреть в SoftICE. Для этого нужно вывести список описателей процесса командой proc -o VirtToPhys. В столбце Handle найти нужный описатель и взяв из столбца Ob Hdr * указатель на заголовок объекта (структура OBJECT_HEADER) вывести в окно дампа содержимое заголовка dd <адрес заголовка>. Первый DWORD - счетчик ссылок, второй - описателей.
Код (Text):
.if eax != INVALID_HANDLE_VALUE mov hDevice, eaxЕсли все ОК, то мы сохраняем описатель устройства, возвращенный CreateFile, в переменной hDevice, и имеем возможность осуществлять операции ввода-вывода с этим устройством, посредством вызова функций DeviceIoControl, ReadFile и WriteFile. DeviceIoControl является универсальной функцией ввода-вывода - ее мы и будем использовать.
Код (Text):
DeviceIoControl proto stdcall hDevice:HANDLE, dwIoControlCode:DWORD, \ lpInBuffer:LPVOID, nInBufferSize:DWORD, \ lpOutBuffer:LPVOID, nOutBufferSize:DWORD, \ lpBytesReturned:LPVOID, lpOverlapped:LPVOIDНе смотря на то, что функция DeviceIoControl принимает даже больше параметров чем CreateFile, тут все достаточно просто.
hDevice
- описатель устройства, которому адресуется запрос ввода-вывода;
dwIoControlCode
- управляющий код ввода-вывода;
Подробнее об этом параметре ниже.
lpInBuffer
- указатель на буфер с входными (относительно устройства) данными или NULL, если для выполнения операции никаких дополнительных данных устройству не требуется;
nInBufferSize
- размер входного буфера в байтах;
lpOutBuffer
- указатель на буфер с выходными (относительно устройства) данными или NULL, если устройство не возвращает никаких дополнительных данных;
nOutBufferSize
- размер выходного буфера в байтах;
lpBytesReturned
- количество байт, скопированных в выходной буфер при выходе из DeviceIoControl;
lpOverlapped
- указатель на структуру OVERLAPPED.
Т.к. мы собираемся осуществлять только синхронный ввод-вывод (процедура DeviceIoControl не вернет управление до тех пор, пока не отработает вызванная в драйвере процедура), то этот параметр у нас всегда будет равен NULL.
В самом начале я сказал, что код драйвера, по сути, является набором функций, позволяющих решать задачи недоступные коду режима пользователя. Поскольку драйвер устройства может выполнять массу таких задач, необходимо как то дифференцировать запросы. Для этого и предназначен второй параметр dwIoControlCode, называемый управляющим кодом ввода-вывода (I/O control code), который строится по определенным правилам.
Рис. 4-9. Поля управляющего кода ввода-вывода
DeviceType
- идентификатор типа устройства (16 бит).
Может принимать значение в диапазоне 0-0FFFFh, который разбит на две равные половины. Диапазон 0-7FFFh зарезервирован Microsoft, а диапазон 8000h-0FFFFh доступен всем желающим. В файле \include\w2k\ntddk.inc можно найти набор констант FILE_DEVICE_* со значениями из зарезервированного диапазона. Мы будем использовать FILE_DEVICE_UNKNOWN. Можно определить и свой собственный идентификатор.
Access
- запрашиваемые права доступа к устройству.
Т.к. это поле размером 2 бита, то возможны четыре значения:FILE_ANY_ACCESS (0)
- максимальные права доступа;
FILE_READ_ACCESS (1)
- доступ на чтение, т.е. получение данных от устройства;
FILE_WRITE_ACCESS (2)
- доступ на запись, т.е. передачу данных на устройство;
- комбинация последних двух значений.
Function
- собственно и определяет операцию, которую должно выполнить устройство (12 бит).
Может принимать значение в диапазоне 0-FFFh, который также разбит на две равные половины. Диапазон 0-7FFh зарезервирован Microsoft, а диапазон 800h-0FFFh доступен.
Method
- определяет метод ввода-вывода.
Размер поля 2 бита - 4 значения, и все они могут быть использованы для наших целей:METHOD_BUFFERED (0)
- буферизованный ввод-вывод (buffered I/O);
METHOD_IN_DIRECT (1)
METHOD_OUT_DIRECT (2)
- прямой ввод-вывод (direct I/O);
METHOD_NEITHER (3)
- ввод-вывод без управления (neither I/O).
О методах ввода-вывода мы поговорим чуть более подробно, когда будем разбирать исходный код драйвера, в следующей статье. Сейчас важно лишь то, что буферизованный ввод-вывод является самым безопасным, т.к. система берет на себя всю заботу об управлении буферами, что приводит, естественно, к накладным расходам. Но в случае небольшого размера буферов - сравнимого с размером страницы - эти издержки пренебрежимо малы. Именно буферизованный ввод-вывод мы и будем использовать в драйвере VirtToPhys.
Управляющий код можно сформировать вручную, но лучше автоматизировать этот процесс с помощью незамысловатого макроса CTL_CODE.
Код (Text):
CTL_CODE MACRO DeviceType:=<0>, Function:=<0>, Method:=<0>, Access:=<0> EXITM %(((DeviceType) SHL 16) OR ((Access) SHL 14) OR ((Function) SHL 2) OR (Method)) ENDMЭтот макрос определен дважды - в файле \include\winioctl.inc (отсутствует в пакете masm32), который необходимо включить в исходный текст программы управления, и в файле ntddk.inc, включаемом в исходный текст драйвера.
Определение значения управляющего кода ввода-вывода и двух констант вынесено в отдельный файл common.inc, т.к. эти значения требуются как в исходном тексте программы управления, так и в исходном тексте драйвера. Т.о. все изменения в этом файле отразятся на исходных текстах программы управления и драйвера.
Код (Text):
NUM_DATA_ENTRY equ 4 DATA_SIZE equ (sizeof DWORD) * NUM_DATA_ENTRY IOCTL_GET_PHYS_ADDRESS equ CTL_CODE(FILE_DEVICE_UNKNOWN, 800h, METHOD_BUFFERED, FILE_READ_ACCESS + FILE_WRITE_ACCESS)Вернемся к исходному тексту.
Код (Text):
lea esi, adwInBuffer assume esi:ptr DWORD invoke GetModuleHandle, NULL mov [esi][0*(sizeof DWORD)], eax invoke GetModuleHandle, $CTA0("kernel32.dll", szKernel32) mov [esi][1*(sizeof DWORD)], eax invoke GetModuleHandle, $CTA0("user32.dll", szUser32) mov [esi][2*(sizeof DWORD)], eax invoke GetModuleHandle, $CTA0("advapi32.dll", szAdvapi32) mov [esi][3*(sizeof DWORD)], eaxПодготавливаем буфер adwInBuffer для передачи устройству. Буфер содержит виртуальные базовые адреса загрузки самой программы управления VirtToPhys.exe, и трех системных модулей отображенных на адресное пространство нашего процесса.
Код (Text):
lea edi, adwOutBuffer assume edi:ptr DWORD invoke DeviceIoControl, hDevice, IOCTL_GET_PHYS_ADDRESS, \ esi, sizeof adwInBuffer, \ edi, sizeof adwOutBuffer, \ addr dwBytesReturned, NULLВызовом DeviceIoControl передаем устройству заполненный буфер. При этом произойдут процессы похожие на описанные выше, с той лишь разницей, что никаких новых объектов не появится и обращение к устройству произойдет по описателю, что значительно быстрее.
Используя описатель устройства диспетчер ввода-вывода извлечет сведения об обслуживающем его драйвере, сформирует пакет запроса ввода-вывода типа IRP_MJ_DEVICE_CONTROL и направит его драйверу. В драйвере будет вызвана соответствующая процедура. Эта процедура также будет выполнена в контексте потока вызвавшего DeviceIoControl при IRQL = PASSIVE_LEVEL. По значению управляющего кода ввода-вывода IOCTL_GET_PHYS_ADDRESS драйвер определит что конкретно от него требуется, извлечет из входного буфера четыре виртуальных адреса, преобразует из в физические и поместит в выходной буфер.
Код (Text):
.if ( eax != 0 ) && ( dwBytesReturned != 0 ) invoke GetModuleFileName, [esi][0*(sizeof DWORD)], \ addr acModulePath, sizeof acModulePath lea ecx, acModulePath[eax-5] .repeat dec ecx mov al, [ecx] .until al == '\' inc ecx push ecx CTA0 "%s \t%08Xh\t%08Xh ( %s )\n", szFmtMod invoke BigNumToString, [edi][0*(sizeof DWORD)], addr acNumber pop ecx invoke wsprintf, addr acThis, addr szFmtMod, ecx, \ [esi][0*(sizeof DWORD)], \ [edi][0*(sizeof DWORD)], addr acNumber invoke BigNumToString, [edi][1*(sizeof DWORD)], addr acNumber invoke wsprintf, addr acKernel, addr szFmtMod, addr szKernel32, \ [esi][1*(sizeof DWORD)], \ [edi][1*(sizeof DWORD)], addr acNumber invoke BigNumToString, [edi][2*(sizeof DWORD)], addr acNumber invoke wsprintf, addr acUser, addr szFmtMod, addr szUser32, \ [esi][2*(sizeof DWORD)], \ [edi][2*(sizeof DWORD)], addr acNumber invoke BigNumToString, [edi][3*(sizeof DWORD)], addr acNumber invoke wsprintf, addr acAdvapi, addr szFmtMod, addr szAdvapi32, \ [esi][3*(sizeof DWORD)], \ [edi][3*(sizeof DWORD)], addr acNumber invoke wsprintf, addr acBuffer, \ $CTA0("Module:\t\tVirtual:\t\tPhysical:\n\n%s\n%s%s%s"), \ addr acThis, addr acKernel, addr acUser, addr acAdvapi assume esi:nothing assume edi:nothing invoke MessageBox, NULL, addr acBuffer, $CTA0("Modules Base Address"), \ MB_OK + MB_ICONINFORMATION .else invoke MessageBox, NULL, $CTA0("Can't send control code to device."), NULL, \ MB_OK + MB_ICONSTOP .endifЕсли все пройдет удачно, то DeviceIoControl вернет ненулевое значение, а в dwBytesReturned окажется значение количества байт возвращенных устройством в буфере adwOutBuffer. Наша задача теперь - отформатировать эти данные и вывести их на экран. Я не буду подробно описывать этот процесс - тут все достаточно тривиально. В текстовых макросах $CTA0 интенсивно используются стандартные эскейп-последовательности (подробное описание см. \Macros\Strings.mac). Процедура BigNumToString разбивает адрес по три разряда. Закончив фарматирование, выводим информацию на экран.
Рис. 4-10. Результат работы программы VirtToPhys.exe
Код (Text):
invoke CloseHandle, hDeviceКак и полагается поступать с описателями которые больше не нужны, вызовом функции CloseHandle, закрываем описатель устройства. И все повторяется вновь. Используя описатель устройства диспетчер ввода-вывода извлекает указатель на драйвер, формирует пакет запроса ввода-вывода типа IRP_MJ_CLEANUP и направляет его драйверу. В драйвере будет вызвана процедура зарегистрированная на обработку этого типа IRP, если это необходимо драйверу. Эта процедура также будет выполнена в контексте потока вызвавшего CloseHandle при IRQL = PASSIVE_LEVEL. Таким образом драйвер узнает, что описатель его устройства собираются закрыть. Затем счетчик открытых описателей объекта "файл" уменьшается на единицу и становится равным нулю. Счетчик ссылок также уменьшается до нуля, что приводит к удалению объекта из памяти. Кстати, значение счетчика ссылок всегда больше или равно значению счетчика открытых описателей, т.к. каждому описателю соответствует, по крайней мере одна, ссылка. Когда счетчик ссылок уменьшается до нуля диспетчер ввода-вывода формирует пакет запроса ввода-вывода типа IRP_MJ_CLOSE и направляет его драйверу. В драйвере будет вызвана процедура зарегистрированная на обработку этого типа IRP. И опять эта процедура будет выполнена в контексте потока вызвавшего CloseHandle при IRQL = PASSIVE_LEVEL. Таким образом драйвер узнает, что описатель его устройства уже закрыт.
Как происходит обработка пакетов запроса ввода-вывода в драйвере мы поговорим в следующий раз.
Останов и удаление сведений о драйвере из базы данных SCM происходит обычным образом и подробно описано в предыдущих статьях.
В исходном коде VirtToPhys.exe я использовал ANSI версии API функций, что не совсем правильно, т.к. Windows NT по своей природе использует UNICODE. Все ANSI функций являются просто оболочками вокруг своих UNICODE-аналогов. Стандартные включаемые файлы пакета masm32 не поддерживают UNICODE. Поэтому, я и не стал усложнять и без того не простой, для начинающих, код. С помощью утилиты masm32\bin\l2incu.exe можно самостоятельно изготовить UNICODE-включаемые файлы.И последнее. Я тестировал VirtToPhys, как обычно, под Windows 2000 Pro и Windows XP Pro. На более ранних выпусках могут возникнуть проблемы из-за того, что в пространстве имен диспетчера объектов отсутствует каталог "\??". В этом случае вам придется дождаться следующей статьи и перекомпилировать драйвер заменив "\??" на "\DosDevices". Исходные же коды программы управления вместе с откомпилированным драйвером и файлом winioctl.inc в архиве. © Four-F
Драйверы режима ядра: Часть 4: Подсистема ввода-вывода
Дата публикации 12 янв 2003