Ядерный перехват. Часть перваяDeeoni$ (DeeoniS@gmail.com) Хакер, номер #101, стр. 124 Перехват обращений к реестру в Windows VistaНовая ОС от Майкрософт постепенно начинает занимать свою нишу на рынке. Основным аргументом MS в пользу Windows Vista является ее безопасность. Однако в погоне за этой безопасностью сотрудники мегакорпорации перекрыли кислород производителям антивирусов и файрволов. Случайно это было сделано или преднамеренно, судить не нам. А мы разберемся с тем, как жить при подобной несправедливости. Все, наверное, знают замечательную утилиту от Марка Руссиновича под названием RegMon. Эта тузла показывает, какой процесс к какому ключу реестра хочет обратиться. В ОС семейства NT это было реализовано с помощью драйвера, который перехватывал соответствующие системные сервисы. Подобной технологией пользовались практически все антивирусы и персональные файрволы, но с выходом Windows Vista эту «дыру» (по мнению Майкрософт) прикрыли. Официальная причина – чтобы противостоять действию руткитов, однако еще до выхода релиза знаменитый PatchGuarg (так называется технология защиты ядерного кода от патча) поломали раз десять. Разработчики серьезного ПО, естественно, не станут ломать эту «защиту», а воспользуются альтернативными способами. ИнструментарийИтак, мы собрались писать драйвер, а для этого нам понадобятся некоторые дополнительные инструменты. Хочу сразу предупредить, что этот материал ориентирован на людей, которые достаточно хорошо программируют на прикладном уровне, но ни разу не сталкивались с кодом для ядра. Поэтому те, кто имеет подобный опыт, могут просто просмотреть текст, так как что-то новое в нем они вряд ли найдут. Первым делом нам понадобится компилятор. Стандартом здесь является пакет от MS – DDK (Driver Development Kits). В него входит много всего полезного, но, к сожалению, нет оболочки для редактирования кода, так что об этом надо тоже позаботиться. Когда мы начнем писать более-менее серьезные драйверы, нам понадобится отладчик режима ядра. Самыми известными являются SoftICE и WinDbg. SoftICE хорош тем, что драйверы можно отлаживать прямо на своей машине, а вот для WinDbg нужен второй компьютер. Но отлаживать драйвер на своей машине - это то же самое, что самому делать операцию на собственном мозге. Так что советую использовать WinDbg в тандеме с виртуальной машиной. Еще нам пригодится DbgView – утилита от уже упомянутого здесь Руссиновича, которая позволяет просматривать отладочные сообщения от драйвера в реальном времени без отладчика. Писать драйвер мы будем под Висту, поэтому, чтобы избежать лишних проблем, нужно найти последние версии всего упомянутого выше. Основа драйвераТеперь приступим непосредственно к программированию. Написать минимальный драйвер, оказывается, очень просто. Надо всего лишь определить тело функции DriverEntry(). Чтобы наш драйвер успешно загрузился и не вызвал при этом голубого экрана смерти (BsoD), нужно написать следующие строки: Код минимального драйвераNTSTATUS DriverEntry (IN PDRIVER_OBJECT pDriverObject, IN PUNICODE_STRING pRegistryPath ) { NTSTATUS status = STATUS_DEVICE_CONFIGURATION_ERROR; return status; } Если мы скомпилируем драйвер и попробуем его загрузить (о том, как это сделать, я расскажу чуть позже), он завершит свою работу и будет выгружен из памяти, поскольку мы вернули код ошибки. Если бы мы не сделали этого, драйвер висел бы в памяти бесконечно, так как пока у нас нет функции DriverUnload, ответственной за выгрузку драйвера. Теперь надо разобраться с параметрами, передаваемыми в функцию, и их типами, так как некоторые уже могли заметить, что, например, PUNICODE_STRING никогда раньше не встречался в программировании для юзер-мода. Сама функция DriverEntry – это входная точка в модуль, именно этой функции передается управление при загрузке драйвера. Первый параметр pDriverObject – это указатель на объект только что созданного драйвера. Windows является объектно-ориентированной системой; загружая драйвер, система создает объект «драйвер» (driver object), представляющий для нее образ драйвера в памяти. Через этот объект система управляет драйвером. На самом деле, это вовсе не объект в классическом понимании, а просто некая структура данных типа DRIVER_OBJECT, определение которой можно посмотреть здесь: \include\w2k\ntddk.inc. Некоторые поля этой структуры заполняет система, некоторые придется заполнять нам самим. Обращаясь к этой структуре, система и управляет драйвером. Следующий параметр – это pRegistryPath. Он является указателем на unicode-строку, которая, в свою очередь, указывает на путь к разделу реестра, содержащему параметры инициализации драйвера. Структуру этого раздела, как и многое другое, мы рассматривать не будем, так как объем статьи, да и всего журнала, не может вместить всего, чего бы нам хотелось. Однако в конце статьи будут приведены ссылки и названия книг, которые надо прочитать, чтобы достаточно хорошо разбираться в программировании режима ядра. Но вернемся к нашему параметру. Как уже можно было заметить, это не обычная строка, а структура, в которой содержится указатель на unicode-строку, содержащую имя раздела. Этот указатель драйвер может использовать для добавления в реестр какой-либо нужной в дальнейшем информации. В этом случае необходимо сохранить путь к подразделу реестра, но не сам указатель, поскольку по выходу из процедуры DriverEntry он потеряет всякий смысл. Но обычно этого не требуется. О формате данных UNICODE_STRING следует сказать особо. В отличие от режима пользователя, режим ядра оперирует строками в формате UNICODE_STRING. Эта структура определена в файле \include\w2k\ntdef.inc следующим образом: UNICODE_STRING typedef struct _UNICODE_STRING { USHORT Length; USHORT MaximumLength; #ifdef MIDL_PASS [size_is(MaximumLength / 2), length_is((Length) / 2) ] USHORT * Buffer; #else // MIDL_PASS PWSTR Buffer; #endif // MIDL_PASS } UNICODE_STRING; typedef UNICODE_STRING *PUNICODE_STRING; typedef const UNICODE_STRING *PCUNICODE_STRING; Length - содержит текущую длину строки в байтах (не в символах!), не считая завершающего нуля. MaximumLength - максимальный размер буфера (также в байтах), в котором эта строка содержится. Buffer - указатель на саму unicode-строку. Главное достоинство этого формата в том, что он явно определяет как текущую длину строки, так и ее максимально возможную длину. При операциях с такой строкой это позволяет обойтись без некоторых дополнительных вычислений. Классификация драйверовТеперь немного отвлечемся от практики и погрузимся в теорию. Со времен Windows 2000 все драйверы устройств можно разделить на два основных типа: пользовательского режима (User Mode Drivers) и режима ядра (Kernel Mode Drivers). Драйверы User Mode, в свою очередь, делятся на драйверы виртуальных устройств (Virtual Device Drivers, VDD), использующиеся для поддержки программ MS-DOS, и драйверы принтеров (Printer Drivers). Драйверы Kernel Mode подразделяются на драйверы файловой системы (File System Drivers), которые реализуют ввод/вывод на локальные и сетевые диски, унаследованные драйверы (Legacy Drivers), написанные для предыдущих версий Windows NT, драйверы видеоадаптеров (Video Drivers), реализующие графические операции, драйверы потоковых устройств (Streaming Drivers), реализующие ввод/вывод видео и звука, WDM-драйверы (Windows Driver Model, WDM), поддерживающие технологии Plug’n’Play и управления электропитанием. Как следует из самого названия, драйвер устройства - это программа, предназначенная для управления каким-то устройством, причем устройство это не обязательно должно быть физическим. Оно может быть логическим или, как в нашем случае, виртуальным. Также драйверы можно разделить на одноуровневые и многоуровневые. Большинство драйверов, управляющих физическими устройствами, является многоуровневыми (layered drivers). Обработка запроса ввода/вывода разделяется между несколькими дровами. Каждый выполняет свою часть работы. Например, запрос на чтение файла передается драйверу файловой системы, который, выполнив некоторые операции (например, разбиение запроса на несколько частей), передает его «ниже» - драйверу диска, а тот, в свою очередь, отправляет запрос драйверу шины. Кроме того, между ними можно добавить любое количество драйверов-фильтров (например, шифрующих данные). Выполнив запрос, нижестоящий драйвер (lower-level driver) передает его результаты «наверх», вышестоящему (higher-level driver). Уровни запросов прерыванийПрерывание - неотъемлемая часть любой операционной системы. Оно требует обработки, поэтому выполнение текущего кода прекращается и управление передается обработчику прерывания. Существуют как аппаратные, так и программные прерывания. Они обслуживаются в соответствии с их приоритетом. Windows 2000 (как и Виста) использует схему приоритетов прерываний, известную под названием «уровни запросов прерываний» (interrupt request levels, IRQL). Всего существует 32 уровня: с нулевого (passive), имеющего самый низкий приоритет, по 31-ый (high) с самым высоким приоритетом. Причем прерывания с IRQL=0 (passive) по IRQL=2 (DPC\dispatch) являются программными, а с IRQL=3 (device 1) по IRQL=31 (high) - аппаратными. Не путай уровни приоритета прерываний с уровнями приоритетов потоков - это разные вещи. Прерывание с уровнем IRQL=0, строго говоря, прерыванием не является, поскольку оно не может прервать работу никакого кода (ведь для этого код должен выполняться на еще более низком уровне прерывания, а такого уровня нет). На этом IRQL выполняются потоки пользовательского режима. Следует заметить, что на уровне прерывания passive можно вызывать любые функции ядра, а также обращаться к страницам памяти, сброшенным в файл подкачки. В DDK в описании каждой функции обязательно указано, на каком уровне прерывания ее можно вызывать. На более высоких уровнях прерывания (DPC/dispath и выше) попытка обращения к странице, отсутствующей в физической памяти, приводит к краху системы, так как диспетчер памяти (Memory Manager) не может обработать ошибку страницы. Компиляция кодаТеперь, когда мы немного разобрались с теорией, попробуем скомпилировать наш драйвер. Первым делом создадим файл main.c и напишем там следующие строки: Содержание файла main.c#include <ntddk.h> NTSTATUS DriverEntry (IN PDRIVER_OBJECT pDriverObject, IN PUNICODE_STRING pRegistryPath ) { NTSTATUS status = STATUS_DEVICE_CONFIGURATION_ERROR; return status; } Ничего нового, только добавился заголовочный файл, содержащий основные определения для программирования в режиме ядра. Рядом с main.c сохраним файл sources со следующим содержанием: Содержание файла sourcesTARGETNAME=basic_driver TARGETPATH=obj TARGETTYPE=DRIVER SOURCES=main.c Этот файл определяет имя драйвера, файл с исходным кодом и т.д. И последнее, что нам надо, - это makefile. В нем будет всего одна строка: «!INCLUDE $(NTMAKEENV)\makefile.def». Теперь, если на машине уже установлено DDK, заходим в соответствующий пункт главного меню системы, выбираем пункт Build Environments, затем ОС, под которую будем собирать драйвер, и кликаем, допустим, на Windows XP x86 Checked Build Environment. Помимо Checked Build Environment, есть еще и Free Build Environment. Checked предназначена для тестовых сборок Windows, а Free - для релизов. После того как мы выбрали соответствующую оболочку, в командной строке переходим в директорию, в которой мы сохранили исходный код нашего драйвера, вбиваем команду build. Если все в порядке, должна появиться папка, в которой будет лежать наш драйвер с расширением sys. Загрузка драйвераЗагрузить драйвер в систему, чтобы он работал, можно двумя способами: документированным и не очень. Документированный способ на удивление прост – загрузить драйвер можно при помощи диспетчера служб, то есть SCM-менеджера. Работа с драйвером сходна с работой со службой - вызываются одни и те же функции. Лишь передаваемые им параметры будут немного отличаться от обычных. Заострять на этом внимание я не буду, так как статья уже подходит к концу, а еще надо рассказать, где про все это можно почитать более подробно. Дополнительная литература и ссылкиВсе сказанное выше настолько мало затрагивает тему написания драйверов, что человек, знакомый с процессом написания модулей для режима ядра, может возмутиться по поводу слабости изложенного материала. Но цель этой статьи вовсе не научить кодить в Kernel Mode, а лишь ввести в данную тему, для того чтобы в следующем номере рассказать о более интересных вещах. А для этого надо хорошо усвоить принципы программирования в режиме ядра. Итак, чтобы освоить этот вид программирования, советую почитать следующие книги. В первую очередь, это «Внутреннее устройство Microsoft Windows 2000», написанная Дэвидом Соломоном и Марком Руссиновичем. Книга постоянно обновляется, поэтому ее название сейчас может немного отличаться. Хотя в этой книге нет ни одной строчки исходного кода, прежде всего она для программистов. «Недокументированные возможности Windows 2000» Свена Шрайбера - сугубо практическая книга, в которой раскрыто множество тайн Windows 2000. Из отечественных авторов могу посоветовать В.П. Солдатова с его трудом «Программирование драйверов Windows». Первая часть книги читается довольно живенько и несет в себе много полезной информации. Для ленивых рекомендую «Программирование драйверов и систем безопасности» Сорокиной и Щербакова - чтиво представляет собой краткий курс молодого бойца в области ядерного программирования. Также неплохо иметь под рукой «Справочник по базовым функциям API Windows NT/2000» Гари Неббета. Теперь немного скажу об онлайн-ресурсах, которые могут пригодиться в этом нелегком деле. Во-первых, стоит отметить цикл статей «Драйверы режима ядра», который написал небезызвестный Four-F. Найти эти статьи можно на http://wasm.ru. На мой взгляд, это один из самых лучших русскоязычных трудов на тему программирования в режиме ядра. Исходный код примеров, правда, написан на ассемблере, но это не должно стать преградой для человека, желающего научиться кодить драйверы. Еще одним хорошим ресурсом по теме является http://osronline.com. Сайт англоязычный, но люди, при разработке драйверов сталкивающиеся с проблемой, которую не удается решить с помощью отечественных программистов, идут именно сюда. Местный форум – просто сокровищница знаний по Kernel-Mode кодингу. Это все?Итак, как я уже говорил, эта статья ознакомительная. Единственная ее цель - подготовить читателя к следующему материалу, в котором я затрону непосредственно проблемы перехвата обращений к реестру в Windows Vista. Причем с помощью этого механизма можно не только узнать о том, что некое приложение sex.exe пытается записаться в ключ автозагрузки в реестре, но и помешать ему сделать это прямо на уровне ядра. Так что, как говорится, всем учить матчасть. |