Драйвер, это просто

в 9:48, , рубрики: ddk, driver, PureBasic, Драйвер, системное программирование, метки: , , ,

Многие считают что самому создать драйвер для Windows это что-то на грани фантастики. Но на самом деле это не так. Конечно, разработка драйвера для какого-то навороченного девайса бывает не простой задачей. Но ведь тоже самое можно сказать про создание сложных программ или игр. В разработке простого драйвера нет ничего сложного и я попытаюсь на примерах это показать.

Сперва нам нужно определится в чем мы же будем создавать наш первый драйвер. Поскольку материал ориентирован на новичков, то язык программирования был выбран один из простых, и это не Си или ассемблер, а бейсик. Будем использовать один из диалектов бейсика — PureBasic. Из коробки он не обучен создавать драйверы, но у него удачный набор файлов, используемых для компиляции и небольшое шаманство позволяет добавить эту возможность. Процесс компиляции состоит из нескольких этапов. Если кратко, то он происходит следующим образом: Сначала транслятор «перегоняет» basic-код в ассемблер, который отдается FASM'у (компилятор ассемблера), который создает объектный файл. Далее в дело вступает линкер polink, создающий исполняемый файл. Как компилятор ассемблера, так и линкер могут создавать драйверы и если немного изменить опции компиляции, то получим не исполняемый файл, типа EXE или DLL, а драйвер режима ядра (SYS).

Скачать немного модифицированную бесплатную демо версию PureBasic 4.61 x86 можно на файлопомойке, зеркало.
Если нужно создать драйвер для x64 системы, качайте эту версию, зеркало.

Дистрибутивы имеют небольшие размеры, около 3 МБ каждый. С помощью этой версии можно создавать только драйвера.
Скачиваем, распаковываем и запускаем, кликнув по файлу «PureBasic Portable». При этом запустится IDE и вылезет окошко с сообщением что это демо-версия и списком ограничений. Из него наиболее существенным является ограничение числа строк кода, равное 800, а для создания простых драйверов этого может хватить. Остальные ограничения в нашем случае, не существенны.

Окно IDE с загруженным кодом драйвера показано на скрине.

image

Компиляция драйвера выполняется через меню «Компилятор» (это если кто не понял).
image

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

Код драйвера:

Declare DriverEntry(*DriverObject, *RegistryPath)

!public PureBasicStart
!section '.code' code readable executable align 8
!PureBasicStart:
*A=@DriverEntry()
!jmp [p_A] ; Переход в процедуру DriverEntry().

#IOCTL_MyPlus = $200

!extrn PB_PokeL
    
CompilerSelect #PB_Compiler_Processor
  CompilerCase #PB_Processor_x86
    !extrn _IoCompleteRequest@8 ; Объявление импортируемых функций ядра.
    !extrn _RtlInitUnicodeString@8
    !extrn _IoCreateDevice@28
    !extrn _IoDeleteDevice@4
    !extrn _IoCreateSymbolicLink@8
    !extrn _IoDeleteSymbolicLink@4

    !extrn _PB_PeekI@4
    
    Import "ntoskrnl.lib" 
  CompilerCase #PB_Processor_x64
    
    !extrn IoCompleteRequest; Объявление импортируемых функций ядра.
    !extrn RtlInitUnicodeString
    !extrn IoCreateDevice
    !extrn IoDeleteDevice
    !extrn IoCreateSymbolicLink
    !extrn IoDeleteSymbolicLink
    !extrn PB_PeekI
    
    ImportC "ntoskrnl.lib"
  CompilerEndSelect
  
    
; Импорт функций ядра системы.
  IoCompleteRequest(*IRP, PriorityBoost)
  RtlInitUnicodeString(*UString, *String)
  IoCreateDevice(*DriverObject, DeviceExtensionSize, *UDeviceName, DeviceType, DeviceCharacteristics, Exclusive, *DeviceObject)
  IoDeleteDevice(*DeviceObject)
  IoCreateSymbolicLink(*SymbolicLinkName, *DeviceName)
  IoDeleteSymbolicLink(*SymbolicLinkName)
EndImport

Structure MyData ; Данные, передаваемые в драйвер.
  Plus_1.l
  Plus_2.l
EndStructure


