Многие считают что самому создать драйвер для Windows это что-то на грани фантастики. Но на самом деле это не так. Конечно, разработка драйвера для какого-то навороченного девайса бывает не простой задачей. Но ведь тоже самое можно сказать про создание сложных программ или игр. В разработке простого драйвера нет ничего сложного и я попытаюсь на примерах это показать.
Сперва нам нужно определится в чем мы же будем создавать наш первый драйвер. Поскольку материал ориентирован на новичков, то язык программирования был выбран один из простых, и это не Си или ассемблер, а бейсик. Будем использовать один из диалектов бейсика — PureBasic. Из коробки он не обучен создавать драйверы, но у него удачный набор файлов, используемых для компиляции и небольшое шаманство позволяет добавить эту возможность. Процесс компиляции состоит из нескольких этапов. Если кратко, то он происходит следующим образом: Сначала транслятор «перегоняет» basic-код в ассемблер, который отдается FASM'у (компилятор ассемблера), который создает объектный файл. Далее в дело вступает линкер polink, создающий исполняемый файл. Как компилятор ассемблера, так и линкер могут создавать драйверы и если немного изменить опции компиляции, то получим не исполняемый файл, типа EXE или DLL, а драйвер режима ядра (SYS).
Скачать немного модифицированную бесплатную демо версию PureBasic 4.61 x86 можно на файлопомойке, зеркало.
Если нужно создать драйвер для x64 системы, качайте эту версию, зеркало.
Дистрибутивы имеют небольшие размеры, около 3 МБ каждый. С помощью этой версии можно создавать только драйвера.
Скачиваем, распаковываем и запускаем, кликнув по файлу «PureBasic Portable». При этом запустится IDE и вылезет окошко с сообщением что это демо-версия и списком ограничений. Из него наиболее существенным является ограничение числа строк кода, равное 800, а для создания простых драйверов этого может хватить. Остальные ограничения в нашем случае, не существенны.
Окно IDE с загруженным кодом драйвера показано на скрине.
Компиляция драйвера выполняется через меню «Компилятор» (это если кто не понял).
Теперь определимся что будет делать наш первый драйвер. Обычно при изучении программирования начинают с простых вещей, скажем, выполнения математических операций и вывода результата. Вот пусть наш драйвер делает тоже самое, ведь банальная математика производимая в режиме ядра это очень круто!
Код драйвера:
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