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

Все статьи

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



Вот мы и добрались до исходного текста простейших драйверов. Полнофункциональные нас ждут впереди. Все исходные тексты драйверов я буду оформлять в виде *.bat файла, который, на самом деле, является комбинацией *.bat и *.asm файлов, но имеет расширение .bat.


 ;@echo off
 ;goto make


 .386                      ; начало исходного текста драйвера

   ; остальной код драйвера

 end DriverEntry           ; конец исходного текста драйвера


 :make
 \masm32\bin\ml /nologo /c /coff driver.bat
 \masm32\bin\link /nologo /driver /base:0x10000 /align:32 /out:driver.sys /subsystem:native driver.obj

 del driver.obj

 echo.
 pause

Если такой "самокомпилирующийся" файл запустить, то произойдет следущее. Первые две команды закомментарены, поэтому, они игнорируются компилятором masm, но принимаются командным процессором, который, в свою очередь, игнорирует символ "точка с запятой". Управление передается на метку :make, за которой находятся инструкции для компилятора и компоновщика. Все, что находится за директивой ассемблера end, игнорируется компилятором masm. Таким образом, весь текст между командой goto make и меткой :make, игнорируется командным процессором, но принимается компилятором masm. А все, что вне (включая команду goto make и метку :make), игнорируется компилятором masm, но принимается командным процессором. Этот метод чрезвычайно удобен, т.к. исходный текст "помнит" с какими параметрами его нужно компилировать. Я буду применять такую технику в исходных текстах драйверов, а в исходных текстах программ управления, буду пользоваться обычным методом.

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

/driver

- Указывает компоновщику, что нужно сформировать файл драйвера режима ядра Windows NT;

/base:0x10000

- Устанавливает предопределенный адрес загрузки образа драйвера равным 10000h. Я уже говорил про это в предыдущей статье;

/align:32

- Память режима ядра - драгоценный ресурс. Поэтому, файлы драйверов имеют более "мелкое" выравнивание секций;

/out:driver.sys

- По умолчанию компоновщик производит файлы с расширением .exe. При наличии ключа /dll файл будет иметь расширение .dll. Нам нужно получить файл с расшрением .sys;

/subsystem:native

- В PE-заголовке имеется поле, указывающее загрузчику образа исполняемого файла, для какой подсистемы этот файл предназначен: Win32, POSIX или OS/2. Это нужно для того, чтобы поместить образ в необходимое ему окружение. Подсистема Win32 автоматически запускается при загрузке системы. Если же запускается файл, предназначенный для функционирования, например, в подсистеме POSIX, то сначала операционная система запускает саму подсистему POSIX. Таким образом, с помощью этого ключа можно указать компоновщику, какая подсистема необходима. Когда мы компилируем *.exe или *.dll, то указываем под этим ключем значение windows, которое означает, что файлу требуется подсистема Win32. Драйверу вообще не нужна ни одна из подсистем, т.к. он работает в естественной (native) для самой операционной системы среде.




Самый простой драйвер режима ядра

Вот исходный текст простейшего драйвера режима ядра.


 ;@echo off
 ;goto make

 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
 ;
 ; simplest - Самый простой драйвер режима ядра
 ;
 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

 .386
 .model flat, stdcall
 option casemap:none

 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
 ;                              В К Л Ю Ч А Е М Ы Е    Ф А Й Л Ы                                     
 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

 include \masm32\include\w2k\ntstatus.inc
 include \masm32\include\w2k\ntddk.inc

 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
 ;                                              К О Д                                                
 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

 .code

 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
 ;                                       DriverEntry                                                 
 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

 DriverEntry proc pDriverObject:PDRIVER_OBJECT, pusRegistryPath:PUNICODE_STRING

     mov eax, STATUS_DEVICE_CONFIGURATION_ERROR
     ret

 DriverEntry endp

 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
 ;                                                                                                   
 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

 end DriverEntry

 :make
 \masm32\bin\ml /nologo /c /coff simplest.bat
 \masm32\bin\link /nologo /driver /base:0x10000 /align:32 /out:simplest.sys /subsystem:native simplest.obj

 del simplest.obj

 echo.
 pause

Как и у любого другого выполнимого модуля, у драйвера должна быть точка входа, на которую система передаст управление после загрузки драйвера в память. Как и полагается в программе на ассемблере, точкой входа является первая инструкция, обозначенная меткой указанной в директиве end. У нас, как и в текстах на с, это DriverEntry, которая оформлена в виде процедуры, принимающей два параметра. Имя процедуры, естественно, может быть любым. Прототип DriverEntry выглядит так:


 DriverEntry proto DriverObject:PDRIVER_OBJECT, RegistryPath:PUNICODE_STRING

К сожалению, Microsoft отошла от принципа "венгерской нотации" при составлении заголовочных файлов и документации DDK. Возможно, это связано с большим количеством специфических типов данных, используемых в DDK. Хотя, в обозначении типов кое-что осталось. В исходных текстах я буду придерживаться этого принципа везде, где только возможно, т.к. настолько привык им пользоваться, что исходники не использующие "венгерскую нотацию" мне кажутся совершенно нечитабельными. Поэтом, легким движением руки, DriverObject превращается в pDriverObject, а RegistryPath в pusRegistryPath.

Типы данных PDRIVER_OBJECT и PUNICODE_STRING определены в файлах \include\w2k\ntddk.inc и \include\w2k\ntdef.inc соответственно.


 PDRIVER_OBJECT   typedef PTR DRIVER_OBJECT
 PUNICODE_STRING  typedef PTR UNICODE_STRING

pDriverObject

- указатель на объект только что созданного драйвера.

Windows является объектно-ориентированной системой. Поэтому, понятие объект распространяется на все, что только можно, и что нельзя тоже. И объект "драйвер" не является исключением. Загружая драйвер, система создает объект "драйвер" (driver object), представляющий для нее образ драйвера в памяти. Через этот объект система управляет драйвером. Звучит красиво, но не дает никакого представления о том, что же в действительности происходит. Если отбросить всю эту объектно-ориентированную мишуру, то станет очевидно, что объект "драйвер" представляет собой обыкновенную структуру данных типа DRIVER_OBJECT (определена в \include\w2k\ntddk.inc). Некоторые поля этой структуры заполняет система, некоторые придется заполнять нам самим. Обращаясь к этой структуре, система и управляет драйвером. Итак, как вы наверное уже поняли, первым параметром, передающимся в функцию DriverEntry, как раз и является указатель на эту самую структуру (или пользуясь объектно-ориентированной терминологией - объект "драйвер"). Используя этот указатель, мы можем (и будем, но позже) заполнить соответствующие поля структуры DRIVER_OBJECT. Но, в рассматриваемых в этой части статьи драйверах этого не требуется, поэтому мы, пока, оставим pDriverObject без внимания.

pusRegistryPath

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

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

О формате данных UNICODE_STRING следует сказать особо. В отличие от режима пользователя, режим ядра оперирует строками в формате UNICODE_STRING. Эта структура определена в файле \include\w2k\ntdef.inc следующим образом:


 UNICODE_STRING STRUCT
     woLength        WORD    ?  ; длина строки в байтах (не символах)
     MaximumLength   WORD    ?  ; длина буфера содержащего строку в байтах (не символах)
     Buffer          PWSTR   ?  ; указатель на буфер содержащий строку
 UNICODE_STRING ENDS

woLength

- (мне пришлось изменить оригинальное имя Length, т.к. оно является зарезервированным словом) содержит текущую длину строки в байтах (не в символах!), не считая завершающего нуля.

MaximumLength

- максимальный размер буфера (также в байтах), в котором эта строка содержится.

Buffer

- указатель на саму Unicode-строку.

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

Почему в процедуру DriverEntry передаются именно эти два указателя? Потому, что доступ к ним (особенно к первому) является ключевым моментом в инициализации и последующей жизни драйвера. Подробнее об этом мы поговорим в следующих статьях. Пока же, мы рассматриваем простейшие драйверы, время жизни которых, ограничено временем выполнения процедуры DriverEntry. Что же мы можем тут полезного (или вредного) сделать? Ну, вредного хоть отбавляй. Мы ведь уже в нулевом кольце защиты. Можно, например, выполнить такой код:


 xor eax, eax
 xchg [eax], eax

Это приведет к остановке системы и появлению BSOD (Blue Screen Of Death). А выполнение такого кода приведет к перезагрузке компьютера:


 mov al, 0FEh
 out 64h, al

Такой радикальный способ, прервать попытку исследования программы, иногда встречается в защитах. Честно говоря, я и сам на это не раз попадался ;-)

В этих двух случаях, процедура DriverEntry никогда не вернет управление. Поэтому, возвращаемое ей значение не важно. Если же действия выполняемые DriverEntry будут более конструктивными, как, например, в драйвере beeper.sys, то надо вернуть системе некое значение, указывающее на то, как прошла инициализация драйвера. Если вернуть STATUS_SUCCESS, то инициализация считается успешной, и драйвер остается в памяти. Любое другое значение STATUS_* указывает на ошибку, и в этом случае драйвер выгружается системой. Вышеприведенный драйвер (\src\Article2-3\simplest\simplest.sys) является самым простым, какой только можно себе представить. Единственное что он делает, это позволяет себя загрузить. Т.к. ничего кроме этого он сделать больше не может, то возвращает код ошибки STATUS_DEVICE_CONFIGURATION_ERROR. Я просто подобрал подходящее по смыслу значение (полный список можно посмотреть в файле \include\w2k\ntstatus.inc). Если возвратить STATUS_SUCCESS, то драйвер так и останется болтаться в памяти без дела, и выгрузить его средствами SCM будет невозможно, т.к. мы не определили процедуру отвечающую за выгрузку драйвера. Эта процедура должна находиться в самом драйвере. Она выполняет действия, зеркальные по отношению к DriverEntry. Если драйвер выделил себе какие-то ресурсы, например, память, то в процедуре выгрузки эта память должна быть возвращена системе. И только сам драйвер знает об этом. Но, тут я немного забежал вперед. Пока нам это не понадобится.


Драйвер режима ядра beeper.sys

Теперь перейдем к рассмотрению драйвера, программу управления которым, мы писали в прошлый раз. Мне пришлось переименовать его из beep.sys в beeper.sys, потому что, как оказалось, в NT4 и в некоторых версиях XP уже существует драйвер beep.sys. Вобще говоря, beep.sys есть во всех версиях NT (\%SystemRoot%\System32\Drivers\beep.sys), но он еще должен быть зарегистрирован в реестре. Как бы там ни было, надеюсь beeper.sys будет уникальным. Вот его исходный текст:


 ;@echo off
 ;goto make

 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
 ;
 ;  beeper - Драйвер режима ядра
 ;  Пищит системным динамиком
 ;
 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

 .386
 .model flat, stdcall
 option casemap:none

 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
 ;                              В К Л Ю Ч А Е М Ы Е    Ф А Й Л Ы                                     
 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

 include \masm32\include\w2k\ntstatus.inc
 include \masm32\include\w2k\ntddk.inc

 include \masm32\include\w2k\hal.inc

 includelib \masm32\lib\w2k\hal.lib

 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
 ;                           С И М В О Л Ь Н Ы Е    К О Н С Т А Н Т Ы                                
 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

 TIMER_FREQUENCY        equ 1193167                   ; 1,193,167 Гц
 OCTAVE                 equ 2                         ; множитель октавы

 PITCH_C                equ 523                       ; До        -  523,25 Гц
 PITCH_Cs               equ 554                       ; До диез   -  554,37 Гц
 PITCH_D                equ 587                       ; Ре        -  587,33 Гц
 PITCH_Ds               equ 622                       ; Ре диез   -  622,25 Гц
 PITCH_E                equ 659                       ; Ми        -  659,25 Гц
 PITCH_F                equ 698                       ; Фа        -  698,46 Гц
 PITCH_Fs               equ 740                       ; Фа диез   -  739,99 Гц
 PITCH_G                equ 784                       ; Соль      -  783,99 Гц
 PITCH_Gs               equ 831                       ; Соль диез -  830,61 Гц
 PITCH_A                equ 880                       ; Ля        -  880,00 Гц
 PITCH_As               equ 988                       ; Ля диез   -  987,77 Гц
 PITCH_H                equ 1047                      ; Си        - 1046,50 Гц


 ; Нам нужны три звука для до-мажорного арпеджио (до, ми, соль)

 TONE_1                 equ TIMER_FREQUENCY/(PITCH_C*OCTAVE)
 TONE_2                 equ TIMER_FREQUENCY/(PITCH_E*OCTAVE)
 TONE_3                 equ (PITCH_G*OCTAVE)           ; для HalMakeBeep

 DELAY                  equ 1800000h                   ; для моей ~800mHz машины

 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
 ;                                         М А К Р О С Ы                                             
 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

 DO_DELAY MACRO
     mov eax, DELAY
     .while eax
         dec eax
     .endw
 ENDM

 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
 ;                                              К О Д                                                
 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

 .code

 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
 ;                                            MakeBeep1                                              
 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

 MakeBeep1 proc dwPitch:DWORD

     ; Прямой доступ к оборудованию через порты ввода-вывода

     cli

     mov al, 10110110y
     out 43h, al

     mov eax, dwPitch
     out 42h, al

     mov al, ah
     out 42h, al

     ; включить динамик

     in al, 61h
     or  al, 11y
     out 61h, al

     sti

     DO_DELAY

     cli

     ; выключить динамик

     in al, 61h
     and al, 11111100y
     out 61h, al

     sti

     ret

 MakeBeep1 endp

 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
 ;                                            MakeBeep2                                              
 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

 MakeBeep2 proc dwPitch:DWORD

     ; Прямой доступ к оборудованию используя функции
     ; WRITE_PORT_UCHAR и READ_PORT_UCHAR из модуля hal.dll

     cli

     invoke WRITE_PORT_UCHAR, 43h, 10110110y

     mov eax, dwPitch
     and eax, 0FFh
     invoke WRITE_PORT_UCHAR, 42h, eax
     mov eax, dwPitch
     shr eax, 8
     and eax, 0FFh
     invoke WRITE_PORT_UCHAR, 42h, eax

     ; включить динамик

     invoke READ_PORT_UCHAR, 61h
     or  al, 11y
     and eax, 0FFh
     invoke WRITE_PORT_UCHAR, 61h, eax

     sti

     DO_DELAY	

     cli

     ; выключить динамик

     invoke READ_PORT_UCHAR, 61h
     and al, 11111100y
     and eax, 0FFh
     invoke WRITE_PORT_UCHAR, 61h, eax

     sti

     ret

 MakeBeep2 endp

 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
 ;                                       DriverEntry                                                 
 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

 DriverEntry proc pDriverObject:PDRIVER_OBJECT, pusRegistryPath:PUNICODE_STRING

     invoke MakeBeep1, TONE_1
     invoke MakeBeep2, TONE_2

     ; Прямой доступ к оборудованию используя функцию HalMakeBeep из модуля hal.dll

     invoke HalMakeBeep, TONE_3
     DO_DELAY
     invoke HalMakeBeep, 0

     mov eax, STATUS_DEVICE_CONFIGURATION_ERROR
     ret

 DriverEntry endp

 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
 ;                                                                                                   
 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

 end DriverEntry

 :make
 \masm32\bin\ml /nologo /c /coff beeper.bat
 \masm32\bin\link /nologo /driver /base:0x10000 /align:32 /out:beeper.sys /subsystem:native beeper.obj

 del beeper.obj

 echo.
 pause