; Прцедура обмена данными с программой.
Procedure DeviceIoControl(*DeviceObject.DEVICE_OBJECT, *pIrp.IRP)
  Protected *Stack.IO_STACK_LOCATION
  Protected *InpBuff, *OutBuff
  Protected InBuffSize, OutBuffSize
  Protected ntStatus, *MyData.MyData
  
  ntStatus = #STATUS_SUCCESS ; Все ОК.
  
  *Stack = *pIrpTailOverlayCurrentStackLocation
  
  ; Размеры буферов (см. WinAPI функцию DeviceIoControl())
  InBuffSize  = *StackParametersDeviceIoControlInputBufferLength
  OutBuffSize  = *StackParametersDeviceIoControlOutputBufferLength
  
  If InBuffSize >= SizeOf(Integer) And OutBuffSize >= 4
    
    Select *StackParametersDeviceIoControlIoControlCode
      Case #IOCTL_MyPlus
        
        *Point = *pIrpSystemBuffer
        If *Point
          *MyData = PeekI(*Point)
          
          If *MyData
            Result.l = *MyDataPlus_1 + *MyDataPlus_2
            PokeL(*pIrpSystemBuffer, Result)
            *pIrpIoStatusInformation = 4
          Else
            ntStatus = #STATUS_BUFFER_TOO_SMALL
            *pIrpIoStatusInformation = 0
          EndIf
          
        EndIf
                
      Default
        ntStatus = #STATUS_UNSUCCESSFUL
        *pIrpIoStatusInformation = 0
    EndSelect
  
  Else
    ntStatus = #STATUS_BUFFER_TOO_SMALL ; Размер буфера слишком мал.
    *pIrpIoStatusInformation = 0
  EndIf
  
  *pIrpIoStatusStatus = ntStatus
  IoCompleteRequest(*pIrp, #IO_NO_INCREMENT)
  
  ProcedureReturn ntStatus
EndProcedure


; Выгрузка драйвера. Вызывается при завершении работы драйвера.
Procedure UnloadDriver(*DriverObject.DRIVER_OBJECT)
  Protected uniDOSString.UNICODE_STRING
  
  ; Инициализация объектов-строк.
  RtlInitUnicodeString(@uniDOSString, ?DosDevices)
  ; Удаление символьной связи.
  IoDeleteSymbolicLink (@uniDOSString)
  
  ; Удаление устройства.
  IoDeleteDevice(*DriverObjectDeviceObject)
EndProcedure


; Вызывается при доступе к драйверу с помощью функци CreateFile().
Procedure CreateDispatch(*DeviceObject.DEVICE_OBJECT, *pIrp.IRP)
  *pIrpIoStatusInformation = 0
  *pIrpIoStatusStatus = #STATUS_SUCCESS
  IoCompleteRequest(*pIrp, #IO_NO_INCREMENT)
  ProcedureReturn #STATUS_SUCCESS
EndProcedure


; Вызывается при осовбождении драйвера функцией CloseHandle().
Procedure CloseDispatch(*DeviceObject.DEVICE_OBJECT, *pIrp.IRP)
  *pIrpIoStatusInformation = 0
  *pIrpIoStatusStatus = #STATUS_SUCCESS
  IoCompleteRequest(*pIrp, #IO_NO_INCREMENT)
  ProcedureReturn #STATUS_SUCCESS
EndProcedure


; Процедура загрузки драйвера. Вызывается однократно при его запуске.
Procedure DriverEntry(*DriverObject.DRIVER_OBJECT, *RegistryPath.UNICODE_STRING)
  Protected deviceObject.DEVICE_OBJECT
  Protected uniNameString.UNICODE_STRING
  Protected uniDOSString.UNICODE_STRING
  
  
  ; Инициализация объектов-строк.
  RtlInitUnicodeString(@uniNameString, ?Device)
  RtlInitUnicodeString(@uniDOSString, ?DosDevices)
  
  ; Создание устройства.
  status = IoCreateDevice(*DriverObject, 0, @uniNameString, #FILE_DEVICE_UNKNOWN, 0, #False, @deviceObject)
  If status <> #STATUS_SUCCESS
    ProcedureReturn status
  EndIf
    
  ; Создане символьной связи между именем этого устройства и именем,
  ; находящимся в видимой области для user-mode, для того, чтобы
  ; приложение могло получить доступ к этому устройству.
  status = IoCreateSymbolicLink(@uniDOSString, @uniNameString)
  If status <> #STATUS_SUCCESS
    IoDeleteDevice(@deviceObject)
    ProcedureReturn status
  EndIf
  
  ; Указатель на функцию выгрузки драйвера.
  *DriverObjectDriverUnload = @UnloadDriver()
  
  *DriverObjectMajorFunction[#IRP_MJ_CREATE] = @CreateDispatch()
  *DriverObjectMajorFunction[#IRP_MJ_CLOSE]  = @CloseDispatch()
  
  ; Указываем какая функция будет обрабатывать запросы WinAPI DeviceIoControl().
  *DriverObjectMajorFunction[#IRP_MJ_DEVICE_CONTROL] = @DeviceIoControl()
	
	ProcedureReturn #STATUS_SUCCESS
EndProcedure



; Имя драйвра (юникод).
DataSection
  Device:
  !du 'DevicepbDrPlus', 0, 0
  
  DosDevices:
  !du 'DosDevicespbDrPlus', 0, 0
EndDataSection

Может показаться что это куча бессмысленного кода, но это не так.

У каждого драйвера должна быть точка входа, обычно у нее имя DriverEntry() и выполнена она в виде процедуры или функции. Как видите, в этом драйвере есть такая процедура. Если посмотрите на начало кода, то в первых строках увидите как ей передается управление. В этой процедуре происходит инициализация драйвера. Там же назначается процедура завершения работы драйвера, которая в нашем случае имеет имя UnloadDriver(). Процедуры CreateDispatch() и CloseDispatch() назначаются обработчиками соединения и отсоединения проги из юзермода.
Процедура DeviceIoControl() будет обрабатывать запросы WinAPI функции DeviceIoControl(), являющейся в данном драйвере связью с юзермодом. В конце кода расположена так называемая ДатаСекция (DataSection), в которой находятся имена драйвера, сохраненные в формате юникода (для этого использована одна из фишек ассемблера FASM).

Теперь рассмотрим как драйвер будет взаимодействовать с внешним миром. Это происходит в процедуре DeviceIoControl(). В ней отслеживается одно сообщение, а именно — #IOCTL_MyPlus, которое отправляет юзермодная прога, когда ей нужно сложить два числа в режиме ядра (круто звучит, правда?). Когда такое сообщение получено, то считываем из системного буфера, адрес указателя на структуру со слагаемыми, производим сложение и результат помещаем в системный буфер. Собственно это основная задача нашего первого драйвера.

Видите сколько понадобилось кода для выполнения простейшей математической операции — сложения двух чисел?

А теперь рассмотрим программу, работающую с этим драйвером. Она написана на том же PureBasic.


#DriverName = "pbDrPlus"
#IOCTL_MyPlus = $200

XIncludeFile "..DrUserModeFramework.pbi"

Structure MyData ; Данные, передаваемые в драйвер.
  Plus_1.l
  Plus_2.l
EndStructure

; Абсолютный путь к файлу-драйверу.
DrFile.s = GetPathPart(ProgramFilename())+#DriverName+".sys"

; Загружает драйвер и если успешно, то порлучаем его хэндл.
hDrv=OpenDriver(DrFile, #DriverName, #DriverName, #DriverName)

If hDrv=0
  ; Деинсталляция драйвера из системы.
  Driver_UnInstall(#DriverName)
  MessageRequester("", "Ошибка загрузки драйвера")
  End
EndIf

; Обмен данными с драйвером.
Procedure.q Plus(hDrv, x1, x2)
  Protected MyData.MyData, Result, *Point
  
  MyDataPlus_1=x1
  MyDataPlus_2=x2
  *Point = @MyData
  DeviceIoControl_(hDrv, #IOCTL_MyPlus, @*Point, SizeOf(MyData), @Result, 4, @BytesReturned, 0)
  
  ProcedureReturn Result
EndProcedure


OpenWindow(1,300,300,140,90,"Title",#PB_Window_SystemMenu|#PB_Window_ScreenCentered)
StringGadget(1,10,10,50,20,"")
StringGadget(2,10,40,50,20,"")
TextGadget(3,70,30,70,20,"")
Repeat
  ev=WaitWindowEvent()
  If ev=#PB_Event_Gadget
    op1=Val(GetGadgetText(1))
    op2=Val(GetGadgetText(2))
    Result = Plus(hDrv, op1, op2)
    SetGadgetText(3,Str(Result))
  EndIf
Until ev=#PB_Event_CloseWindow


; Если драйвер загружен, то закрываем связь с ним.
If hDrv
  CloseHandle_(hDrv)
  hDrv=0
EndIf

; Деинсталляция драйвера из системы.
Driver_UnInstall(#DriverName)

При старте программы вызывается функция OpenDriver(), которая загружает драйвер. Для упрощения, имя драйвера, имя службы и описание службы заданы одинаковыми — «pbDrPlus». Если загрузка неудачная, то выводится соответствующее сообщение и программа завершает свою работу.

Процедура Plus() осуществляет связь с драйвером. Ей передаются хэндл, доступа к драйверу и слагаемые числа, которые помещаются в структуру и указатель на указатель которой, передается драйверу. Результат сложения чисел будет в переменной «Result».

Далее следует код простейшего GUI калькулятора, скопированного из википедии.

Когда закроют окно, то перед завершением работы программы, закрывается связь с драйвером и производится его деинсталляция из системы.

Исходные коды драйвера и программы, можно найти в папке «Examples», PureBasic на файлопомойке, ссылку на который давал в начале статьи.

PS.
Помните, работа в ядре чревата мелкими неожиданностями аля, BSOD (синий экран смерти), поэтому экспериментируйте осторожно и обязательно всё сохраняйте перед запуском драйвера.

За возможную потерю данных, я ответственности не несу!

Автор: hachik

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js