Драйверы режима ядра: Часть 3: Простейшие драйверы — Архив WASM.RU
Вот мы и добрались до исходного текста простейших драйверов. Полнофункциональные нас ждут впереди. Все исходные тексты драйверов я буду оформлять в виде *.bat файла, который, на самом деле, является комбинацией *.bat и *.asm файлов, но имеет расширение .bat.
Код (Text):
;@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) для самой операционной системы среде.
Самый простой драйвер режима ядра
Вот исходный текст простейшего драйвера режима ядра.
Код (Text):
;@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 выглядит так:
Код (Text):
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 соответственно.
Код (Text):
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 следующим образом:
Код (Text):
UNICODE_STRING STRUCT woLength WORD ? ; длина строки в байтах (не символах) MaximumLength WORD ? ; длина буфера содержащего строку в байтах (не символах) Buffer PWSTR ? ; указатель на буфер содержащий строку UNICODE_STRING ENDS
woLength
- (мне пришлось изменить оригинальное имя Length, т.к. оно является зарезервированным словом) содержит текущую длину строки в байтах (не в символах!), не считая завершающего нуля.
MaximumLength
- максимальный размер буфера (также в байтах), в котором эта строка содержится.
Buffer
- указатель на саму Unicode-строку.
Главное достоинство этого формата в том, что он явно определяет, как текущую длину строки, так и ее максимально возможную длину. Это позволяет, при операциях с такой строкой, обойтись без некоторых дополнительных вычислений.
Почему в процедуру DriverEntry передаются именно эти два указателя? Потому, что доступ к ним (особенно к первому) является ключевым моментом в инициализации и последующей жизни драйвера. Подробнее об этом мы поговорим в следующих статьях. Пока же, мы рассматриваем простейшие драйверы, время жизни которых, ограничено временем выполнения процедуры DriverEntry. Что же мы можем тут полезного (или вредного) сделать? Ну, вредного хоть отбавляй. Мы ведь уже в нулевом кольце защиты. Можно, например, выполнить такой код:
Код (Text):
xor eax, eax xchg [eax], eaxЭто приведет к остановке системы и появлению BSOD (Blue Screen Of Death). А выполнение такого кода приведет к перезагрузке компьютера:
Код (Text):
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 будет уникальным. Вот его исходный текст:
Код (Text):
;@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.
Код (Text):
mov al, 10110110y out 43h, alВыводом в порт 43h двоичного числа 10110110, мы помещаем в управляющий регистр таймера значение, определяющее номер канала, которым мы будем управлять, тип операции, режим работы канала и формат счетчика.
Код (Text):
mov eax, dwPitch out 42h, al mov al, ah out 42h, alЗатем, в порт 42h выводим 16-битное начальное значение счетчика. Сначала младший байт, затем старший.
Код (Text):
in al, 61h or al, 11y out 61h, alИ, наконец, посредством вывода в порт 61h значения, с установленными 0-ым и 1-ым битами, включаем динамик.
Код (Text):
DO_DELAY MACRO mov eax, DELAY .while eax dec eax .endw ENDMДаем данамику позвучать некоторое время, пользуясь макросом DO_DELAY. Да - примитивно, но - эффективно ;-)
Код (Text):
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.
Код (Text):
;::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: ; ; 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Ничего нового в самой процедуре загрузки нет, за исключением нескольких моментов.
Код (Text):
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", для того, чтобы позже к нему обратиться. Если добавление параметра прошло без ошибок, то запускаем драйвер. Зачем нужен этот дополнительный параметр вы узнаете позже, когда мы будем разбирать текст драйвера.
Код (Text):
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. Просто, я стараюсь придерживаться в программировании правила "хорошего тона": нагадил - подотри ;-)
Код (Text):
.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.
Код (Text):
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, выдергиваем по одному байту нужной нам информации, попутно форматируя получающуюся строку.
Код (Text):
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, к рассмотрению исходного текста которого мы и переходим.
Код (Text):
;@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. Приведу их описание составленное стараниями Дейла Робертса и моими тоже.
Код (Text):
Ke386QueryIoAccessMap proto stdcall dwFlag:DWORD, pIopm:PVOIDКопирует текущую IOPM размером 2000h из TSS в буфер, указатель на который содержится в параметре pIopm.
dwFlag
0 - заполнить буфер единичными битами (т.е запретить доступ ко всем портам);
1 - скопировать текущую IOPM из TSS в буфер.pIopm
- указатель на блок памяти для приема IOPM, размером не менее 2000h байт.
При успешном завершении, возвращает в регистре al ненулевое значение.
Если произошла ошибка, то al равен нулю.
Код (Text):
Ke386SetIoAccessMap proto stdcall dwFlag:DWORD, pIopm:PVOIDКопирует переданную IOPM длинной 2000h из буфера, указатель на который содержится в параметре pIopm, в TSS.
dwFlag
только 1 - разрешает копирование. При любом другом значении функция возвращает ошибку.
pIopm
- указатель на блок памяти содержащий IOPM, размером не менее 2000h байт.
При успешном завершении, возвращает в регистре al ненулевое значение.
Если произошла ошибка, то al равен нулю.
И еще одна очень полезная, также полностью недокументированная, функция из модуля ntoskrnl.exe.
Код (Text):
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.
Код (Text):
lea ecx, oa InitializeObjectAttributes ecx, pusRegistryPath, 0, NULL, NULLДля последующего вызова функции ZwOpenKey нам потребуется указатель на заполненную структуру OBJECT_ATTRIBUTES (\include\w2k\ntdef.inc). Для ее заполнения я использую макрос InitializeObjectAttributes. Можно заполнить и "вручную":
Код (Text):
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 находится еще на стадии разработки, так что не советую использовать его способом отличным от приведенного выше. Если что не так - я не виноват ;-)
Код (Text):
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 ее содержащей:
Код (Text):
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, мы резервируем на стеке для него место, а после вызова извлекаем значение. Я постоянно пользуюсь таким приемом - очень удобно.
Код (Text):
.if ( eax != STATUS_OBJECT_NAME_NOT_FOUND ) && ( ecx != 0 ) invoke MmAllocateNonCachedMemory, IOPM_SIZE .if eax != NULL mov pIopm, eaxЕсли вызов ZwQueryValueKey прошел успешно, выделяем с помощью функции MmAllocateNonCachedMemory кусочек памяти в пуле неподкачиваемой памяти (такая память никогда не сбрасывается на диск), размером 2000h байт - максимальный размер карты разрешения ввода-вывода. Сохраняем указатель в переменной pIopm.
Код (Text):
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 в буфер.
Код (Text):
.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. Я, просто потихоньку, ввожу вас в курс дела.
Код (Text):
invoke ObDereferenceObject, pProcess .else mov status, STATUS_OBJECT_TYPE_MISMATCH .endifПредыдущий вызов функции PsLookupProcessByProcessId, увеличил количество ссылок на обьект процесса. Система раздельно хранит количество открытых описателей обьекта и количество предоставленных ссылок на объект. Описателями, в основном, пользуется код режима пользователя, ссылками - только код режима ядра. Пока, хотя бы одно из этих значений, не равно нулю, система не удаляет объект из памяти, считая что он еще используется каким-то кодом. Вызовом функции ObDereferenceObject мы уменьшаем количество ссылок на обьект процесса.
Код (Text):
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 портам, одним махом:
Код (Text):
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. До встречи в следующей статье, где мы поговорим о подсистеме ввода-вывода вообще, и о диспетчере ввода-вывода в частности. © Four-F
Драйверы режима ядра: Часть 3: Простейшие драйверы
Дата публикации 18 дек 2002