Задача этого драйвера, исполнять на системном динамике восходящее до-мажорное арпеджио. Что это такое, вы, наверное уже послушали. Для этого драйвер использует инструкции процессора in и out, обращаясь к соответствующим портам ввода-вывода. Общеизвестно, что доступ к портам ввода-вывода - это свято охраняемый Windows NT системный ресурс. Попытка обращения к любому из них, как на ввод, так и на вывод, из режима пользователя, неизбежно приводит к завершению приложения. Но, на самом деле, есть способ обойти и это ограничение, т.е. обращаться к портам ввода-вывода прямо из третьего кольца. В этом вы убедитесь ниже. Правда, для этого, опять таки, нужен драйвер.

На материнской плате находится устройство системный таймер, который является перепрограммируемым. Таймер содержит несколько каналов, 2-ой управляет системным динамиком компьютера, генерируя прямоугольные импульсы с частотой 1193180/<начальное значение счетчика> герц. Начальное значение счетчика является 16-битным, и устанавливается через порт 42h. 1193180 Гц - частота тактового генератора таймера. Тут есть одна тонкость, которую я не совсем понимаю. Функция QueryPerformanceFrequency из kernel32.dll действительно возвращает значение 1193180. Оно просто жестко зашито в тело функции. Но дизассемблировав hal.dll, в функции HalMakeBeep я обнаружил несколько другое значение, равное 1193167 Гц. Его я и использую. Возможно, здесь учтена какая-то временная задержка, или что-то подобное. В любом случае, пищать системным динамиком нам это никак не помешает. Я не буду подробно останавливаться на описании системного таймера. Эту тему очень любят мусолить почти в каждой книжке по программированию на ассемблере. Достаточно подробную информацию можно найти в сети.

Итак, первый звук до-мажорного арпеджио мы воспроизводим пользуясь процедурой MakeBeep1.


 mov al, 10110110y
 out 43h, al

Выводом в порт 43h двоичного числа 10110110, мы помещаем в управляющий регистр таймера значение, определяющее номер канала, которым мы будем управлять, тип операции, режим работы канала и формат счетчика.


 mov eax, dwPitch
 out 42h, al

 mov al, ah
 out 42h, al

Затем, в порт 42h выводим 16-битное начальное значение счетчика. Сначала младший байт, затем старший.


 in al, 61h
 or  al, 11y
 out 61h, al

И, наконец, посредством вывода в порт 61h значения, с установленными 0-ым и 1-ым битами, включаем динамик.


 DO_DELAY MACRO
     mov eax, DELAY
     .while eax
         dec eax
     .endw
 ENDM

Даем данамику позвучать некоторое время, пользуясь макросом DO_DELAY. Да - примитивно, но - эффективно ;-)


 in al, 61h
 and al, 11111100y
 out 61h, al

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

Второй звук (ми) мы воспроизводим посредством процедуры MakeBeep2, тем же самым образом, но используя для обращения к портам ввода-вывода функции WRITE_PORT_UCHAR и READ_PORT_UCHAR из модуля hal.dll. Помимо этих двух, в модуле hal.dll имеется целый набор подобных функций. Они призваны скрыть межплатформенные различия. Вспомните, что я говорил про HAL в первой части статьи. Для процессора alpha, например, внутренняя реализация этих функций будет совершенно другой, но для драйвера ничего не изменится. Я использовал эти функции для разнообразия. Просто, чтобы показать, что такие функции есть.

Третий звук (соль) мы воспроизводим пользуясь функцией HalMakeBeep, находящейся в модуле hal.dll. Внутри этой функции происходят события, полностью аналогичные двум предыдущим случаям. Опять же, имеется в виду модуль hal.dll для платформы x86. При этом, в качестве параметра, нужно использовать не частное частоты тактового генератора таймера и начального значения счетчика, а само значение частоты, которую мы хотим воспроизвести. В начале файла beeper.bat определены все 12 нот. Я использую только до, ми и соль. Остальные оставлены для вашего будущего супер-пуппер синтезатора ;-). Для выключения динамика, надо вызвать HalMakeBeep еще раз, передав в качестве аргумента 0.

На этом работу драйвера beeper.sys можно считать законченной. Он возвращает системе код ошибки и благополучно удаляется из памяти. На всякий случай повторяю: код ошибки нужно вернуть, только для того, чтобы система удалила драйвер из памяти. Все что мог, он уже сделал. Когда мы доберемся до полнофункциональных драйверов, то, естественно, будем возвращать STATUS_SUCCESS.

