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

Дата публикации 18 дек 2002

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



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

Код (Text):
  1.  
  2.  
  3.  ;@echo off
  4.  ;goto make
  5.  
  6.  
  7.  .386                      ; начало исходного текста драйвера
  8.  
  9.    ; остальной код драйвера
  10.  
  11.  end DriverEntry           ; конец исходного текста драйвера
  12.  
  13.  
  14.  :make
  15.  \masm32\bin\ml /nologo /c /coff driver.bat
  16.  \masm32\bin\link /nologo /driver /base:0x10000 /align:32 /out:driver.sys /subsystem:native driver.obj
  17.  
  18.  del driver.obj
  19.  
  20.  echo.
  21.  pause
  22.  
  23.  

Если такой "самокомпилирующийся" файл запустить, то произойдет следущее. Первые две команды закомментарены, поэтому, они игнорируются компилятором 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):
  1.  
  2.  
  3.  ;@echo off
  4.  ;goto make
  5.  
  6.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  7.  ;
  8.  ; simplest - Самый простой драйвер режима ядра
  9.  ;
  10.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  11.  
  12.  .386
  13.  .model flat, stdcall
  14.  option casemap:none
  15.  
  16.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  17.  ;                              В К Л Ю Ч А Е М Ы Е    Ф А Й Л Ы                                    
  18.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  19.  
  20.  include \masm32\include\w2k\ntstatus.inc
  21.  include \masm32\include\w2k\ntddk.inc
  22.  
  23.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  24.  ;                                              К О Д                                                
  25.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  26.  
  27.  .code
  28.  
  29.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  30.  ;                                       DriverEntry                                                
  31.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  32.  
  33.  DriverEntry proc pDriverObject:PDRIVER_OBJECT, pusRegistryPath:PUNICODE_STRING
  34.  
  35.      mov eax, STATUS_DEVICE_CONFIGURATION_ERROR
  36.      ret
  37.  
  38.  DriverEntry endp
  39.  
  40.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  41.  ;                                                                                                  
  42.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  43.  
  44.  end DriverEntry
  45.  
  46.  :make
  47.  \masm32\bin\ml /nologo /c /coff simplest.bat
  48.  \masm32\bin\link /nologo /driver /base:0x10000 /align:32 /out:simplest.sys /subsystem:native simplest.obj
  49.  
  50.  del simplest.obj
  51.  
  52.  echo.
  53.  pause
  54.  
  55.  

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

Код (Text):
  1.  
  2.  
  3.  DriverEntry proto DriverObject:PDRIVER_OBJECT, RegistryPath:PUNICODE_STRING
  4.  
  5.  

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

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

Код (Text):
  1.  
  2.  
  3.  PDRIVER_OBJECT   typedef PTR DRIVER_OBJECT
  4.  PUNICODE_STRING  typedef PTR UNICODE_STRING
  5.  
  6.  

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):
  1.  
  2.  
  3.  UNICODE_STRING STRUCT
  4.      woLength        WORD    ?  ; длина строки в байтах (не символах)
  5.      MaximumLength   WORD    ?  ; длина буфера содержащего строку в байтах (не символах)
  6.      Buffer          PWSTR   ?  ; указатель на буфер содержащий строку
  7.  UNICODE_STRING ENDS
  8.  
  9.  

woLength

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

MaximumLength

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

Buffer

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

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

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

Код (Text):
  1.  
  2.  
  3.  xor eax, eax
  4.  xchg [eax], eax
  5.  
  6.  

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

Код (Text):
  1.  
  2.  
  3.  mov al, 0FEh
  4.  out 64h, al
  5.  
  6.  

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

