С фильтром, пожалуйста!Александр Эккерт (aleksandr-ehkkert@rambler.ru) Хакер, номер #115, стр. 115-096-1 Азбука руткит-кодера: фильтры в ядре WindowsСкажи мне, часто ли бессонными ночами ты мечтал накодить программу, которая бы без ведома пользователя тихонечко отфильтровывала сетевой трафик? Или обращения к файловой системе? И при этом модифицировала бы файлы на лету? Или подменяла веб-страницы? Сегодня мы поговорим об основополагающей руткит-технике – фильтрах в режиме ядра Windows. Тема эта очень захватывающая и при прямых руках может принести неслыханные дивиденды. Тебе покорятся и невидимая модификация файлов на диске, и подмена содержимого веб-страниц и редирект сетевого трафика, в общем – полный список! Немного теории, или «устройство» в WindowsЧто такое объект «устройство»? В OC Windows диспетчер ввода-вывода определяет «устройство» (или, более точно, «\Device\<device_name>») для представления того физического, логического или виртуального устройства, чей драйвер был загружен в систему. Согласно существующим правилам, каждый драйвер должен создавать объект «устройство» для того физического (виртуального) устройства, которым он управляет. Формат объекта «устройство» определяется структурой DEVICE_OBJECT, описание которой ты без труда найдешь в DDK. Если быть очень кратким, то объекты устройства в ядре Windows могут быть:
Из вышеописанного следует, что утверждение «мышь имеет драйвер» – неоднозначно. Открою страшную тайну – мышью 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 элементов. Опишем самые важные из них:
Здесь важно уяснить, что в 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). Перечислим три возможных способа «воткнуть» фильтр:
Сейчас не будем углубляться в эти дебри. Оставляю эксперименты с фильтрами в качестве домашнего задания. Поговорим о 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 хорошо описана. Не ленись читать! |