Программа scp.exe производит загрузку драйвера beeper.sys по требованию. Для того, чтобы закончить с этим вопросом, думаю, будет уместно попробовать загрузить его автоматически, раз уж мы так подробно разобрали этот вопрос в прошлый раз. Проще всего это сделать так: закомментарьте вызов функции DeleteService, в вызове функции CreateService замените SERVICE_DEMAND_START на SERVICE_AUTO_START, а SERVICE_ERROR_IGNORE на SERVICE_ERROR_NORMAL, перекомпилируйте csp.asm и запустите. В реестре останется соответствующая запись. Теперь можете забыть об этом до следующей перезагрузки системы. Драйвер beeper.sys сам напомнит о себе, а в журнале событий системы останется запись о произошедшей ошибке. Посмотреть на нее можно с помощью оснастки Администрирование > Просмотр событий (Administrative Tools > Event Viewer).

Рис. 3-1. Сообщение об ошибке



Не забудьте удалить после этого подраздел реестра, соответствующий драйверу beeper.sys, иначе до-ми-соль будут звучать при каждой загрузке.


Драйвер режима ядра giveio.sys

Теперь рассмотрим программу управления другим драйвером - giveio.sys.


 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
 ;
 ;  DateTime.asm
 ;
 ;  Программа управления драйвером giveio.sys
 ;
 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

 .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\Macros\Strings.mac

 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
 ;                                         М А К Р О С Ы                                             
 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

 CMOS MACRO by:REQ
     mov al, by
     out 70h, al
     in al, 71h

     mov ah, al
     shr al, 4
     add al, '0'

     and ah, 0Fh
     add ah, '0'
     stosw
 ENDM

 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
 ;                                           К О Д                                                   
 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

 .code

 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
 ;                                          DateTime                                                 
 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

 DateTime proc uses edi

 LOCAL acDate[16]:CHAR
 LOCAL acTime[16]:CHAR
 LOCAL acOut[64]:CHAR

     ; Подробнее смотри Ralf Brown's Interrupt List

     ;:::::::::::::::::: Установим формат таймера ::::::::::::::::::

     mov al, 0Bh               ; Управляющий регистр B
     out 70h, al
     in al, 71h

     push eax                  ; Сохраним старый фармат таймера
     and al, 11111011y         ; Бит 2: Формат - 0: упакованный двоично-десятичный, 1: двоичный
     or al, 010y               ; Бит 1: 24/12 формат часа - 1 включает 24-часовой режим
     out 71h, al

     ;:::::::::::::::::::: Получим текущую дату ::::::::::::::::::::

     lea edi, acDate

     CMOS 07h                  ; Число месяца
     mov al, '.'
     stosb

     CMOS 08h                  ; Месяц
     mov al, '.'
     stosb

     CMOS 32h                  ; Две старшие цифры года
     CMOS 09h                  ; Две младшие цифры года

     xor eax, eax              ; Завершим строку нулем
     stosb

     ;:::::::::::::::::::: Получим текущее время :::::::::::::::::::

     lea edi, acTime

     CMOS 04h                  ; Часы
     mov al, ':'
     stosb

     CMOS 02h                  ; Минуты
     mov al, ':'
     stosb

     CMOS 0h                   ; Секунды

     xor eax, eax              ; Завершим строку нулем
     stosb

     ;:::::::::::::: Восстановим старый формат таймера :::::::::::::

     mov al, 0Bh
     out 70h, al
     pop eax
     out 71h, al

     ;::::::::::::::::: Покажем текущие дату и время :::::::::::::::

     invoke wsprintf, addr acOut, $CTA0("Date:\t%s\nTime:\t%s"), addr acDate, addr acTime
     invoke MessageBox, NULL, addr acOut, $CTA0("Current Date and Time"), MB_OK

     ret

 DateTime endp

 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
 ;                                         start                                                     
 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

 start proc

 LOCAL fOK:BOOL
 LOCAL hSCManager:HANDLE
 LOCAL hService:HANDLE
 LOCAL acDriverPath[MAX_PATH]:CHAR

 LOCAL hKey:HANDLE
 LOCAL dwProcessId:DWORD

     and fOK, 0        ; Предположим, что произойдет ошибка

     ; Открываем базу данных SCM

     invoke OpenSCManager, NULL, NULL, SC_MANAGER_CREATE_SERVICE
     .if eax != NULL
         mov hSCManager, eax

         push eax
         invoke GetFullPathName, $CTA0("giveio.sys"), sizeof acDriverPath, addr acDriverPath, esp
         pop eax

         ; Регистрируем драйвер

         invoke CreateService, hSCManager, $CTA0("giveio"), $CTA0("Current Date and Time fetcher."), \
                 SERVICE_START + DELETE, SERVICE_KERNEL_DRIVER, SERVICE_DEMAND_START, \
                 SERVICE_ERROR_IGNORE, addr acDriverPath, NULL, NULL, NULL, NULL, NULL

         .if eax != NULL
             mov hService, eax

             invoke RegOpenKeyEx, HKEY_LOCAL_MACHINE, \
                                     $CTA0("SYSTEM\\CurrentControlSet\\Services\\giveio"), \
                                     0, KEY_CREATE_SUB_KEY + KEY_SET_VALUE, addr hKey

             .if eax == ERROR_SUCCESS

                 ; Добавляем в реестр идентификатор текущего процесса

                 invoke GetCurrentProcessId
                 mov dwProcessId, eax
                 invoke RegSetValueEx, hKey, $CTA0("ProcessId", szProcessId), NULL, REG_DWORD, \
                                         addr dwProcessId, sizeof DWORD

                 .if eax == ERROR_SUCCESS                
                     invoke StartService, hService, 0, NULL
                     inc fOK                ; Устанавливаем флаг
                     invoke RegDeleteValue, hKey, addr szProcessId
                 .else
                     invoke MessageBox, NULL, $CTA0("Can't add Process ID into registry."), \
                                         NULL, MB_ICONSTOP
                 .endif
                
                 invoke RegCloseKey, hKey

             .else
                 invoke MessageBox, NULL, $CTA0("Can't open registry."), NULL, MB_ICONSTOP
             .endif

             ; Удаляем драйвер из базы данных SCM

             invoke DeleteService, hService
             invoke CloseServiceHandle, hService
         .else
             invoke MessageBox, NULL, $CTA0("Can't register driver."), NULL, MB_ICONSTOP
         .endif
         invoke CloseServiceHandle, hSCManager
     .else
         invoke MessageBox, NULL, $CTA0("Can't connect to Service Control Manager."), \
                            NULL, MB_ICONSTOP
     .endif

     ; Если все ОК, получаем и показываем текущие дату и время

     .if fOK
         invoke DateTime
     .endif

     invoke ExitProcess, 0

 start endp

 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
 ;                                                                                                   
 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

 end start

Ничего нового в самой процедуре загрузки нет, за исключением нескольких моментов.


 invoke RegOpenKeyEx, HKEY_LOCAL_MACHINE, \
                         $CTA0("SYSTEM\\CurrentControlSet\\Services\\giveio"), \
                         0, KEY_CREATE_SUB_KEY + KEY_SET_VALUE, addr hKey

 .if eax == ERROR_SUCCESS
     invoke GetCurrentProcessId
     mov dwProcessId, eax
     invoke RegSetValueEx, hKey, $CTA0("ProcessId", szProcessId), NULL, REG_DWORD, \
                           addr dwProcessId, sizeof DWORD

     .if eax == ERROR_SUCCESS                
         invoke StartService, hService, 0, NULL

