Драйверы режима ядра: Часть 4: Подсистема ввода-вывода

Дата публикации 12 янв 2003

Драйверы режима ядра: Часть 4: Подсистема ввода-вывода — Архив WASM.RU

В первой части я сказал, что разрабатываемые нами драйверы можно считать DLL режима ядра. С определенной долей условности это действительно так. Зачем еще нужен драйвер, который не управляет каким-либо реальным устройством? Только для того, чтобы служить проводником в режим ядра. При этом код драйвера, по сути, является набором функций, позволяющих решать задачи недоступные коду режима пользователя. Когда нужно решить одну из таких задач, вызывается соответствующая функция. Принципиальная разница (не считая уровня привилегий) в том, что, в случае обычной DLL, мы получаем (явно или неявно) адрес интересующей нас функции и передаем туда управление. В случае драйвера режима ядра, такой сценарий был бы крайне опасен с точки зрения безопасности системы. Поэтому, система предоставляет посредника в лице диспетчера ввода-вывода (I/O manager), который является одним из компонентов подсистемы ввода-вывода (I/O subsystem). Диспетчер ввода-вывода отвечает за формирование пакета запроса ввода-вывода (I/O request packet, IRP) и посылку его драйверу для дальнейшей обработки. Весьма упрощенная схема взаимодействия диспетчера ввода-вывода с приложениями пользовательского режима и драйверами устройств приведена на рис 4-1.

Рис. 4-1. Упрощенная схема ввода-вывода



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

Код режима пользователя вынужден заказывать операцию ввода-вывода на устройство. Именно на устройство. Дело в том, что в процедуре DriverEntry драйвер должен создать устройство (или несколько), которым он будет управлять. В нашем случае это устройство виртуальное. Создание устройства, конечно же, не следует понимать буквально, даже если драйвер призван управлять реальным устройством. Это означает создание в памяти соответствующих структур данных, олицетворяющих для системы само устройство. Создавая устройство, драйвер как бы говорит диспетчеру ввода-вывода: "Вот устройство, которым я буду управлять. Если к тебе придет запрос ввода-вывода на это устройство, ты отправляй его ко мне, а я уж разберусь, что с ним делать." Только драйвер знает как работать со своим устройством. Диспетчер ввода-вывода просто занимается формированием пакетов запроса ввода-вывода и направлением их нужным драйверам. А код режима пользователя вообще не знает, и не должен знать, какой драйвер или драйверы какое устройство обслуживают.


Программа управления драйвером VirtToPhys.sys

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

Вот этот исходный код.

Код (Text):
  1.  
  2.  
  3.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  4.  ;
  5.  ; VirtToPhys.asm - Программа управления драйвером VirtToPhys
  6.  ;
  7.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  8.  
  9.  .386
  10.  .model flat, stdcall
  11.  option casemap:none
  12.  
  13.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  14.  ;                              В К Л Ю Ч А Е М Ы Е    Ф А Й Л Ы                                    
  15.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  16.  
  17.  include \masm32\include\windows.inc
  18.  
  19.  include \masm32\include\kernel32.inc
  20.  include \masm32\include\user32.inc
  21.  include \masm32\include\advapi32.inc
  22.  
  23.  includelib \masm32\lib\kernel32.lib
  24.  includelib \masm32\lib\user32.lib
  25.  includelib \masm32\lib\advapi32.lib
  26.  
  27.  include \masm32\include\winioctl.inc
  28.  
  29.  include \masm32\Macros\Strings.mac
  30.  
  31.  include common.inc
  32.  
  33.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  34.  ;                                              К О Д                                                
  35.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  36.  
  37.  .code
  38.  
  39.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  40.  ;                                      BigNumToString                                              
  41.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  42.  
  43.  BigNumToString proc uNum:UINT, pacBuf:LPSTR
  44.  
  45.  ; переводит число в строку разделяя разряды пробелами
  46.  
  47.  local acNum[32]:CHAR
  48.  local nf:NUMBERFMT
  49.  
  50.      invoke wsprintf, addr acNum, $CTA0("%u"), uNum
  51.  
  52.      and nf.NumDigits, 0
  53.      and nf.LeadingZero, FALSE
  54.      mov nf.Grouping, 3
  55.      mov nf.lpDecimalSep, $CTA0(".")
  56.      mov nf.lpThousandSep, $CTA0(" ")
  57.      and nf.NegativeOrder, 0
  58.      invoke GetNumberFormat, LOCALE_USER_DEFAULT, 0, addr acNum, addr nf, pacBuf, 32
  59.  
  60.      ret
  61.  
  62.  BigNumToString endp
  63.  
  64.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  65.  ;                                       start                                                      
  66.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  67.  
  68.  start proc uses esi edi
  69.  
  70.  local hSCManager:HANDLE
  71.  local hService:HANDLE
  72.  local acModulePath[MAX_PATH]:CHAR
  73.  local _ss:SERVICE_STATUS
  74.  local hDevice:HANDLE
  75.  
  76.  local adwInBuffer[NUM_DATA_ENTRY]:DWORD
  77.  local adwOutBuffer[NUM_DATA_ENTRY]:DWORD
  78.  local dwBytesReturned:DWORD
  79.  
  80.  local acBuffer[256+64]:CHAR
  81.  local acThis[64]:CHAR
  82.  local acKernel[64]:CHAR
  83.  local acUser[64]:CHAR
  84.  local acAdvapi[64]:CHAR
  85.  
  86.  local acNumber[32]:CHAR
  87.  
  88.      invoke OpenSCManager, NULL, NULL, SC_MANAGER_ALL_ACCESS
  89.      .if eax != NULL
  90.          mov hSCManager, eax
  91.  
  92.          push eax
  93.          invoke GetFullPathName, $CTA0("VirtToPhys.sys"), \
  94.                                  sizeof acModulePath, addr acModulePath, esp
  95.          pop eax
  96.  
  97.          invoke CreateService, hSCManager, $CTA0("VirtToPhys"), \
  98.                                $CTA0("Virtual To Physical Address Converter"), \
  99.                                SERVICE_START + SERVICE_STOP + DELETE, SERVICE_KERNEL_DRIVER, \
  100.                                SERVICE_DEMAND_START, SERVICE_ERROR_IGNORE, addr acModulePath, \
  101.                                NULL, NULL, NULL, NULL, NULL
  102.  
  103.          .if eax != NULL
  104.              mov hService, eax
  105.  
  106.              ; в драйвере будет вызвана функция DriverEntry
  107.              invoke StartService, hService, 0, NULL
  108.              .if eax != 0
  109.  
  110.                  ; драйверу будет направлен IRP типа IRP_MJ_CREATE
  111.                  invoke CreateFile, $CTA0("\\\\.\\slVirtToPhys"), GENERIC_READ + GENERIC_WRITE, \
  112.                                  0, NULL, OPEN_EXISTING, 0, NULL
  113.  
  114.                  .if eax != INVALID_HANDLE_VALUE
  115.                      mov hDevice, eax
  116.  
  117.                      lea esi, adwInBuffer
  118.                      assume esi:ptr DWORD
  119.                      invoke GetModuleHandle, NULL
  120.                      mov [esi][0*(sizeof DWORD)], eax
  121.                      invoke GetModuleHandle, $CTA0("kernel32.dll", szKernel32)
  122.                      mov [esi][1*(sizeof DWORD)], eax
  123.                      invoke GetModuleHandle, $CTA0("user32.dll", szUser32)
  124.                      mov [esi][2*(sizeof DWORD)], eax
  125.                      invoke GetModuleHandle, $CTA0("advapi32.dll", szAdvapi32)
  126.                      mov [esi][3*(sizeof DWORD)], eax
  127.  
  128.                      lea edi, adwOutBuffer
  129.                      assume edi:ptr DWORD
  130.                      ; драйверу будет направлен IRP типа IRP_MJ_DEVICE_CONTROL
  131.                      invoke DeviceIoControl, hDevice, IOCTL_GET_PHYS_ADDRESS, \
  132.                                              esi, sizeof adwInBuffer, \
  133.                                              edi, sizeof adwOutBuffer, \
  134.                                              addr dwBytesReturned, NULL
  135.  
  136.                      .if ( eax != 0 ) && ( dwBytesReturned != 0 )
  137.  
  138.                          invoke GetModuleFileName, [esi][0*(sizeof DWORD)], \
  139.                                                    addr acModulePath, sizeof acModulePath
  140.  
  141.                          lea ecx, acModulePath[eax-5]
  142.                          .repeat
  143.                              dec ecx
  144.                              mov al, [ecx]
  145.                          .until al == '\'
  146.                          inc ecx
  147.                          push ecx
  148.  
  149.                          CTA0 "%s \t%08Xh\t%08Xh   ( %s )\n", szFmtMod
  150.  
  151.                          invoke BigNumToString, [edi][0*(sizeof DWORD)], addr acNumber
  152.                          pop ecx
  153.                          invoke wsprintf, addr acThis, addr szFmtMod, ecx, \
  154.                                           [esi][0*(sizeof DWORD)], \
  155.                                           [edi][0*(sizeof DWORD)], addr acNumber
  156.  
  157.                          invoke BigNumToString, [edi][1*(sizeof DWORD)], addr acNumber
  158.                          invoke wsprintf, addr acKernel, addr szFmtMod, addr szKernel32, \
  159.                                           [esi][1*(sizeof DWORD)], \
  160.                                           [edi][1*(sizeof DWORD)], addr acNumber
  161.  
  162.                          invoke BigNumToString, [edi][2*(sizeof DWORD)], addr acNumber
  163.                          invoke wsprintf, addr acUser, addr szFmtMod, addr szUser32, \
  164.                                           [esi][2*(sizeof DWORD)], \
  165.                                           [edi][2*(sizeof DWORD)], addr acNumber
  166.  
  167.                          invoke BigNumToString, [edi][3*(sizeof DWORD)], addr acNumber
  168.                          invoke wsprintf, addr acAdvapi, addr szFmtMod, addr szAdvapi32, \
  169.                                           [esi][3*(sizeof DWORD)], \
  170.                                           [edi][3*(sizeof DWORD)], addr acNumber
  171.  
  172.                          invoke wsprintf, addr acBuffer, \
  173.                                           $CTA0("Module:\t\tVirtual:\t\tPhysical:\n\n%s\n%s%s%s"), \
  174.                                           addr acThis, addr acKernel, addr acUser, addr acAdvapi
  175.  
  176.                          assume esi:nothing
  177.                          assume edi:nothing
  178.                          invoke MessageBox, NULL, addr acBuffer, $CTA0("Modules Base Address"), \
  179.                                             MB_OK + MB_ICONINFORMATION
  180.                      .else
  181.                          invoke MessageBox, NULL, $CTA0("Can't send control code to device."), NULL, \
  182.                                             MB_OK + MB_ICONSTOP
  183.                      .endif
  184.                      ; драйверу будет направлен IRP типа IRP_MJ_CLOSE
  185.                      invoke CloseHandle, hDevice
  186.                  .else
  187.                      invoke MessageBox, NULL, $CTA0("Device is not present."), NULL, MB_ICONSTOP
  188.                  .endif
  189.                  ; в драйвере будет вызвана функция DriverUnload
  190.                  invoke ControlService, hService, SERVICE_CONTROL_STOP, addr _ss
  191.              .else
  192.                  invoke MessageBox, NULL, $CTA0("Can't start driver."), NULL, MB_OK + MB_ICONSTOP
  193.              .endif
  194.              invoke DeleteService, hService
  195.              invoke CloseServiceHandle, hService
  196.          .else
  197.              invoke MessageBox, NULL, $CTA0("Can't register driver."), NULL, MB_OK + MB_ICONSTOP
  198.          .endif
  199.          invoke CloseServiceHandle, hSCManager
  200.      .else
  201.          invoke MessageBox, NULL, $CTA0("Can't connect to Service Control Manager."), NULL, \
  202.                             MB_OK + MB_ICONSTOP
  203.      .endif
  204.  
  205.      invoke ExitProcess, 0
  206.  
  207.  start endp
  208.  
  209.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  210.  ;                                                                                                  
  211.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  212.  
  213.  end start
  214.  
  215.  