В этих двух случаях, процедура 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):
  1.  
  2.  
  3.  ;@echo off
  4.  ;goto make
  5.  
  6.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  7.  ;
  8.  ;  beeper - Драйвер режима ядра
  9.  ;  Пищит системным динамиком
  10.  ;
  11.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  12.  
  13.  .386
  14.  .model flat, stdcall
  15.  option casemap:none
  16.  
  17.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  18.  ;                              В К Л Ю Ч А Е М Ы Е    Ф А Й Л Ы                                    
  19.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  20.  
  21.  include \masm32\include\w2k\ntstatus.inc
  22.  include \masm32\include\w2k\ntddk.inc
  23.  
  24.  include \masm32\include\w2k\hal.inc
  25.  
  26.  includelib \masm32\lib\w2k\hal.lib
  27.  
  28.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  29.  ;                           С И М В О Л Ь Н Ы Е    К О Н С Т А Н Т Ы                                
  30.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  31.  
  32.  TIMER_FREQUENCY        equ 1193167                   ; 1,193,167 Гц
  33.  OCTAVE                 equ 2                         ; множитель октавы
  34.  
  35.  PITCH_C                equ 523                       ; До        -  523,25 Гц
  36.  PITCH_Cs               equ 554                       ; До диез   -  554,37 Гц
  37.  PITCH_D                equ 587                       ; Ре        -  587,33 Гц
  38.  PITCH_Ds               equ 622                       ; Ре диез   -  622,25 Гц
  39.  PITCH_E                equ 659                       ; Ми        -  659,25 Гц
  40.  PITCH_F                equ 698                       ; Фа        -  698,46 Гц
  41.  PITCH_Fs               equ 740                       ; Фа диез   -  739,99 Гц
  42.  PITCH_G                equ 784                       ; Соль      -  783,99 Гц
  43.  PITCH_Gs               equ 831                       ; Соль диез -  830,61 Гц
  44.  PITCH_A                equ 880                       ; Ля        -  880,00 Гц
  45.  PITCH_As               equ 988                       ; Ля диез   -  987,77 Гц
  46.  PITCH_H                equ 1047                      ; Си        - 1046,50 Гц
  47.  
  48.  
  49.  ; Нам нужны три звука для до-мажорного арпеджио (до, ми, соль)
  50.  
  51.  TONE_1                 equ TIMER_FREQUENCY/(PITCH_C*OCTAVE)
  52.  TONE_2                 equ TIMER_FREQUENCY/(PITCH_E*OCTAVE)
  53.  TONE_3                 equ (PITCH_G*OCTAVE)           ; для HalMakeBeep
  54.  
  55.  DELAY                  equ 1800000h                   ; для моей ~800mHz машины
  56.  
  57.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  58.  ;                                         М А К Р О С Ы                                            
  59.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  60.  
  61.  DO_DELAY MACRO
  62.      mov eax, DELAY
  63.      .while eax
  64.          dec eax
  65.      .endw
  66.  ENDM
  67.  
  68.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  69.  ;                                              К О Д                                                
  70.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  71.  
  72.  .code
  73.  
  74.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  75.  ;                                            MakeBeep1                                              
  76.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  77.  
  78.  MakeBeep1 proc dwPitch:DWORD
  79.  
  80.      ; Прямой доступ к оборудованию через порты ввода-вывода
  81.  
  82.      cli
  83.  
  84.      mov al, 10110110y
  85.      out 43h, al
  86.  
  87.      mov eax, dwPitch
  88.      out 42h, al
  89.  
  90.      mov al, ah
  91.      out 42h, al
  92.  
  93.      ; включить динамик
  94.  
  95.      in al, 61h
  96.      or  al, 11y
  97.      out 61h, al
  98.  
  99.      sti
  100.  
  101.      DO_DELAY
  102.  
  103.      cli
  104.  
  105.      ; выключить динамик
  106.  
  107.      in al, 61h
  108.      and al, 11111100y
  109.      out 61h, al
  110.  
  111.      sti
  112.  
  113.      ret
  114.  
  115.  MakeBeep1 endp
  116.  
  117.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  118.  ;                                            MakeBeep2                                              
  119.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  120.  
  121.  MakeBeep2 proc dwPitch:DWORD
  122.  
  123.      ; Прямой доступ к оборудованию используя функции
  124.      ; WRITE_PORT_UCHAR и READ_PORT_UCHAR из модуля hal.dll
  125.  
  126.      cli
  127.  
  128.      invoke WRITE_PORT_UCHAR, 43h, 10110110y
  129.  
  130.      mov eax, dwPitch
  131.      and eax, 0FFh
  132.      invoke WRITE_PORT_UCHAR, 42h, eax
  133.      mov eax, dwPitch
  134.      shr eax, 8
  135.      and eax, 0FFh
  136.      invoke WRITE_PORT_UCHAR, 42h, eax
  137.  
  138.      ; включить динамик
  139.  
  140.      invoke READ_PORT_UCHAR, 61h
  141.      or  al, 11y
  142.      and eax, 0FFh
  143.      invoke WRITE_PORT_UCHAR, 61h, eax
  144.  
  145.      sti
  146.  
  147.      DO_DELAY  
  148.  
  149.      cli
  150.  
  151.      ; выключить динамик
  152.  
  153.      invoke READ_PORT_UCHAR, 61h
  154.      and al, 11111100y
  155.      and eax, 0FFh
  156.      invoke WRITE_PORT_UCHAR, 61h, eax
  157.  
  158.      sti
  159.  
  160.      ret
  161.  
  162.  MakeBeep2 endp
  163.  
  164.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  165.  ;                                       DriverEntry                                                
  166.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  167.  
  168.  DriverEntry proc pDriverObject:PDRIVER_OBJECT, pusRegistryPath:PUNICODE_STRING
  169.  
  170.      invoke MakeBeep1, TONE_1
  171.      invoke MakeBeep2, TONE_2
  172.  
  173.      ; Прямой доступ к оборудованию используя функцию HalMakeBeep из модуля hal.dll
  174.  
  175.      invoke HalMakeBeep, TONE_3
  176.      DO_DELAY
  177.      invoke HalMakeBeep, 0
  178.  
  179.      mov eax, STATUS_DEVICE_CONFIGURATION_ERROR
  180.      ret
  181.  
  182.  DriverEntry endp
  183.  
  184.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  185.  ;                                                                                                  
  186.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  187.  
  188.  end DriverEntry
  189.  
  190.  :make
  191.  \masm32\bin\ml /nologo /c /coff beeper.bat
  192.  \masm32\bin\link /nologo /driver /base:0x10000 /align:32 /out:beeper.sys /subsystem:native beeper.obj
  193.  
  194.  del beeper.obj
  195.  
  196.  echo.
  197.  pause
  198.  
  199.  

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

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

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