Перед запуском драйвера, мы создаем в подразделе реестра, соответствующем драйверу, дополнительный параметр ProcessId, и устанавливаем его значение равным идентификатору текущего процесса, т.е. процесса программы управления. Обратите внимание на то, что вызывая макрос $CTA0, я указываю метку szProcessId, которой будет помечен текст "ProcessId", для того, чтобы позже к нему обратиться. Если добавление параметра прошло без ошибок, то запускаем драйвер. Зачем нужен этот дополнительный параметр вы узнаете позже, когда мы будем разбирать текст драйвера.


         inc fOK
         invoke RegDeleteValue, hKey, addr szProcessId
     .else
         invoke MessageBox, NULL, $CTA0("Can't add Process ID into registry."), \
                             NULL, MB_ICONSTOP
     .endif
                
     invoke RegCloseKey, hKey

Получив управление от функции StartService, мы считаем, что драйвер успешно отработал и устанавливаем флаг fOK. Вызов функции RegDeleteValue делать не обязательно. Все равно, весь раздел реестра будет удален последующим вызовом DeleteService. Просто, я стараюсь придерживаться в программировании правила "хорошего тона": нагадил - подотри ;-)


 .if fOK
     invoke DateTime
 .endif

Удалив драйвер из базы данных SCM и закрыв все открытые описатели, мы вызывает процедуру DateTime, предварительно проверив флаг fOK.

На материнской плате компьютера имеется специальная микросхема, выполненная по технологии CMOS (Complementary Metal-Oxide Semiconductor, Металл-Окисел-Полупроводник с Комплементарной структурой, КМОП), и питающаяся от батарейки. В этой микросхеме реализован еще один таймер, называемый часами реального времени (Real Time Clock, RTC), который работает постоянно, даже при выключенном питании компьютера. Помимо таймера, в этой микросхеме имеется небольшой блок памяти, в котором хранится собственно текущее время, а также кое-какая информация о физических параметрах компьютера. Достаточно подробно об этом можно узнать в справочнике "Ralf Brown's Interrupt List". Получить содержимое памяти CMOS можно обратившись к портам ввода-вывода 70h и 71h.


 mov al, 0Bh               ; Управляющий регистр B
 out 70h, al
 in al, 71h

 push eax                  ; Сохраним старый фармат таймера
 and al, 11111011y         ; Бит 2: Формат - 0: упакованный двоично-десятичный, 1: двоичный
 or al, 010y               ; Бит 1: 24/12 формат часа - 1 включает 24-часовой режим
 out 71h, al

Сначала устанавливаем удобный нам формат данных, которые мы будем получать, используя управляющий регистр B. Хотя, по умолчанию, он и так установлен, но тем не менее. Нам удобно получать данные в упакованном двоично-десятичном формате (в одном байте две цифры - по 4 бита на каждую). Поскольку, у нас принята 24-часовая система деления суток, то этот формат мы и устанавливаем.

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


 invoke wsprintf, addr acOut, $CTA0("Date:\t%s\nTime:\t%s"), addr acDate, addr acTime
 invoke MessageBox, NULL, addr acOut, $CTA0("Current Date and Time"), MB_OK

Получив текущие дату и время, составляем из них единую строку и выводим ее на экран. Управляющая последовательность \t вставляет символ горизонтальной табуляции, а \n перевода строки (подробнее см. \Macros\Strings.mac). И на экране мы должны увидеть:

Рис. 3-2. Результат работы программы DateTime.exe



Самым странным, в вышеприведенном тексте, является обращение к портам ввода-вывода прямо из режима пользователя. Как я уже упомянул выше, доступ к портам ввода-вывода свято охраняется Windows NT. И тем не менее, мы к ним обратились. Это стало возможно благодаря драйверу giveio.sys, к рассмотрению исходного текста которого мы и переходим.


 ;@echo off
 ;goto make


 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
 ;
 ;  giveio - Драйвер режима ядра
 ;
 ;  Дает прямой доступ к портам ввода-вывода из режима пользователя
 ;   Основан на исходном тексте Дейла Робертса
 ;
 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

 .386
 .model flat, stdcall
 option casemap:none

 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
 ;                              В К Л Ю Ч А Е М Ы Е    Ф А Й Л Ы                                     
 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

 include \masm32\include\w2k\ntstatus.inc
 include \masm32\include\w2k\ntddk.inc
 include \masm32\include\w2k\ntoskrnl.inc

 includelib \masm32\lib\w2k\ntoskrnl.lib

 include \masm32\Macros\Strings.mac

 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
 ;                           С И М В О Л Ь Н Ы Е    К О Н С Т А Н Т Ы                                
 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

 IOPM_SIZE equ 2000h

 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
 ;                                              К О Д                                                
 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

 .code

 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
 ;                                       DriverEntry                                                 
 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

 DriverEntry proc pDriverObject:PDRIVER_OBJECT, pusRegistryPath:PUNICODE_STRING

 LOCAL status:NTSTATUS
 LOCAL oa:OBJECT_ATTRIBUTES
 LOCAL hKey:HANDLE
 LOCAL kvpi:KEY_VALUE_PARTIAL_INFORMATION
 LOCAL pIopm:PVOID
 LOCAL pProcess:LPVOID

     invoke DbgPrint, $CTA0("giveio: Entering DriverEntry")
        
     mov status, STATUS_DEVICE_CONFIGURATION_ERROR

     lea ecx, oa
     InitializeObjectAttributes ecx, pusRegistryPath, 0, NULL, NULL

     invoke ZwOpenKey, addr hKey, KEY_READ, ecx
     .if eax == STATUS_SUCCESS

         push eax
         invoke ZwQueryValueKey, hKey, $CCOUNTED_UNICODE_STRING("ProcessId", 4), \
                                KeyValuePartialInformation, addr kvpi, sizeof kvpi, esp
         pop ecx

         .if ( eax != STATUS_OBJECT_NAME_NOT_FOUND ) && ( ecx != 0 )

             invoke DbgPrint, $CTA0("giveio: Process ID: %X"), \
                                 dword ptr (KEY_VALUE_PARTIAL_INFORMATION PTR [kvpi]).Data

             ; выделяем буфер для карты разрешения ввода-вывода

             invoke MmAllocateNonCachedMemory, IOPM_SIZE
             .if eax != NULL
                 mov pIopm, eax

                 lea ecx, kvpi
                 invoke PsLookupProcessByProcessId, \
                         dword ptr (KEY_VALUE_PARTIAL_INFORMATION PTR [ecx]).Data, addr pProcess
                 .if eax == STATUS_SUCCESS

                     invoke DbgPrint, $CTA0("giveio: PTR KPROCESS: %08X"), pProcess

                     invoke Ke386QueryIoAccessMap, 0, pIopm
                     .if al != 0

                         ; Открываем доступ к порту 70h

                         mov ecx, pIopm
                         add ecx, 70h / 8
                         mov eax, [ecx]
                         btr eax, 70h MOD 8
                         mov [ecx], eax


                        ; Открываем доступ к порту 71h

                         mov ecx, pIopm
                         add ecx, 71h / 8
                         mov eax, [ecx]
                         btr eax, 71h MOD 8
                         mov [ecx], eax

                         invoke Ke386SetIoAccessMap, 1, pIopm
                         .if al != 0
                             invoke Ke386IoSetAccessProcess, pProcess, 1
                             .if al != 0
                                 invoke DbgPrint, $CTA0("giveio: I/O permission is successfully given")
                             .else
                                 invoke DbgPrint, $CTA0("giveio: I/O permission is failed")
                                 mov status, STATUS_IO_PRIVILEGE_FAILED
                             .endif
                         .else
                             mov status, STATUS_IO_PRIVILEGE_FAILED
                         .endif
                     .else
                         mov status, STATUS_IO_PRIVILEGE_FAILED
                     .endif
                     invoke ObDereferenceObject, pProcess
                 .else
                     mov status, STATUS_OBJECT_TYPE_MISMATCH
                 .endif
                 invoke MmFreeNonCachedMemory, pIopm, IOPM_SIZE
             .else
                 invoke DbgPrint, $CTA0("giveio: Call to MmAllocateNonCachedMemory failed")
                 mov status, STATUS_INSUFFICIENT_RESOURCES
             .endif
         .endif
         invoke ZwClose, hKey
     .endif

     invoke DbgPrint, $CTA0("giveio: Leaving DriverEntry")

     mov eax, status
     ret

 DriverEntry endp

 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
 ;                                                                                                   
 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

 end DriverEntry

 :make
 \masm32\bin\ml /nologo /c /coff giveio.bat
 \masm32\bin\link /nologo /driver /base:0x10000 /align:32 /out:giveio.sys /subsystem:native giveio.obj

 del giveio.obj

 echo.
 pause

