Простой 32-битный драйвер на ассемблере

Метки:  ,

В предыдущей статье, описывающей алгоритм установки драйвера в систему Windows, мы слегка разбирались в том, как проходят стадии обнаружения оборудования и установки соответствующих драйверов. Вскоре после этого родилась идея перейти к практической части и сделать попытку написания собственного, самого простого драйвера на ассемблере. Нет, не каких-то монументальных циклов статей, а исключительно для себя, так сказать, что бы как-то уже начать изучение области разработки драйверов на Ассемблере под операционную систему Windows. После продолжительного исследования направления выяснилось, что более-менее серьезная разработка драйверов для Windows ведется на языке программирования C/C++, это объясняется тем, что данный язык является фактически "языком системы", то есть на нем, собственно, многие части системы и написаны. Однако это вовсе не исключает утверждения, что и на ассемблере вполне себе можно разработать драйвер. Да, соглашусь что делать это не так уж и сподручно, по сравнению, например, с тем же C, поскольку для последнего даже выпущен специализированный пакет разработки (DDK), но тем не менее все же возможно.

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

И, поскольку драйвер первый и самый простейший, то начнем мы, пожалуй, с написания драйвера под 32-битную версию системы Windows. Понимаю что сейчас на дворе уже 21 век, и доминирование 64-битных операционных систем на рынке с каждым годом все заметнее, тем не менее, на этапе "нулевого знания" пример с 32-битным драйверов будет наиболее показателен. Соответственно:

Рассматриваемый в статье драйвер является 32-битным, поэтому работа его в 64-битной системе не гарантирована, и даже более того - невозможна.

После изучения некоторого количества материала, найденного в Сети, стало очевидным что не имея опыта программирования в режиме ядра, довольно сложно изобрести что-нибудь оригинальное и, что самое главное, это оригинальное потом еще и реализовать :) Первое, что приходит в незамутненную опытом голову после прочтения теории драйвера - это реализация элементарного драйвера, содержащего одну единственную процедуру инициализации DriverEntry. Как вы уже должны были понять по указанному выше материалу - без неё при написании драйвера нам никак не обойтись, она должна присутствовать в каждом без исключения драйвере. Но чем эта процедура будет у нас заниматься, какой код мы могли бы разместить внутри? Понятное дело, что для простого драйвера на ассемблере и логика должна быть достаточно простой, поскольку при любом ином раскладе мы рискуем запутать процесс обучения настолько, что сложно будет что-либо понять. Можно было бы просто-напросто скомпилировать пустой каркас драйвера, указав процедуру DriverEntry с инструкцией возврата ret, но при таком подходе как мы получим какое-либо подтверждение о том, что наш драйвер выполняется? Поэтому, было бы интересно получить какой-нибудь осязаемый результат, например визуальное или аудиальное подтверждение. С пониманием, как именно выводить на экран монитора из кода защищенного режима (драйвера) у нас пока тяжеловато, а вот со звуком дела обстоят попроще, поскольку в Сети можно встретить примеры с выводом на встроенный динамик персонального компьютера, всеми уже заезженная вдоль и поперек задача. И в нашем примере я решил реализовать проигрывание известной мелодии под названием "Имперский марш" из фильма Звездные войны (так же известную как "тема Дарта Вейдера"). Традиционно, для начала приведем исходный код драйвера:

Первой строкой в исходном коде задается формат выходного (получаемого по завершении процесса компиляции) исполняемого файла. Но в нашем примере впервые для многих встречается неизвестный нам ранее формат native и указание на некое "смещение" со значением 10000h. Как мы уже писали в этой статье, native - это "родной" (нативный) формат приложения, обычно используемый для драйверов (фактически библиотек/приложений режима ядра, которым не требуется инициализация подсистемы Win32 на стадии подготовки образа к исполнению). Значение 10000h - это предпочитаемый базовый адрес образа, то есть адрес загрузки драйвера в адресное пространство ядра, по которому он хотел бы расположиться. Тем не менее, загрузчик образов, видя значение в поле ImageBase, самостоятельно принимает решение о загрузке образа по указанному адресу, в подавляющем большинстве случаев загрузчик размещает драйвер по другому адресу. Но для того, что бы выполнить подобное перемещение, требуется изменить указатели переходов, переменных и других данных в коде драйвера, ведь компилятор жестко привязывает адреса в коде к выбираемому при компиляции базовому смещению. Именно для этой ситуации и существует таблица перемещений (relocation table, секция .relocs в конце), где указаны все адреса, которые требуют изменения. Затем производится связывание (fix-up) импорта.

