Всем привет. Появилось время и решил написать что-то необычное на VB6, а именно попытаться написать драйвер. Сразу скажу до этого я никогда не писал драйвера и не имею никакого опыта программирования в режиме ядра. Драйвер, по моим задумкам, должен будет читать память недоступную в пользовательском режиме, а именно в диапазоне 0x80000000 - 0xffffffff (в режиме по-умолчанию, без IMAGE_FILE_LARGE_ADDRESS_AWARE). Сразу приведу исходный код драйвера который получился:
Итак, драйвер должен иметь точку входа DriverEntry, которую вызывает диспетчер ввода/вывода при загрузке драйвера. В параметрах передается указатель на объект-драйвер и указатель на строку с именем ключа реестра, соответствующего загружаемому драйверу. В процедуре Init мы создаем 2 строки, одна с названием устройства, другая с названием ссылки на устройство. Т.к. мы не можем использовать рантайм в режиме ядра, то приходится создавать строку в виде статического массива, обернутого в пользовательский тип, тем самым VB6 выделяет память под этот массив в стеке. Если использовать строку, то неизбежно будет вызвана одна из функций рантайма для копирования и присваивания строки, а этого мы допустить не можем. Далее мы вызываем IoCreateDevice, которая создает объект-устройство. Объект-устройство является получателем запросов ввода/вывода и к нему мы будем получать доступ при вызове функции CreateFile из пользовательского режима. В качестве первого параметра передается указатель на объект-драйвера; вторым параметром передаем 0, т.к. у нас нет структуры расширения устройства и нам не нужно выделять память; третьим параметром мы передаем имя устройства, оно нам понадобится для реализации доступа к устройству; четвертым параметром передается тип устройства (см. ниже); в пятом мы передаем 0, т.к. у нас "нестандартное устройство"; в шестом передаем False, т.к. нам не нужен монопольный режим; последний параметр - выходной. В качестве имени устройства мы должны использовать строку вида \Device\DeviceName (где DeviceName - TrickMemReader), это имя нам понадобится для того, чтобы мы могли создать ссылку на него, которая в свою очередь нужна для доступа к устройству из пользовательского режима. Тип устройства у нас - FILE_DEVICE_MEMREADER. Все нестандартные устройства должны иметь тип либо FILE_DEVICE_UNKNOWN, либо число от 0x8000 - 0xffff. Я создал константу FILE_DEVICE_MEMREADER со значением 0x8000, что соответствует первому свободному номеру. При успехе, создается устройство и заполняется структура DEVICE_OBJECT. После нужно создать связь по имени между устройством из режима ядра и пользовательским режимом. В качестве имени мы используем \DosDevices\TrickMemReader, из пользовательского режима мы будем обращаться к нему через ссылку '\\.\TrickMemReader". Ссылка создается через IoCreateSymbolicLink. Далее мы определяем callback-процедуры, которые будут вызываться при определенных событиях:
DriverUnload - при деинициализации драйвера;
DriverCreateClose - при открытии и закрытии устройства;
DriverDeviceControl - при вызове DeviceIoControl.
Все. Теперь мы возвращаем STATUS_SUCCESS , что соответствует успешному выполнению.
Теперь рассмотрим процедуру DriverUnload. Здесь все просто - мы удаляем связь и созданное устройство. В функции обработки открытия и закрытия устройства DriverCreateClose, в статусе запроса мы возвращаем успех, и возвращаем IRP пакет менеджеру ввода/вывода. Обмен данными между приложением и устройством осуществляется через IRP-пакеты. IRP-пакет состоит из 2-х частей: заголовок и стек переменной длины. Часть структуры представлена типом IRP. Итак, теперь мы добавляем функциональность нашему драйверу в функции DriverDeviceControl. В эту функцию диспетчер ввода/вывода будет передавать IRP-пакет с данными переданными из клиентского приложения, которые мы будем формировать вызовом функции DeviceIoControl. В качестве параметров мы будем передавать 2 Long числа: 1-е адрес, откуда производить чтение, 2-е количество байт для чтения. Также одним из передаваемых параметров в IRP-пакете, при вызове DeviceIoControl, является управляющий код ввода/вывода (IOCTL), который представляет собой структуру из типа устройства, номера функции, типа передачи данных и тип доступа. Можно определить несколько таких кодов для разных операций и использовать их. Я определил код так IOCTL_READ_MEMORY = 0x80002000, 8000 - соответствует типу нешего девайса (FILE_DEVICE_MEMREADER); номер функции = 0x800, значения ниже зарезервированы, для пользовательских функций разрешены значения 0x800 - 0xFFF; тип передачи данных - 0x0 (METHOD_BUFFERED), это значит что мы будем принимать/передавать данные через буфер, определяемый параметром SystemBuffer IRP-пакета); тип доступа - FILE_ANY_ACCESS. Наглядно:
Итак, в функции DriverDeviceControl мы получаем указатель на стек ввода/вывода IRP-запроса с помощью функции IoGetCurrentIrpStackLocation, которая возвращает его из параметра lpCurStackLocation. При упехе (если указатель ненулевой) копируем в локальную структуру IO_STACK_LOCATION параметры на которые указывает этот указатель. Теперь мы проверяем IOCTL-код и поле AssociatedIrp, которое представляет собой объединение (в VB6 нет объединений) в котором хранится указатель на SystemBuffer. Т.к. у нас тип передачи данных соответствует METHOD_BUFFERED, то в параметре SystemBuffer содержится указатель на буфер с параметрами (адрес и размер) DeviceIoControl, в этом буфере мы также можем возвратить данные которые записываются в выходной буфер DeviceIoControl. Теперь если у нас данные содержат корректные значения (IOCTL и SystemBuffer), то мы копируем в локальные переменные параметры (lpPointer, DataSize). Далее проверяем размер буфера. Размер системного буфера ввода/вывода содержится в параметре DeviceIoControl.OutputBufferLength. Если запрошенное количество байт не больше чем размер системного буфера, то все отлично. Теперь мы должны вычислить количество страниц памяти занимаемой данными которые мы хотим скопировать. Для этого мы определяем виртуальный адрес начала страницы соответствующей переданному указателю, и т.к. размер страниц кратен 4 кБ (0x1000) мы просто зануляем 12 бит указателя. Далее мы проверяем в цикле не будет ли вызвано исключение Page fault с помощью функции MmIsAddressValid. Если страница отсутствует в ОЗУ, то функция возвратит False. Таким образом мы проверяем количество страниц которые занимает нужный нам участок памяти и количество страниц которые мы сможем прочитать. Далее мы вычисляем реальный размер данных, которые мы сможем прочесть и при необходимости корректируем размер. Далее в заголовок IRP-пакета мы копируем размер данных, который мы можем прочесть и успешный статус. Поле IoStatus.Information пакета соответствует значению которое возвращает DeviceIoControl в параметре lpBytesReturned. Далее копируем в SystemBuffer нужное количество байт с помощью RtlMoveMemory и возвращаем IRP-пакет менеджеру ввода/вывода. Возвращаем статус успешной операции. Во всех остальных случаях возвращаем ошибку STATUS_INVALID_PARAMETER и нулевой размер данных. Все, код драйвера готов.
Приступим к компиляции. Т.к. мы не можем использовать рантайм, все API-функции мы объявляем в TLB, для того чтобы они попали в импорт:
PS. InterlockedExchange - я оставил, т.к. вначале драйвер имел немного другую структуру, в последствии оставил объявление в TLB. В драйвере она не попадет в импорт.
Для того чтобы драйвер работал нужно сделать три вещи:
В поле Subsystem структуры IMAGE_OPTIONAL_HEADER PE-файла драйвера должно быть значение IMAGE_SUBSYSTEM_NATIVE что соответствует драйверу режима ядра.
Указать в качестве точки входа нашу процедуру DriverEntry
Добавить секцию релокации, для того чтобы драйвер мог загружаться по любому адресу.
Исключить MSVBVM60 из иморта.
Для первых 3-х пунктов добавляем ключи компиляции в vbp-файл, со следующим содержимым:
Компилируем проект со всеми опциями оптимизации. Для исключения рантайма из импорта, я использую свою утилиту Patch, которую я использовал тут. Я немного доработал ее, т.к. изначально не мог запустить драйвер и долго ломал голову из-за чего это происходит, а причина была в контрольной сумме. После исключения из импорта библиотеки контрольная сумма поменялась, а я не обновлял ее. В EXE файлах, DLL и т.д. это поле не проверяется, а в драйверах проверяется. Для проверки смотрим импорт в любой программе для просмотра PE:
Как видим рантайма нет. Что нам и требовалось.
Для тестирования драйвера я написал простую программку, которая загружает драйвер и работает с ним.
Драйвер должен лежать в той же папке что и программа. Код прокомментирован, так что я не буду описывать его работу.
Для отладки драйвера нужно использовать ядерный отладчик. Отлаживать будем на виртуальной системе (VMware) - Windows XP. В качестве отладчика возьмем Syser, выберем наш драйвер и нажмем Load. Система остановится, и мы перейдем в окно отладчика:
Мы находимся в начале функции DriverEntry. Первый CALL соответствует вызову функции Init. Если мы проследим пошагово (F8) что там внутри, то увидим как заполняется структура и вызывается RtlInitUnicodeString для имени устройства и символической связи. Второй CALL соответствует функции NT_SUCCESS, смотрим что она возвращает TRUE (в регистре EAX) и код прыгает после проверки (TEST EAX, EAX) на ноль (False) дальше:
Как видно код заталкивает в стек параметры для функции IoCreateDevice от последнего к первому с помощью PUSH'ей. Начнем проверку параметров. Проверим имя устройства (3-й параметр - PUSH 0f8a2c010), для этого введем команду d 0f8a2c010 (что значит просмотреть дамп памяти по адресу f8a2c010 и смотрим содержимое:
первые 8 байт - это наша переменная DeviceName. Первые два слова - соответственно длина строки и максимальная длина строки в байтах. Следующее двойное слово - указатель на строку, смотрим (d f8a2c0d8 учитываем порядок байтов little-endian):
, то что нужно там Unicode строка с именем устройства. Если посмотреть на параметр Device (последний выходной параметр - PUSH 0f8a2c020), то можно увидеть что он отличается от имени на 0x10 байт. Теперь посмотрим на декларации переменных, переменная Device задекларирована после DeviceName и DeviceLink, общей длиной 8 + 8 = 0x10 байт. Т.е. порядок расположения переменных в памяти соответствует порядку объявления в коде. Проверяем первый неконстантный параметр ESI, в самом начале в него копируется значение по адресу ESP+0xC. Регистр ESP - указывает на вершину стека. Если пройти в начало функции DriverEntry, то можно увидеть сохранение в стеке двух регистров ESI и EDI (по соглашению StdCall эти регистры находятся в списке сохраняемых, т.е. процедура не должна изменять их после вызова). DriverObject передается в первой паременной, т.е. ближе всех к вершине стека, также после всех параметров сохраняется адрес возврата - т.е. параметр DriverObject до выполнения первой инструкции в функции DriverEntry находится по адресу ESP+4 (стек растет в сторону уменьшения адресов), после двух PUSH'ей он соответственно смещается еще на 8 байт, в итоге DriverObject находится по адресу ESP+0С, все правильно. Параметры корректные, можно вызывать функцию. Жмем F10 чтобы не заходить внутрь IoCreateDevice и смотрим значение регистра EAX после вызова, там должно быть неотрицательное число, что сигнализирует что функция отработала без ошибок. У меня она возвратила 0 (STATUS_SUCCESS), все отлично. Дальше идет уже знакомая процедура по адресу 0xF8A2B750 - NT_SUCCESS:
При успехе идет прыжок на 0xf8a2b7bf, где идет заталкивание в стек параметров для функции IoCreateSymbolicLink. Параметр DeviceName мы уже проверяли, проверяем DeviceLink:
То что нужно. Жмем F10, тестируем EAX, при успехе идем дальше при неудаче, удаляем девайс и выходим с ошибкой. Процедура по адресу 0xf8a2bbb0 - это GetAddr, которая просто возвращает переданное ей значение:
Дальше идет копирование адресов по смещениям DriverObject, если посмотреть деларации то можно увидеть что по смещению 0x34 записывается адрес DriverUnload, по смещению 0x38 - MajorFunction(0) и т.д. Записываемые значения соответствуют адресам функций в нашем драйвере. Дальше происходит обнуление EAX (возвращаемой значение) и выход из процедуры DriverEntry. Все работает без ошибок, идем дальше. Итак, чтобы отследить работу драйвера мы поставим точку останова на функцию DriverDeviceControl. Адрес ее можно взять по только что записанным смещениям в структуре DRIVER_OBJECT либо найти простым просмотром и анализированием кода. В моем тесте адрес равен 0xf8a2b870, переходим на него (. 0xf8a2b870) и нажимаем F9, ставя точку останова. Напротив инструкции установится маркер:
Теперь при вызове этой функции отладчик остановит выполнение кода и даст нам возможность пошагово выполнить код. Функции DriverCreateClose и DriverUnload я не буду описывать, т.к. там все просто. Жмем F5, тем самым продолжая выполнение в обычном режиме. Нас тут же переносит обратно в Windows. Теперь мы запускаем наше тестовое приложение, вводим какой-нибудь адрес (например 81234567) и жмем на кнопку Read. Наш вызов перехватывает отладчик и мы можем продолжить тестировать код функции DriverDeviceControl.
Подробно внутри я не буду описывать код, остановлюсь на самом копировании:
Сразу смотрим на стек (регистр ESP), видим что передаются правильные параметры. На всякий случай делаем дамп, потом сравним:
Нажимаем F5 - и возвращаемся в Windows. Смотрим на дамп уже в нашей программе:
Как видим все отлично скопировалось. Попробуем скопировать данные на границе страниц, так чтобы одна страница отсутствовала. Экспериментальным методом была найдена такая страница вот что получаем:
Как видим, что данные скопировались корректно, где не получилось там у нас отображаются вопросительные знаки. В выходном параметре DeviceIoControl у нас возвращается количество реально прочитанных байт, его мы и используем для отображения вопросительных знаков.
________________________________________ _____________________________________
Как видим на VB6 можно написать простой драйвер, а если использовать ассемблерные вставки можно и посерьезнее что-нибудь написать. Всем спасибо за внимание. Удачи!
Сурово
Без строк весьма грустно, но можно запилить препроцессор, который будет заменять строки на массив байт
Visual Basic
Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
PrivateSub CreateName(sName AsString)
Dim i AsLongDim q AsStringDim o AsStringFor i = 1 To Len(sName)
q = Space(18)
LSet q = "S.D(" & i - 1 & ") = &H" & Hex(AscW(Mid$(sName, i, 1))) & ":"If ((i - 1) Mod 5) = 0 Then o = o & vbNewLine & q Else o = o & q
Next
Debug.Print o
EndSub
Что такое замыкание? В простейшем определении, замыкание - это функция, которая запоминает и хранит доступ к переменным из охватывающей её области видимости, даже когда эта функция вызывается за. . .
В монолитных системах сервисы обычно общаются напрямую через HTTP-запросы. Простой подход, удобный для начала разработки — но что происходит, когда система растёт? Синхронное взаимодействие быстро. . .
CI/ CD для Python-разработчиков – это насущная необходимость. Представьте: вы пишете код, запускаете тесты, собираете пакет, отправляете его в репозиторий, развёртываете приложение. А теперь умножьте. . .
Связывание в C++ — одна из тех "невидимых" технических сторон программирования, о которой многие имеют лишь поверхностное представление, хотя эта концепция критически влияет на производительность,. . .
Многопоточное программирование в C# — мощный инструмент, позволяющий использовать преимущества современных многоядерных процессоров и создавать отзывчивые приложения. Однако наряду с преимуществами,. . .
Результаты исследования от команды MCM (март 2025 г. )
В рамках наших текущих исследований мы продолжаем изучать гены, которые имеют наибольшую вероятность развития рака легких, выявленные в рамках. . .
Рекурсивные типы - это типы данных, которые определяются через самих себя или в сочетании с другими типами, которые в свою очередь ссылаются на исходный тип. В мире программирования такие структуры. . .
Работа с последовательностями данных – одна из фундаментальных задач, с которой сталкивается каждый разработчик. C++ прошел длинный путь в эволюции средств для манипуляции коллекциями – от. . .
Когда разработчики начинают погружаться в мир микросервисов, они часто сталкиваются с парадоксальным правилом: "два сервиса не должны делить один источник данных". Эта мантра звучит повсюду в. . .