Код (Text):
  1.  
  2.  
  3.  mov al, 10110110y
  4.  out 43h, al
  5.  
  6.  

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

Код (Text):
  1.  
  2.  
  3.  mov eax, dwPitch
  4.  out 42h, al
  5.  
  6.  mov al, ah
  7.  out 42h, al
  8.  
  9.  

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

Код (Text):
  1.  
  2.  
  3.  in al, 61h
  4.  or  al, 11y
  5.  out 61h, al
  6.  
  7.  

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

Код (Text):
  1.  
  2.  
  3.  DO_DELAY MACRO
  4.      mov eax, DELAY
  5.      .while eax
  6.          dec eax
  7.      .endw
  8.  ENDM
  9.  
  10.  

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

Код (Text):
  1.  
  2.  
  3.  in al, 61h
  4.  and al, 11111100y
  5.  out 61h, al
  6.  
  7.  

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

Второй звук (ми) мы воспроизводим посредством процедуры 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):
  1.  
  2.  
  3.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  4.  ;
  5.  ;  DateTime.asm
  6.  ;
  7.  ;  Программа управления драйвером giveio.sys
  8.  ;
  9.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  10.  
  11.  .386
  12.  .model flat, stdcall
  13.  option casemap:none
  14.  
  15.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  16.  ;                              В К Л Ю Ч А Е М Ы Е    Ф А Й Л Ы                                    
  17.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  18.  
  19.  include \masm32\include\windows.inc
  20.  
  21.  include \masm32\include\kernel32.inc
  22.  include \masm32\include\user32.inc
  23.  include \masm32\include\advapi32.inc
  24.  
  25.  includelib \masm32\lib\kernel32.lib
  26.  includelib \masm32\lib\user32.lib
  27.  includelib \masm32\lib\advapi32.lib
  28.  
  29.  include \masm32\Macros\Strings.mac
  30.  
  31.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  32.  ;                                         М А К Р О С Ы                                            
  33.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  34.  
  35.  CMOS MACRO by:REQ
  36.      mov al, by
  37.      out 70h, al
  38.      in al, 71h
  39.  
  40.      mov ah, al
  41.      shr al, 4
  42.      add al, '0'
  43.  
  44.      and ah, 0Fh
  45.      add ah, '0'
  46.      stosw
  47.  ENDM
  48.  
  49.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  50.  ;                                           К О Д                                                  
  51.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  52.  
  53.  .code
  54.  
  55.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  56.  ;                                          DateTime                                                
  57.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  58.  
  59.  DateTime proc uses edi
  60.  
  61.  LOCAL acDate[16]:CHAR
  62.  LOCAL acTime[16]:CHAR
  63.  LOCAL acOut[64]:CHAR
  64.  
  65.      ; Подробнее смотри Ralf Brown's Interrupt List
  66.  
  67.      ;:::::::::::::::::: Установим формат таймера ::::::::::::::::::
  68.  
  69.      mov al, 0Bh               ; Управляющий регистр B
  70.      out 70h, al
  71.      in al, 71h
  72.  
  73.      push eax                  ; Сохраним старый фармат таймера
  74.      and al, 11111011y         ; Бит 2: Формат - 0: упакованный двоично-десятичный, 1: двоичный
  75.      or al, 010y               ; Бит 1: 24/12 формат часа - 1 включает 24-часовой режим
  76.      out 71h, al
  77.  
  78.      ;:::::::::::::::::::: Получим текущую дату ::::::::::::::::::::
  79.  
  80.      lea edi, acDate
  81.  
  82.      CMOS 07h                  ; Число месяца
  83.      mov al, '.'
  84.      stosb
  85.  
  86.      CMOS 08h                  ; Месяц
  87.      mov al, '.'
  88.      stosb
  89.  
  90.      CMOS 32h                  ; Две старшие цифры года
  91.      CMOS 09h                  ; Две младшие цифры года
  92.  
  93.      xor eax, eax              ; Завершим строку нулем
  94.      stosb
  95.  
  96.      ;:::::::::::::::::::: Получим текущее время :::::::::::::::::::
  97.  
  98.      lea edi, acTime
  99.  
  100.      CMOS 04h                  ; Часы
  101.      mov al, ':'
  102.      stosb
  103.  
  104.      CMOS 02h                  ; Минуты
  105.      mov al, ':'
  106.      stosb
  107.  
  108.      CMOS 0h                   ; Секунды
  109.  
  110.      xor eax, eax              ; Завершим строку нулем
  111.      stosb
  112.  
  113.      ;:::::::::::::: Восстановим старый формат таймера :::::::::::::
  114.  
  115.      mov al, 0Bh
  116.      out 70h, al
  117.      pop eax
  118.      out 71h, al
  119.  
  120.      ;::::::::::::::::: Покажем текущие дату и время :::::::::::::::
  121.  
  122.      invoke wsprintf, addr acOut, $CTA0("Date:\t%s\nTime:\t%s"), addr acDate, addr acTime
  123.      invoke MessageBox, NULL, addr acOut, $CTA0("Current Date and Time"), MB_OK
  124.  
  125.      ret
  126.  
  127.  DateTime endp
  128.  
  129.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  130.  ;                                         start                                                    
  131.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  132.  
  133.  start proc
  134.  
  135.  LOCAL fOK:BOOL
  136.  LOCAL hSCManager:HANDLE
  137.  LOCAL hService:HANDLE
  138.  LOCAL acDriverPath[MAX_PATH]:CHAR
  139.  
  140.  LOCAL hKey:HANDLE
  141.  LOCAL dwProcessId:DWORD
  142.  
  143.      and fOK, 0        ; Предположим, что произойдет ошибка
  144.  
  145.      ; Открываем базу данных SCM
  146.  
  147.      invoke OpenSCManager, NULL, NULL, SC_MANAGER_CREATE_SERVICE
  148.      .if eax != NULL
  149.          mov hSCManager, eax
  150.  
  151.          push eax
  152.          invoke GetFullPathName, $CTA0("giveio.sys"), sizeof acDriverPath, addr acDriverPath, esp
  153.          pop eax
  154.  
  155.          ; Регистрируем драйвер
  156.  
  157.          invoke CreateService, hSCManager, $CTA0("giveio"), $CTA0("Current Date and Time fetcher."), \
  158.                  SERVICE_START + DELETE, SERVICE_KERNEL_DRIVER, SERVICE_DEMAND_START, \
  159.                  SERVICE_ERROR_IGNORE, addr acDriverPath, NULL, NULL, NULL, NULL, NULL
  160.  
  161.          .if eax != NULL
  162.              mov hService, eax
  163.  
  164.              invoke RegOpenKeyEx, HKEY_LOCAL_MACHINE, \
  165.                                      $CTA0("SYSTEM\\CurrentControlSet\\Services\\giveio"), \
  166.                                      0, KEY_CREATE_SUB_KEY + KEY_SET_VALUE, addr hKey
  167.  
  168.              .if eax == ERROR_SUCCESS
  169.  
  170.                  ; Добавляем в реестр идентификатор текущего процесса
  171.  
  172.                  invoke GetCurrentProcessId
  173.                  mov dwProcessId, eax
  174.                  invoke RegSetValueEx, hKey, $CTA0("ProcessId", szProcessId), NULL, REG_DWORD, \
  175.                                          addr dwProcessId, sizeof DWORD
  176.  
  177.                  .if eax == ERROR_SUCCESS                
  178.                      invoke StartService, hService, 0, NULL
  179.                      inc fOK                ; Устанавливаем флаг
  180.                      invoke RegDeleteValue, hKey, addr szProcessId
  181.                  .else
  182.                      invoke MessageBox, NULL, $CTA0("Can't add Process ID into registry."), \
  183.                                          NULL, MB_ICONSTOP
  184.                  .endif
  185.                
  186.                  invoke RegCloseKey, hKey
  187.  
  188.              .else
  189.                  invoke MessageBox, NULL, $CTA0("Can't open registry."), NULL, MB_ICONSTOP
  190.              .endif
  191.  
  192.              ; Удаляем драйвер из базы данных SCM
  193.  
  194.              invoke DeleteService, hService
  195.              invoke CloseServiceHandle, hService
  196.          .else
  197.              invoke MessageBox, NULL, $CTA0("Can't register driver."), NULL, MB_ICONSTOP
  198.          .endif
  199.          invoke CloseServiceHandle, hSCManager
  200.      .else
  201.          invoke MessageBox, NULL, $CTA0("Can't connect to Service Control Manager."), \
  202.                             NULL, MB_ICONSTOP
  203.      .endif
  204.  
  205.      ; Если все ОК, получаем и показываем текущие дату и время
  206.  
  207.      .if fOK
  208.          invoke DateTime
  209.      .endif
  210.  
  211.      invoke ExitProcess, 0
  212.  
  213.  start endp
  214.  
  215.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  216.  ;                                                                                                  
  217.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  218.  
  219.  end start
  220.  
  221.  

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

