Издательский дом ООО "Гейм Лэнд"ЖУРНАЛ ХАКЕР #115, ИЮЛЬ 2008 г.

С фильтром, пожалуйста!

Александр Эккерт (aleksandr-ehkkert@rambler.ru)

Хакер, номер #115, стр. 115-096-1

Азбука руткит-кодера: фильтры в ядре Windows

Скажи мне, часто ли бессонными ночами ты мечтал накодить программу, которая бы без ведома пользователя тихонечко отфильтровывала сетевой трафик? Или обращения к файловой системе? И при этом модифицировала бы файлы на лету? Или подменяла веб-страницы?

Сегодня мы поговорим об основополагающей руткит-технике – фильтрах в режиме ядра Windows. Тема эта очень захватывающая и при прямых руках может принести неслыханные дивиденды. Тебе покорятся и невидимая модификация файлов на диске, и подмена содержимого веб-страниц и редирект сетевого трафика, в общем – полный список!

Немного теории, или «устройство» в Windows

Что такое объект «устройство»? В OC Windows диспетчер ввода-вывода определяет «устройство» (или, более точно, «\Device\<device_name>») для представления того физического, логического или виртуального устройства, чей драйвер был загружен в систему. Согласно существующим правилам, каждый драйвер должен создавать объект «устройство» для того физического (виртуального) устройства, которым он управляет. Формат объекта «устройство» определяется структурой DEVICE_OBJECT, описание которой ты без труда найдешь в DDK. Если быть очень кратким, то объекты устройства в ядре Windows могут быть:

  • Physically Device Object (PDO) – представляет собой какое-то физическое устройство в системе;
  • Functional Device Object (FDO) – реализует всю функциональную нагрузку какого-то устройства;
  • Filtering Device Object (FiDO) – объект устройства, создаваемый фильтр-драйвером.

Из вышеописанного следует, что утверждение «мышь имеет драйвер» – неоднозначно. Открою страшную тайну – мышью PS/2 управляет не один драйвер, а целых три: mouclass.sys -> i8042.sys -> ACPI.sys. И так со всеми устройствами в Windows! Рассказывать про организацию стека устройств в ядре Windows мы сейчас не будем – уж больно обширная тема, да и в интернете информации можно добыть предостаточно (например, поищи книгу Уолтера Оуни «Programming the Microsoft Windows Driver Model»). В программировании драйверов под Win есть интересная особенность – можно написать драйвер-фильтр, который реально встроить в указанную цепочку драйверов и получить неограниченный контроль над любым устройством в Windows, будь то клавиатура, файловая система или сетевая карта!

IRP-пакеты

Подсистема ввода-вывода в ядре Windows управляется путем пересылки между устройствами своеобразных пакетов – так называемых IRP-пакетов (I/O Request Packet).

При вызове какого-либо системного сервиса (например, чтении или записи файла) диспетчер ввода-вывода создает IRP-пакет и отправляет его в путешествие вниз по стеку устройств, отвечающих за файловую систему. Результат обработки запроса также вернется инициатору в виде IRP-пакета, который будет содержать всю необходимую информацию.

Глубоко не вдаваясь в назначение полей IRP-пакета, отметим, что он состоит из двух логических частей: фиксированной, неизменяемой при прохождении стека, и динамичной – так называемого стека ввода-вывода, где хранится информация, изменяемая при прохождении от устройства к устройству.

