Development/Windows

#1 WFP - 개요

geunyeong 2021. 11. 28. 22:31

Table of Contents

    Abstract

    WFP는 Windows Filtering Platform이란 뜻이다. Windows 시스템에서 네트워크 필터링 프로그램을 만들기 위한 인터페이스와 API를 제공한다. WFP를 통해 트래픽을 허용, 차단할 수 있고, 패킷 데이터를 볼 수도 있으며, 변조도 가능하다. WFP는 기존의 네트워크 필터링 기술인 TDI/NDIS 필터를 개선하고 대체하기 위해 만들어졌다. Windows Vista, 서버 제품군의 경우 2008부터 WFP가 지원됐는데, 10년이 넘었음에도 생각보다 자료가 많지 않아 본 글을 작성하게 됐다.

    WFP는 Windows 시스템에서 방화벽 어플리케이션을 만드는데 사용된다. Vista 이상부터는 Windows의 기본 방화벽 프로그램도 WFP로 만들어졌다. Windows 시스템에서 Host-based Firewall이나 Host-based IDS/IPS를 개발하는 데 관심있는 사람이라면 WFP에 대해서 잘 알고 있어야 한다.

    WFP

    TDI/NDIS

    WFP가 있기 전에는 파일 시스템 필터 드라이버와 마찬가지로 NIC 디바이스 스택에 직접 올릴 드라이버를 작성해 네트워크 필터링 어플리케이션을 작성했다. TDI(Transport Driver Interface, tdi.sys)는 Windows의 전송 계층 기능에 접근하기 위한 Low-Level 커널 모드 네트워킹 스택 인터페이스다. tcpip.sys 드라이버는 Windows의 네트워킹 스택 가장 위에서 TDI를 제공했다. tcpip.sys가 제공한 TDI를 이용하는 어플리케이션들을 TDI Client라고 한다. TDI Client는  tcpip.sys가 제공하는 TDI를 통해 프로토콜 주소 지정, 데이터그램/스트림 송수신, 연결 시작 및 종료 감지 등의 기능을 활용할 수 있었다.

    https://networkencyclopedia.com/transport-driver-interface-tdi/

    Transport Driver Interface

    TDI를 통해 네트워크 필터링 어플리케이션을 만들면 아래 그림과 같은 구조가 된다.

    https://codemachine.com/articles/tdi_overview.html

    TDI Driver Types

    NDIS(Network Driver Interface Spec, ndis.sys)는 TDI보다 더 낮은 위치에 존재하며 물리 디바이스인 NIC에 직접 Attach 되는 드라이버다. NIC와 어플리케이션 간 통신을 위한 데이터 포맷 등(Spec)을 규정한 것이다. OSI Ref Model 상으로 2-3계층에 해당하는 기능을 제공한다. NIC 하드웨어의 복잡성을 숨기고 네트워크 및 데이터링크 계층의 기능을 제공하는 드라이버를 작성하기 위한 표준 인터페이스 역할을 한다고 생각하면 된다. NDIS Mini Port 드라이버를 작성해 로드하면 NIC로 전달되거나 NIC로부터 들어오는 트래픽을 제어할 수 있다.

    아래 그림은 Windows 시스템의 전체적인 네트워킹 스택을 나타낸 그림이다.

    https://apprize.best/microsoft/internals/7.html

    OSI model and Windows networking components

    WFP

    Overview

    WFP(Windows Filtering Platform)는 앞서 설명한 TDI와 NDIS 필터와 같은 기존의 패킷 필터링 기술을 대체하기 위해 만들어졌다.

    WFP는 Windows 시스템에서 네트워크 필터링 어플리케이션을 만들기 위한 플랫폼과 API를 제공하는 서비스 집합이다. 드라이버 개발자는 WFP가 제공하는 API를 사용해 Windows 시스템에 존재하는 여러 네트워킹 스택 계층에서 패킷 처리를 위한 코드를 작성할 수 있다. 이 패킷처리에는 패킷 차단도 가능하기 때문에 WFP를 사용하면 Firewall, IDS/IPS, Network Monitring 등 어플리케이션을 만들 수 있다. 참고로 WFP가 Windows 시스템의 방화벽인 것은 아니다. 방화벽과 같은 어플리케이션을 제공하는 프로그램을 만드는 개발 플랫폼이다. Vista부터 Windows 시스템의 기본 방화벽은 WFP로 구현됐다.

    Windows Filtering Platform Architecture Overview

    WFP Components/Filter Engine

    필터 엔진 tcpip.sys와 우리가 제작한 WFP 어플리케이션 사이에 위치하며, 네트워크 데이터에 대한 모든 피렅링 작업을 수행하는 구성 요소들로 이루어져 있다. 커널 모드 필터 엔진(KM Filter Engine)은 TCP/IP 스택의 전송 및 네트워크 계층에서 필터링 작업을 수행한다. 이 필터링 작업에는 허용, 차단을 포함해 WFP 어플리케이션의 Callout(콜백같은 것)을 호출하는 기능도 존재한다. 이 콜아웃에서도 허용, 차단이 가능하다. 커널 모드 필터 엔진은 약 50개 정도 되는 Filtering Layer를 가지고 있다. Filtering Layer는 쉽게 말해 네트워크 이벤트 종류를 의미한다. 주요 Filtering Layer는 아래와 같다. Windows는 Filtering Layer를 매우 세분화했다. 더 많은 Filtering Layer를 보려면 MSDN을 참고하자.

    https://docs.microsoft.com/en-us/windows/win32/fwp/management-filtering-layer-identifiers-
    • IP Packet
      • Inbound IPv4 Packet
      • Inbound IPv6 Packet
      • Outbound IPv4 Packet
      • Outbound IPv6 Packet
      • Discarded Inbound IPv4 Packet
      • Discarded Inbound IPv6 Packet
      • Discarded Outbound IPv4 Packet
      • Discarded Outbound IPv6 Packet
      • Forwarding IPv4 Packet
      • Forwarding IPv6 Packet
      • Discarded Forwarding IPv4 Packet
      • Discarded Forwarding IPv6 Packet
      • 위 패킷들은 IPSec이 Outbound의 경우 처리되고 난 후, Inbound의 경우 처리되기 전의 패킷 데이터다.
    •  Transport
      • Inbound IPv4 Packet
      • Inbound IPv6 Packet
      • Outbound IPv4 Packet
      • Outbound IPv6 Packet
      • Discarded Inbound IPv4 Packet
      • Discarded Inbound IPv6 Packet
      • Discarded Outbound IPv4 Packet
      • Discarded Outbound IPv6 Packet
      • 위 패킷들은 IPSec이 Outbound의 경우 처리되기 전, Inbound의 경우 처리되고 난 후의 패킷 데이터다.
    • ALE(Application Layer Enforcement)
      • When call bind()
      • When call listen()
      • When call accept()
      • When call connect()
      • When call close()
      • When establised 3 way handshake

    WFP Components/Filter

    분류를 제어하는 규칙이다. WFP 어플리케이션 개발자는 필터 엔진에 필터라는 걸 등록하면, 필터 엔진은 이 필터에 포함된 조건(Condition)을 보고 조건을 모두 충족하는 트래픽에 대해 필터에 포함된 작업(Action)을 수행한다. 필터는 Sublayer라는 곳에 존재하며, Sublayer는 필터 중재에 사용되는 구성 요소로, 쉽게 말해 서로 다른 필터가 서로 다른 결과(허용 혹은 차단)를 낼 때 이를 중재하기 위한 가중치를 결정한다. 필터는 아래와 같은 내용을 담고 있다고 보면 된다.

    • 원격지 IP가 10.10.10.10인 트래픽은 BLOCK하라.
    • 원격지 Port가 4000보다 큰 트래픽은 Callout을 호출하라

    필터는 FWPM_FILTER라는 구조체로 표현되며 구문은 아래와 같다.

    typedef struct FWPM_FILTER0_ {
      GUID                   filterKey;
      FWPM_DISPLAY_DATA0     displayData;
      UINT32                 flags;
      GUID                   *providerKey;
      FWP_BYTE_BLOB          providerData;
      GUID                   layerKey;
      GUID                   subLayerKey;
      FWP_VALUE0             weight;
      UINT32                 numFilterConditions;
      FWPM_FILTER_CONDITION0 *filterCondition;
      FWPM_ACTION0           action;
      union {
        UINT64 rawContext;
        GUID   providerContextKey;
      };
      GUID                   *reserved;
      UINT64                 filterId;
      FWP_VALUE0             effectiveWeight;
    } FWPM_FILTER0;

    위 멤버 중 numFilterConditions는 필터에 포함된 조건의 개수를, filterCondition은 필터 배열을 가지고 있다. filterKey와  filterId는 각 필터를 고유하게 구분하기 위한 식별자 역할을 하며, displayData는 개발자가 이 필터의 역할에 대해 Comment를 남길 수 있도록 하는 멤버다. subLayerKey는 필터가 위치할 서브레이어를 나타내는 것으로, 별도로 주어지지 않는다면 기본 서브레이어인 FWPM_SUBLAYER_UNIVERSAL에 배치된다. 마지막으로 action은 filterCondition에 설정된 모든 조건을 True로 만족하는 트래픽을 허용(Permit)할 지, 차단(Block)할 지, 콜아웃을 호출할 지를 나타낸다. 즉 앞선 예시 중 "~한 트래픽은 BLOCK하라."에 해당한다.

    WFP Components/Condition

    조건(Condition)은 앞서 필터에서 든 예시 중 "원격지 IP가 10.10.10.10인"을 담당하는 구성 요소다. 하나의 필터엔 조건이 0개 이상 있을 수 있으며, 0개라면 모든 트래픽에 대해서라는 의미고 2개 이상이라면 각 조건의 결과를 AND로 연산한다. 즉 조건이 복수개라면 모든 조건이 True인 트래픽에 대해서만 필터에 정의된 작업(Action)을 수행한다고 보면 된다.

    단, 예외가 있다. 만약 비교하는 대상이 동일하다면 이는 OR로 연산된다. 만약 하나의 필터에 등록된 2개의 조건 중 첫번째 조건이 "원격지 IP가 10.10.10.10"이고, 두번째 조건이 "원격지 IP가 20.20.20.20"이라면 이는 OR로 연산되어 "원격지 IP가 10.10.10.10이거나 20.20.20.20인 트래픽에 대해 작업을 수행한다"가 되버린다.

    조건은 FWPM_FILTER_CONDITION이라는 구조체로 표현되며 구문은 아래와 같다.

    typedef struct FWPM_FILTER_CONDITION0_ {
      GUID                 fieldKey;
      FWP_MATCH_TYPE       matchType;
      FWP_CONDITION_VALUE0 conditionValue;
    } FWPM_FILTER_CONDITION0;

    fieldKey는 조건을 매칭할 비교 대상을 의미한다. 로컬 혹은 원격지의 IP, Port 등이 올 수 있다. 더 많은 fieldKey 값을 보고 싶다면 MSDN을 참고하자. MSDN에 나오는 Filtering Condition Identifiers를 fieldKey에 넣으면 필터 엔진이 conditionValue의 값과 트래픽을 matchType에 맞게 비교한다.

    https://docs.microsoft.com/en-us/windows/win32/fwp/filtering-condition-identifiers-

    matchType은 비교 대상과의 비교 연산을 의미한다. conditionValue와 같은지(EQUAL) 혹은 큰지(GREATER), 작은지(LESS), 이상인지(GREATER_OR_EQUAL), 이하인지(LESS_OR_EQUAL), 다른지(NOT_EQUAL) 등을 설정하는 멤버다. FWP_MATCH_TYPE은 enum 타입으로, 아래와 같이 정의되어 있다.

    typedef enum FWP_MATCH_TYPE_ {
      FWP_MATCH_EQUAL,
      FWP_MATCH_GREATER,
      FWP_MATCH_LESS,
      FWP_MATCH_GREATER_OR_EQUAL,
      FWP_MATCH_LESS_OR_EQUAL,
      FWP_MATCH_RANGE,
      FWP_MATCH_FLAGS_ALL_SET,
      FWP_MATCH_FLAGS_ANY_SET,
      FWP_MATCH_FLAGS_NONE_SET,
      FWP_MATCH_EQUAL_CASE_INSENSITIVE,
      FWP_MATCH_NOT_EQUAL,
      FWP_MATCH_PREFIX,
      FWP_MATCH_NOT_PREFIX,
      FWP_MATCH_TYPE_MAX
    } FWP_MATCH_TYPE;

     conditionValue는 실제 비교 값이다. 원격지 포트가 4000이상인 트래픽을 조건으로 만들고 싶다면 아래와 같이 하면 된다. conditionValue는 FWP_CONDITION_VALUE라는 구조체로 표현된다.

    typedef struct FWP_CONDITION_VALUE0_ {
      FWP_DATA_TYPE type;
      union {
        UINT8                 uint8;
        UINT16                uint16;
        UINT32                uint32;
        UINT64                *uint64;
        INT8                  int8;
        INT16                 int16;
        INT32                 int32;
        INT64                 *int64;
        float                 float32;
        double                *double64;
        FWP_BYTE_ARRAY16      *byteArray16;
        FWP_BYTE_BLOB         *byteBlob;
        SID                   *sid;
        FWP_BYTE_BLOB         *sd;
        FWP_TOKEN_INFORMATION *tokenInformation;
        FWP_BYTE_BLOB         *tokenAccessInformation;
        LPWSTR                unicodeString;
        FWP_BYTE_ARRAY6       *byteArray6;
        FWP_V4_ADDR_AND_MASK  *v4AddrMask;
        FWP_V6_ADDR_AND_MASK  *v6AddrMask;
        FWP_RANGE0            *rangeValue;
      };
    } FWP_CONDITION_VALUE0;
    • fieldKey는 FWPM_CONDITION_IP_REMOTE_PORT
    • matchType은 FWP_MATCH_GREATER_OR_EQUAL
    • conditionValue.type은 FWP_UINT16, conditionValue.uint16은 4000

    WFP Component/Callout

    콜백 함수같은 구성 요소라고 보면 된다. 좀더 정확히 말하면 필터에 등록한 조건을 만족하는 트래픽을 처리할 때 사용되는 기능의 집합이자 오브젝트 같은 느낌이다. 콜아웃은 FWPS_CALLOUT과 FWPM_CALLOUT 구조체로 표현된다. FWPS_CALLOUT은 필터 엔진에 콜아웃을 등록할 때 사용하고, FWPM_CALLOUT은 콜아웃의 상태를 지정하는 데 사용된다.

    FWPS_CALLOUT 구조체의 classifyFn 멤버가 바로 조건을 모두 만족하는 트래픽에 대해 콜백 함수처럼 불릴 함수다. FWPS_CALLOUT 구조체를 만들어 FwpsCalloutRegister 함수를 호출해 콜아웃을 필터 엔진에 먼저 등록해야 한다.

    typedef struct FWPS_CALLOUT0_ {
      GUID                                calloutKey;
      UINT32                              flags;
      FWPS_CALLOUT_CLASSIFY_FN0           classifyFn;
      FWPS_CALLOUT_NOTIFY_FN0             notifyFn;
      FWPS_CALLOUT_FLOW_DELETE_NOTIFY_FN0 flowDeleteFn;
    } FWPS_CALLOUT0;
    FWPS_CALLOUT_CLASSIFY_FN0 FwpsCalloutClassifyFn0;
    
    void FwpsCalloutClassifyFn0(
      [in]      const FWPS_INCOMING_VALUES0 *inFixedValues,
      [in]      const FWPS_INCOMING_METADATA_VALUES0 *inMetaValues,
      [in, out] void *layerData,
      [in]      const FWPS_FILTER0 *filter,
      [in]      UINT64 flowContext,
      [in, out] FWPS_CLASSIFY_OUT0 *classifyOut
    )
    {...}

    FWPM_CALLOUT 구조체는 아래와 같이 생겼다. applicableLayer는 해당 콜아웃이 기능할 수 있는 Filtering Layer를 의미한다. calloutKey와 calloutId는 콜아웃 오브젝트를 고유하게 식별할 수 있는 식별자다. FWPM_CALLOUT 구조체를 만들고 FwpmCalloutAdd를 사용하면 된다.

    typedef struct FWPM_CALLOUT0_ {
      GUID               calloutKey;
      FWPM_DISPLAY_DATA0 displayData;
      UINT32             flags;
      GUID               *providerKey;
      FWP_BYTE_BLOB      providerData;
      GUID               applicableLayer;
      UINT32             calloutId;
    } FWPM_CALLOUT0;

    WFP Flow Chart

    아래 그림은 커널 모드에서 동작하는 WFP 어플리케이션의 모습을 그린 것이다. tcpip.sys가 트래픽을 Filtering Layer에 맞게 필터 엔진에 전달하면 필터 엔진은 등록된 여러 필터 중에 해당 Filtering Layer를 처리할 수 있는 모든 필터를 찾는다. 그 후 해당 필터의 조건을 참조하며, 모든 조건을 만족한다면 필터에 등록된 작업을 수행하는 식이다. 이 작업이 PERMIT이라면 트래픽은 허용될 것이고, BLOCK이라면 차단될 것이다. CALLOUT이라면 작업에 등록된 calloutKey를 통해 CALLOUT 객체를 찾고, CALLOUT 객체에 등록된 classifyFn을 호출한다. 그리고 classifyFn이 반환한 PERMIT, BLOCK 등에 따라 적절한 트래픽 처리를 한다.

    Kernel Mode Filter Engine Works

    Note

    WFP는 미니 필터와 다르게 어느 한 필터가 BLOCK을 반환해도 다른 필터에게 트래픽 데이터가 전달된다. 그 이유는 어느 한 필터가 BLOCK을 반환해도 다른 필터가 이를 번복하는 게 가능하기 때문이다. 물론 보통은 BLOCK을 반환하면 해당 트래픽은 차단되지만 다른 필터가 이를 뒤집어 트래픽을 허용할 수도 있다는 것을 알아두자. 자세한 내용은 아래 링크를 참고하면 좋다.

    https://docs.microsoft.com/en-us/windows/win32/fwp/filter-arbitration
    https://tailscale.com/blog/windows-firewall/

    Close

    이상으로 WFP의 개요와 구성 요소들에 대해서 알아봤다. 다음 글 부터는 WFP 어플리케이션을 만드는 방법에 대해 적어보려 한다.

    Github

    main 브랜치에 코드가 안 보인다면 dev를 확인해보라.

    https://github.com/geun-yeong/wfp-example