Код (Text):
  1.  
  2.  
  3.  invoke RegOpenKeyEx, HKEY_LOCAL_MACHINE, \
  4.                          $CTA0("SYSTEM\\CurrentControlSet\\Services\\giveio"), \
  5.                          0, KEY_CREATE_SUB_KEY + KEY_SET_VALUE, addr hKey
  6.  
  7.  .if eax == ERROR_SUCCESS
  8.      invoke GetCurrentProcessId
  9.      mov dwProcessId, eax
  10.      invoke RegSetValueEx, hKey, $CTA0("ProcessId", szProcessId), NULL, REG_DWORD, \
  11.                            addr dwProcessId, sizeof DWORD
  12.  
  13.      .if eax == ERROR_SUCCESS                
  14.          invoke StartService, hService, 0, NULL
  15.  
  16.  

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

Код (Text):
  1.  
  2.  
  3.          inc fOK
  4.          invoke RegDeleteValue, hKey, addr szProcessId
  5.      .else
  6.          invoke MessageBox, NULL, $CTA0("Can't add Process ID into registry."), \
  7.                              NULL, MB_ICONSTOP
  8.      .endif
  9.                
  10.      invoke RegCloseKey, hKey
  11.  
  12.  

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

Код (Text):
  1.  
  2.  
  3.  .if fOK
  4.      invoke DateTime
  5.  .endif
  6.  
  7.  

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

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

