Идея написания драйвера псевдоустройства возникла не сразу. В самом начале пути для меня на первом плане стоял вопрос – а пригодится ли такая программа вообще? Востребуется ли рядовым программистом, а тем более пользователем, механизм блокировки дисковых квот или, скажем, запись в память с атрибутом «нет доступа»? Теперь, когда получены первые результаты испытаний в операционной системе Windows NT, сомнений больше не осталось. По моему мнению наиболее полезной эта программа окажется для экспертов в области информационной безопасности, использующих ОС WindowsNT, т.е. данная программа поможет разработчикам эффективных приложений для обеспчение информационной безопасности WindoewsNT.
1. Актуальность темы
Идея написания драйвера псевдоустройства возникла не сразу. В самом начале пути для меня на первом плане стоял вопрос – а пригодится ли такая программа вообще? Востребуется ли рядовым программистом, а тем более пользователем, механизм блокировки дисковых квот или, скажем, запись в память с атрибутом «нет доступа»?
Теперь, когда получены первые результаты испытаний в операционной системе Windows NT, сомнений больше не осталось. По моему мнению наиболее полезной эта программа окажется для экспертов в области информационной безопасности, использующих ОС WindowsNT, т.е. данная программа поможет разработчикам эффективных приложений для обеспчение информационной безопасности WindoewsNT.
Мы действуем в обход традиционных схем, производя недопустимые в обычном API операции. Но... мы не используем недокументированные функции и структуры. Все, что здесь описывается, в ОС Windows было предусмотрено, и я не удивлюсь, если программные аналоги драйвера уже написаны сторонними разработчиками.
Я предполагаю наличие у читателя не базовых знаний о подсистеме Win32, а, как минимум, знакомства со следующей литературой:
1. Рихтер Джеффри, "Windows для профессионалов: создание эффективных Win32 приложений с учётом специфики 64-разрядной версии Windows"
2. Саймон Ричард, "Windows 2000 API. Энциклопедия программиста"
3. Microsoft® Windows®XP Driver Development Kit, Microsoft Corporation 2001
4. Microsoft® Windows®XP Software Development Kit, Microsoft Corporation 2001
5. Understanding and Using Execution Context in NT Drivers, OSR Open Systems Resources, Inc.
6. Defining Custom Device I/O Control Codes, OSR Open Systems Resources, Inc.
Желательно также иметь представление об архитектурной модели драйверов устройств. Особый акцент сделан на взаимодействии программной среды с системным менеджером ввода/вывода.
2. Режим ядра и пользовательский режим
Микропроцессор Pentium имеет четыре уровня привилегий (privilege levels), известных также как кольца (rings), которые управляют, например, доступом к памяти, возможностью использовать некоторые критичные команды процессора (такие как команды, связанные с защитой) и т.д. Каждый поток выполняется на одном из этих уровней привилегий. Кольцо 0 - наиболее привилегированный уровень, с полным доступом ко всей памяти и ко всем командам процессора. Кольцо 3 - наименее привилегированный уровень.
Для обеспечения совместимости с системами на базе процессоров, отличных от тех, что выпускает компания Intel, Windows NT поддерживает только два уровня привилегий - кольца 0 и 3. Если поток работает в кольце 0, говорят, что он выполняется в режиме ядра (kernel mode). В литературе режим ядра иногда также называют режимом супервизора. Если поток выполняется в кольце 3, говорят, что он работает в пользовательском режиме (user mode). В пользовательском режиме работают приложения и подсистемы обеспечения среды (Win32, OS/2 и POSIX). Графическая система, драйверы графических устройств, драйверы принтеров и диспетчер управления окнами выполняются в режиме ядра (см. схему «Архитектура Windows NT в упрощённом виде»). Режим выполнения user mode значительно более надежен, но требует дополнительных затрат, которые снижают общую производительность процесса. Выполняемый в пользовательском режиме код не может нарушить целостности исполняющей системы Windows NT, ядра и драйверов устройств. Интересно, что драйверы устройств работают в режиме ядра. Это обстоятельство имеет два следствия. Во-первых, в отличие от неправильно выполняющегося приложения неправильно работающий драйвер устройства может нарушить работу всей системы, так как он имеет доступ и ко всему системному коду и ко всей памяти. Во-вторых, прикладной программист может получить доступ к защищённым ресурсам, написав драйвер псевдоустройства (fake device), хоть это и не лёгкая задача.
3. Контекст выполнения потока
Наши знания о контекстах выполнения никогда не были и не будут исчерпывающими. Одной из основных причин этого является нежелание разработчиков операционных систем типа Windows открывать платформо-зависимые детали их реализации. Между тем, понимание того, как операционная система использует концепцию контекстов выполнения позволит писать более эффективные и интересные программы.
Контекст исполнения (execution context) - это моментальный снимок состояния среды выполняющегося потока и его родительского процесса. В документации SDK и DDK объединённые контексты User- режима (Ring 3) характеризуются следующими компонентами:
Структура контекста:
1. процессорно-зависимая таблица состояния регистров (platform-dependent register state).
2. стек потока пользовательского режима в адресном пространстве родительского процесса.
3. стек потока режима ядра (kernel stack).
4. блок переменных окружения потока (Thread Environment Block, TEB).
5. блок переменных окружения процесса (Process Environment Block, PEB).
6. таблица соответствия физической и виртуальной памяти.
7. таблица описателей (handles) объектов ядра.
8. информация для планировщика
Некоторые из этих элементов располагаются в адресном пространстве родительского процесса, другие – в компактных структурах объектов ядра «процесс» и «поток».
Контекст виртуальной памяти, пожалуй, один из наиболее интересных аспектов контекста выполнения для программистов. По умолчанию NT отображает пользовательский процесс на два нижних гигабайта виртуальной памяти, а код операционной системы на два верхних гигабайта. Когда выполняется поток кольца 3 его адресное виртуальное пространство находится в пределах от 0 до 2 ГБ, а все адреса выше 2 ГБ будут маркированы как «не доступны», исключая тем самым прямой доступ к коду и структурам операционной системы. Специфика организации виртуальной памяти в Windows NT такова, что действительный виртуальный адрес X в среде процесса P (где X <= 2 ГБ) будет указывать на ту же физическую память, что и адрес X в виртуальном адресном пространстве ядра. Конечно, это верно, если только процесс P – текущий процесс и (поэтому) страницы физической памяти отображены на два нижних гигабайта виртуальных адресов. Другими словами, это верно, если P – контекст текущего процесса. Таким образом, виртуальные адреса пользовательского режима и режима ядра в пределах двух первых гигабайт соответствуют одинаковым физическим страницам памяти в одном и том же контексте процесса.
Использование контекста планировщика не менее интересно. Когда поток находится в режиме ожидания (например, вызвав Win32 функцию WaitForSingleObject(…) для занятого объекта ядра), то контекст планировщика хранит информацию о причине ожидания (wait reason). Пока ресурс занят или пока не произошло особое событие, система переводит поток в режим ожидания, исключая его из числа планируемых, и берёт на себя роль агента, действующего в интересах спящего потока. Она выведет его из ждущего режима, когда объект освободится.
Контекст выполнения также включает таблицу описателей (дескрипторов) объектов ядра. Сведения о структуре этой таблицы и управлению ей недокументированны. Значение описателя представляет собой индекс в таблице описателей, принадлежащих процессу, и таким образом идентифицирует место, где хранится информация, связанная с объектом ядра. Фактические значения описателей представляют собой байтовые смещения нужной записи от начала таблицы описателей.
Элементы процессорно-зависимой таблицы состояния регистров зависят от конкретного типа процессора (см. Листинг 1).
4. Переключение контекста выполнения
Переключение контекста потоков происходит либо когда исполняющийся поток добровольно освобождает процессор, либо когда один поток вытесняется другим потоком, имеющим к тому времени более высокий приоритет и готовым к выполнению, либо при переключении между пользовательским и привилегированным режимами для обычного выполнения и обращения к подсистемам. Переключение на процесс подсистемы вызывает одно контекстное переключение для потока приложения, обратное переключение вызывает другое контекстное переключение для потока подсистемы. Заметим, что прикладной поток может переключаться из пользовательского режима в режим ядра при вызове некоторых АРI -функций, которые требуют более высокого уровня привилегий, например, связанных с доступом к файлам или с выполнением функций, ориентированных на графические операции (см. «Схема API-вызова»).
В действительности некоторые пользовательские потоки могут работать в режиме ядра даже больше времени, чем в пользовательском режиме. Но как только завершается выполнение той части кода, которая относится к режиму ядра, пользовательский поток автоматически переключается обратно в пользовательский режим, Такой подход лишает возможности писать код, предназначенный для работы в режиме ядра, программист может только вызывать выполняющиеся в режиме ядра системные функции (system functions).
Следующие разделы данной работы затрагивают особенности среды выполнения драйверов устройств. Материал содержит множество малоизученных, и потому особенно интересных деталей внутренней архитектуры Windows NT.
5. Контексты драйверов режима ядра
Большинство драйверов Windows NT в привычном для нас смысле не имеют контекста.
Когда мы спрашиваем, в каком контексте выполняется та или иная функция драйвера, мы на самом деле задаём вопрос: «Какой текущий поток ассоциирован диспетчером ядра с этой функцией?» Стандартные функции драйвера режима ядра могут выполняться в трёх различных типах контекстов:
контекст системного процесса
контекст определенного пользовательского потока (процесса)
контекст произвольного пользовательского потока (процесса)
Во время выполнения части каждого драйвера режима ядра могут выполняться в каждом из этих трёх перечисленных контекстах. Например, функция драйвера DriverEntry(…) всегда выполняется в контексте системного процесса. Контекст системного процесса не имеет ни ассоциированного с ним контекста user-потока (а значит и TEB), ни user-процесса отображённого на два нижних гигабайта виртуального адресного пространства ядра. С другой стороны, отложенные вызовы функций (deferred procedure calls) выполняются в контексте произвольного пользовательского потока. Это значит, что во время работы DPC любой пользовательский поток может быть «текущим» потоком, и поэтому любой пользовательский процесс может быть отображённым (mapped) на два нижних гигабайта виртуального адресного пространства ядра.
Контекст выполнения стандартных диспетчерских функций представляет особый интерес. В большинстве случаев диспетчерская функция будет выполняться в контексте вызвавшего её пользовательского потока (см. схему «Следование контексту вызвавшего user-потока» в приложениях). Когда пользовательский поток опрашивает устройство вызовом функции ввода/вывода, например, Win32 функции ReadFile(…), происходит запрос системных служб. В процессорах с архитектурой Intel, подобные запросы ввода/вывода выполняются посредством программных прерываний, которые преодолевают шлюз прерываний (interrupt gate). Шлюз прерываний меняет текущий режим привилегий на режим ядра, переводит поток на стек режима ядра, а затем вызывает диспетчера системных служб. Диспетчер системных служб, в свою очередь, вызывает требуемую функцию операционной системы, которая и обрабатывает запрос. Для ReadFile(…) – это функция NtReadFile(…) подсистемы ввода/вывода. Функция NtReadFile(…) формирует пакет данных по запросу ввода/вывода (IRP) и вызывает стандартную диспетчерскую функцию драйвера, которая соответствует файловому объекту, указанному файловым дескриптором в параметрах вызова ReadFile(…). Всё это происходит на IRQL PASSIVE_LEVEL.
Во всё время описанного выше процесса не происходило ничего, что могло бы изменить контекст текущего пользовательского потока (и процесса). В этом примере диспетчерская функция драйвера выполняется в контексте user-потока, вызвавшего ReadFile(…). Другими словами, когда выполняется диспетчерская функция драйвера, пользовательский(!) поток выполняет код драйвера режима ядра.
Всегда ли диспетчерская функция будет выполняться в контексте вызвавшего user-потока? В секции 16.4.1.1 пятой версии DDK говорится, что «Только в драйверах NT верхнего уровня, таких как драйвера файловой системы (FSDs), диспетчерская функция будет гарантированно вызвана в контексте такого потока пользовательского режима». Как видно из нашего примера, это не совсем верно. Без сомнения, FSD будет вызвана в контексте запрашивающего user-потока. Фактически же, любой драйвер, вызванный напрямую по пользовательскому запросу ввода/вывода без посредства любого другого драйвера, будет обязательно вызвана в контексте запрашивающего user-потока. Это относится и к FSD. Но это также значит, что в абсолютном большинстве написанных сторонними программистами драйверов режима ядра, вызванные напрямую диспетчерские функции будут выполнены в контексте запрашивающего user-потока.
Фактически диспетчерская функция драйвера НЕ будет выполнена в контексте запрашивающего user-потока, только когда пользовательский запрос обрабатывается одним из драйверов верхнего уровня, такого как драйвер файловой системы. Если драйвер верхнего уровня передаст запрос потоку системного процесса, то это в большинстве случаев вызовет изменения в контексте выполнения. Когда же, наконец, IRP будет передан драйверу нижнего уровня (целевому драйверу), нет никакой уверенности, что контекст соответствует первоначальному.
Основная идея этого раздела состоит в том, что когда user-поток напрямую обращается к устройству (без посредства других драйверов), диспетчерские функции драйвера устройства ВСЕГДА будут выполнены в контексте запрашивающего потока. Впоследствии мы используем эту особенность архитектуры NT в нашем проекте.
6. Последствия
Каковы последствия того, что диспетчерская функция выполняется в контексте запрашивающего потока? Далеко не все из них оказываются полезными на практике. Например, предположим, что драйвер создаёт файл вызовом функции ZwCreateFile(…) из стандартной диспетчерской функции. Когда этот же драйвер попытается прочитать такой файл, используя, скажем, ZwReadFile(…), чтение завершится успешно только в том случае, если будет произведено в контексте того же пользовательского потока (процесса), в контексте которого файл был создан. А всё потому что файловые дескрипторы хранятся в таблице дескрипторов user-процесса, а не драйвера. Продолжая наш пример, если вызов ZwReadFile(…) произведён в контексте того же потока, то драйвер может дождаться завершения операции чтения, установив объект ядра «событие» (event). Что происходит тогда? Текущий поток переводится в режим ядра, где он спит, пока диспетчер снова не вернёт его в очередь планируемых потоков. Как громоздко для простого асинхронного запроса ввода\вывода! Когда объект ядра «событие» освободится, сигнализируя о завершении вызова ZwReadFile(…), драйвер продолжит выполнение только когда user-поток станет одним из X потоков наибольшего приоритета на платформе с Х процессорами.
Выполнение в контексте запрашивающего потока может сыграть на руку программистам.
Например, вызов функции ZwSetInformationThread(…), используя в качестве дескриптора число -2 (что означает «текущий поток»), позволит драйверу изменить все текущие свойства потока. Аналогично, вызовом ZwSetInformationProcessd(…) с дескриптором, возвращаемым функцией NtCurrentProcess(…) (который в ntddk.h определён как 1) позволяет драйверу изменять характеристики текущего процесса. Не забывайте, что, так как все вызовы происходят в режиме ядра, не производится проверок безопасности. Поэтому возможно таким путём изменять атрибуты потока и/или процесса, которые он самостоятельно изменить не может.
Самое же полезное последствие выполнения драйвера в контексте запрашивающего потока связано с виртуальным адресным пространством. Представьте, к примеру, драйвер простейшего устройства разделяемой памяти (shared-memory type device), которое напрямую используется пользовательским приложением. Будем считать, что операция записи в такое устройство вызывает копирование одного килобайта данных из пользовательского буфера в разделяемую память устройства, и что разделяемая память устройства всегда доступна. Традиционный дизайн драйвера такого устройства, вероятно, использовал буферный ввод/вывод, так как размер данных меньше одной страницы. Поэтому для такого запроса операции записи диспетчер ввода/вывода разместит буфер (в пуле свободной памяти) такого же размера, как и пользовательский буфер и скопирует содержимое пользовательского буфера в буфер пула. Менеджер ввода/вывода затем вызовет стандартную диспетчерскую функцию драйвера, передав указатель на буфер пула в пакете по запросу ввода/вывода (в IRP, а точнее в параметре Irp.AssosiatedIrp.SystemBuffer). Драйвер затем скопирует данные из буфера пула в область разделяемой памяти устройства. Как эффективно подобное решение? Одни и те же данные всегда копируются дважды, не говоря уже о том, что менеджеру ввода/вывода требуется зарезервировать место в пуле. Я бы не назвал такой выход самым эффективным.
Скажем, мы попытались увеличить производительность подобной системы, опять используя традиционные методы. Мы модифицировали драйвер и теперь применяем прямой ввод/вывод. В таком случае, страница, содержащая пользовательские данные, будет проверена на доступность менеджером ввода/вывода, который создаст указатель типа MDL и передаст его в IRP (точнее в Irp.MdlAddress). Теперь, когда драйвер получит IRP, ему требуется полученный MDL преобразовать в системный адрес для последующей операции копирования. Это производится вызовом функции IoGetSystemAddressForMdl(…), а она, свою очередь, вызывает MmMapLockedPages(…), чтобы отобразить содержимое таблицы страниц на адресное пространство режима ядра. С указателем, возвращённым IoGetSystemAddressForMdl(…), драйвер наконец-то может скопировать данные из пользовательского буфера в разделяемую память устройства. Какова эффективность этого решения? Конечно, оно лучше предыдущего, но и здесь проецирование памяти не может не сказаться на скорости всего процесса.
Итак, какова же альтернатива? Учитывая, что пользовательское приложение напрямую общается с драйвером, мы знаем, что стандартная диспетчерская функция драйвера всегда будет вызвана в контексте запрашивающего потока. В результате мы можем избежать сложностей прямого и буферного ввода/вывода – откажемся от них опцией «не использовать ввод вывод» (METHOD_NEITHER). Драйвер укажет, что не хочет использовать ввод/вывод, обнулив битовый флаги DO_DIRECT_IO и DO_BUFFERD_IO для объекта устройства. Когда вызывается стандартная диспетчерская функция драйвера, буфер пользовательских данных уже будет расположен в IRP (точнее в Irp.UserBuffer). Учитывая, что виртуальные адреса пользовательского режима и режима ядра в пределах двух первых гигабайт соответствуют одинаковым физическим страницам памяти в одном и том же контексте, драйвер может сразу использовать адрес буфера и скопировать данные в разделяемую область памяти. Конечно, чтобы избежать проблем с доступом к пользовательскому буферу, драйвер должен заключить операцию копирования в блоке try…except. В итоге – никакого проецирования, никакого повторного копирования, никаких размещений в пуле. Только прямое копирование. Вот что я бы назвал элегантным решением задачи!
Но даже у нашего элегантного решения существует одна тонкость. Что произойдёт , если клиентский поток передаст указатель на адрес, который действителен для драйвера, но вызовет ошибку в рамках пользовательского процесса? Блок try…except не поймает эту ошибку. Например, мы имеем указатель на память, которая спроецирована в адресное пространство процесса с доступом «только для чтения» - она же в режиме ядра доступна как для чтения, так и для записи! Тогда драйвер просто запишет данный в память, доступную приложению «только для чтения». Вызовет ли это ошибку? Всё зависит от конкретного случая. Только Вы можете решить, оправдан ли потенциальный риск преимуществами простоты дизайна.
7. Драйвер псевдоустройства
Когда запрос к драйверу не ЧТЕНИЕ и не ЗАПИСЬ, что же это? Это IOCTL, конечно!
IOCTL – это общепринятое сокращение для «I/O Control». IOCTL-ы –это такие функции, которые передаются драйверу, чтобы совершить особую операцию. В отличии от IRP_MJ_READ и IRP_MJ_WRITE, для которых параметр TransferType определяется в зависимости от комбинации битовых флагов объекта драйвера, IOCTL определяет TransferType на основе универсального макроса CTL_CODE(…):
CTL_CODE(DeviceType, Function, TransferType, RequiredAccess)
Где:
DeviceType – уникальный идентификатор типа устройства. Значения DeviceType на интервале от 32768 до 65535 зарезервированы для сторонних программистов.
Function - уникальный код IOCTL-а, позволяющий однозначно идентифицировать её для драйвера. Коды пользовательских функций на интервале от 2048 до 4095 отведены сторонним разработчикам.
TransferType - выбор метода трансфера для буфера в вызове функции DeviceIoControl(..)
Значения параметра TransferType могут быть: METHOD_BUFFERED,
METHOD_IN_DIRECT, METHOD_OUT_DIRECT и METHOD_NEITHER;
RequiredAccess – атрибуты доступа, необходимые клиентской программе. Значения параметра RequiredAccess могут быть:
FILE_ANY_ACCESS, FILE_READ_DATA и FILE_WRITE_DATA.
В листинге №2 макросом CTL_CODE(…) определён IOCTL_SWITCH_CONTEXTS – именно его код передаётся интерфейсной библиотекой вторым параметром в вызове функции DeviceIoControl(…).
Итоговый пример (см. Листинги 3,4) демонстрирует возможности драйвера, выполняющегося в контексте запрашивающего user-потока. Дистрибутив с драйвером называется “Context Switching Driver” и поставляется со всеми файлами исходного кода. В драйвере реализованы процедуры создания, закрытия и единственная IOCTL, использующая METHOD_NEITHER. Когда пользовательское приложение вызывает IOCTL, оно предоставляет указатель на тип void как входной буфер IOCTL и указатель на функцию (принимающую указатель на void единственным параметром) через буфер возврата. Во время выполнения IOCTL, драйвер вызывает указанную пользовательскую функцию с аргументом PVOID. В итоге пользовательская функция выполняется в адресном пространстве пользовательского процесса, но в режиме ядра!
Если говорить о Windows NT, то область того, чего бы не могла выполнить пользовательская функция в режиме ядра очень мала. Такая функция может вызывать любые функции подсистемы Win32, выводить диалоговые окна, производить файловый ввод/вывод. Единственное отличие в том, что она выполняется в режиме ядра, в стеке режима ядра, а когда приложение работает в режиме ядра, оно не зависит ни от проверок привилегий, ни от политики дисковых квот, ни от системы проверок атрибутов защиты памяти. Ваши возможности с таким драйвером ограничены лишь Вашим воображением (и чувством самосохранения, разумеется).
8. Приложения Примечание: Исходные тексты как и сам продукт не прилагаются к данной статье, но все желающие могут обратиться за ними ко мне на e-mail: matlion@ngs.ru.
а) Схема API-вызова,
б) Схема «Архитектура Windows NT в упрощённом виде»,
в) Схема «Следование контексту вызвавшего user-потока»,
г) Листинг 1
/*WinNT.h*/
#ifdef _X86_ //структура относится к процессорной модели x86
typedef struct _CONTEXT {
//набор битовых флагов, определяющий содержимое структуры
DWORD ContextFlags;
// Отладочные регистры
DWORD Dr0;
DWORD Dr1;
DWORD Dr2;
DWORD Dr3;
DWORD Dr6;
DWORD Dr7;
//регистры с плавающей точкой
FLOATING_SAVE_AREA FloatSave;
// Сегментные регистры
DWORD SegGs;
DWORD SegFs;
DWORD SegEs;
DWORD SegDs;
// Целочисленные регистры
DWORD Edi;
DWORD Esi;
DWORD Ebx;
DWORD Edx;
DWORD Ecx;
DWORD Eax;
// Регистры управления
DWORD Ebp;
DWORD Eip;
DWORD SegCs;
DWORD EFlags;
DWORD Esp;
DWORD SegSs;
// Дополнительные регистры
BYTE ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];
} CONTEXT;
#endif
//ALPHA, _ MIPS_, _AMD64_, _M_IA64 и др.
д) Листинг 2
/*++
Имя модуля:
spurp.h
Описание:
Определяет относящиеся к SPURP тип устройства,
функцию обработки и код IOCTL для приложений и драйвера
--*/
#ifndef SPURP_H
#define SPURP_H
//макрос, определяющий код нового IOCTL
#define IOCTL_SWITCH_CONTEXTS CTL_CODE (FILE_DEVICE_UNKNOWN,0x802,\
METHOD_NEITHER,FILE_READ_DATA | FILE_WRITE_DATA)
#define SPURP_DEVICE_NAME_U L"\\Device\\SPURP"
#define SPURP_WIN_DEVICE_NAME_U L"\\DosDevices\\SPURP"
NTSTATUS
DriverEntry(
IN PDRIVER_OBJECT DriverObject,
IN PUNICODE_STRING registryPath
);
NTSTATUS
SwitchStackDispatchIoctl(
IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp);
VOID
SpurpUnload(
IN PDRIVER_OBJECT DriverObject
);
#endif
е) Листинг 3
/*++
Имя модуля:
spurp.c
Описание:
драйвер, осуществляющий выполнение функций пользователя
с переданными аргументами в режиме ядра
--*/
#include
#include "spurp.h"
#ifdef ALLOC_PRAGMA
#pragma alloc_text (INIT, DriverEntry)
#pragma alloc_text (PAGE, SwitchStackDispatchIoctl)
#pragma alloc_text (PAGE, SpurpUnload)
#endif
NTSTATUS
DriverEntry(
IN PDRIVER_OBJECT DriverObject,
IN PUNICODE_STRING RegistryPath
)
{
NTSTATUS status = STATUS_SUCCESS;
UNICODE_STRING unicodeDeviceName;
UNICODE_STRING unicodeWinDeviceName;
PDEVICE_OBJECT deviceObject;
UNREFERENCED_PARAMETER (RegistryPath);
(void) RtlInitUnicodeString(&unicodeDeviceName,SPURP_DEVICE_NAME_U);
status = IoCreateDevice(
DriverObject,
0,
&unicodeDeviceName,
FILE_DEVICE_UNKNOWN,
0,
(BOOLEAN) TRUE,
&deviceObject
);
if (!NT_SUCCESS(status))
{
return status;
}
//
// Выделяем память и инициализируем UNICODE - строку с именем
// нашего устройства, видимым в подсистеме Win32
//
(void)RtlInitUnicodeString( &unicodeWinDeviceName, SPURP_WIN_DEVICE_NAME_U);
status = IoCreateSymbolicLink(
(PUNICODE_STRING) &unicodeWinDeviceName,
(PUNICODE_STRING) &unicodeDeviceName
);
if (!NT_SUCCESS(status))
{
IoDeleteDevice(deviceObject);
return status;
}
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = SwitchStackDispatchIoctl;
DriverObject->DriverUnload = SpurpUnload;
return status;
}
//++
// SwitchStackDispatchIoctl
//
// This is the dispatch routine which processes
// Device I/O Control functions sent to this device
//
// Параметры:
// DeviceObject - указатель а объект устройства
// Irp - указатель на пакет данных ввода/вывода (I/O Request Packet)
//
// Возвращаемое значение:
// NSTATUS статус завершения IRP
//
//--
NTSTATUS SwitchStackDispatchIoctl(PDEVICE_OBJECT DeviceObject,PIRP Irp)
{
PIO_STACK_LOCATION Io_s;
NTSTATUS Status;
//
// Получить указатель на текущее положение стека ввода/вывода (I/O Stack)
//
Io_s = IoGetCurrentIrpStackLocation(Irp);
//
// Make sure this is a valid IOCTL for us...
//
if(Io_s->Parameters.DeviceIoControl.IoControlCode != IOCTL_SWITCH_CONTEXTS)
{
Status = STATUS_INVALID_PARAMETER;
}
else
{
//
// Получить указатель на вызываемую функцию...
//
VOID (*UserFunctToCall)(PULONG) = Irp->UserBuffer;
//
// и аргумент для передачи
//
PVOID UserArg;
UserArg = Io_s->Parameters.DeviceIoControl.Type3InputBuffer;
//
// Вызвать функцию пользователя с переданными параметрами
//
(VOID)(*UserFunctToCall)((UserArg));
Status = STATUS_SUCCESS;
}
Irp->IoStatus.Status = Status;
Irp->IoStatus.Information = 0;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return(Status);
}
//++
// SpurpUnload
//
// Эта функция освобождает занятые драйвером
// ресурсы и удаляет его объект устройства (device object).
//
// Параметры:
// DeviceObject - указатель на объект устройства
//
// Возвращаемое значение:
// VOID (функция прекращает работу драйвера)
//
//--
VOID SpurpUnload(IN PDRIVER_OBJECT DriverObject)
{
UNICODE_STRING uniWin32NameString;
//
// Удалить видимое пользователю имя устройства
//
RtlInitUnicodeString( &uniWin32NameString, SPURP_WIN_DEVICE_NAME_U );
IoDeleteSymbolicLink( &uniWin32NameString );
//
// Удалить устойство
//
IoDeleteDevice(DriverObject->DeviceObject);
return;
}
ж) Листинг 4
;Имя модуля:
; spurp.inf
;Описание:
; данные установки для драйвера spurp.sys
[Version]
Signature = "$Windows NT$"
Class=System
ClassGUID={4d36e97d-e325-11ce-bfc1-08002be10318}
Provider=%MATLION%
DriverVer= 8/21/2003
[DestinationDirs]
SPURP.Files.x86_12 = 12
[SourceDisksNames.x86]
0=%Desc_x860%
[SourceDisksNames.ia64]
[SourceDisksFiles.x86]
spurp.sys=0,\\spurp,
[SourceDisksFiles.ia64]
[Manufacturer]
%MATLION%=MATLION
[MATLION]
%SPURPDesc%=SPURP_Inst,mics
[SPURP_Inst.ntx86]
CopyFiles = SPURP.Files.x86_12
[SPURP_Inst.ntx86.Services]
AddService = spurp,0x00000002,SPURP_Service_Instx86,
[SPURP_Service_Instx86]
ServiceType = %SERVICE_KERNEL_DRIVER%
StartType = %SERVICE_SYSTEM_START%
ErrorControl = %SERVICE_ERROR_NORMAL%
ServiceBinary = %12%\spurp.sys
[SPURP.Files.x86_12]
spurp.sys
[SPURP_EventLog_Inst]
AddReg = SPURP_EventLog_Inst.AddReg
[SPURP_EventLog_Inst.AddReg]
HKR,,EventMessageFile,%REG_EXPAND_SZ%,"%%SystemRoot%%\System32\IoLogMsg.dll"
HKR,,TypesSupported,%REG_DWORD%,7
[Strings]
; *******Localizable Strings*******
MATLION= "Leonid Arokin"
Desc_x860= "Win2000"
SPURPDesc= "Context switching driver"
; *******Non Localizable Strings*******
SERVICE_BOOT_START = 0x0
SERVICE_SYSTEM_START = 0x1
SERVICE_AUTO_START = 0x2
SERVICE_DEMAND_START = 0x3
SERVICE_DISABLED = 0x4
SERVICE_KERNEL_DRIVER = 0x1
SERVICE_ERROR_IGNORE = 0x0
SERVICE_ERROR_NORMAL = 0x1
SERVICE_ERROR_SEVERE = 0x2
SERVICE_ERROR_CRITICAL = 0x3
REG_EXPAND_SZ = 0x00020000
REG_DWORD = 0x00010001
--------------окончание статьи----------------
От автора к издателям:
Собственно, это мое исследование, в процессе которого я написал драйвер псевдоустройства.
Смысл этой разработки оговорен в тексте исследования, а о возможностях, думаю, и так всем понятно (исполнение команд на уровне ядра из режима пользователя, что существенно расширяет возможности использования системы).
В Матрице безопасности выбор очевиден