В состав структуры DRIVER_OBJECT входит массив MajorFunction. В него помещаются «dispatch routines» – указатели на процедуры, предназначенные для обработки разных типов IRP-пакетов. Каждый элемент этого массива соответствует своему типу IRP. Если, например, драйверу необходимо обрабатывать запрос типа IRP_MJ_READ, уведомляющий о чтении файла, то он должен поместить в соответствующую позицию массива MajorFunction указатель на функцию, которой и будут направляться запросы этого типа. Если такая функциональность драйверу не нужна, то этот элемент массива MajorFunction можно оставить равным NULL. Всего в массиве 28 элементов. Опишем самые важные из них:

  1. IRP_MJ_CREATE – код передается при создании нового объекта либо при обращении к уже существующему объекту-файлу и соответствует Win32-функции CreateFile.
  2. IRP_MJ_READ – код передается при чтении существующего объекта-файла и соответствует Win32-функции ReadFile().
  3. IRP_MJ_WRITE – код передается по стеку устройств при записи в существующий файл и соответствует, как ты уже понял, Win32-функции WriteFile().
  4. IRP_MJ_DEVICE_CONTROL – код будет передан в стек в тех случаях, когда устройство контролируется посредством IOCTL-запросов. Это, кстати, один из способов «общения» драйвера с юзермодным приложением. Соответствует Win32-функции DeviceIoControl().
  5. IRP_MJ_INTERNAL_DEVICE_CONTROL – код пересылается при взаимодействии драйверов друг с другом.

Здесь важно уяснить, что в IRP-пакете содержится указатель на структуру IO_STATUS_BLOCK, которая, в свою очередь, содержит в себе MJ-код. При прохождении IRP-пакета через стек устройств I/O диспетчер просматривает, какой MJ-код содержит пакет и вызывает в драйвере ту процедуру, которая должна обрабатывать такие IRP-пакеты.

Окончательный список кодов ввода-вывода можно без труда найти в заголовочных файлах в пакете WDK (DDK). Но наибольший интерес представляют собой не MJ-коды ввода-вывода. Как ты, наверное, успел догадаться, управлять всеми устройствами в Windows двадцатью восьмью вариантами кодов невозможно. В структуре IRP-пакета существуют так называемые MinorFunction, которые детализируют управление устройством, указывая, что ему нужно сделать конкретно. Самое обидное, что кодов обработчиков минорных функций много, но они мало документированы и их описание приходится по крупицам собирать в Сети. К примеру, при подключении новых устройств драйвер шины в ядре Win вызовом ядерной функции IoInvalidateDevicerelations создает IRP-пакет с Major-кодом «IRP_MJ_PNP» и с Minor-кодом «IRP_MN_QUERY_DEVICE_RELATIONS», который вернет PnP-диспетчеру список специфических для данного устройства «зависимостей» от шины. Тебе, как разработчику руткитов, должна быть понятна одна вещь: самое интересное при перехвате IRP-пакетов – это коды минорных функций, потому что они конкретизируют управление устройством.

Перехват IRP-пакетов

С теорией хватит, настало время перейти к главному блюду – организации перехвата IRP-пакетов в ядре.

Существует два прямых способа для организации перехвата IRP-пакетов в ядре – заменить функции обработчики IRP на свои или же создать виртуальное устройство и присоединить его к интересующему нас.

Первый метод прост. Мы просто ставим хуки на функции обработки IRP-пакетов в массиве MajorFunction, который имеется в структуре DRIVER_OBJECT. Найти ее не составит труда.

typedef struct _DRIVER_OBJECT {

...

PDRIVER_DISPATCH MajorFunction

[IRP_MJ_MAXIMUM_FUNCTION + 1];

...

} DRIVER_OBJECT;

Способ не лишен как достоинств, так и недостатков – его легко установить, но легко и обнаружить. Проверка целостности таблиц обработчиков IRP-пакетов реализуется практически всеми антивирусами и файрволами.

Подмена IRP-обработчиков в драйвере tcpip.sys

NTSTATUS TcpHook(IN PFILE_OBJECT pFile_tcp, IN

PDEVICE_OBJECT pDev_tcp, IN PDRIVER_OBJECT

pDrv_tcpip)

{

UNICODE_STRING tcp_device;

RtlInitUnicodeString (&tcp_device, "\\Device\\Tcp");

status = IoGetDeviceObjectPointer(&tcp_device,

FILE_READ_DATA, &pFile_tcp, &pDev_tcp);

pDrv_tcpip = pDev_tcp->DriverObject;

old_IRP_MJ_DeviceControl = pDrv_tcpip->

MajorFunction[IRP_MJ_DEVICE_CONTROL];

return NT_SUCCESS(status);

}

Второй способ перехвата вполне легален и хорошо документирован. Необходимо присоединиться к тому устройству, которое нас интересует. К примеру, для организации перехвата (и, если необходимо, работы с сетевым трафиком в ядре) нужно создать устройство вызовом kernel-функции IoCreateDevice. После чего вызовом IoAttachDeviceToDeviceStack (или IoAttachDevice) присоединить его к одному из устройств (или – сразу ко всем), которые создает драйвер tcpip.sys – \Device\Ip, \Device\Tcp, \Device\RawIp, \Device\Udp. После этого созданное нами устройство будет помещено в стек интересующего нас устройства, и все адресованные фильтруемому устройству пакеты будут проходить через созданный нами device.

Присоединение созданного устройства к клавиатуре

NTSTATUS HookKeyboard(IN PDRIVER_OBJECT

pDriverObject, IN PDEVICE_OBJECT device_to_attach)

{

PDEVICE_OBJECT keyboard_device,

top_stack_device;

NTSTATUS status =

IoCreateDevice(pDriverObject,sizeof

(DEVICE_EXTENSION), NULL,

FILE_DEVICE_KEYBOARD,0, true, &keyboard_device);

RtlZeroMemory(keyboard_device->

DeviceExtension,sizeof(DEVICE_EXTENSION));

PDEVICE_EXTENSION с keyboard_device_extension =

(PDEVICE_EXTENSION)keyboard_device->

DeviceExtension;

UNICODE_STRING keyboardDeviceName;

top_stack_device = IoAttachDeviceToDeviceStack

(keyboard_device, device_to_attach);

return NT_SUCCESS(status);

}

Здесь device_to_attach – указатель на DEVICE_OBJECT устройства «\\Device\\KeyboardClass0», полученный вызовом функции IoGetDeviceObjectPointer(), а top_stack_device – указатель на то устройство, которое находится на самой вершине стека устройств. Необязательно это будет созданное нами устройство, потому что в стеке устройств уже могут находиться другие фильтры.

Теперь, если кто-то попытается открыть устройство «\\Device\\KeyboardClass0» вызовом CreateFile(), будет открыт девайс, находящийся на вершине стека.

Ну что ж, возрадуемся – все запросы, которые будут направляться клавиатуре, пройдут через созданное нами устройство-фильтр, и мы сможем делать с ними все, что захотим. Но это только половина дела. Далее нам нужно позаботиться о получении содержимого пакета – ведь для этого мы и городили огород! Для начала надо предусмотреть в нашем драйвере установку обработчика той MajorFunction, обработку которой нам необходимо перехватить. Выглядит это примерно так:

(DRIVER_OBJECT*)->MajorFunction

[IRP_MJ_DEVICE_CONTROL] = Dispatch.

И, разумеется, надо не забыть предусмотреть реализацию функцию Dispatch, примерно вот таким образом:

Фильтруем отправку данных по сети

NTSTATUS Dispatch(IN PDEVICE_OBJECT

pDeviceObject, IN PIRP Irp)

{

stack = IoGetCurrentIrpStackLocation(Irp);

if (stack->MajorFunction ==

RP_MJ_INTERNAL_DEVICE_CONTROL)

{

if (stack->MinorFunction == TDI_SEND)

{

mdlBuffer = MmGetSystemAddressForMdlSafe

(Irp->MdlAddress,

LowPagePriority);

if (mdlBuffer)

{

//...Химичим с пакетом...

}

}

}

IoSkipCurrentIrpStackLocation(Irp);

return IoCallDriver(

((PDEVICE_EXTENSION)pDeviceObject->DeviceExtension)

->top_stack_device, Irp);

}