Код (Text):
  1.  
  2.  
  3.  mov al, 0Bh               ; Управляющий регистр B
  4.  out 70h, al
  5.  in al, 71h
  6.  
  7.  push eax                  ; Сохраним старый фармат таймера
  8.  and al, 11111011y         ; Бит 2: Формат - 0: упакованный двоично-десятичный, 1: двоичный
  9.  or al, 010y               ; Бит 1: 24/12 формат часа - 1 включает 24-часовой режим
  10.  out 71h, al
  11.  
  12.  

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

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

Код (Text):
  1.  
  2.  
  3.  invoke wsprintf, addr acOut, $CTA0("Date:\t%s\nTime:\t%s"), addr acDate, addr acTime
  4.  invoke MessageBox, NULL, addr acOut, $CTA0("Current Date and Time"), MB_OK
  5.  
  6.  

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

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



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

Код (Text):
  1.  
  2.  
  3.  ;@echo off
  4.  ;goto make
  5.  
  6.  
  7.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  8.  ;
  9.  ;  giveio - Драйвер режима ядра
  10.  ;
  11.  ;  Дает прямой доступ к портам ввода-вывода из режима пользователя
  12.  ;   Основан на исходном тексте Дейла Робертса
  13.  ;
  14.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  15.  
  16.  .386
  17.  .model flat, stdcall
  18.  option casemap:none
  19.  
  20.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  21.  ;                              В К Л Ю Ч А Е М Ы Е    Ф А Й Л Ы                                    
  22.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  23.  
  24.  include \masm32\include\w2k\ntstatus.inc
  25.  include \masm32\include\w2k\ntddk.inc
  26.  include \masm32\include\w2k\ntoskrnl.inc
  27.  
  28.  includelib \masm32\lib\w2k\ntoskrnl.lib
  29.  
  30.  include \masm32\Macros\Strings.mac
  31.  
  32.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  33.  ;                           С И М В О Л Ь Н Ы Е    К О Н С Т А Н Т Ы                                
  34.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  35.  
  36.  IOPM_SIZE equ 2000h
  37.  
  38.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  39.  ;                                              К О Д                                                
  40.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  41.  
  42.  .code
  43.  
  44.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  45.  ;                                       DriverEntry                                                
  46.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  47.  
  48.  DriverEntry proc pDriverObject:PDRIVER_OBJECT, pusRegistryPath:PUNICODE_STRING
  49.  
  50.  LOCAL status:NTSTATUS
  51.  LOCAL oa:OBJECT_ATTRIBUTES
  52.  LOCAL hKey:HANDLE
  53.  LOCAL kvpi:KEY_VALUE_PARTIAL_INFORMATION
  54.  LOCAL pIopm:PVOID
  55.  LOCAL pProcess:LPVOID
  56.  
  57.      invoke DbgPrint, $CTA0("giveio: Entering DriverEntry")
  58.        
  59.      mov status, STATUS_DEVICE_CONFIGURATION_ERROR
  60.  
  61.      lea ecx, oa
  62.      InitializeObjectAttributes ecx, pusRegistryPath, 0, NULL, NULL
  63.  
  64.      invoke ZwOpenKey, addr hKey, KEY_READ, ecx
  65.      .if eax == STATUS_SUCCESS
  66.  
  67.          push eax
  68.          invoke ZwQueryValueKey, hKey, $CCOUNTED_UNICODE_STRING("ProcessId", 4), \
  69.                                 KeyValuePartialInformation, addr kvpi, sizeof kvpi, esp
  70.          pop ecx
  71.  
  72.          .if ( eax != STATUS_OBJECT_NAME_NOT_FOUND ) &amp;&amp; ( ecx != 0 )
  73.  
  74.              invoke DbgPrint, $CTA0("giveio: Process ID: %X"), \
  75.                                  dword ptr (KEY_VALUE_PARTIAL_INFORMATION PTR [kvpi]).Data
  76.  
  77.              ; выделяем буфер для карты разрешения ввода-вывода
  78.  
  79.              invoke MmAllocateNonCachedMemory, IOPM_SIZE
  80.              .if eax != NULL
  81.                  mov pIopm, eax
  82.  
  83.                  lea ecx, kvpi
  84.                  invoke PsLookupProcessByProcessId, \
  85.                          dword ptr (KEY_VALUE_PARTIAL_INFORMATION PTR [ecx]).Data, addr pProcess
  86.                  .if eax == STATUS_SUCCESS
  87.  
  88.                      invoke DbgPrint, $CTA0("giveio: PTR KPROCESS: %08X"), pProcess
  89.  
  90.                      invoke Ke386QueryIoAccessMap, 0, pIopm
  91.                      .if al != 0
  92.  
  93.                          ; Открываем доступ к порту 70h
  94.  
  95.                          mov ecx, pIopm
  96.                          add ecx, 70h / 8
  97.                          mov eax, [ecx]
  98.                          btr eax, 70h MOD 8
  99.                          mov [ecx], eax
  100.  
  101.  
  102.                         ; Открываем доступ к порту 71h
  103.  
  104.                          mov ecx, pIopm
  105.                          add ecx, 71h / 8
  106.                          mov eax, [ecx]
  107.                          btr eax, 71h MOD 8
  108.                          mov [ecx], eax
  109.  
  110.                          invoke Ke386SetIoAccessMap, 1, pIopm
  111.                          .if al != 0
  112.                              invoke Ke386IoSetAccessProcess, pProcess, 1
  113.                              .if al != 0
  114.                                  invoke DbgPrint, $CTA0("giveio: I/O permission is successfully given")
  115.                              .else
  116.                                  invoke DbgPrint, $CTA0("giveio: I/O permission is failed")
  117.                                  mov status, STATUS_IO_PRIVILEGE_FAILED
  118.                              .endif
  119.                          .else
  120.                              mov status, STATUS_IO_PRIVILEGE_FAILED
  121.                          .endif
  122.                      .else
  123.                          mov status, STATUS_IO_PRIVILEGE_FAILED
  124.                      .endif
  125.                      invoke ObDereferenceObject, pProcess
  126.                  .else
  127.                      mov status, STATUS_OBJECT_TYPE_MISMATCH
  128.                  .endif
  129.                  invoke MmFreeNonCachedMemory, pIopm, IOPM_SIZE
  130.              .else
  131.                  invoke DbgPrint, $CTA0("giveio: Call to MmAllocateNonCachedMemory failed")
  132.                  mov status, STATUS_INSUFFICIENT_RESOURCES
  133.              .endif
  134.          .endif
  135.          invoke ZwClose, hKey
  136.      .endif
  137.  
  138.      invoke DbgPrint, $CTA0("giveio: Leaving DriverEntry")
  139.  
  140.      mov eax, status
  141.      ret
  142.  
  143.  DriverEntry endp
  144.  
  145.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  146.  ;                                                                                                  
  147.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  148.  
  149.  end DriverEntry
  150.  
  151.  :make
  152.  \masm32\bin\ml /nologo /c /coff giveio.bat
  153.  \masm32\bin\link /nologo /driver /base:0x10000 /align:32 /out:giveio.sys /subsystem:native giveio.obj
  154.  
  155.  del giveio.obj
  156.  
  157.  echo.
  158.  pause
  159.  
  160.  

