Обычно драйверы для Windows пишут на языке C++, возможно потому что DDK фирма Майкрософт предоставила именно для этого языка. Но энтузиасты портировали DKK для других языков. Сейчас мы поговорим о создании драйверов на языке бейсик, который на первый взгляд не подходит для этого (хотя бы потому что бейсик считается простым языком, не предназначенным для системного программирования), но это только на первый взгляд. Теперь давайте определится с диалектом бейсика и компилятором, который будем использовать при разработке драйверов. Наверное стоит выбрать бейсик от фирмы Майкрософт, ведь по логике, он должен лучше всего подходить для разработки под Windows. Претендентами были выбраны Small Basic, VB и VB.NET.
Small Basic и VB.NET были исключены почти сразу. Первый из-за ограниченных возможностей и наличия внешнего рантайма, а второй из-за компиляции только в управляемый код, что нам не подходит, ведь на уровне ядра нет .NET Framework.
VB для создания драйверов в принципе подходит и в соседнем блоге это доказали, но методы создания драйвера, а так же внешний рантайм, ограничивают возможности и снижают удобство разработки.
Поэтому был продолжен поиск требуемого компилятора бейсика, но уже от сторонних фирм, не имеющих прямого отношения к Майкрософт. Подходящим компилятором оказался PureBasic, фирмы Fantaisie Software. Правда он не бесплатный. На данный момент, стоимость индивидуальной лицензии на все версии и платформы составляет 79€.
PureBasic подошел в первую очередь потому, что в его дистрибутиве есть почти все что нужно для компиляции драйвера и необходимо лишь немного изменить ключи ликовки чтобы получить не приложение, а драйвер. Но обо всем по порядку. Прежде рассмотрим процесс компиляции в PureBasic. Он проходит в несколько этапов. Сначала бейсик-код транслируется в ассемблер, а затем при помощи компилятора ассемблера FASM создается объектный файл, который линкуется программой Polink, с функциями PureBasic, находящимися в статических библиотеках и в результате создается исполняемый файл. Чтобы получился драйвер, а не приложение, достаточно изменить ключ линковки с на Code
Скопировано | 1
| /SUBSYSTEM:native /driver |
|
При этом будет создан драйвер, но он окажется не рабочим, т. к. не решена еще одна проблема - несмотря на то что получился драйвер, он использует WinAPI функции вместо функций ядра. Точнее, это вшитый в него рантайм вызывает некоторые WinAPI функции при инициализации и завершении работы. Есть как минимум два варианта решения этой проблемы.- Исключить рантайм из кода.
- Заставить рантайм использовать функции ядра вместо WinAPI.
Первый вариант реализовать не сложно. В процессе компиляции получаем ассемблерный код и несложный препроцессор, находящийся между транслятором бейсик кода (pbcompiler.exe) и ассемблером FASM справится с этой задачей. Но у такого решения есть большой минус - многие возможности станут недоступными. Например, станут недоступными строковые переменные, динамические и ассоциативные массивы, связные списки, перестанут работать многие функции и т. д. Это не лучшее решение, ведь теряется вся простота бейсика.
Второй вариант выглядит трудноосуществимым, ведь функции PureBasic скомпилированы и хранятся в статических библиотеках. Их декомпиляция, модификация и сборка, займут много времени и велика вероятность допустить ошибку, что сделать довольно просто учитывая что придется модифицировать ассемблерный код многих функций.
Так как же быть, ведь без рантайма отсутствуют многие возможности языка, но и использовать его в драйвере не представляется возможным из-за использования WinAPI функции вместо функций ядра?
На самом деле есть довольно простой способ без модификации кода рантайма и функций PureBasic, "отучить" их от WinAPI, причем не просто "отучить", но заставить использовать функции ядра. Звучит невероятно, правда? Но это возможно. Напомню, что рантайм и все функции PureBasic скомпилированы и находятся в статических библиотеках. Причем импорт WinAPI функций осуществляется посредством импорта символов из других статических библиотек и явно не указано из каких. Ничего не мешает подменить статические библиотеку из которой импортируются символы WinAPI функций (это например user32.lib, kernel32.lib и др.), на нашу из которой экспортируются символы с именами как в WinAPI функций. Таким образом чтобы отвязать рантайм и функции от WinAPI, требуется написать n-ое количество функций-переходников, которые являются в той или иной мере, аналогами WinAPI, но из них будут вызываться функции ядра. Конечно не для всех WinAPI функций есть аналоги в ядре, но во многих случаях это решаемо.
Приведу пример аналога WinAPI функции Sleep(), которая вызывает функцию ядра KeDelayExecutionThread(). PureBasic
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
| ProcedureDLL RndLib_Sleep(TimeMS)
Protected Time.q
Time=TimeMS*10000 ; Перевод времени из миллисекунд и сотни наносекунд.
Time=Time-(Time*2) ; Для задержки относительно текущего времени, число должно быть отрицательным.
ProcedureReturn KeDelayExecutionThread(#KernelMode, #False, @Time)
!public _Procedure0 as '_Sleep@4'
!public __imp__Sleep
!__imp__Sleep:
!dd _Procedure0
EndProcedure |
|
Как наверное поняли, код написан на PureBasic, но есть проблема, PureBasic не поддерживает создание статических библиотек, что странно, учитывая что в его дистрибутиве есть все необходимое для этого. Поэтому сборка статической библиотеки производилась в два этапа. Сначала утилитой coffIT из этого кода создавался в объектный файл (утилита при этом использовала pbcompiler и fasm), а затем, собиралась статическая библиотека при помощи утилиты polib, находящейся в папке Compilers дистрибутива PureBasic.
В этом коде, все что находится после символа "!" не обрабатывается компилятором PureBasic, а передается fasm в неизменном виде. Оператор public создает общедоступные метки, которые не будут удалены при компиляции и останутся в статической библиотеке. Именно это нам и нужно. Линкер вместо WinAPI функции Sleep() использует нашу, а поскольку есть вызов функции KeDelayExecutionThread, то линкер попытается ее найти в статических библиотеках и обнаружив в "ntoskrnl.lib", добавит функцию в импорт драйвера. WinAPI функция Sleep() туда не попадет, поскольку она заменена на нашу. Аналогичным образом подменяются и другие функции. Это позволило не только успешно инициализировать рантайм, но и заставить работать многие функции PureBasic, среди которых оказались и те, которые в драйвере вряд ли понадобятся. Например функции получения хеша (CRC32, MD5, SHA1 и т. д.) данных в памяти, а так же функции работы с регулярными выражениями.
В результате всего этого, был создан пакет файлов, который находится во вложении.
Установка.- Установить PureBasic 5.11 x86 в любую папку.
- Открыть папку с установленным PureBasic и в папке "Compilers" переименовать файлы Fasm.exe и Polink.exe в Fasm_.exe и Polink_.exe.
- Открыть папку "PureLibraries\Windows\Libraries\" и удалить все файлы.
- Извлечь архив в папку с установленным PureBasic 5.11 x86.
- Запустить программу "IDE PB5.11 x86 Patch.exe", которая изменит расширение сохраняемых исполняемых файлов с .exe на .sys.
Подчеркну что нужна версия PureBasic именно 5.11 x86. С другими версиями этот пакет может быть не совместим.
Отдельно нужно сказать про память ядра. Она существует двух типов - подкачиваемая и неподкачиваемая.
Цитата с wasm.ru
Системные кучи (к пользовательским кучам не имеют никакого отношения) представлены двумя так называемыми пулами памяти, которые, естественно, располагаются в системном адресном пространстве:
Пул неподкачиваемой памяти (Nonpaged Pool). Назван так потому, что его страницы никогда не сбрасываются в файл подкачки, а значит, никогда и не подкачиваются назад. Т. е. этот пул всегда присутствует в физической памяти и доступен при любом IRQL. Одна из причин его существования в том, что обращение к такой памяти не может вызвать ошибку страницы (Page Fault). Такие ошибки приводят к краху системы, если происходят при IRQL >= DISPATCH_LEVEL.
Пул подкачиваемой памяти (Paged Pool). Назван так потому, что его страницы могут быть сброшены в файл подкачки, а значит должны быть подкачаны назад при последующем к ним обращении. Эту память можно использовать только при IRQL строго меньше DISPATCH_LEVEL.
Оба пула находятся в системном адресном пространстве, а значит, доступны из контекста любого процесса. Для выделения памяти в системных пулах существует набор функций ExAllocatePoolXxx, а для возвращения выделенной памяти всего одна - ExFreePool.
По умолчанию используется только неподкачиваемая память что необходимо для инициализации рантайма, которая хоть и происходит на уровне PASSIVE_LEVEL, но ведь в дальнейшем может произойти обращение к рантайму при более высоком IRQL и если он окажется равен или выше DISPATCH_LEVEL и "очень повезет" что требуемая область памяти окажется в файле подкачки - получим BSoD "на ровном месте". Но использовать для всего только неподкачиваемую память, тоже нерационально. Для возможности выбора типа памяти (подкачиваемая или нет), был добавлен макрос.Возможные значения.PureBasic
Скопировано | 1
2
3
4
5
| Enumeration
#Pool_NonPaged ; Использовать только неподкачиваемую память. Это по умолчанию.
#Pool_Paged ; Использовать только подкачиваемую память.
#Pool_Auto ; Автоматический выбор типа памяти в зависимости от IRQL.
EndEnumeration |
|
Узнать текущий тип памяти можно с помощью макроса.Смена типа памяти действует только на последующие выделения памяти, а тип уже выделенной памяти не меняется. И кроме того, это действует только на функции PureBasic. На вызов функции ядра ExAllocatePool() и ей подобных, не распространяется. Так же нужно следить за освобождением памяти. Даже если драйвер был выгружен из памяти, это не освободит ресурсы и память будет занята до перезагрузки системы. Если память неподкачиваемая, это равносильно уменьшению размера физической памяти!
В конце процедуры DriverUnload() нужно добавить строку, которая освободит память используемую рантаймом PureBasic.Теперь рассмотрим простой пример драйвера, который только информирует о своей загрузке и выгрузке.PureBasic
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| Declare DriverEntry(*DriverObject, *RegistryPath)
*Point=@DriverEntry()
!jmp [p_Point]
IncludePath #PB_Compiler_Home+"DDK\"
XIncludeFile "ntddk.pbi"
XIncludeFile "ntstatus.pbi"
XIncludeFile "ntfunct.pbi"
Procedure DriverUnload(*DriverObject.DRIVER_OBJECT)
DbgPrint("Unload Driver")
!CALL _PB_EOP ; Освобождение ресурсов.
EndProcedure
Procedure DriverEntry(*DriverObject.DRIVER_OBJECT, *RegistryPath.UNICODE_STRING)
DbgPrint("Load Driver")
*DriverObject\DriverUnload = @DriverUnload()
ProcedureReturn #STATUS_SUCCESS
EndProcedure |
|
Рассмотрим код подробнее. Оператор Declare объявляет процедуру с именем DriverEntry(). Это необходимо потому что компилятор однопроходный, а процедура расположена ниже по коду относительно обращения к ней. Затем в переменную *Point помещается указатель на процедуру DriverEntry() и происходит переход по адресу в переменной *Point, т. е. в процедуру DriverEntry(). Возможно возникнет вопрос, почему не указать в качестве точки входа процедуру DriverEntry() и избавится от подобного прыжка по адресу? Сделать такое конечно можно, но при этом рантайм останется не инициализированным.
Оператор IncludeFile определяет путь к подключаемым файлам, а оператор XIncludeFile подключает их, причем делает это только один раз, т. е. если в исходнике окажется несколько подключений одного и того же файла, то будет подключен только один экземпляр, а остальные проигнорированы.
При выполнении кода процедуры DriverEntry() выводится отладочное сообщение "Load Driver", записывается в структуру *DriverObject адрес процедуры выгрузки драйвера и возвращается значение успешного выполнения.
Когда драйвер выгружается, будет выполнена процедура адрес которой сохранен в поле DriverUnload структуры *DriverObject. В данном коде это процедура DriverUnload(). В ней отсылается отладочное сообщение "Unload Driver", а затем, вызывается подпрограмма "_PB_EOP". Это необходимо чтобы рантайм "убрал за собой", т. е. освободил все те ресурсы что были использованы им при инициализации.
В этой же процедуре нужно освободить все ресурсы, используемые драйвером (например, память, различные хендлы и т. д.). Потому что даже если драйвер был выгружен, это не освободит ресурсы и они будут заняты до перезагрузки системы. В случае памяти, если она неподкачиваемая, это равносильно уменьшению размера физической памяти!
Все глобальные объекты (строки, массивы, связанные списки) так же нужно уничтожать при выгрузке драйвера. Наиболее просто это сделать если их поместить в общую структуру. Тогда все сведется к одной строке - очистке структуры функцией ClearStructure().
Компиляция драйвера не сложнее создания обычного приложения. В меню "Компилятор", нужно кликнуть по пункту "Создать драйвер" и получим скомпилированный драйвер в указанной папке.
После компиляции в результате которой, получили sys-файл, его можно проверить в действии, но сначала взглянем на импорт. Как видим, от WinAPI не осталось и следа.
Драйвером импортируется функция KeGetCurrentIrql() из hal.dll, используемая для определения текущего IRQL при автоматическом выборе типа памяти (подкачиваемая или нет). Из ntoskrnl.exe импортируются функции DbgPrint(), memset(), ExAllocatePool() и ExFreePool(). Первую можно видеть в коде драйвера и она используется для вывода отладочных сообщений. Вторая функция предназначена для заполнения памяти заданными данными. В нашем случае, она используется для обнуления (очистки) памяти. Третья и четвертая функция, выделяют и освобождают память. Этих функций достаточно для инициализации рантайма, причем действительно нужны только две ExAllocatePool() и ExFreePool(), а остальные можно или исключить при определенных условиях (функция KeGetCurrentIrql()) или заменить на небольшой участок кода (функция memset()). Довольно неплохо если учитывать что рантайм PureBasic изначально рассчитан на WinAPI, а не на функции ядра.
Размер файла драйвера получился равным 2 КБ. Возможно покажется что это много для такого простого драйвера, но в этот объем входит статически прилинкованный рантайм и пара функций-переходников эмулирующих WinAPI и вызывающих функции ядра. Если отказаться это всего этого и вырезать рантайм, то размер драйвера получится около 570 байт. Но нужно ли это? Отсутствие рантайма не позволит использовать многие возможности языка.
Теперь давайте запустим драйвер и посмотрим как он работает. Лучше это делать на виртуальной машине. Для этого понадобятся утилиты KmdManager и Dbgview. Первая запускает драйвер, а вторая отображает то, что было отправлено функцией DbgPrint(). После установки на PureBasic предлагаемого архива, эти утилиты будут доступны через меню "Инструменты". Их следует запускать с правами администратора. Для Windows 7 нужно выполнить эту рекомендацию.

Сообщение от Убежденный
Далее по поводу DbgPrint(Ex). Как известно, на Windows Vista и выше весь
отладочный вывод фильтруется, поэтому чтобы увидеть свои отладочные сообщения,
нужно включить соответствующий флаг в реестре на гостевой машине.
Ключ HKLM\SYSTEM\CurrentControlSet\Control\Se ssion Manager\Debug Print Filter.
Я использую параметр DEFAULT (тип REG_DWORD) со значением 0xf (15), это включает полный отладочный вывод. Хотя возможны и другие варианты.
Также нужно убедится что в меню "Capture" программы Dbgview есть галочка в пункте "Capture Kernel". Если ее там нет, ставим и перезапускаем программу.
В программе KmdManager указываем путь к драйверу и последовательно нажимаем на кнопки "Register", "Run", "Stop" и "Unregister". Если все сделано правильно, увидим примерно такую картину.
Это довольно простой драйвер, но его код можно еще сильнее упростить до такого.PureBasic
Скопировано | 1
2
3
4
5
| ImportC "ntoskrnl.lib" ; Импорт cdecl-функций.
DbgPrint(String.s)
EndImport
DbgPrint("Load Driver") |
|
Код максимально прост. Импортируется функция DbgPrint() из "ntoskrnl.lib", а затем она вызывается отправляя отладочное сообщение "Load Driver".
Возможно те "кто в теме" подумают что этот код нормально работать не будет и вызовет BSoD и они будут по своему правы. Дело вот в чем. При выходе из функции DriverEntry() производится освобождение 8 байт из стека (2 локальных переменных - параметра процедуры, по 4 байта каждый), а затем возврат по текущему адресу в стеке. В этом коде ничего подобного нет. Как же он тогда работает? На самом деле все это есть. Препроцессор, находящийся между pbcompiler и fasm добавляет такой ассемблерный код.Assembler
Скопировано | 1
2
3
| CALL _PB_EOP
MOV eax,-1073741438
RET 8 |
|
Чтобы было понятней, привожу весь ассемблерный код драйвера, передаваемый FASMу.Assembler
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
| format MS COFF
extrn _DbgPrint
extrn _ExitProcess@4
extrn _GetModuleHandleA@4
extrn _HeapCreate@12
extrn _HeapDestroy@4
extrn _memset
extrn PB_StringBase
extrn _SYS_InitString@0
extrn _SYS_FreeStrings@0
extrn _PB_StringBasePosition
public _PB_Instance
public _PB_ExecutableType
public _PB_OpenGLSubsystem
public _PB_MemoryBase
public PB_Instance
public PB_MemoryBase
public _PB_EndFunctions
macro pb_public symbol
{
public _#symbol
public symbol
_#symbol:
symbol:
}
macro pb_align value { rb (value-1) - ($-_PB_DataSection + value-1) mod value }
macro pb_bssalign value { rb (value-1) - ($-_PB_BSSSection + value-1) mod value }
public PureBasicStart
section '.code' code readable executable
PureBasicStart:
PUSH dword I_BSSEnd-I_BSSStart
PUSH dword 0
PUSH dword I_BSSStart
CALL _memset
ADD esp,12
PUSH dword 0
CALL _GetModuleHandleA@4
MOV [_PB_Instance],eax
PUSH dword 0
PUSH dword 4096
PUSH dword 0
CALL _HeapCreate@12
MOV [PB_MemoryBase],eax
CALL _SYS_InitString@0
;
PUSH dword _S1
CALL _DbgPrint
ADD esp,4
CALL _PB_EOP
MOV eax,-1073741438
RET 8
_PB_EOP_NoValue:
; PUSH dword 0
_PB_EOP:
CALL _PB_EndFunctions
CALL _SYS_FreeStrings@0
PUSH dword [PB_MemoryBase]
CALL _HeapDestroy@4
RET
; CALL _ExitProcess@4
_PB_EndFunctions:
RET
section '.data' data readable writeable
_PB_DataSection:
_PB_OpenGLSubsystem: db 0
pb_public PB_DEBUGGER_LineNumber
dd -1
pb_public PB_DEBUGGER_IncludedFiles
dd 0
pb_public PB_DEBUGGER_FileName
db 0
pb_public PB_Compiler_Unicode
dd 0
pb_public PB_Compiler_Thread
dd 0
pb_public PB_Compiler_Purifier
dd 0
_PB_ExecutableType: dd 0
public _SYS_StaticStringStart
_SYS_StaticStringStart:
_S1: db "Load Driver",0
pb_public PB_NullString
db 0
public _SYS_StaticStringEnd
_SYS_StaticStringEnd:
align 4
align 4
s_s:
dd 0
dd -1
align 4
section '.bss' readable writeable
_PB_BSSSection:
align 4
I_BSSStart:
_PB_MemoryBase:
PB_MemoryBase: rd 1
_PB_Instance:
PB_Instance: rd 1
align 4
PB_DataPointer rd 1
align 4
align 4
align 4
align 4
I_BSSEnd:
section '.data' data readable writeable
SYS_EndDataSection: |
|
Теперь рассмотрим драйвер по сложнее, в котором используются функции PureBasic, которые не работали бы без инициализации рантайма и подмены некоторых WinAPI функций на их аналоги в ядре, т. к. было бы невозможно использование двусвязного списка и строк.PureBasic
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| Declare DriverEntry(*DriverObject, *RegistryPath)
*Point=@DriverEntry()
!jmp [p_Point]
IncludePath #PB_Compiler_Home+"DDK\"
XIncludeFile "ntddk.pbi"
XIncludeFile "ntstatus.pbi"
XIncludeFile "ntfunct.pbi"
Procedure DriverUnload(*DriverObject.DRIVER_OBJECT)
!CALL _PB_EOP ; Освобождение ресурсов.
EndProcedure
Procedure DriverEntry(*DriverObject.DRIVER_OBJECT, *RegistryPath.UNICODE_STRING)
SetPoolMode(#Pool_Auto) ; Автоматический выбор типа памяти в зависимости от IRQL.
NewList x()
For i=1 To 10
If AddElement(x())
x()=i*10
EndIf
Next i
DbgPrint("Size list "+ListSize(x())+" items")
ForEach x()
DbgPrint(Str(x()))
Next
*DriverObject\DriverUnload = @DriverUnload()
ProcedureReturn #STATUS_SUCCESS
EndProcedure |
|
В процедуре DriverEntry() в первую очередь включатся автоматический выбор типа памяти (по умолчанию используется только неподкачиваемая память). Затем создается связный список с именем x, после чего в цикле в него добавляется 10 элементов. Далее формируется строка сообщающая о количестве элементов в списке и в цикле ForEach выводится текущее содержимое элементов списка.
В этом примере связный список является локальным и он автоматически уничтожается при завершении работы процедуры в которой он создан. Если бы он был бы глобальным, то необходимо было самим его очищать, т. е. вызывать функцию FreeList() когда список становился не нужным. Но в любом случае, чтобы избежать утечки памяти, это потребовалось бы сделать в процедуре DriverUnload() вызываемой при выгрузке драйвера.
Если запустить драйвер, увидим такую картину.
В папке Examples архива, есть много примеров различных драйверов, как простых, только показывающих работу функций PureBasic, переведенных с WinAPI на функции ядра, так и примеров по сложнее, среди которых можно найти коды драйверов, предоставляющих доступ к портам компьютера, защищающих процесс от завершения, скрывающих указанные процессы в диспетчере задач, а так же драйвер обрабатывающий прерывания от LPT порта.
|