Код драйвера основан на хорошо известных изысканиях Дейла Робертса, восходящих аж к 96 году прошлого века, в области предоставления процессу режима пользователя доступа к портам ввода-вывода на платформе Windows NT. Я решил, что здесь это будет очень кстати. Перевод статьи Дейла Робертса "Прямой ввод-вывод в среде Windows NT" можно почитать http://void.ru/?do=printable&id=701.

Я не буду подробно останавливаться на теории, т.к. достаточно подробно это описано в вышеупомянутой статье. Если очень коротко, то процессор поддерживает гибкий механизм защиты, позволяющий операционной системе предоставлять доступ к любому подмножеству портов ввода-вывода для каждого отдельно взятого процесса. Это возможно благодаря карте разрешения ввода-вывода (I/O Permission Map, IOPM). Немного подробнее про эту карту здесь: http://www.sasm.narod.ru/docs/pm/pm_tss/chap_5.htm. Про сегмент состояния задачи (Task State Segment, TSS), также активно принимающий в этом участие, можно почитать там же: http://www.sasm.narod.ru/docs/pm/pm_tss/chap_3.htm.

Каждый процесс может иметь свою собственную IOPM. Каждый бит в этой карте соответствует байтовому порту ввода-вывода. Если он (бит) установлен, то доступ к соответствующему порту запрещен, если сброшен - разрешен. Поскольку, пространство портов ввода-вывода в архитектуре x86 составляет 65535, то максимальный размер IOPM равен 2000h байт.

Всё, что сказано выше о I/O Permission Map верно, но не для операционных систем Windows NT+. Разработчики этих систем отказались от использования отдельного TSS для каждого процесса, по причине худшей производительности, а фирма Intel задумывала именно так и процессоры этой фирмы такую возможность поддерживают. Операционные систем Windows NT+ используют один TSS на все процессы. Поскольку TSS глобален, то и IOPM тоже. Это значит, что любые манипуляции с ней отражаются на все выполняющиеся, а также те, которые будут выполняться процессы.

Для манипулирования IOPM в модуле ntoskrnl.exe имеются две полностью недокументированные функции: Ke386QueryIoAccessMap и Ke386SetIoAccessMap. Приведу их описание составленное стараниями Дейла Робертса и моими тоже.



 Ke386QueryIoAccessMap proto stdcall dwFlag:DWORD, pIopm:PVOID

Копирует текущую IOPM размером 2000h из TSS в буфер, указатель на который содержится в параметре pIopm.

dwFlag

0 - заполнить буфер единичными битами (т.е запретить доступ ко всем портам);
1 - скопировать текущую IOPM из TSS в буфер.

pIopm

- указатель на блок памяти для приема IOPM, размером не менее 2000h байт.

При успешном завершении, возвращает в регистре al ненулевое значение.
Если произошла ошибка, то al равен нулю.



 Ke386SetIoAccessMap proto stdcall dwFlag:DWORD, pIopm:PVOID

Копирует переданную IOPM длинной 2000h из буфера, указатель на который содержится в параметре pIopm, в TSS.

dwFlag

только 1 - разрешает копирование. При любом другом значении функция возвращает ошибку.

pIopm

- указатель на блок памяти содержащий IOPM, размером не менее 2000h байт.

При успешном завершении, возвращает в регистре al ненулевое значение.
Если произошла ошибка, то al равен нулю.


И еще одна очень полезная, также полностью недокументированная, функция из модуля ntoskrnl.exe.



 Ke386IoSetAccessProcess proto stdcall pProcess:PTR KPROCESS, dwFlag:DWORD

Разрешает/запрещает использование IOPM для процесса.

pProcess

- указатель на структуру KPROCESS (чуть подробней ниже).

dwFlag

0 - запретить доступ к портам ввода-вывода, установкой смещения IOPM за границу сегмента TSS;
1 - разрешить доступ к портам ввода-вывода, устанавливая смещение IOPM в пределах TSS равным 88h.

При успешном завершении, возвращает в регистре al ненулевое значение.
Если произошла ошибка, то al равен нулю.


По префиксу в имени функции можно определить к какому компоненту она относится: Ke - ядро, Ob - диспетчер объектов, Ps - поддержка процессов, Mm - диспетчер памяти и т.д.


Для доступа к объектам код режима пользователя использует описатели (handles), которые являются ни чем иным как индексами в системных таблицах, в которых содержится сам указатель на объект. Ну а что такое, на самом деле, объект мы уже немного поговорили выше. Таким образом, посредством описателей система отрезает код режима пользователя от прямого доступа к объекту. Код режима ядра, напротив, пользуется именно указателями, т.к. он и есть сама система и имеет право делать с объектами что хочет. Функция Ke386IoSetAccessProcess требует, в качестве первого параметра, указатель на объект "процесс" (process object), т.е. на структуру KPROCESS (см. \include\w2k\w2kundoc.inc. Я специально поставил префикс "w2k", т.к. в Windows XP недокументированные структуры сильно отличаются. Так что, использовать этот файлик при компиляции драйвера предназначенного для XP, не самая лучшая идея). Код функции Ke386IoSetAccessProcess устанавливает член IopmOffset структуры KPROCESS в соответствующее значение.

Раз мы будем вызывать функцию Ke386IoSetAccessProcess, нам потребуется указатель на объект "процесс". Его можно получить разными способами. Я выбрал наиболее простой - по идентификатору. Именно поэтому, в модуле DateTime, мы получаем идентификатор текущего процесса и помещаем его в реестр. В данном случае мы используем реестр просто для передачи данных в драйвер. Т.к. процедура DriverEntry выполняется в контексте процесса System, нет возможности узнать, какой процесс на самом деле запустил драйвер. Вторым параметром, pusRegistryPath, в процедуре DriverEntry мы имеем указатель на раздел реестра, содержащий параметры инициализации драйвера. Мы воспользуемся им, чтобы извлечь из реестра идентификатор процесса.


Теперь можно перейти к разбору кода драйвера giveio.sys.


 lea ecx, oa
 InitializeObjectAttributes ecx, pusRegistryPath, 0, NULL, NULL

Для последующего вызова функции ZwOpenKey нам потребуется указатель на заполненную структуру OBJECT_ATTRIBUTES (\include\w2k\ntdef.inc). Для ее заполнения я использую макрос InitializeObjectAttributes. Можно заполнить и "вручную":


 lea ecx, oa
 xor eax, eax
 assume ecx:ptr OBJECT_ATTRIBUTES
 mov [ecx].dwLength, sizeof OBJECT_ATTRIBUTES
 mov [ecx].RootDirectory, eax                       ; NULL
 push pusRegistryPath
 pop [ecx].ObjectName
 mov [ecx].Attributes, eax                          ; 0
 mov [ecx].SecurityDescriptor, eax                  ; NULL
 mov [ecx].SecurityQualityOfService, eax            ; NULL
 assume ecx:nothing

Макрос InitializeObjectAttributes находится еще на стадии разработки, так что не советую использовать его способом отличным от приведенного выше. Если что не так - я не виноват ;-)


 invoke ZwOpenKey, addr hKey, KEY_READ, ecx
 .if eax == STATUS_SUCCESS

     push eax
     invoke ZwQueryValueKey, hKey, $CCOUNTED_UNICODE_STRING("ProcessId", 4), \
                             KeyValuePartialInformation, addr kvpi, sizeof kvpi, esp
     pop ecx

Вызовом функции ZwOpenKey получаем описатель раздела реестра в переменной hKey. Вторым параметром в эту функцию передаются права доступа, третьим - указатель на структуру OBJECT_ATTRIBUTES, заполненную на предыдущем этапе. С помощью функции ZwQueryValueKey получаем значение идентификатора процесса, записанное в параметре реестра ProcessId. Вторым параметром в эту функцию передается указатель на инициализированную структуру UNICODE_STRING, содержащую имя параметра реестра, значение которого мы хотим получить. Я стараюсь использовать возможности препроцессора masm на "полную катушку", поэтому, и тут использую самописный макрос $CCOUNTED_UNICODE_STRING (все там же - \Macros\Strings.mac). Обратите внимание на то, что я указываю выравнивание строки по границе двойного слова (выравнивание самой структуры UNICODE_STRING жестко прописано в макросе и равно двойному слову). Какой-то особой необходимости в этом тут нет, просто, я даю вам возможность оценить гибкость и удобство моих макросов. Рекламная пауза ;-) Если органически не перевариваете макросы, то можно использовать традиционный способ определения Unicode-строки, и структуры UNICODE_STRING ее содержащей:


 usz dw 'U', 'n', 'i', 'c', 'o', 'd', 'e', ' ', 's', 't', 'r', 'i', 'n', 'g', 0
 us UNICODE_STRING {sizeof usz - 2, sizeof usz, offset usz}

Меня этот способ никогда не вдохновлял, поэтому, я и написал для этой цели макросы COUNTED_UNICODE_STRING, $COUNTED_UNICODE_STRING, CCOUNTED_UNICODE_STRING, $CCOUNTED_UNICODE_STRING (см. \Macros\Strings.mac).

Третий параметр функции ZwQueryValueKey определяет тип запрашиваемой информации. KeyValuePartialInformation - символьная константа равная 2 (\include\w2k\ntddk.inc). Четвертый и пятый параметры - указатель на структуру KEY_VALUE_PARTIAL_INFORMATION и ее размер соответственно. В члене Data этой структуры мы и получим значение идентификатора процесса. Последний параметр - указатель на переменную, размером DWORD, в которую будет записано количество скопированных из реестра байт. Перед самым вызовом ZwQueryValueKey, мы резервируем на стеке для него место, а после вызова извлекаем значение. Я постоянно пользуюсь таким приемом - очень удобно.


 .if ( eax != STATUS_OBJECT_NAME_NOT_FOUND ) && ( ecx != 0 )
     invoke MmAllocateNonCachedMemory, IOPM_SIZE
     .if eax != NULL
         mov pIopm, eax

Если вызов ZwQueryValueKey прошел успешно, выделяем с помощью функции MmAllocateNonCachedMemory кусочек памяти в пуле неподкачиваемой памяти (такая память никогда не сбрасывается на диск), размером 2000h байт - максимальный размер карты разрешения ввода-вывода. Сохраняем указатель в переменной pIopm.


 lea ecx, kvpi
 invoke PsLookupProcessByProcessId, \
           dword ptr (KEY_VALUE_PARTIAL_INFORMATION PTR [ecx]).Data, addr pProcess
 .if eax == STATUS_SUCCESS
     invoke Ke386QueryIoAccessMap, 0, pIopm

Передавая в функцию PsLookupProcessByProcessId полученный ранее идентификатор процесса, получаем указатель на KPROCESS в переменной pProcess. Вызовом функции Ke386QueryIoAccessMap, копируем IOPM в буфер.


 .if al != 0

     mov ecx, pIopm
     add ecx, 70h / 8
     mov eax, [ecx]
     btr eax, 70h MOD 8
     mov [ecx], eax

     mov ecx, pIopm
     add ecx, 71h / 8
     mov eax, [ecx]
     btr eax, 71h MOD 8
     mov [ecx], eax

     invoke Ke386SetIoAccessMap, 1, pIopm
     .if al != 0
         invoke Ke386IoSetAccessProcess, pProcess, 1
         .if al != 0
             ; доступ получен
         .else
             mov status, STATUS_IO_PRIVILEGE_FAILED
         .endif
     .else
          mov status, STATUS_IO_PRIVILEGE_FAILED
     .endif
 .else
     mov status, STATUS_IO_PRIVILEGE_FAILED
 .endif