Код драйвера основан на хорошо известных изысканиях Дейла Робертса, восходящих аж к 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):
  1.  
  2.  
  3.  Ke386QueryIoAccessMap proto stdcall dwFlag:DWORD, pIopm:PVOID
  4.  
  5.  

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

dwFlag

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

pIopm

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

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


Код (Text):
  1.  
  2.  
  3.  Ke386SetIoAccessMap proto stdcall dwFlag:DWORD, pIopm:PVOID
  4.  
  5.  

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

dwFlag

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

pIopm

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

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


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


Код (Text):
  1.  
  2.  
  3.  Ke386IoSetAccessProcess proto stdcall pProcess:PTR KPROCESS, dwFlag:DWORD
  4.  
  5.  

Разрешает/запрещает использование 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):
  1.  
  2.  
  3.  lea ecx, oa
  4.  InitializeObjectAttributes ecx, pusRegistryPath, 0, NULL, NULL
  5.  
  6.  

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

Код (Text):
  1.  
  2.  
  3.  lea ecx, oa
  4.  xor eax, eax
  5.  assume ecx:ptr OBJECT_ATTRIBUTES
  6.  mov [ecx].dwLength, sizeof OBJECT_ATTRIBUTES
  7.  mov [ecx].RootDirectory, eax                       ; NULL
  8.  push pusRegistryPath
  9.  pop [ecx].ObjectName
  10.  mov [ecx].Attributes, eax                          ; 0
  11.  mov [ecx].SecurityDescriptor, eax                  ; NULL
  12.  mov [ecx].SecurityQualityOfService, eax            ; NULL
  13.  assume ecx:nothing
  14.  
  15.  

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

