(Note this blog was written in between kiddo’s naps, so I make even fewer guarantees of grammatical correctness than usual)

I was recently chatting with a mate about the ETW Events for the Windows Filtering Platform (WFP), which is essentially the traffic filtering engine that underlies modern Firewalls on Windows.

I wanted to test what events get generated by the Microsoft-Windows-WFP Provider when a firewall rule blocks a connection, so I ran Sealighter with the following config:

{
    "session_properties": {
        "session_name": "Sealighter-Trace",
        "output_format": "stdout"
    },
    "user_traces": [
        {
            "trace_name": "wfp-trace",
            "provider_name": "Microsoft-Windows-WFP"
        }
    ]
}

I then started up a simple python http.server on port 9999, created a Windows Firewall rule to block this port, and then tried to hit the site. This generated the following Sealighter Log:

{
    "header": {
        "activity_id": "{00000000-0000-0000-8C58-4A6737DA32E3}",
        "event_flags": 576,
        "event_id": 1001,
        "event_name": "",
        "event_opcode": 0,
        "event_version": 2,
        "process_id": 0,
        "provider_name": "Microsoft-Windows-WFP",
        "task_name": "[GQJ72",
        "thread_id": 0,
        "timestamp": "2020-10-30 20:57:13Z",
        "trace_name": "wfp-trace"
    },
    "properties": {
        "AppId": "...\\python.exe",
        "CurrentProfile": 1,
        "DestinationvSwitchPort": 0,
        "EnterpriseId": "NULL",
        "FilterId": 867414,
        "LayerId": 44,
        "LocalAddress": "0200270FAC1500010000000000000000",
        "LocalAddressLength": 16,
        "Loopback": "BOOLEAN",
        "OriginalProfile": 1,
        "PacketDirection": 14593,
        "PolicyFlags": 0,
        "ReauthReason": 0,
        "RemoteAddress": "020087EEAC1509320000000000000000",
        "RemoteAddressLength": 16,
        "ScopeId": 0,
        "SourcevSwitchPort": 0,
        "Timestamp": "1601-01-01 00:00:00Z",
        "UserSID": "TEST\\TESTUSER",
        "vSwitchId": ""
    },
}

Parsing the Event

Great, so we get an event! Looking at the LocalAddress and RemoteAddress fields, I guessed they were SOCKADDR_IN structs. These can be decoded easily using Python:

import socket
import struct
import binascii
import ipaddress

# From 'LocalAddress'
data_str = "0200270FAC1500010000000000000000"

data = binascii.unhexlify(data_str)
family, port, addr, zero = struct.unpack("!HHIQ", data)

# Family needs to be converted to host order, should be 2 i.e. AF_INET
family = socket.htons(family)
if family != 2 or zero != 0:
    print("Error parsing")
else:
    ip_addr =  str(ipaddress.ip_address(addr))
    print(f"IP: {ip_addr}")
    print(f"Port: {port}")

Which prints out:

IP: 172.21.0.1
Port: 9999

Awesome, looks to be the event we’re looking for. However, one potential bit of information is missing, and indeed is missing from the entire event - What Transport? We can’t tell from this event if the traffic blocked was UDP, TCP, ICMP, or something else. It would be great if this data was in the event, but alas it’s missing.

There is a way we might be able to tell though, and that’s by looking up the FilterId.

Parsing the Filter

The FilterID is the unique id of the rule that led to this connection being blocked. A good way to get the structure of what rules look like is to use netsh to dump out all the rules in XML:

netsh wfp show filter
# This creates a file called 'filters.xml' in the local directory

In my case, the filter looked like the following:

