Вот мы и добрались до исходного текста простейших драйверов. Полнофункциональные нас ждут впереди. Все исходные тексты драйверов я буду оформлять в виде *.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 - заполнить буфер единичными битами (т.е запретить доступ ко всем
портам); |
|
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; |
При успешном завершении, возвращает в регистре 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. До встречи в следующей статье, где мы поговорим о подсистеме ввода-вывода вообще, и о диспетчере ввода-вывода в частности.