Код (Text):
  1.  
  2.  
  3.  invoke ZwOpenKey, addr hKey, KEY_READ, ecx
  4.  .if eax == STATUS_SUCCESS
  5.  
  6.      push eax
  7.      invoke ZwQueryValueKey, hKey, $CCOUNTED_UNICODE_STRING("ProcessId", 4), \
  8.                              KeyValuePartialInformation, addr kvpi, sizeof kvpi, esp
  9.      pop ecx
  10.  
  11.  

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

Код (Text):
  1.  
  2.  
  3.  usz dw 'U', 'n', 'i', 'c', 'o', 'd', 'e', ' ', 's', 't', 'r', 'i', 'n', 'g', 0
  4.  us UNICODE_STRING {sizeof usz - 2, sizeof usz, offset usz}
  5.  
  6.  

Меня этот способ никогда не вдохновлял, поэтому, я и написал для этой цели макросы 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):
  1.  
  2.  
  3.  .if ( eax != STATUS_OBJECT_NAME_NOT_FOUND ) &amp;&amp; ( ecx != 0 )
  4.      invoke MmAllocateNonCachedMemory, IOPM_SIZE
  5.      .if eax != NULL
  6.          mov pIopm, eax
  7.  
  8.  

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

Код (Text):
  1.  
  2.  
  3.  lea ecx, kvpi
  4.  invoke PsLookupProcessByProcessId, \
  5.            dword ptr (KEY_VALUE_PARTIAL_INFORMATION PTR [ecx]).Data, addr pProcess
  6.  .if eax == STATUS_SUCCESS
  7.      invoke Ke386QueryIoAccessMap, 0, pIopm
  8.  
  9.  

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