<item>
    <filterKey>{6c00482c-d352-45f7-9519-920c643b8671}</filterKey>
    <displayData>
        <name>AAAAAA2</name>
        <description/>
    </displayData>
    <flags numItems="1">
        <item>FWPM_FILTER_FLAG_INDEXED</item>
    </flags>
    <providerKey>{decc16ca-3f33-4346-be1e-8fb4ae0f3d62}</providerKey>
    <providerData>
        <data>e3bc000000000000</data>
        <asString>........</asString>
    </providerData>
    <layerKey>FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4</layerKey>
    <subLayerKey>{b3cdd441-af90-41ba-a745-7c6008ff2301}</subLayerKey>
    <weight>
        <type>FWP_UINT8</type>
        <uint8>10</uint8>
    </weight>
    <filterCondition numItems="3">
        <item>
            <fieldKey>FWPM_CONDITION_IP_LOCAL_PORT</fieldKey>
            <matchType>FWP_MATCH_EQUAL</matchType>
            <conditionValue>
                <type>FWP_UINT16</type>
                <uint16>9999</uint16>
            </conditionValue>
        </item>
        <item>
            <fieldKey>FWPM_CONDITION_ORIGINAL_PROFILE_ID</fieldKey>
            <matchType>FWP_MATCH_EQUAL</matchType>
            <conditionValue>
                <type>FWP_UINT32</type>
                <uint32>1</uint32>
            </conditionValue>
        </item>
        <item>
            <fieldKey>FWPM_CONDITION_IP_PROTOCOL</fieldKey>
            <matchType>FWP_MATCH_EQUAL</matchType>
            <conditionValue>
                <type>FWP_UINT8</type>
                <uint8>6</uint8>
            </conditionValue>
        </item>
    </filterCondition>
    <action>
        <type>FWP_ACTION_BLOCK</type>
        <filterType/>
    </action>
    <rawContext>0</rawContext>
    <reserved/>
    <filterId>867414</filterId>
    <effectiveWeight>
        <type>FWP_UINT64</type>
        <uint64>11530200208486964736</uint64>
    </effectiveWeight>
</item>

So we can see that our rule has a FWPM_CONDITION_IP_PROTOCOL == 6 condition, i.e. the rules matches on TCP traffic.

If we wanted to automate the whole process, we can use the WFP APIs to get the rule. This can be done in any language, here’s an example in C:

int lookupFilterID(UINT64 filterID)
{
    // Connect to WFP Engine
    HANDLE engineHandle = NULL;
    result = FwpmEngineOpen0(NULL, RPC_C_AUTHN_WINNT, NULL, NULL, &engineHandle);
    if (result != ERROR_SUCCESS) {
        printf("FwpmEngineOpen0 failed. Return value: %d.\n", result);
        return 1;
    }

    // Lookup Filter
    FWPM_FILTER0* filter = NULL;
    result = FwpmFilterGetById0(engineHandle, filterID, &filter);
    if (result == FWP_E_FILTER_NOT_FOUND) {
        printf("Filter %s not found\n", filterString);
        return 1;
    }
    else if (result != ERROR_SUCCESS) {
        printf("FwpmFilterGetById0 failed. Return value: %d.\n", result);
        return 1;
    }

    // Loop over the filter's conditions, looking for a protocol condition
    BOOL foundConditional = FALSE;
    for (size_t i = 0; i < filter->numFilterConditions; i++)
    {
        FWPM_FILTER_CONDITION0 filterCondition = filter->filterCondition[i];
        if (IsEqualGUID(&filterCondition.fieldKey, &FWPM_CONDITION_IP_PROTOCOL)) {
            foundConditional = TRUE;
            BYTE protocol = filterCondition.conditionValue.uint8;
            if (protocol == IPPROTO_TCP) {
                printf("Filter %s: TCP\n", filterString);
            }
            else if (protocol == IPPROTO_UDP) {
                printf("Filter %s: UDB\n", filterString);
            }
            else {
                printf("Filter %s: Other(%d)\n", filterString, protocol);
            }
            break;
        }
    }
    if (!foundConditional) {
        printf("No Protocol Conditional\n");
    }
    return 0;
}

Now this whole process hinges on the Rule specifying what Protocol it is filtering on - If it was just say “block anything to Python”, or “Block anything from this IP”, then the rule wouldn’t have this conditional, and we’d need another way finding this information out.

In the C example, we could also get the Name and Description in the filter’s displayData field, as this matches to the Windows Firewall rule, which you could get using e.g. PowerShell:

$rules = Get-NetFirewallRule |? {$_.DisplayName -eq "AAAAAA2"}

However these aren’t unique Keys, so currently I’m not sure how to 100pc Map a Windows Firewall rule -> WFP Filter.