Почему для драйвера так важно описывать секцию таблицы перемещений в исходном коде, ведь для типовых Win32-приложений мы этого не делаем? Я думаю, что ответ тут заключается в том, что для типового приложения создается виртуальное адресное пространство процесса, поэтому загрузчик всегда размещает код по предпочитаемому адресу. Для драйвера же он подобного обеспечить не может, поскольку драйвер загружается в адресное пространство ядра, где совсем всё по-другому.

В строке 2 кода драйвера мы наблюдаем определение точки входа (директива entry) драйвера на функцию под названием DriverEntry. На основании диалекта FASM, точкой входа является первая инструкция метки, указанной после директивы entry. После прохождения процедуры загрузки драйвера в память, менеджер ввода-вывода вызывает данную функцию, иными словами система передает управление по на первый байт функции DriverEntry после загрузки драйвера в память.

Код функции DriverEntry всегда выполняется в качестве одного из потоков процесса System, в контексте этого же процесса.

Как мы помним из общей теории драйвера, это функция инициализации, которая должна присутствовать в каждом драйвере, она входит в минимально-необходимый реализуемый набор функций. В штатных приложениях точка входа задается на произвольную часть кода, однако в драйвере точка входа является адресом функции. Давайте обратимся к официальной документации MSDN, и посмотрим прототип процедуры выглядит следующим образом:

NTSTATUS DriverEntry( _In_ struct _DRIVER_OBJECT *DriverObject, _In_ PUNICODE_STRING RegistryPath );

или более привычный нам по диалекту FASM:

DriverEntry DriverObject:DRIVER_OBJECT, RegistryPath:UNICODE_STRING

Как мы видим, процедура принимает два входных параметра: указатели на некие структуры с именами DriverObject и RegistryPath предопределенных типов. Данные типы определены в файле включения \INCLUDE\DDK\ntddk.inc пакета FASM и их определения выглядят следующим образом:

и

где:

  • DriverObject - указатель на объект только что созданного драйвера (структура DRIVER_OBJECT). Первый параметр, передающийся в функцию DriverEntry. Перед вызовом процедуры DriverEntry диспетчер ввода-вывода, наряду с другими действиями по подготовке кода драйвера к запуску, создает объект "драйвер" (driver object), который представляет ключевые параметры драйвера для системы. Получается, что именно посредством этого объекта система "управляет" нашим драйвером. Объект представляет собой структуру данных типа DRIVER_OBJECT, некоторые поля которой заполняет система, а другие придется инициализировать нам самим, и часто это производится именно в процедуре DriverEntry. Используя этот указатель в коде, мы можем заполнить требуемые нам поля структуры DRIVER_OBJECT, однако в рассматриваемом нами простейшем драйвере в этом нет необходимости.
  • RegistryPath - указатель на раздел реестра (дерево \Registry\Machine\System\CurrentControlSet\Services\Имя_драйвера), содержащий параметры инициализации драйвера. Фактически это указатель на структуру типа UNICODE_STRING, которая содержит указатель непосредственно на саму Unicode-строку, содержащую имя раздела. Указатель используется в коде драйвера для чтения или записи в реестр ключевой информации, которую драйвер может в дальнейшем использовать. В нашем случае мы, опять же, не работаем с данным параметром.

Сама структура UNICODE_STRING выглядит следующим образом:

Поле Описание
Length Длина строки в байтах (не в символах), без учета нуль-терминатора (символа завершающего нуля);
MaximumLength Длина в байтах (не в символах) буфера, указываемого через член структуры Buffer;
Buffer Указатель непосредственно на Unicode-строку (не всегда, кстати, завершающейся нулем).

Итак, вернемся к разбору кода драйвера. В строка 10-34 у нас размещаются константы нот и соответствующие им частоты, из них у нас будет формироваться мелодия в секции данных. Чуть ниже по листингу мы видим функцию DriverEntry, в которой происходит вызов внутренней процедуры PlaySound, являющейся фактически основной полезной нагрузкой драйвера, именно она проигрывает мелодию на спикере. Давайте подробнее рассмотрим код самой процедуры PlaySound (размещенный в строках 48-71). Начинается он с сохранения регистров, которые меняются в самой процедуре, потом выставляется флаг направления, как вы уже догадались для осуществления цикла чтения нот и их проигрыша. Загружаем в регистр esi указатель на буфер, который указывается входным параметром buffer процедуры, этот буфер должен содержать проигрываемую мелодию в следующем формате:

Частота, Задержка, Частота, Задержка, Частота, Задержка, Частота, Задержка ...