Вызов IoCallDriver отправит твой модифицированный IRP-пакет дальше по стеку устройств.

Кстати, возникает закономерный вопрос – где нам искать буфер, в котором пользователь передает данные?

Рассмотрим три варианта. Первый – в буфере самого IRP-пакета (так называемый «буферизованный ввод-вывод»); указатель на буфер будет лежать здесь – AssociatedIrp.SystemBuffer.

Второй – «прямой ввод-вывод». В IRP-пакете хранится лишь указатель на кусок ядерной памяти в виде MDL-структуры, где лежат пользовательские данные. В этом случае ищи данные вот здесь – Irp -> MdlAddress.

И, наконец, третий вариант – «ввод-вывод без управления». Это когда диспетчер ввода-вывода помещает в поле DeviceIoControl.Type3InputBuffer структуры IO_STACK_LOCATION указатель на пользовательский входной буфер, а в поле UserBuffer IRP-пакета – указатель на пользовательский выходной буфер и оставляет драйверу возможность управлять ими самостоятельно.

Размещение драйвера-фильтра

Куда можно приаттачить созданный нами драйвер-фильтр? По умолчанию вызов IoAttachDeviceToDeviceStack закинет его сразу над фильтруемым девайсом. А если, скажем, требуется разместить фильтр ПОД устройством? Такое тоже возможно! В реестре Windows, в ветке реестра, которая описывает то или иное устройство, существует возможность определения, куда и какие фильтры подгружать системе при загрузке устройства (более подробно – читай в WDK).

Перечислим три возможных способа «воткнуть» фильтр:

  • над PDO; способ не документирован, но реализовать можно;
  • между PDO и FDO; для этого нужно использовать ключ реестра LowerFilters;
  • над FDO; для этого используется ключ реестра UpperFilters.

Сейчас не будем углубляться в эти дебри. Оставляю эксперименты с фильтрами в качестве домашнего задания.

Поговорим о FastIO

Что такое FastIO? Как ты уже понял, в основе взаимодействия между устройствами в ядре Windows лежит IRP-пакет. Механизм хоть и надежен, но на самом деле не слишком быстр, ведь ядру приходится тратить много времени на обработку IRP-пакетов. В тех случаях, когда критична скорость выполнения запроса, IRP заменяет FastIO – концепция «быстрого ввода-вывода», при которой драйвер регистрирует «точки ввода-вывода». К примеру, FastIO используется в драйверах файловых систем NTFS, FAT, HDFS, CDFS.

Использование FastIO в современных операционных системах предполагается в двух случаях. Во-первых, для обеспечения интеграции с механизмом кэширования данных, в частности при работе с файловой системой. Во-вторых, FastIO, используется для обеспечения работы с файлами, промаппированными (спроецированными) в оперативную память.

Хитрость вместо напутствия

Мы научились кодить простые фильтры. Они помогут тебе поставить на колени любые устройства в Windows. Единственный недостаток устройств-фильтров в ядре – их легко обнаружить. Подскажу одну хитрость. Фильтры можно сделать невыгружаемыми. Для этого достаточно «забыть» про реализацию функции выгрузки драйвера.

Фильтрация IRP – тема сложная и, к сожалению, ее не раскрыть в пределах одной журнальной статьи. Азы в фильтрации тебе придется изучать самому, а если что непонятно – пиши, будем разбираться вместе!

DVD

На компакт-диске лежат сорцы простого драйвера-фильтра TCP-трафика в ядре Windows, доки по фильтрации в Windows и FastIO, а еще – утилиты для просмотра объектов ядра Windows – WinObjEx.

WWW

Обязательно к прочтению:

INFO

В пакете WDK (DDK) технология фильтрации IRP хорошо описана. Не ленись читать!

Содержание