Не считая кода подготавливающего информацию для ввода в устройство и кода ответственного за форматирование и вывод на экран полученных данных, принципиально нового здесь немного - всего три функции: 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):
  1.  
  2.  
  3.  CreateFile proto stdcall     lpFileName:LPCSTR,            dwDesiredAccess:DWORD, \
  4.                               dwShareMode:DWORD,            lpSecurityAttributes:LPVOID, \
  5.                               dwCreationDistribution:DWORD, dwFlagsAndAttributes:DWORD, \
  6.                               hTemplateFile:HANDLE
  7.  
  8.  

Вопреки своему названию, эта функция создает или открывает существующий (многие 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):
  1.  
  2.  
  3.  invoke CreateFile, $CTA0("\\\\.\\slVirtToPhys"), GENERIC_READ + GENERIC_WRITE, \
  4.                                                   0, NULL, OPEN_EXISTING, 0, NULL
  5.  
  6.  

С последними пятью параметрами все, вроде более-менее, ясно. Во втором параметре мы передаем комбинацию флагов 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):
  1.  
  2.  
  3.                  .if eax != INVALID_HANDLE_VALUE
  4.                      mov hDevice, eax
  5.  
  6.  

Если все ОК, то мы сохраняем описатель устройства, возвращенный CreateFile, в переменной hDevice, и имеем возможность осуществлять операции ввода-вывода с этим устройством, посредством вызова функций DeviceIoControl, ReadFile и WriteFile. DeviceIoControl является универсальной функцией ввода-вывода - ее мы и будем использовать.