В регистр ecx загружается счетчик цикла, он же размерность массива (деленная на 8, поскольку каждая итерация цикла будет проигрывать целую тональность, а она у нас имеет по 4 байта на частоту и 4 на задержку). Следом происходит загрузка в регистр eax двойного слова из памяти, указанной смещением из регистра esi, это у нас частота. И считанная частота передается на вход следующей по коду функции HalMakeBeep, которая работает с динамиком напрямую через порты. Функция HalMakeBeep экспортируется системной библиотекой hal.dll и является, честно говоря, не самым лучшим выбором, поскольку не является документированной и, соответственно, официально разработчиками не обеспечивается ее неизменное состояние от версии к версии системы. Тем не менее, что-то более простое для вывода звука придумать сложно, в противном случае для вывода мелодии нам пришлось бы программировать порты напрямую, что создало бы нам для первого раза довольно избыточную логику, которая (в свою очередь) потянула бы за собой задачу по изучению принципов работы перепрограммируемого интервального таймер и программируемого контроллера периферийного интерфейса Intel 8255? А библиотека hal.dll предоставляет нам готовую функцию одной из библиотек ядра, являющейся самым "низким" уровнем перед оборудованием компьютера и используемых для сопряжения с аппаратной частью, функции её просты и интуитивно-понятны, поэтому то выбор был сделать в её пользу.

Если в коде вызова функции вы активно работаете с регистрами общего назначения (eax, ebx, ecx, edx ...), то будьте внимательны, поскольку многие функции режима ядра (как впрочем и некоторые функции Win32 API обычных приложений) в своем теле не сохраняют/не восстанавливают состояния некоторых регистров общего назначения, что в ситуации с кодом режима ядра приводит порой к падению в BSOD. Решением проблемы видится самостоятельное сохранение регистров перед вызовом и последующее восстановление.

Поэтому вызов функции HalMakeBeep у нас обрамляет пара инструкций pushad-popad, что не является оптимальным решением, но для нашего говнокода вполне сгодится :) Следуем далее по коду, в строке 60 у нас загружается следующее в буфере двойное слово, которое используется как задержка для звучащей в данный момент частоты, таким образом создается длительность звучания.
Как вы уже поняли, мелодия в нашем случае представляет собой набор частот с соответствующими задержками. Хотелось бы отдельно поговорить о задержках, и обратить внимания на два связанных с этим момента:

  • указаны в массиве в качестве числа (например: 350*M), поэтому на разных конфигурациях ПК они будут звучать с разным темпом, все зависит от значения константы M (указанной в строке 7).
  • реализованы в коде в виде пустого цикла из пары инструкций dec + jnz (строки 61-62), то есть грузят ЦП.
Более элегантным способом была бы реализация задержки с использованием функций KeStallExecutionProcessor, KeDelayExecutionThread, но моих скудных знаний не хватает на то, что бы их правильно использовать.

Ну, вернемся к основному коде, далее в приложении (строка 64) динамик у нас отключается подачей значения 0 на вход функции HalMakeBeep. Ну и после этого код у нас входит в цикл повторения заданное количество раз всех описанных выше действий, перебирая каждую записи в буфере мелодии.
После того, как код функции DriverEntry отработал, управление возвращается диспетчеру ввода-вывода (строки 44-45). Но тут вот есть один интересный момент, функция DriverEntry, как и подавляющее большинство других функций Windows, должна вернуть код возврата, сигнализирующий о статусе завершения функции. В штатном режиме, когда инициализация драйвера проходит успешно, функция возвращает статус STATUS_SUCCESS, но в нашем случае мы пишем драйвер-пустышку, который кроме кода своей процедуры инициализации (проигрывание мелодии) ничего не делает, так зачем же он нам в памяти ядра постоянно загруженным? Поэтому есть предложение не оставлять его в памяти, а посему в нашем коде мы возвращаем системе в регистре eax значение STATUS_DEVICE_CONFIGURATION_ERROR (NTSTATUS код 0C0000182h), то есть код фиктивной ошибки, после чего система должна удалить драйвер из памяти (в действительности же этого не происходит и драйвер висит в памяти со статусом Stopped).
Сама мелодия размещается в блоке данных с меткой sound_buffer (строки 73-77). Блок содержит, как мы уже отмечали выше, чередующиеся значения частоты и задержки. Частота у нас задается константами, определенными в начале исходного кода драйвера (строки 10-34) и носящими имена соответствующих нот. Каждой константе (ноте) соответствует своя частота.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *