Что такое низкоуровневые драйверы
Вот и пришло время второй статьи из цикла о написании драйверов под Windows. Сейчас я тебя немного расстрою: в предыдущей статье я обещала, что в этой мы приступим собственно к практике. Но данная статья это, скорее, "полупрактика". Глупо "с места в карьер" бросаться разрабатывать драйвера режима ядра
(так как, если ты не забыл, в данном цикле мы разбираем именно этот тип драйверов), хотя бы поверхностно не изучив особенности и приёмы программирования в режиме ядра, что мы и сделаем в первой части этой статьи. Ну а во второй мы наконец - то разберём структуру настоящего драйвера под Windows
(Legacy и немного WDM) : его основные функции, их взаимодействие, а также параметры, принимаемые ими. Так что к третьей статье этого цикла ты уже, надо думать, будешь основательно подготовлен к написанию своего первого драйвера. Начинаем.
Особенности и приёмы программирования в режиме ядра
Программирование в режиме ядра имеет массу особенностей, для прикладников очень непривычных, а для новичков довольно сложных. Во-первых, у режима ядра своё, отличное от такового в пользовательском режиме, API. Кроме того, для кода, выполняющегося в режиме ядра, имеет очень большое значение его уровень IRQL, так как приложениям, выполняющимся на высоких уровнях IRQL, недоступны многие функции, к которым имеют доступ приложения низких IRQL уровней, и наоборот. Всё это необходимо учитывать.
Во-вторых, в режиме ядра есть свои дополнительные описатели типов. Полный их список можно найти в заголовочном файле ntdef.h. Его содержание примерно таково:
typedef unsigned char USHAR
typedef unsigned short USHORT
typedef unsigned long ULONG
.
Зачем это нужно? Ну, во-первых, для красоты. тьфу, для унификации стиля классических C - типов данных и нововведённых - таких, как WCHAR
(двухбайтный Unicode символ), LARGE_INTEGER (который, на самом деле, является объединением) и т.д. А также для унификации исходников для 32 - разрядных платформ и надвигающихся 64 - разрядных.
В исходниках драйверов часто встречаются макроопределения
IN, OUT, OPTIONAL. Что они означают? А ровным счётом ничего, и введены они только для повышения удобочитаемости исходника. OPTIONAL обозначает необязательные параметры, IN - параметры, передаваемые внутрь функции, например, OUT - соответственно, наоборот. А вот IN OUT означает, что параметр передаётся внутрь функции, а затем возвращается из неё обратно.
Функции драйвера, за исключением DriverEntry (главная процедура драйвера, подробнее см. во второй части статьи), могут называться как угодно, тем не менее, существуют определённые "правила хорошего тона" при разработке драйверов, в том числе и для именования процедур: например, все функции, относящиеся к HAL, желательно предварять префиксом HAL и т.д. Сама Microsoft практически постоянно следует этому правилу. А имена типов данных и макроопределения в листингах DDK написаны сплошь заглавными буквами. Советую тебе поступать также при разработке своих драйверов. Это и в самом деле во много раз повышает удобство работы с листингом.
А теперь, чтобы тебе стали более или менее понятны основные различия между программированием в пользовательском и системном режимах, расскажу об основных функциях для работы с памятью и реестром в режиме ядра.
Начнём с функций для работы с памятью, а для начала поговорим собственно об устройстве и работе с памятью в Windows. Единое 4-х гигабайтное адресное пространство памяти Windows
(я имею в виду 32-х разрядные версии Windows) делится на две части: 2 гигабайта для пользовательского пространства и 2 гигабайта для системного. 2 гигабайта системного пространства доступны для всех потоков режима ядра. Системное адресное пространство делится на следующие части:
Видов адресов в режиме ядра три: физические
(реально указывающие на область физической памяти), виртуальные
(которые перед использованием транслируются в физические), и логические
(используемые HAL уровнем при общении с устройствами; он же и отвечает за работу с такими адресами). Функции режима ядра, отвечающие за выделение и освобождение виртуальной памяти, отличаются от таковых в пользовательском режиме. Также, находясь на уровне режима ядра, становится возможным использовать функции выделения и освобождения физически непрерывной памяти. Разберём все эти функции поподробнее.
1) PVOID ExAllocatePool (уровень IRQL, на котором может выполняться эта функция - < DISPATCH_LEVEL) - выделяет область памяти. Принимает два параметра: параметр
(POOL_TYPE) , в котором содержится значение, означающее, какого типа область памяти нужно выделить: PagedPool - страничная, NonPagedPool - нестраничная
(в этом случае функцию можно вызвать с любого IRQL уровня). Второй параметр
(ULONG) - размер запрашиваемой области памяти. Функция возвращает указатель на выделенную область памяти, и NULL, если выделить память не удалось.
2) VOID ExFreePool (IRQL<DISPATCH_LEVEL) - освобождает область памяти. Принимает параметр
(PVOID) - указатель на освобождаемую область памяти. Если высвобождается нестраничная память, то данная функция может быть вызвана с
DISPATCH_LEVEL. Возвращаемое значение - void.
3) PVOID MmAllocateContiguousMemory (IRQL==PASSIVE_LEVEL) - выделяет физически непрерывную область памяти. Принимает два параметра. Первый параметр
(ULONG) - размер запрашиваемой области памяти, второй - параметр
(PHYSICAL_ADDRESS), означающий верхний предел адресов для запрашиваемой области. Возвращаемое значение: виртуальный адрес выделенной области памяти или NULL
(при неудаче).
4) VOID MmFreeContiguousMemory (IRQL==PASSIVE_LEVEL) - освобождает физически непрерывную область памяти. Принимает единственный параметр
(PVOID) - указатель на область памяти, выделенную ранее с использованием функции MmAllocateContiguousMemory. Возвращаемое значение -
void.
5) BOOLEAN MmIsAddressValid (IRQL<=DISPATCH_LEVEL) - делает проверку виртуального адреса. Принимает параметр
(PVOID) - виртуальный адрес, нуждающийся в проверке. Функция возвращает TRUE, если адрес "валидный"
(т.е. присутствует в виртуальной памяти), и FALSE - в противном случае.
6) PHYSICAL_ADDRESS MmGetPhysicalAddress (IRQL - любой) - определяет физический адрес по виртуальному. Принимает параметр
(PVOID), содержащий анализируемый виртуальный адрес. Возвращаемое значение - полученный физический адрес.
Основные функции для работы с памятью рассмотрели, перейдём к таковым для работы с реестром. Сначала поговорим о функциях доступа к реестру, предоставляемых диспетчером ввода - вывода, потом о драйверных функциях прямого доступа к реестру, а затем о самом богатом по возможностям и удобству семействе функций для работы с реестром -
Zw
Драйверные функции, предоставляемые диспетчером ввода - вывода.
2) IoGetDeviceProperty - данная функция запрашивает из реестра установочную информацию об устройстве.
3) IoOpenDeviceRegistryKey - возвращает дескриптор доступа к подразделу реестра для драйвера или устройства по указателю на его объект.
Драйверные функции для прямого доступа к реестру.
1) RtlCheckRegistryKey - проверяет, существует ли указанный
подраздел внутри подраздела, переданного первым параметром. Что и каким
образом передавать в первом параметре - в рамках статьи всё не перечислить, отсылаю к ntddk.h и wdm.h. Если существует - возвращается
STATUS_SUCCESS.
2) RtlCreateRegistryKey - создаёт подраздел внутри раздела реестра, указанного вторым параметром. Далее - всё то же самое, что и у
RtlCheckRegistryKey.
3) RtlWriteRegistryValue - записывает значение параметра реестра. Первый параметр - куда пишем, второй - в какой подраздел
(если его нет, то он будет создан), а третий - какой параметр создаём.
4) RtlDeleteRegistryValue - удаляет параметр из подраздела. Параметры те же самые, что и у RtlWriteRegistryValue
(только с необходимыми поправками, конечно).
5) RtlQueryRegistryValues - данная функция позволяет за один вызов получить значения сразу нескольких параметров указанного подраздела.
И напоследок функции для работы с реестром семейства
Zw
1) ZwCreateKey - открывает доступ к подразделу реестра. Если такового нет - создаёт новый. Возвращает дескриптор открытого объекта.
2) ZwOpenKey - открывает доступ к существующему подразделу реестра.
3) ZwQueryKey - возвращает информацию о подразделе.
4) ZwEnumerateKey - возвращает информацию о вложенных подразделах уже открытого ранее подраздела.
5) ZwEnumerateValueKey - возвращает информацию о параметрах и их значениях открытого ранее подраздела.
6) ZwQueryValueKey - возвращает информацию о значении параметра в открытом ранее разделе реестра. Полнота возвращаемой информации определяется третьим параметром, передаваемым функции, который может принимать следующие значения
(дополнительные разъяснения не требуются, так как они имеют "говорящие" имена):
KeyValueBasicInformation, KeyValuePartialInformation и KeyValueFullInformation.
7) ZwSetValueKey - создаёт или изменяет значение параметра в открытом ранее подразделе реестра.
8) ZwFlushKey - принудительно сохраняет на диск изменения, сделанные в открытых функциями ZwCreateKey и ZwSetValueKey подразделах.
9) ZwDeleteKey - удаляет открытый подраздел из реестра.
10) ZwClose - закрывает дескриптор открытого ранее подраздела реестра, предварительно сохранив сделанные изменения на диске.
Практически все вышеперечисленные функции для работы с реестром должны вызываться с уровня IRQL
PASSIVE_LEVEL.
Думаю, пока достаточно. Конечно, у всех вышеперечисленных функций есть масса нюансов в применении. Да и вообще функций режима ядра - великое множество, их ничуть не меньше, чем в пользовательском режиме. Но моя задача была не рассказать обо всех API - функциях режима ядра
(что даже в рамках цикла невозможно сделать), а продемонстрировать отличия функций режима ядра, от таковых в пользовательском режиме, и хоть немного рассказать о нюансах их применения
(взять, к примеру, то, что в пользовательском режиме не имеет значения, в потоке какого приоритета будет выполняться приложение: оно будет иметь такой же полный доступ ко всем API функциям пользовательского режима, как и любые другие приложения; на уровне ядра, как ты только что, убедился, это не так). Ну а за более или менее полным списком и описанием всех этих API - функций советую обратиться к библии Гарри Нэббета.
Ну вот и всё, теперь ты готов к разговору о структуре драйвера, который мы сейчас и начнём.
Структура драйвера
Я уже говорила, что драйвер фактически можно представить как довольно-таки обычную dll-ку уровня ядра. Таким образом, далее можно представить драйвер просто как набор процедур, периодически вызываемых внешними программами. Несмотря на то, что процедуры драйверов для разных устройств сильно отличаются, есть общая структура и общие функции для всех драйверов. Главные из них мы сейчас и рассмотрим.
1) Поле DriverUnload - для регистрации собственной функции Unload, вызываемой при выгрузке драйвера. Подробнее о ней.
Эта функция вызывается только при динамической выгрузке драйвера
(т.е. происшедшей не в результате завершения работы системы). Legacy драйвера в этой функции выполняют полное освобождение всех занятых драйвером системных ресурсов. WDM драйвера выполняют такое освобождение в функции RemoveDevice при удалении каждого устройства
(если драйвер обслуживает несколько устройств). Функция Unload вызывается с уровня IRQL PASSIVE_LEVEL, принимает единственный параметр
(PDRIVER_OBJECT) - указатель на объект драйвера, и возвращает
void.
2) Поле DriverStartIo - для регистрации собственной функции
StartIo. Вкратце, регистрация функции StartIo нужна для участия в System Queuing - создании очередей необработанных запросов системными средствами, в отличие от DriverQueuing - когда то же самое реализуется средствами самого драйвера.
3) Поле AddDevice в подструктуре DriverExtension - для регистрации WDM драйвером своей процедуры
AddDevice.
4) Поле MajorFunction - для регистрации драйвером точек входа в свои рабочие процедуры.
Если выгрузка драйвера происходит в результате завершения работы системы, то функция Unload не вызывается. Это понятно: при выключении системы можно не заботиться об освобождении памяти и т.д. Поэтому вызывается функция Shutdown, которая просто предоставляет драйверу возможность оставить устройство в приемлемом состоянии покоя.
Может случиться так, что твоему драйверу необходимо будет получить управление при крахе системы. В этом случае ему нужно зарегистрировать callback процедуру Bugcheck. Если драйвер правильно выполнил регистрацию этой функции, то он будет вызван во время исполнения crash процесса.
Я перечислила основные функции драйвера для его загрузки и выгрузки. Теперь перейдём к рабочим процедурам драйвера.
Поговорим о следующих рабочих процедурах: обслуживания ввода - вывода
(включающих в себя процедуры передачи данных и обслуживания прерываний), callback процедуры для синхронизации доступа к объектам и некоторые другие.
Все драйверы должны иметь обработчик CreateDispatch, обрабатывающий пользовательский запрос CreateFile. Если драйверу нужно обрабатывать пользовательский запрос CloseHandle, то он должен иметь обработчик
CloseDispatch.
Перейдём к процедурам передачи данных. Это обработчики пользовательских запросов ReadFile, WriteFile и
DeviceIoControl.
Процедуру StartIo я уже рассмотрела, поэтому перейдём к процедуре обслуживания прерываний
(ISR - Interrupt Service Routine, напоминаю на всякий случай). Данная процедура вызвается диспетчером прерываний ядра
(Kernel`s Interrupt Dispatcher) при каждой генерации прерывания устройством, и она обязана полностью обслужить это прерывание.
Теперь о callback процедурах сихронизации доступа к объектам. Для начала разберёмся, в чём различия принципов сихронизации доступа к объектам в пользовательском и ядерном режимах. Например, в пользовательском режиме, если какой - либо поток обратился к объекту, уже занятому другим потоком, то он
(первый поток) запросто может быть заблокирован до лучших времён. Как ты сам понимаешь, в режиме ядра такая внеплановая "заморозка" потоков неприемлема, поэтому и применяется другая технология сихронизации. И заключается она в следующем. Когда какой-либо поток обращается к объекту, уже занятому другим потоком, то он оставляет свой запрос в очереди запросов. Если драйвер предварительно зарегистрировал особую callback функцию, то диспетчер ввода - вывода при освобождении
требуемого ресурса, уведомит об этом драйвер, вызвав callback - функцию. Таким образом, обеспечивается гарантия ответа на любой запрос к ресурсу, даже если он
(ответ) будет состоять только в том, чтобы уведомить о задержке в обработке и помещении запроса в очередь. Функции, это реализующие: IoAllocateController
(использующаяся для синхронизации доступа к контроллеру), AdapterControl
(использующаяся для синхронизации доступа к DMA каналам
(чаще всего)) и SynchCritSection (использующаяся для корректного обращения к ресурсам; точнее - эта функция позволяет коду с низким уровнем IRQL сделать работу при уровне DIRQL устройства без опасения возникновения конфликтов с ISR).
Также можно упомянуть ещё таймерные процедуры
(нужные для драйверов, выполняющих точный отсчёт временных интервалов; обычно реализуется с использованием IoTimer
(но не всегда)), процедуру IoCompletion (позволяющую WDM драйверу, работающему внутри многослойной драйверной структуры, получать уведомление о завершении обработки IRP запроса, направленного к драйверу нижнего уровня) и CancelRoutine
(если драйвер зарегистрирует эту callback процедуру при помощи вызова IoSetCancelRoutine, то диспетчер ввода - вывода сможет уведомить его об удалении IRP запросов, находящихся в ожидании обработки, что может понадобиться диспетчеру, если пользовательское приложение, инициировавшее эти IRP запросы, неожиданно завершит свою работу после снятия задачи диспетчером задач
(прошу прощения за необходимую тавтологию)).
Ну вот и всё, мы закончили обзор структуры драйвера и основных его процедур. Примеры их практического применения
(а также некоторых других функций) ты увидишь в третьей, заключительной статье этого цикла, в которой мы напишем свой полноценный Legacy драйвер.
Уфф, наконец-то мы покончили со скучной теорией. В этой статье ты узнал про некоторые приёмы программирования в режиме ядра, изучил структуру драйвера и его основные функции. Теперь ты полностью подготовлен
(на сей раз уже окончательно) к написанию своего первого
(или двадцать первого - не знаю) полноценного Legacy драйвера под Windows. Это мы проделаем в заключительной части моего рассказа о программировании драйверов под Windows. А пока что слегка отдохнём. Да не облысеют твои пятки!
Как уважаемый хабрапользователь наверняка знает, «драйвер устройства» — это компьютерная программа управляющая строго определенным типом устройства, подключенным к или входящим в состав любого настольного или переносного компьютера.
Основная задача любого драйвера – это предоставление софтового интерфейса для управления устройством, с помощью которого операционная система и другие компьютерные программы получают доступ к функциям данного устройства, «не зная» как конкретно оно используется и работает.
Обычно драйвер общается с устройством через шину или коммуникационную подсистему, к которой подключено непосредственное устройство. Когда программа вызывает процедуру (очередность операций) драйвера – он направляет команды на само устройство. Как только устройство выполнило процедуру («рутину»), данные посылаются обратно в драйвер и уже оттуда в ОС.
Любой драйвер является зависимым от самого устройства и специфичен для каждой операционной системы. Обычно драйверы предоставляют схему прерывания для обработки асинхронных процедур в интерфейсе, зависимом от времени ее исполнения.
Любая операционная система обладает «картой устройств» (которую мы видим в диспетчере устройств), для каждого из которых необходим специфический драйвер. Исключения составляют лишь центральный процессор и оперативная память, которой управляет непосредственно ОС. Для всего остального нужен драйвер, который переводит команды операционной системы в последовательность прерываний – пресловутый «двоичный код».
Как работает драйвер и для чего он нужен?
Основное назначение драйвера – это упрощение процесса программирования работы с устройством.
Он служит «переводчиком» между хардовым (железным) интерфейсом и приложениями или операционными системами, которые их используют. Разработчики могут писать, с помощью драйверов, высокоуровневые приложения и программы не вдаваясь в подробности низкоуровневого функционала каждого из необходимых устройств в отдельности.
Как уже упоминалось, драйвер специфичен для каждого устройства. Он «понимает» все операции, которые устройство может выполнять, а также протокол, с помощью которого происходит взаимодействие между софтовой и железной частью. И, естественно, управляется операционной системой, в которой выполняет конкретной приложение либо отдельная функция самой ОС («печать с помощью принтера»).
Если вы хотите отформатировать жесткий диск, то, упрощенно, этот процесс выглядит следующим образом и имеет определенную последовательность: (1) сначала ОС отправляет команду в драйвер устройства используя команду, которую понимает и драйвер, и операционная система. (2) После этого драйвер конкретного устройства переводит команду в формат, который понимает уже только устройство. (3) Жесткий диск форматирует себя, возвращает результат драйверу, который уже впоследствии переводит эту команду на «язык» операционной системы и выдает результат её пользователю (4).
Как создается драйвер устройства
Для каждого устройства существует свой строгий порядок выполнения команд, называемой «инструкцией». Не зная инструкцию к устройству, невозможно написать для него драйвер, так как низкоуровневые машинные команды являются двоичным кодом (прерываниями) которые на выходе отправляют в драйвер результат, полученный в ходе выполнения этой самой инструкции.
При создании драйвера для Линукса, вам необходимо знать не только тип шины и ее адрес, но и схематику самого устройства, а также весь набор электрических прерываний, в ходе исполнения которых устройство отдает результат драйверу.
Написание любого драйвера начинается с его «скелета» — то есть самых основных команд вроде «включения/выключения» и заканчивая специфическими для данного устройства параметрами.
И чем драйвер не является
Часто драйвер устройства сравнивается с другими программами, выполняющими роль «посредника» между софтом и/или железом. Для того, чтобы расставить точки над «i», уточняем:
- Драйвер не является интерпретатором, так как не исполняется напрямую в софтовом слое приложения или операционной системы.
- Драйвер не является компилятором, так как не переводит команды из одного софтового слоя в другой, такой же.
Ну и на правах рекламы – вы всегда знаете, где скачать новейшие драйвера для любых устройств под ОС Windows.
Меня всегда интересовало низкоуровневое программирование – общаться напрямую с оборудованием, жонглировать регистрами, детально разбираться как что устроено. Увы, современные операционные системы максимально изолируют железо от пользователя, и просто так в физическую память или регистры устройств что-то записать нельзя. Точнее я так думал, а на самом деле оказалось, что чуть ли не каждый производитель железа так делает!
В чём суть, капитан?
В архитектуре x86 есть понятие «колец защиты» («Ring») – режимов работы процессора. Чем ниже номер текущего режима, тем больше возможностей доступно исполняемому коду. Самым ограниченным «кольцом» является «Ring 3», самым привилегированным – «Ring -2» (режим SMM). Исторически сложилось, что все пользовательские программы работают в режиме «Ring 3», а ядро ОС – в «Ring 0»:
Режимы работы x86 процессора
В «Ring 3» программам запрещены потенциально опасные действия, такие как доступ к I/O портам и физической памяти. По логике разработчиков, настолько низкоуровневый доступ обычным программам не нужен. Доступ к этим возможностям имеют только операционная система и её компоненты (службы и драйверы). И всё бы ничего, но однажды я наткнулся на программу RW Everything:
RW Everything действительно читает и пишет практически всё
Эта программа была буквально напичкана именно теми функциями, которые обычно запрещаются программам «Ring 3» - полный доступ к физической памяти, I/O портам, конфигурационному пространству PCI (и многое другое). Естественно, мне стало интересно, как это работает. И выяснилось, что RW Everything устанавливает в систему прокси-драйвер:
Смотрим последний установленный драйвер через OSR Driver Loader
Прокси-драйвера
В итоге получается обходной манёвр – всё, что программе запрещено делать, разработчик вынес в драйвер, программа устанавливает драйвер в систему и уже через него программа делает, что хочет! Более того – выяснилось, что RW Everything далеко не единственная программа, которая так делает. Таких программ не просто много, они буквально повсюду. У меня возникло ощущение, что каждый уважающий себя производитель железа имеет подобный драйвер:
Софт для обновления BIOS (Asrock, Gigabyte, HP, Dell, AMI, Intel, Insyde…)
Софт для разгона и конфигурации железа (AMD, Intel, ASUS, ASRock, Gigabyte)
Софт для просмотра сведений о железе (CPU-Z, GPU-Z, AIDA64)
Софт для обновления PCI устройств (Nvidia, Asmedia)
Во многих из них практически та же самая модель поведения – драйвер получает команды по типу «считай-ка вот этот физический адрес», а основная логика – в пользовательском софте. Ниже в табличке я собрал некоторые прокси-драйвера и их возможности:
Результаты краткого анализа пары десятков драйверов. Могут быть ошибки!
Mem – чтение / запись физической памяти
PCI – чтение / запись PCI Configuration Space
I/O – чтение / запись портов I/O
Alloc – аллокация и освобождение физической памяти
Map – прямая трансляция физического адреса в вирутальный
MSR – чтение / запись x86 MSR (Model Specific Register)
Жёлтым обозначены возможности, которых явно нет, но их можно использовать через другие (чтение или маппинг памяти). Мой фаворит из этого списка – AsrDrv101 от ASRock. Он устроен наиболее просто и обладает просто огромным списком возможностей, включая даже функцию поиска шаблона по физической памяти (!!)
Неполный перечень возможностей AsrDrv101
Чтение / запись RAM
Чтение / запись IO
Чтение / запись PCI Configuration Space
Чтение / запись MSR (Model-Specific Register)
Чтение / запись CR (Control Register)
Чтение TSC (Time Stamp Counter)
Чтение PMC (Performance Monitoring Counter)
Alloc / Free физической памяти
Поиск по физической памяти
Самое нехорошее в такой ситуации - если подобный драйвер остаётся запущенным на ПК пользователя, для обращения к нему не нужно даже прав администратора! То есть любая программа с правами пользователя сможет читать и писать физическую память - хоть пароли красть, хоть ядро пропатчить. Именно на это уже ругались другие исследователи. Представьте, что висящая в фоне софтина, красиво моргающая светодиодиками на матплате, открывает доступ ко всей вашей системе. Или вирусы намеренно ставят подобный драйвер, чтобы закрепиться в системе. Впрочем, любой мощный инструмент можно в нехороших целях использовать.
Через Python в дебри
Конечно же я захотел сделать свой небольшой "тулкит" для различных исследований и экспериментов на базе такого драйвера. Причём на Python, мне уж очень нравится, как просто выглядит реализация сложных вещей на этом языке.
Первым делом нужно установить драйвер в систему и запустить его. Делаем "как положено" и сначала кладём драйвер (нужной разрядности!) в System32:
Раньше в похожих ситуациях я извращался с папкой %WINDIR%\Sysnative, но почему-то на моей текущей системе такого алиаса не оказалось, хотя Python 32-битный. (по идее, на 64-битных системах обращения 32-битных программ к папке System32 перенаправляются в папку SysWOW64, и чтобы положить файлик именно в System32, нужно обращаться по имени Sysnative).
Затем регистрируем драйвер в системе и запускаем его:
А дальше запущенный драйвер создаёт виртуальный файл (кстати, та самая колонка "имя" в таблице с анализом дров), через запросы к которому и осуществляются дальнейшие действия:
И ещё одна полезная программа для ползания по системе, WinObj
Тоже ничего особенного, открываем файл и делаем ему IoCtl:
Вот здесь чутка подробнее. Я долго думал, как же обеспечить адекватную обработку ситуации, когда таких "скриптов" запущено несколько. Не останавливать драйвер при выходе нехорошо, в идеале нужно смотреть, не использует ли драйвер кто-то ещё и останавливать его только если наш скрипт "последний". Долгие упорные попытки получить количество открытых ссылок на виртуальный файл драйвера ни к чему не привели (я получал только количество ссылок в рамках своего процесса). Причём сама система точно умеет это делать - при остановке драйвера с открытым файлом, он остаётся висеть в "Pending Stop". Если у кого есть идеи - буду благодарен.
В конечном итоге я "подсмотрел", как это делают другие программы. Выяснилось, что большинство либо не заморачиваются, либо просто ищут запущенные процессы с тем же именем. Но одна из исследованных программ имела кардинально другой подход, который я себе и перенял. Вместо того, чтобы переживать по количеству ссылок на файл, просто на каждый запрос открываем и закрываем файл! А если файла нет, значит кто-то остановил драйвер и пытаемся его перезапустить:
А дальше просто реверсим драйвер и реализуем все нужные нам вызовы:
Легко и непринуждённо в пару команд читаем физическую память
PCI Express Config Space
Немного отвлечёмся на один нюанс про PCIE Config Space. С этим адресным пространством не всё так просто - со времён шины PCI для доступа к её конфигурационному пространству используется метод с использованием I/O портов 0xCF8 / 0xCFC. Он применён и в нашем драйвере AsrDrv101:
Чтение и запись PCI Config Space
Но через этот метод доступны только 0x100 байт конфигурационного пространства, в то время как в стандарте PCI Express размер Config Space у устройств может быть достигать 0x1000 байт! И полноценно вычитать их можно только обращением к PCI Extended Config Space, которая замаплена где-то в адресном пространстве, обычно чуть пониже BIOS:
Адресное пространство современного x86 компа, 0-4 ГБ
На чипсетах Intel (ну, в их большинстве) указатель на эту область адресного пространства можно взять из конфига PCI устройства 0:0:0 по смещению 0x60, подробнее описано в даташитах:
У AMD я такого не нашёл (наверняка есть, плохо искал), но сам факт неуниверсальности пнул меня в сторону поиска другого решения. Погуглив стандарты, я обнаружил, что указатель на эту область передаётся системе через ACPI таблицу MCFG
А сами ACPI таблицы можно найти через запись RSDP, поискав её сигнатуру по адресам 0xE0000-0xFFFFF, а затем распарсив табличку RSDT. Отлично, здесь нам и пригодится функционал поиска по памяти. Получаем нечто такое:
На всякий случай оставляем вариант для чипсетов Intel
Всё, теперь осталось при необходимости заменить чтение PCI Express Config Space через драйвер на чтение через память. Теперь-то разгуляемся!
Читаем BIOS
В качестве примера применения нашего "тулкита", попробуем набросать скрипт чтения BIOS. Он должен быть "замаплен" где-то в конце 32-битного адресного пространства, потому что компьютер начинает его исполнение с адреса 0xFFFFFFF0. Обычно в ПК стоит флеш-память объёмом 4-16 МБ, поэтому будем "сканировать" адресное пространство с адреса 0xFF000000, как только найдём что-нибудь непустое, будем считать, что тут начался BIOS:
В результате получаем:
Вот так в 10 строчек мы считали BIOS
Но подождите-ка, получилось всего 6 мегабайт, а должно быть 4 или 8 что-то не сходится. А вот так, у чипсетов Intel в адресное пространство мапится не вся флешка BIOS, а только один её регион. И чтобы считать всё остальное, нужно уже использовать SPI интерфейс.
Не беда, лезем в даташит, выясняем, что SPI интерфейс висит на PCI Express:
И для его использования, нужно взаимодействовать с регистрами в BAR0 MMIO по алгоритму:
Задать адрес для чтения в BIOS_FADDR
Задать параметры команды в BIOS_HSFTS_CTL
Прочитать данные из BIOS_FDATA
Пилим новый скрипт для чтения через чипсет:
Исполняем и вуаля - в 20 строчек кода считаны все 8 МБ флешки BIOS! (нюанс - в зависимости от настроек, регион ME может быть недоступен для чтения).
Точно так же можно делать всё, что заблагорассудится - делать снифер USB пакетов, посылать произвольные ATA команды диску, повышать частоту процессора и переключать видеокарты. И это всё - с обычными правами администратора:
Немного помучившись, получаем ответ от SSD на команду идентификации
А если написать свой драйвер?
Некоторые из вас наверняка уже подумали - зачем так изворачиваться, реверсить чужие драйвера, если можно написать свой? И я о таком думал. Более того, есть Open-Source проект chipsec, в котором подобный драйвер уже разработан.
Зайдя на страницу с кодом драйвера, вы сразу наткнетесь на предупреждение:
В этом предупреждении как раз и описываются все опасности, о которых я рассказывал в начале статьи - инструмент мощный и опасный, следует использовать только в Windows режиме Test Mode, и ни в коем случае не подписывать. Да, без специальной подписи на обычной системе просто так запустить драйвер не получится. Поэтому в примере выше мы и использовали заранее подписанный драйвер от ASRock.
Если кто сильно захочет подписать собственный драйвер - понадобится регистрировать собственную компанию и платить Microsoft. Насколько я нагуглил, физическим лицам такое развлечение недоступно.
Точнее я так думал, до вот этой статьи, глаз зацепился за крайне интересный абзац:
У меня под рукой нет Windows DDK, так что я взял 64-битный vfd.sys , скомпилированный неким critical0, и попросил dartraiden подписать его «древне-китайским способом». Такой драйвер успешно загружается и работает, если vfdwin запущена с правами администратора
Драйвер из статьи действительно подписан, и действительно неким китайским ключом:
Как оказалось, сведения о подписи можно просто посмотреть в свойствах.. А я в HEX изучал
Немного поиска этого имени в гугле, и я натыкаюсь на вот эту ссылку, откуда узнаю, что:
есть давно утёкшие и отозванные ключи для подписи драйверов
если ими подписать драйвер - он прекрасно принимается системой
малварщики по всему миру используют это для создания вирусни
Основная загвоздка - заставить майкрософтский SignTool подписать драйвер истёкшим ключом, но для этого даже нашёлся проект на GitHub. Более того, я нашёл даже проект на GitHub для другой утилиты подписи драйверов от TrustAsia, с помощью которого можно подставить для подписи вообще любую дату.
Несколько минут мучений с гугл-переводчиком на телефоне, и мне удалось разобраться в этой утилите и подписать драйвер одним из утекших ключей (который довольно легко отыскался в китайском поисковике):
И в самом деле, китайская азбука
И точно так же, как и AsrDrv101, драйвер удалось без проблем запустить!
А вот и наш драйвер запустился
Из чего делаю вывод, что старая идея с написанием своего драйвера вполне себе годная. Как раз не хватает функции маппинга памяти. Но да ладно, оставлю как TODO.
Выводы?
Как видите, имея права администратора, можно делать с компьютером практически что угодно. Будьте внимательны - установка утилит от производителя вашего железа может обернуться дырой в системе. Ну а желающие поэкспериментировать со своим ПК - добро пожаловать на низкий уровень! Наработки выложил на GitHub. Осторожно, бездумное использование чревато BSODами.
В Windows есть фича "Изоляция ядра", которая включает I/O MMU, защищает от DMA атак и так далее (кстати об этом - в следующих сериях)
Так вот, при включении этой опции, некоторые драйвера (в том числе RW Everything и китайско-подписанный chipsec_hlpr) перестают запускаться:
Драйвер-это основа взаимодействия системы с устройством в ОС Windows.Это одновременно удобно и неудобно.
Про удобства я разъяснять не буду - это и так понятно,
а заострюсь я именно на неудобствах драйверов.
В сложившейся ситуации пользователь полностью подчинён воле производителя
- выпусти тот драйвер - хорошо, а не выпустит.
Только продвинутый пользователь, имеющий голову на плечах
(особенно, если он ешё и программер) не станет мириться с таким положением дел
- он просто возьмёт и сам напишет нужный драйвер.
Это нужно и взломщику: драйвер - это удобное окошко в ring0,
которое является раем для хакера. Но хоть написать драйвер и просто,
да не совсем - есть масса подводных камней. Да и документированность данного вопроса на русском языке оставляет желать лучшего.
Этот цикл статей поможет тебе во всём разобраться.
Приступим.
Хочу сразу же сделать несколько предупреждений.
Данная статья всё-таки подразумевает определённый уровень подготовки.
Драйвера-то ведь пишутся на C(++) с большим количеством ассемблерных вставок.
Поэтому хорошее знание обоих языков весьма желательно (если не сказать - обязательно).
Если же ты пока не можешь этим похвастаться,
но желание писать драйвера есть - что ж, так как эта статья вводная, в конце её будет приведён список полезной литературы,
ссылок и т.д. Но помни: учить тебя в этом цикле статей программированию как таковому я тебя не буду.
Может как-нибудь в другой раз. Согласен? Тогда поехали!
Скоро здесь, возможно, будет стоять твоё имя.
Практически в любом деле, как мне кажется, нужно начинать с теории.
Вот и начнём с неё. Для начала уясним себе поточнее основные понятия.
Первое: что есть драйвер? Драйвер - в сущности
кусок кода ОС, отвечающий за взаимодействие с аппаратурой.
Слово "аппаратура" в данном контексте следует понимать в самом широком смысле.
С момента своего появления как такого до сегодняшнего дня драйвер беспрерывно эволюционировал.
Вот, скажем, один из моментов его развития. Как отдельный и довольно независимый модуль драйвер сформировался не сразу.
Да и сейчас этот процесс до конца не завершён:
ты наверняка сталкивался с тем, что во многих
дистрибутивах никсов для установки/перестановки etc драйверов нужно перекомпилировать ядро,
т.е. фактически заново пересобирать систему.
Вот, кстати ещё один близкий моментец: разные принципы работы с драйверами в Windows 9x и NT.
В первом процесс установки/переустановки драйверов проходит практически без проблем,
во втором же случае это тяжёлое и неблагодарное дело,
для "благополучного" завершения которого нередко приходится прибегать к полной переустановке ОС.
А зато в Windows 9x. так,стоп,открывается широкая и волнующая тема,
которая уведёт меня далеко от темы нынешней статьи,
так что вернёмся к нашим баранам. ой,то есть к драйверам.
В порядке общего развития интересно сравнить особенности драйверов в Windows и *nix(xBSD) системах:
1) Способ работы с драйверами как файлами (подробнее см. ниже)
2) Драйвер, как легко заменяемая честь ОС (учитывая уже сказанные выше примечания)
3) Существование режима ядра
Теперь касательно первого пункта. Это значит,
что функции, используемые при взаимодействии с файлами,
как и с драйверами, практически идентичные (имеется в виду лексически):
open, close, read и т.д. И напоследок стоит отметить идентичность механизма
IOCTL (Input/Output Control Code-код управления вводом-выводом)
-запросов.
Драйвера под Windows делятся на два типа:
Legacy (устаревший) и WDM (PnP). Legacy драйверы (иначе называемые "драйверы в стиле
NT") чрезвычайно криво работают (если работают вообще)
под Windows 98, не работают с PnP устройствами, но зато могут пользоваться старыми функциями
HalGetBusData, HalGetInterruptVector etc, но при этом не имеют поддержки в лице шинных драйверов.
Как видишь, весьма средненький драйвер. То ли дело
WDM: главный плюс - поддержка PnP и приличненькая совместимость:
Windows 98, Me, 2000, XP, 2003, Server 2003 и т.д. с вариациями; но он тоже вынужден за это расплачиваться:
например, он не поддерживает некоторые устаревшие функции
(которые всё таки могут быть полезны). В любом случае,
не нужно ничего воспринимать как аксиому, везде бывают свои исключения.
В некоторых случаях лучше написания Legacy драйвера ничего не придумать.
Как ты наверняка знаешь, в Windows есть два мода работы:
User Mode и Kernel Mode - пользовательский режим и режим ядра соответственно.
Первый - непривилегированный, а второй - наоборот.
Вот во втором чаще всего и сидят драйвера (тем
более, что мы в данный момент говорим именно о драйверах режима ядра).
Главные различия между ними: это доступность всяких привилегированных команд процессора.
Программировать (а уж тем более качественно) в Kernel mode посложнее будет,
чем писать прикладные незамысловатые проги.
А драйвера писать без хорошего знания Kernel mode - никак.
Нужно попариться над назначением выполнения разнообразных работ отдельному подходящему уровню IRQL, желательно выучить новое API (так как в Kernel mode API отличается от прикладного).
в общем, предстоит много всяких радостей. Но тем не менее,
это очень интересно, познавательно, и даёт тебе совершенно иной уровень власти над компьютером.
А раз уж я упомянула про IRQL, разьясню и это понятие.
IRQL (Interrupt Request Level - уровень приоритета выполнения) - это приоритеты,
назначаемые специально для кода, работающего в режиме ядра.
Самый низкий уровень выполнения - PASSIVE_LEVEl. Работающий поток может быть прерван потоком только с более высоким
IRQL.
Ну и напоследок разъясним ещё несколько терминов:
1) ISR (Interrupt Service Routine) - процедура обслуживания прерываний.
Эта функция вызывается драйвером в тот момент,
когда обслуживаемая им аппаратура посылает сигнал прерывания.
Делает самые необходимые на первый момент вещи:
регистрирует callback - функцию и т.д.
2) DpcForISR (Deferred Procedure Call for ISR) - процедура отложенного вызова для обслуживания прерываний.
Эту функцию драйвер регистрирует в момент работы ISR для выполнения основной работы.
3) IRP (Input/Output Request Packet) - пакет запроса на ввод - вывод.
Пакет IRP состоит из фиксированной и изменяющейся частей.
Вторая носит название стека IRP или стека ввода - вывода (IO stack).
4) IO stack location - стек ввода - вывода в пакете IRP.
5) Dispatch Routines (Рабочие процедуры) - эти функции регистрируются в самой первой (по вызову) процедуре драйвера.
6) Major IRP Code - старший код IRP пакета.
7) Minor IRP Code - соответственно, младший код IRP пакета.
8) DriverEntry - эта функция драйвера будет вызвана первой при его загрузке.
9) Layering (Многослойность) - данной возможностью обладают только WDM - драйвера.
Она заключается в наличии реализации стекового соединения между драйверами.
Что такое стековое соединение? Для этого необходимо знать про Device
Stack (стек драйверов) - поэтому я обязательно вспомню про всё это чуточку ниже.
10) Device Stack, Driver Stack (стек устройств, стек драйверов) - всего лишь
объемное дерево устройств. Его, кстати, можно рассмотреть во всех подробностях с помощью программы
DeviceTree (из MS DDK), например.
11) Стековое соединение - как и обещала, объясняю. В стеке драйверов самый верхний драйвер - подключившийся позднее.
Он имеет возможность посылать/переадресовывать IRP запросы другим драйверам,
которые находятся ниже его. Воти всё. Правда,просто?
12) AddDevice - функция, которую обязательно должны поддерживать WDM драйверы.
Её название говорит само за себя.
13) Device Object, PDO, FDO (Объект устройства, физический,
функциональный) - при подключении устройства к шине она создаёт PDO.
А уже к PDO будут подключаться FDO объекты WDM драйверов.
Обьект FDO создаётся самим драйвером устройства при помощи функции IOCreateDevice.
Обьект FDO также может иметь свою символическую ссылку, от которой он будет получать запросы от драйвера.
Это что касается WDM драйверов. С драйверами "в стиле NT" ситуация несколько иная.
Если он не обслуживает реальных/PnP устройств,
то PDO не создаётся. Но для связи с внешним миром без FDO не обойтись.
Поэтому он присутствует и тут.
14) Device Extension (Расширение обьекта устройства) - "авторская" структура,
т.е. она полностью определяется разработчиком драйвера.
Правилом хорошего тона считается, например,
размещать в ней глобальные переменные.
15) Monolithic Driver (Монолитный драйвер) - это драйвер,
который самостоятельно обрабатывает все поступающие
IRP пакеты и сам работает с обслуживаемым им устройством
(в стеке драйверов он не состоит). Данный тип драйверов используется только если обслуживается не
PnР устройство или же всего лишь требуется окошко в ring0.
16) DIRQL (уровни аппаратных прерываний) -
прерывания, поступающие от реальных устройств, имеют наивысший приоритет IRQL,
поэтому для них решено было придумать специальное название
(Device IRQL).
17) Mini Driver (Мини - драйвер) - чуть меньше "полного" драйвера.
Обычно реализуется в виде DLL-ки и имеет оболочку в виде "полного" драйвера.
18) Class Driver (Классовый драйвер) - высокоуровневый драйвер,
который предоставляет поддержку класса устройств.
19) РnP Manager (PnP менеджер) - один из главных компонентов операционной системы.
Состоит из двух частей: PnP менеджера пользовательского и "ядерного" режимов.
Первый в основном взаимодействует с пользователем;
когда тому нужно, например, установить новые драйвера и т.д.
А второй управляет работой, загрузкой и т.д. драйверов.
20) Filter Driver (фильтр - драйвер) - драйверы, подключающиеся к основному драйверу либо сверху
(Upper), либо снизу (Lower). Фильтр драйверы (их может быть несколько) выполняют фильтрацию IRP пакетов.
Как правило, для основного драйвера Filter Drivers неощутимы.
21) Filter Device Object - объект устройства, создаваемый фильтр - драйвером.
22) HAL (Hardware Abstraction Layer) - слой аппаратных абстракций.
Данный слой позволяет абстрагироваться компонентам операционной системы от особенностей конкретной платформы.
23) Synchronization Objects (Обьекты синхронизации) - с помощью этих
объектов потоки корректируют и синхронизируют свою работу.
24) Device ID - идентификатор устройства.
25) DMA (Direct Memory Access) - метод обмена данными между устройством и памятью
(оперативной) в котором центральный процессор не принимает участия.
25) Polling - это особый метод программирования, при котором не устройство посылает сигналы прерывания драйверу,
а сам драйвер периодически опрашивает обслуживаемое им устройство.
26) Port Driver (Порт-драйвер) - низкоуровневый драйвер,
принимающий системные запросы. Изолирует классовые драйверы устройств от аппаратной специфики последних.
Ну вот, пожалуй, и хватит терминов. В будущем,
если нужны будут какие-нибудь уточнения по теме,
я обязательно их укажу. А теперь, раз уж эта статья
теоретическая, давай-ка взглянем на архитектуру Windows NT с высоты птичьего полёта.
Краткий экскурс в архитектуру Windows NT
Наш обзор архитектуры Windows NT мы начнём с разговора об уровнях разграничения привилегий. Я уже упоминала об user и kernel mode.
Эти два понятия тесно связаны с так называемыми кольцами (не толкиеновскими ).
Их ( колец) в виде всего четыре: Ring3,2,1 и 0. Ring3 - наименее привилегированное кольцо,
в котором есть множество ограничений по работе с устройствами,
памятью и т.д. Например, в третьем кольце нельзя видеть адресное пространство других приложений без особого на то разрешения. Естественно,
трояну вирусу etc эти разрешения получить будет трудновато, так что хакеру в третьем кольце жизни никакой. В третьем кольце находится user mode. Kernel mode сидит в нулевом кольце - наивысшем уровне привилегий. В этом кольце можно всё:
смотреть адресные пространства чужих приложений без каких - либо ограничений и разрешений, по своему усмотрению поступать с любыми сетевыми пакетами, проходящими через машину, на всю жизнь скрыть какой-нибудь свой процесс или файл и т.д. и т.п. Естественно,
просто так пролезть в нулевое кольцо не получиться:
для этого тоже нужны дополнительные телодвижения. У легального драйвера с этим проблем нет:
ему дадут все необходимые API - шки, доступ ко всем нужным системным таблицам и проч. Хакерской же нечисти опять приходиться туго:
все необходимые привилегии ему приходиться "выбивать"
незаконным путём. Но это уже тема отдельной статьи, и мы к ней как-нибудь ещё вернёмся. А пока продолжим.
У тебя наверняка возник законный вопрос:
а что же сидит в первом и втором кольцах ? В том то всё и дело,
что программисты из Microsoft почему - то обошли эти уровни своим вниманием. Пользовательское ПО сидит в user mode,а всё остальное (ядро,
драйвера. ) - в kernel mode. Почему они так сделали - загадка, но нам это только на руку. А теперь разберёмся с компонентами (или, иначе говоря, слоями ) операционной системы Windows
NT.
Посмотри на схему - по ней многое можно себе уяснить. Разберём её подробнее.
С пользовательским режимом всё понятно. В kernel mode самый низкий уровень аппаратный. Дальше идёт HAL, выше - диспетчер ввода - вывода и драйвера устройств в одной связке, а также ядрышко вместе с исполнительными компонентами. О HAL я уже говорила, поэтому поподробнее поговорим об исполнительных компонентах. Что они дают? Прежде всего они приносят пользу ядру. Как ты уже наверняка уяснил себе по схеме, ядро отделено от исполнительных компонентов. Возникает вопрос:
почему ? Просто на ядре оставили только одну задачу:
просто управление потоками, а все остальные задачи (управление доступом,
памятью для процессов и т.д.) берут на себя исполнительные компоненты (еxecutive). Они реализованы по модульной схеме, но несколько компонентов её (схему) не поддерживают . Такая концепция имеет свои преимущества:
таким образом облегчается расширяемость системы. Перечислю наиболее важные исполнительные компоненты:
1) System Service Interface (Интерфейс системных служб )
2) Configuration Manager (Менеджер конфигурирования)
3) I/O Manager (Диспетчер ввода-вывода,ДВВ)
4) Virtual Memory Manager,VMM (Менеджер виртуальной памяти)
5) Local Procedure Call,LPC (Локальный процедурный вызов )
6) Process Manager (Диспетчер процессов)
7) Object Manager (Менеджер объектов)
Так как эта статья - первая в цикле, обзорная, подробнее на этом пока останавливаться не будем. В процессе практического обучения написанию драйверов, я буду разъяснять все неясные термины и понятия. А пока перейдём к API.
API (Application Programming Interface) - это интерфейс прикладного программирования. Он позволяет обращаться прикладным программам к системным сервисам через их специальные абстракции. API-интерфейсов несколько, таким образом в Windows-системах присутствуют несколько подсистем. Перечислю:
1) Подсистема Win32.
2) Подсистема VDM (Virtual DOS Machine - виртуальная ДОС - машина)
3) Подсистема POSIX (обеспечивает совместимость UNIX - программ)
4) Подсистемиа WOW (Windows on Windows). WOW 16 обеспечивает совместимость 32-х разрядной системы с 16-битными приложениями. В 64-х разрядных системах есть подсистема WOW 32,
которая обеспечивает аналогичную поддержку 32 - битных приложений.
5) Подсистема OS/2. Обеспечивает совместимость с OS/2 приложениями.
Казалось бы, всё вышеперечисленное однозначно говорит в пользу WINDOWS NT систем!
Но не всё так хорошо. Основа WINDOWS NT (имеются ввиду 32-х разрядные версии) - подсистема Win32. Приложения, заточенные под одну подсистему не могут вызывать функции другой. Все остальные (не Win32) подсистемы существуют в винде только в эмуляции и реализуются функции этих подсистем только через соответствующие функции винды. Убогость и ограниченность приложений, разработанных, скажем, для подсистемы POSIX и запущенных под винду - очевидны.
Увы.
Подсистема Win32 отвечает за графический интерфейс пользователя, за обеспечение работоспособности Win32 API и за консольный ввод - вывод. Каждой реализуемой задаче
соответствуют и свои функции: функции, отвечающие за графический фейс,
за консольный ввод - вывод (GDI - функции) и функции управления потоками,
файлами и т.д. Типы драйверов, наличествующие в Windows, я уже упоминала в разделе терминов:
монолитный драйвер, фильтр - драйвер и т.д. А раз так, то пора закругляться. Наш краткий обзор архитектуры Windows NT можно считать завершённым. Этого тебе пока хватит для общего понимания концепций Windows NT, и концепций написания драйверов под эту ось - как следствие.
Инструменты
Описать и/или упомянуть обо всех утилитах, могущих понадобиться при разработке драйверов - немыслимо. Расскажу только об общих направлениях.
Без чего нельзя обойтись ни в коем случае - это Microsoft DDK (Driver Development Kit ). К этому грандиозному пакету прилагается и обширная документация. Её ценность - вопрос спорный. Но в любом случае, хотя бы ознакомиться с первоисточником информации по написанию драйверов для Windows - обязательно. В принципе, можно компилять драйвера и в Visual Studio, но это чревато долгим и нудным копанием в солюшенах и vcproj-ектах, дабы код твоего драйвера нормально откомпилировался. В любом случае, сорцы придётся набивать в визуальной студии, т.к. в DDK не входит
нормальная IDE. Есть пакеты разработки драйверов и от третьих фирм:
WinDriver или NuMega Driver Studio, например. Но у них есть отличия от майкрософтовского базиса функций (порой довольно большие ) и многие другие мелкие неудобства. Так что DDK - лучший вариант. Если же ты хочешь писать драйвера исключительно на ассемблере, тебе подойдёт KmdKit (KernelMode Driver DevelopmentKit) для MASM32. Правда, этот вариант только для Win2k/XP.
Напоследок нельзя не упомянуть такие хорошие проги, как PE
Explorer, PE Browse Professional Explorer, и такие незаменимые, как дизассемблер IDA и лучший отладчик всех времён и народов SoftICE.
Ну вот и подошла к концу первая статья из цикла про написание драйверов под Windows. Теперь ты достаточно "подкован" по
теоретической части, так что в следующей статье мы перейдём к практике. Желаю тебе удачи в этом интереснейшем деле - написании драйверов! Да не облысеют твои пятки!
Читайте также: