• Партнер

  • Начиная с Server 2008 и Vista в винду был встроен механизм WFP,
    представляющий собой набор API и системных сервисов. С помощью него стало можно
    запрещать и разрешать соединения, управлять отдельными пакетами. Эти
    нововведения были предназначены для упрощения жизни разработчиков различных
    защит. Внесенные в сетевую архитектуру изменения затронули как kernel-mode, так
    и user-mode части системы. В первом случае необходимые функции экспортируются
    fwpkclnt.sys, во втором — fwpuclnt.dll (буквы "k" и "u" в названиях библиотек
    означают kernel и user соответственно). В этой статье мы расскажем о применении
    WFP для перехвата и фильтрации трафика, а после ознакомления с основными
    определениями и возможностями WFP мы напишем свой простой фильтр.

     

    Основные понятия

    Перед началом кодинга нам совершенно необходимо ознакомиться с терминологией
    Microsoft — и для понимания статьи будет полезно, и дополнительную литературу
    читать будет проще :). Итак, поехали.

    Классификация — процесс определения того, что нужно делать с пакетом.
    Из возможных действий: разрешить, блокировать или вызвать callout.

    Callouts — это набор функций в драйвере, которые проводят инспекцию
    пакетов. Они имеют специальную функцию, выполняющую классификацию пакетов. Эта
    функция может принять следующее решение:

    • разрешить (FWP_ACTION_PERMIT);
    • блокировать (FWP_ACTION_BLOCK);
    • продолжить обработку;
    • запросить больше данных;
    • прервать соединение.

    Фильтры (Filters) — правила, указывающие, в каких случаях вызывается
    тот или иной callout. Один драйвер может иметь несколько callout’ов, а
    разработкой драйвера с callout’ом мы и займемся в этой статье. Кстати, колауты
    есть и встроенные, например, NAT-callout.

    Layer — это признак, по которому объединяются различные фильтры (или,
    как говорят в MSDN, "контейнер").

    По правде говоря, документация от Microsoft, выглядит достаточно мутно, пока
    не заглянешь в примеры в WDK. Поэтому, если вдруг надумаешь разрабатывать что-то
    серьезное, нужно непременно с ними ознакомиться. Ну что ж, теперь плавно
    перейдем к практике. Для успешной компиляции и тестов тебе потребуется WDK (Windows
    Driver Kit), VmWare, виртуальная машина с установленной Вистой и отладчик WinDbg.
    Что касается WDK, то у меня лично установлена версия 7600.16385.0 — там есть все
    необходимые либы (поскольку мы будем разрабатывать драйвер, нам нужны только
    fwpkclnt.lib и ntoskrnl.lib) и примеры использования WFP. Ссылки на весь
    инструментарий уже неоднократно приводились, поэтому повторяться не будем.

     

    Coding

    Для инициализации callout’а я написал функцию BlInitialize. Общий алгоритм
    создания callout и добавления фильтра таков:

    1. FWPMENGINEOPEN0 осуществляет открытие сеанса;
    2. FWPMTRANSACTIONBEGIN0 — начало операции с WFP;
    3. FWPSCALLOUTREGISTER0 — создание нового callout;
    4. FWPMCALLOUTADD0 — добавление объекта callout’а в систему;
    5. FWPMFILTERADD0 — добавление нового фильтра(ов);
    6. FWPMTRANSACTIONCOMMIT0 — сохранение изменений (добавленных
      фильтров).

    Обрати внимание, что функции оканчиваются на 0. В Windows 7 некоторые из этих
    функций были изменены, например, появилась FwpsCalloutRegister1 (при 
    сохраненной FwpsCalloutRegister0). Отличаются они аргументами и, как следствие,
    прототипами классифицирующих функций, но для нас это сейчас неважно — 0-функции
    универсальны.

    FwpmEngineOpen0 и FwpmTransactionBegin0 не особо нам интересны — это
    подготовительный этап. Самое интересное начинается с функции
    FwpsCalloutRegister0:

    Прототип FwpsCalloutRegister0

    NTSTATUS NTAPI FwpsCalloutRegister0
    (
        __inout void *deviceObject,
        __in const FWPS_CALLOUT0 *callout,
        __out_opt UINT32 *calloutId
    );

    Я уже говорил, что callout — это набор функций, теперь пришло время
    рассказать об этом подробнее. Структура FWPS_CALLOUT0 содержит указатели на три
    функции — классифицирующую (classifyFn) и две уведомляющие (о
    добавлении/удалении фильтра (notifyFn) и закрытии обрабатываемого потока (flowDeleteFn)).
    Первые две функции являются обязательными, последняя нужна только в случае, если
    ты хочешь мониторить сами пакеты, а не только соединения. Также в структуре
    содержится уникальный идентификатор, GUID колаута (calloutKey).

    Код регистрации callout

    FWPS_CALLOUT sCallout = {0};
    sCallout.calloutKey = *calloutKey;
    sCallout.classifyFn = BlClassify;
    // классифицирующая функция
    sCallout.notifyFn = (FWPS_CALLOUT_NOTIFY_FN0)BlNotify;
    // функция, уведомляющая о добавлении/удалении фильтра
    // создаем новый колаут
    status = FwpsCalloutRegister(deviceObject, &sCallout, calloutId);

    Далее нужно добавить объект-callout в систему и присоединить его к
    определенному уровню (layer) с помощью функции FwpmCalloutAdd0:

    DWORD WINAPI FwpmCalloutAdd0(
    __in HANDLE engineHandle,
    __in const FWPM_CALLOUT0 *callout,
    __in_opt PSECURITY_DESCRIPTOR sd,
    __out_opt UINT32 *id
    );
    typedef struct FWPM_CALLOUT0_ {
        GUID calloutKey;
        FWPM_DISPLAY_DATA0 displayData; // описание callout
        UINT32 flags;
        GUID *providerKey;
        FWP_BYTE_BLOB providerData;
        GUID applicableLayer;
        UINT32 calloutId;
    } FWPM_CALLOUT0;

    В структуре FWPM_CALLOUT0 нам интересно поле applicableLayer — уникальный
    идентификатор уровня, на который добавляется callout. В нашем случае это
    FWPM_LAYER_ALE_AUTH_CONNECT_V4. "v4" в названии идентификатора означает версию
    протокола Ipv4, есть также FWPM_LAYER_ALE_AUTH_CONNECT_V6 для Ipv6. Учитывая
    малую распространенность Ipv6 на настоящий момент, работать мы будем только с
    Ipv4. CONNECT в названии означает, что мы контролируем только установку
    соединения, о входящих и исходящих на этот адрес пакетах речи не идет! Вообще
    уровней, помимо использованного нами, много — они объявлены в заголовочном файле
    fwpmk.h из WDK.

    Добавление объекта-callout в систему

    // название callout
    displayData.name = L"Blocker Callout";
    displayData.description = L"Blocker Callout";
    mCallout.calloutKey = *calloutKey;
    mCallout.displayData = displayData;
    // описание callout
    //FWPM_LAYER_ALE_AUTH_CONNECT_V4
    mCallout.applicableLayer = *layerKey;
    status = FwpmCalloutAdd(gEngineHandle, &mCallout, NULL, NULL);

    Итак, после того, как callout успешно добавлен в систему, нужно создать
    фильтр, то есть указать, в каких случаях будет вызываться наш callout, а именно
    — его классифицирующая функция. Новый фильтр создается функцией FwpmFilterAdd0,
    которой в качестве аргумента передается структура FWPM_FILTER0.

    В FWPM_FILTER0 есть одна или несколько структур FWPM_FILTER_CONDITION0 (их
    число определяется полем numFilterConditions). Поле layerKey заполняется GUID’ом
    уровня (layer), к которому мы хотим присоединиться. В данном случае указываем
    FWPM_LAYER_ALE_AUTH_CONNECT_V4.

    Теперь подробнее рассмотрим заполнение FWPM_FILTER_CONDITION0. Во-первых, в
    поле fieldKey нужно явно указать, что мы хотим контролировать — порт, адрес,
    приложение или что-то еще. В данном случае WPM_CONDITION_IP_REMOTE_ADDRESS
    указывает системе, что нас интересует IP-адрес. Значение fieldKey определяет,
    значения какого типа будут в структуре FWP_CONDITION_VALUE, входящей в
    FWPM_FILTER_CONDITION0. В данном случае в ней содержится ipv4-адрес. Идем
    дальше. Поле matchType определяет, каким образом будет производиться сравнение
    значения в FWP_CONDITION_VALUE с тем, что пришло по сети. Тут вариантов много:
    можно указать FWP_MATCH_EQUAL, что будет означать полное соответствие условию, а
    можно — FWP_MATCH_NOT_EQUAL, то есть, фактически, мы можем добавить таким
    образом исключение фильтрации (адрес, соединение с которым не отслеживается).
    Еще есть варианты FWP_MATCH_GREATER, FWP_MATCH_LESS и другие (см. энум
    FWP_MATCH_TYPE). В данном случае у нас стоит FWP_MATCH_EQUAL.

    Я не стал сильно заморачиваться и просто написал условие на блокирование
    одного выбранного IP-адреса. В случае, когда какое-то приложение попытается
    установить соединение с выбранным адресом, будет вызвана классифицирующая
    функция нашего callout’а. Код, обобщающий сказанное, ты можешь посмотреть на
    врезке "Добавление фильтра в систему".

    Добавление фильтра в систему

    filter.flags = FWPM_FILTER_FLAG_NONE;
    filter.layerKey = *layerKey;
    filter.displayData.name = L"Blocker Callout";
    filter.displayData.description = L"Blocker Callout";
    filter.action.type = FWP_ACTION_CALLOUT_UNKNOWN;
    filter.action.calloutKey = *calloutKey;
    filter.filterCondition = filterConditions;
    // одно условие фильтрации
    filter.numFilterConditions = 1;
    //filter.subLayerKey = FWPM_SUBLAYER_UNIVERSAL;
    filter.weight.type = FWP_EMPTY; // auto-weight.
    // добавляем фильтр на удаленный адрес
    filterConditions[0].fieldKey = FWPM_CONDITION_IP_REMOTE_ADDRESS;
    filterConditions[0].matchType = FWP_MATCH_EQUAL;
    filterConditions[0].conditionValue.type = FWP_UINT32;
    filterConditions[0].conditionValue.uint32 = ntohl(BLOCKED_IP_ADDRESS);
    // добавляем фильтр
    status = FwpmFilterAdd(gEngineHandle, &filter, NULL, NULL);

    Вообще, конечно, фильтрующих условий может быть много. Например, можно
    указать блокирование соединений с определенным удаленным или локальным портом (FWPM_CONDITION_IP_REMOTE_PORT
    и FWPM_CONDITION_IP_LOCAL_PORT соответственно). Можно вылавливать все пакеты
    определенного протокола или определенного приложения. И это еще не все! Можно,
    например, заблокировать трафик определенного пользователя. В общем, есть где
    разгуляться.

    Впрочем, вернемся к фильтру. Классифицирующая функция в нашем случае просто
    блокирует соединение с указанным адресом (BLOCKED_IP_ADDRESS), возвращая
    FWP_ACTION_BLOCK:

    Код нашей classify-функции

    void BlClassify(
    const FWPS_INCOMING_VALUES* inFixedValues,
    const FWPS_INCOMING_METADATA_VALUES* inMetaValues,
    VOID* packet,IN const FWPS_FILTER* filter,
    UINT64 flowContext,FWPS_CLASSIFY_OUT* classifyOut)
    {
        // заполняем структуру FWPS_CLASSIFY_OUT0
        if(classifyOut){ // блокируем пакет
            classifyOut->actionType =
    FWP_ACTION_BLOCK;
            // при блокировании пакета нужно
    сбрасывать FWPS_RIGHT_ACTION_WRITE
            classifyOut->rights&=~FWPS_RIGHT_ACTION_WRITE;
        }
    }

    На практике функция классификации также может установить FWP_ACTION_PERMIT,
    FWP_ACTION_CONTINUE и др.

    И в заключение при выгрузке драйвера нужно удалить все установленные
    callout’ы (угадай, что будет, если система попытается вызвать callout
    выгруженного драйвера? Правильно, BSOD). Для этого существует функция
    FwpsCalloutUnregisterById. В качестве параметра ей передается 32-битный
    идентификатор callout’а, возвращенный функцией FwpsCalloutRegister.

    Завершение работы callout’а

    NTSTATUS BlUninitialize(){
        NTSTATUS ns;
        if(gEngineHandle){
            FwpmEngineClose(gEngineHandle);
           
        }
        if(gBlCalloutIdV4){
            ns =FwpsCalloutUnregisterById(gBlCalloutIdV4);
        }
        return ns;
    }

    Как видишь, программирование WFP-фильтра — не такая сложная задача, поскольку
    MS предоставили нам весьма удобный API. Кстати, в нашем случае мы устанавливали
    фильтр в драйвере, но это можно делать и из юзермода! Например, семпл из wdk
    msnmntr (монитор трафика MSN Messenger-а) так и поступает — это позволяет не
    перегружать kernel-mode часть фильтра.

     

    Свой GUID

    Для регистрации callout ему нужен уникальный идентификатор. Для того, чтобы
    получить свой GUID (Globally Unique Identifier), используй guidgen.exe, входящий
    в Visual Studio. Лежит тулза в (VS_Path)\Common7\Tools. Вероятность коллизии
    очень мала, поскольку длина GUID составляет 128 бит, и всего доступно 2^128
    идентификаторов.

     

    Отладка фильтра

    Для отладки дров удобно использовать связку Windbg+VmWare. Для этого нужно
    настроить как гостевую систему (в виде которой выступает Vista), так и отладчик
    WinDbg. Если у WinXP для удаленной отладки нужно было редактировать boot.ini, то
    для Vista+ есть консольная утилита bcdedit. Как обычно, нужно включить отладку:

    BCDedit /dbgsettings SERIAL DEBUGPORT:1 BAUDRATE:115200 BCDedit /debug
    ON (или BCDedit /set debug ON)

    Далее нужно настроить последовательный порт удаленной системы (см.
    соответствующую иллюстрацию).

    Теперь все готово! Запускаем батник с нижеприведенным текстом:

    start windbg -b -k com:pipe,port=\\.\pipe\com_1,resets=0

    и лицезреем отладочный вывод в окне windbg (см. картинку).

     

    Заключение

    Как видишь, область применения WFP довольно широка. Тебе решать, как
    применить эти знания — во зло или во благо 🙂

    Подписаться
    Уведомить о
    0 комментариев
    Межтекстовые Отзывы
    Посмотреть все комментарии