Сбрасываем биты соответствующие портам ввода-вывода 70h и 71h, и записываем модифицированную IOPM. Вызовом функции Ke386IoSetAccessProcess разрешаем доступ. Обратите внимание, что Microsoft предусмотрела специальный код ошибки STATUS_IO_PRIVILEGE_FAILED. В принципе, здесь совершенно не важно, какой код ошибки мы вернем системе при выходе из DriverEntry. Я, просто потихоньку, ввожу вас в курс дела.


     invoke ObDereferenceObject, pProcess
 .else
     mov status, STATUS_OBJECT_TYPE_MISMATCH
 .endif

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


             invoke MmFreeNonCachedMemory, pIopm, IOPM_SIZE
         .else
             mov status, STATUS_INSUFFICIENT_RESOURCES
         .endif
     .endif
     invoke ZwClose, hKey
 .endif

С помощью функции MmFreeNonCachedMemory освобождаем выделенный буфер, и, вызовом функции ZwClose, закрываем описатель раздела реестра.

Работа сделана - драйвер больше не нужен. Т.к. он возвращает один из кодов ошибки, система удаляет его из памяти. Но теперь, код режима пользователя имеет доступ к двум портам ввода-вывода, чем он и пользуется, обращаясь к памяти CMOS.

В этом примере я обратился к памяти CMOS, просто, для разнообразия. Можно было, как в предыдущем драйвере beeper.sys, попищать системным динамиком. Оставляю это вам, в качестве домашнего задания. Надо будет открыть доступ к соответствующим портам ввода-вывода. Вызвать процедуру MakeBeep1, предварительно убрав из ее тела каманды cli и sti, т.к. выполнять привилегированные команды процессора в режиме пользователя, вам никто не разрешит. Вызывать функции из модуля hal.dll, естественно, тоже нельзя, т.к. они находятся в адресном пространстве ядра. Максимум, что вы можете себе позволить - это предоставить доступ ко всем 65535 портам, одним махом:


 invoke MmAllocateNonCachedMemory, IOPM_SIZE
 .if eax != NULL
     mov pIopm, eax
     invoke RtlZeroMemory, pIopm, IOPM_SIZE

     lea ecx, kvpi
     invoke PsLookupProcessByProcessId, \
                 dword ptr (KEY_VALUE_PARTIAL_INFORMATION PTR [ecx]).Data, addr pProcess
     .if eax == STATUS_SUCCESS
         invoke Ke386SetIoAccessMap, 1, pIopm
         .if al != 0
             invoke Ke386IoSetAccessProcess, pProcess, 1
         .endif
         invoke ObDereferenceObject, pProcess
     .endif
     invoke MmFreeNonCachedMemory, pIopm, IOPM_SIZE
 .else
     mov status, STATUS_INSUFFICIENT_RESOURCES
 .endif

Помните только, что баловство с системным динамиком и чтение памяти CMOS, достаточно безобидное занятие. Но обращение к каким-то другим портам может быть небезопасно, т.к. в режиме пользователя его невозможно синхронизировать.


Пара слов об отладке

Сейчас мы уже более предметно можем поговорить об этом увлекательном процессе. Как я уже говорил в первой части, удобнее всего использовать в качестве отладчика SoftICE.

Базовой техникой является расстановка в нужных местах исходного текста отладочного прерывания int 3. При этом нужно убедиться, что в SoftICE включено отслеживание этого прерывания. В более поздних версиях SoftICE, для адресов режима ядра (>80000000h), это сделано автоматически. Проверить это можно с помощью команды i3here. Если отлов int 3 не включен, сделать это можно с помощью той же команды i3here on (выключается - i3here off). Очень советую прописать эту команду прямо в параметры инициализации SoftICE. Если вы забудите это сделать при следующей загрузке системы, и запустите драйвер с таким прерыванием, то BSOD не заставит себя ждать. Есть еще одна команда приводящая к тому же результату - bpint 3. Разница в том, что в первом случае, вы окажетесь в SoftICE на инструкции следующей за int 3, а во втором, прямо на int 3. Можно сделать и так: bpint 3 do "r eip eip+1", но это менее удобно.

В коде драйвера giveio я неоднократно вызывал функцию DbgPrint. Эта функция выводит на консоль отладчика форматированные сообщения. SoftICE прекрасно их понимает. Можно использовать утилиту DebugView Марка Руссиновича http://sysinternals.com/ntw2k/utilities.shtml


Что в архиве

В архиве к этой статье, помимо исходных кодов примеров и макросов, вы обнаружите:

\tools\protoize

- утилита конвертации библиотечных .lib файлов во включаемые .inc файлы сделанная f0dder;

Некоторые inc-файлы в каталоге \include\w2k\ изготовлены с ее помощью. Правда, все __cdecl-функции мне пришлось фиксить руками :-(

\tools\KmdManager

- утилита динамической загрузки/выгрузки драйверов (с исходниками, конечно). Порывшись хорошенько в сети, вы обнаружите несколько подобных инструментов, как с консольным, так и с графическим интерфейсом, но все они чем-либо да не устраивали меня. Поэтому, я написал свою собственную. Пока она не поддерживает буферов ввода-вывода, но, думаю, в следующей версии я этот недостаток исправлю. Если захотите ее перекомпилировать, то потребуется мой пакет cocomac v1.2;

\include\w2k

- необходимые включаемые файлы;

\lib\w2k

- необходимые библиотечные файлы.

В связи с тем, что Microsoft прекратила свободное распространение DDK, у вас могут возникнуть некоторые проблемы при компиляции драйверов. Прежде всего - это отсутствие .lib файлов. В этом каталоге находятся файлы от свободного выпуска Windows 2000, но подойдут без проблем и для Windows XP, и, думаю, для Windows NT4.0 тоже. Надеюсь, Microsoft на меня за это не очень обидится ;-)



Что почитать

Документацию DDK, помимо сайта http://www.microsoft.com/, можно посмотреть тут: "Windows XP SP1 DDK Documentation On-line".

Все Zw* функции и некоторые структуры описаны подробно в книге Гэри Неббета "Справочник по базовым функциям API Windows NT/2000", Издательский дом "Вильямс", 2002. В сети можно найти электронную версию этой книги: Gary Nebbett, "Windows NT-2000 Native API Reference".

Вобщем, на первых порах, можно обойтись и без DDK. Если чувствуете, что чего-то не хватает - ищите в сети. При желании найти можно многое.


Все драйверы я тестировал под Windows 2000 Pro и Windows XP Pro. Но все должно работать и на более ранних выпусках Windows NT. До встречи в следующей статье, где мы поговорим о подсистеме ввода-вывода вообще, и о диспетчере ввода-вывода в частности.

2002-2013 (c) wasm.ru