Код (Text):
  1.  
  2.  
  3.  DeviceIoControl proto stdcall hDevice:HANDLE,         dwIoControlCode:DWORD, \
  4.                                lpInBuffer:LPVOID,      nInBufferSize:DWORD, \
  5.                                lpOutBuffer:LPVOID,     nOutBufferSize:DWORD, \
  6.                                lpBytesReturned:LPVOID, lpOverlapped:LPVOID
  7.  
  8.  

Не смотря на то, что функция 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):
  1.  
  2.  
  3.  CTL_CODE MACRO DeviceType:=<0>, Function:=<0>, Method:=<0>, Access:=<0>
  4.      EXITM %(((DeviceType) SHL 16) OR ((Access) SHL 14) OR ((Function) SHL 2) OR (Method))
  5.  ENDM
  6.  
  7.  

Этот макрос определен дважды - в файле \include\winioctl.inc (отсутствует в пакете masm32), который необходимо включить в исходный текст программы управления, и в файле ntddk.inc, включаемом в исходный текст драйвера.

Определение значения управляющего кода ввода-вывода и двух констант вынесено в отдельный файл common.inc, т.к. эти значения требуются как в исходном тексте программы управления, так и в исходном тексте драйвера. Т.о. все изменения в этом файле отразятся на исходных текстах программы управления и драйвера.

Код (Text):
  1.  
  2.  
  3.  NUM_DATA_ENTRY         equ 4
  4.  DATA_SIZE              equ (sizeof DWORD) * NUM_DATA_ENTRY
  5.  IOCTL_GET_PHYS_ADDRESS equ CTL_CODE(FILE_DEVICE_UNKNOWN, 800h, METHOD_BUFFERED, FILE_READ_ACCESS + FILE_WRITE_ACCESS)
  6.  
  7.  

Вернемся к исходному тексту.

Код (Text):
  1.  
  2.  
  3.                      lea esi, adwInBuffer
  4.                      assume esi:ptr DWORD
  5.                      invoke GetModuleHandle, NULL
  6.                      mov [esi][0*(sizeof DWORD)], eax
  7.                      invoke GetModuleHandle, $CTA0("kernel32.dll", szKernel32)
  8.                      mov [esi][1*(sizeof DWORD)], eax
  9.                      invoke GetModuleHandle, $CTA0("user32.dll", szUser32)
  10.                      mov [esi][2*(sizeof DWORD)], eax
  11.                      invoke GetModuleHandle, $CTA0("advapi32.dll", szAdvapi32)
  12.                      mov [esi][3*(sizeof DWORD)], eax
  13.  
  14.  

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

Код (Text):
  1.  
  2.  
  3.                      lea edi, adwOutBuffer
  4.                      assume edi:ptr DWORD
  5.                      invoke DeviceIoControl, hDevice, IOCTL_GET_PHYS_ADDRESS, \
  6.                                              esi, sizeof adwInBuffer, \
  7.                                              edi, sizeof adwOutBuffer, \
  8.                                              addr dwBytesReturned, NULL
  9.  
  10.  

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