Код (Text):
  1.  
  2.  
  3.  .if al != 0
  4.  
  5.      mov ecx, pIopm
  6.      add ecx, 70h / 8
  7.      mov eax, [ecx]
  8.      btr eax, 70h MOD 8
  9.      mov [ecx], eax
  10.  
  11.      mov ecx, pIopm
  12.      add ecx, 71h / 8
  13.      mov eax, [ecx]
  14.      btr eax, 71h MOD 8
  15.      mov [ecx], eax
  16.  
  17.      invoke Ke386SetIoAccessMap, 1, pIopm
  18.      .if al != 0
  19.          invoke Ke386IoSetAccessProcess, pProcess, 1
  20.          .if al != 0
  21.              ; доступ получен
  22.          .else
  23.              mov status, STATUS_IO_PRIVILEGE_FAILED
  24.          .endif
  25.      .else
  26.           mov status, STATUS_IO_PRIVILEGE_FAILED
  27.      .endif
  28.  .else
  29.      mov status, STATUS_IO_PRIVILEGE_FAILED
  30.  .endif
  31.  
  32.  

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

Код (Text):
  1.  
  2.  
  3.      invoke ObDereferenceObject, pProcess
  4.  .else
  5.      mov status, STATUS_OBJECT_TYPE_MISMATCH
  6.  .endif
  7.  
  8.  

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

Код (Text):
  1.  
  2.  
  3.              invoke MmFreeNonCachedMemory, pIopm, IOPM_SIZE
  4.          .else
  5.              mov status, STATUS_INSUFFICIENT_RESOURCES
  6.          .endif
  7.      .endif
  8.      invoke ZwClose, hKey
  9.  .endif
  10.  
  11.  

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

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

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

Код (Text):
  1.  
  2.  
  3.  invoke MmAllocateNonCachedMemory, IOPM_SIZE
  4.  .if eax != NULL
  5.      mov pIopm, eax
  6.      invoke RtlZeroMemory, pIopm, IOPM_SIZE
  7.  
  8.      lea ecx, kvpi
  9.      invoke PsLookupProcessByProcessId, \
  10.                  dword ptr (KEY_VALUE_PARTIAL_INFORMATION PTR [ecx]).Data, addr pProcess
  11.      .if eax == STATUS_SUCCESS
  12.          invoke Ke386SetIoAccessMap, 1, pIopm
  13.          .if al != 0
  14.              invoke Ke386IoSetAccessProcess, pProcess, 1
  15.          .endif
  16.          invoke ObDereferenceObject, pProcess
  17.      .endif
  18.      invoke MmFreeNonCachedMemory, pIopm, IOPM_SIZE
  19.  .else
  20.      mov status, STATUS_INSUFFICIENT_RESOURCES
  21.  .endif
  22.  
  23.  

Помните только, что баловство с системным динамиком и чтение памяти 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


0 2.740
archive

archive
New Member

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