Используя описатель устройства диспетчер ввода-вывода извлечет сведения об обслуживающем его драйвере, сформирует пакет запроса ввода-вывода типа IRP_MJ_DEVICE_CONTROL и направит его драйверу. В драйвере будет вызвана соответствующая процедура. Эта процедура также будет выполнена в контексте потока вызвавшего DeviceIoControl при IRQL = PASSIVE_LEVEL. По значению управляющего кода ввода-вывода IOCTL_GET_PHYS_ADDRESS драйвер определит что конкретно от него требуется, извлечет из входного буфера четыре виртуальных адреса, преобразует из в физические и поместит в выходной буфер.

Код (Text):
  1.  
  2.  
  3.                      .if ( eax != 0 ) && ( dwBytesReturned != 0 )
  4.  
  5.                          invoke GetModuleFileName, [esi][0*(sizeof DWORD)], \
  6.                                                    addr acModulePath, sizeof acModulePath
  7.  
  8.                          lea ecx, acModulePath[eax-5]
  9.                          .repeat
  10.                              dec ecx
  11.                              mov al, [ecx]
  12.                          .until al == '\'
  13.                          inc ecx
  14.                          push ecx
  15.  
  16.                          CTA0 "%s \t%08Xh\t%08Xh   ( %s )\n", szFmtMod
  17.  
  18.                          invoke BigNumToString, [edi][0*(sizeof DWORD)], addr acNumber
  19.                          pop ecx
  20.                          invoke wsprintf, addr acThis, addr szFmtMod, ecx, \
  21.                                           [esi][0*(sizeof DWORD)], \
  22.                                           [edi][0*(sizeof DWORD)], addr acNumber
  23.  
  24.                          invoke BigNumToString, [edi][1*(sizeof DWORD)], addr acNumber
  25.                          invoke wsprintf, addr acKernel, addr szFmtMod, addr szKernel32, \
  26.                                           [esi][1*(sizeof DWORD)], \
  27.                                           [edi][1*(sizeof DWORD)], addr acNumber
  28.  
  29.                          invoke BigNumToString, [edi][2*(sizeof DWORD)], addr acNumber
  30.                          invoke wsprintf, addr acUser, addr szFmtMod, addr szUser32, \
  31.                                           [esi][2*(sizeof DWORD)], \
  32.                                           [edi][2*(sizeof DWORD)], addr acNumber
  33.  
  34.                          invoke BigNumToString, [edi][3*(sizeof DWORD)], addr acNumber
  35.                          invoke wsprintf, addr acAdvapi, addr szFmtMod, addr szAdvapi32, \
  36.                                           [esi][3*(sizeof DWORD)], \
  37.                                           [edi][3*(sizeof DWORD)], addr acNumber
  38.  
  39.                          invoke wsprintf, addr acBuffer, \
  40.                                           $CTA0("Module:\t\tVirtual:\t\tPhysical:\n\n%s\n%s%s%s"), \
  41.                                           addr acThis, addr acKernel, addr acUser, addr acAdvapi
  42.  
  43.                          assume esi:nothing
  44.                          assume edi:nothing
  45.                          invoke MessageBox, NULL, addr acBuffer, $CTA0("Modules Base Address"), \
  46.                                             MB_OK + MB_ICONINFORMATION
  47.                      .else
  48.                          invoke MessageBox, NULL, $CTA0("Can't send control code to device."), NULL, \
  49.                                             MB_OK + MB_ICONSTOP
  50.                      .endif
  51.  
  52.  

Если все пройдет удачно, то DeviceIoControl вернет ненулевое значение, а в dwBytesReturned окажется значение количества байт возвращенных устройством в буфере adwOutBuffer. Наша задача теперь - отформатировать эти данные и вывести их на экран. Я не буду подробно описывать этот процесс - тут все достаточно тривиально. В текстовых макросах $CTA0 интенсивно используются стандартные эскейп-последовательности (подробное описание см. \Macros\Strings.mac). Процедура BigNumToString разбивает адрес по три разряда. Закончив фарматирование, выводим информацию на экран.

Рис. 4-10. Результат работы программы VirtToPhys.exe



Код (Text):
  1.  
  2.  
  3.                      invoke CloseHandle, hDevice
  4.  
  5.  

Как и полагается поступать с описателями которые больше не нужны, вызовом функции 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


0 2.110
archive

archive
New Member

Регистрация:
27 фев 2017
Публикаций:
532