ExfilGuardian
Backend

Kernel Driver

WFP packet interception, DKOM bypass, anti-kill protection, and registry defense

Overview

The kernel driver (driver/) is a #![no_std] Rust WDM (Windows Driver Model) kernel-mode driver that runs at Ring 0. It intercepts all network traffic via WFP (Windows Filtering Platform), packages it into ExfilPacket structs, and delivers them to the user-mode agent through an inverted-call IRP model.

The driver is a separate Cargo workspace because it requires the nightly toolchain, the WDK allocator, and cannot link against the standard library.

driver/src/
├── lib.rs              # DriverEntry + DriverUnload, DKOM bypass
├── device/
│   ├── core.rs         # Device object creation (\Device\ExfilGuardian)
│   ├── io.rs           # ExfilPacket struct, IRP queue, IOCTL dispatch
│   └── mod.rs
├── security/
│   ├── wfp.rs          # WFP bindgen bindings (auto-generated)
│   ├── filter.rs       # WFP callout registration, packet extraction
│   ├── process.rs      # ObRegisterCallbacks anti-kill protection
│   ├── registry.rs     # CmRegisterCallbackEx registry protection
│   └── mod.rs
└── utils/
    ├── string.rs       # UNICODE_STRING helpers
    └── mod.rs

Build Requirements

  • Toolchain: Rust nightly (required by #![no_std] kernel features)
  • Platform: Windows only (WDK headers, kernel-mode linking)
  • Dependencies: wdk, wdk-sys, wdk-alloc, wdk-panic

Driver Lifecycle

The DriverEntry function initializes four subsystems in sequence:

#[unsafe(export_name = "DriverEntry")]
pub unsafe extern "system" fn driver_entry(
    driver: &mut DRIVER_OBJECT,
    _registry_path: PCUNICODE_STRING,
) -> NTSTATUS {
    // 1. Device object (\\.\ExfilGuardian) for agent communication
    device::core::init(driver);

    // 2. DKOM bypass: set FORCE_INTEGRITY flag in LDR_DATA_TABLE_ENTRY
    //    Required for ObRegisterCallbacks without an EV-signed driver
    let ldr_entry = driver.DriverSection as *mut LDR_DATA_TABLE_ENTRY;
    (*ldr_entry).Flags |= 0x20;

    // 3. Registry protection (CmRegisterCallbackEx)
    security::registry::init(driver);

    // 4. Process anti-kill (ObRegisterCallbacks)
    security::process::init();

    // 5. WFP network interception
    security::filter::init(device::core::get_device_object());

    STATUS_SUCCESS
}

On unload (DriverUnload), each subsystem is torn down in reverse order: WFP, registry, process callbacks, device object.

ExfilPacket Struct

The shared data structure between driver and agent, passed via METHOD_BUFFERED IOCTL:

#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct ExfilPacket {
    pub protocol: u8,          // IANA protocol number
    pub direction: u8,         // 0 unknown, 1 outbound, 2 inbound
    pub ip_version: u8,        // 4 or 6
    pub padding: u8,           // Alignment
    pub pid: u32,              // Owning process PID (from WFP metadata)
    pub local_ip: [u8; 16],    // Source IP (IPv4 in first 4 bytes, or IPv6)
    pub remote_ip: [u8; 16],   // Destination IP
    pub local_port: u16,       // Source port
    pub remote_port: u16,      // Destination port
    pub payload_len: u32,      // Captured payload length (<= 4096)
    pub wire_size: u32,        // Real datagram/chunk byte count (uncapped)
    pub payload: [u8; 4096],   // Static buffer (METHOD_BUFFERED)
}

Total struct size: 4,148 bytes. The payload buffer is statically allocated at 4 KiB to avoid dynamic allocation on the kernel hot path; wire_size carries the true byte volume even when the payload is truncated. The agent mirrors this layout byte-for-byte as ExfilPacketKernel.

IRP Model (Inverted Call)

The driver uses an inverted-call model for delivering packets to the agent:

  1. The agent opens a handle to \\.\ExfilGuardian via CreateFile.
  2. The agent calls DeviceIoControl(IOCTL_EXFIL_GET_PACKET) which pends.
  3. The IRP is queued in a kernel-side LIST_ENTRY protected by a KSPIN_LOCK.
  4. When WFP intercepts a packet, the driver dequeues an IRP, copies the ExfilPacket into the IRP's system buffer, and completes the IRP.
  5. The agent receives the packet and immediately issues a new IOCTL.

The IOCTL code is defined as:

// CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS)
pub const IOCTL_EXFIL_GET_PACKET: u32 = (0x00000022 << 16) | (0x800 << 2);

Cancel Routine

If the agent crashes or is stopped while an IRP is pending, the cancel_routine safely dequeues the IRP from the list, releases the spinlock, and completes the IRP with STATUS_CANCELLED. This prevents kernel memory leaks and blue-screen conditions.

WFP Integration

The driver registers four WFP callouts under a single max-weight sublayer: FWPM_LAYER_DATAGRAM_DATA_V4 / _V6 for connectionless traffic (UDP, ICMP) and FWPM_LAYER_STREAM_V4 / _V6 for TCP. This covers IPv4 and IPv6, and both datagram and stream data — so HTTPS uploads (TCP) and DNS-over-UDP are all observed. Two classify functions back these layers: exfil_classify_fn (datagram) and exfil_stream_classify_fn (stream).

Initialization Sequence

The WFP setup runs inside a single BFE (Base Filtering Engine) transaction:

  1. FwpmEngineOpen0: Open a dynamic WFP session (FWPM_SESSION_FLAG_DYNAMIC)
  2. FwpmTransactionBegin0: Start an atomic transaction
  3. FwpsCalloutRegister3: Register the classify/notify/flow-delete callbacks
  4. FwpmSubLayerAdd0: Add a sublayer with maximum weight (0xFFFF)
  5. FwpmCalloutAdd0: Register the callout in the BFE
  6. FwpmFilterAdd0: Add an inspection filter (FWP_ACTION_CALLOUT_INSPECTION) with zero conditions (intercept all)
  7. FwpmTransactionCommit0: Commit atomically

Classify Function

The exfil_classify_fn callback is invoked by WFP for every matching packet. It:

  1. Extracts the 5-tuple (protocol, local/remote IP, local/remote port) from FWPS_INCOMING_VALUES0.
  2. Allocates an ExfilPacket on the non-paged pool via alloc_zeroed (the kernel stack is only 24 KiB; stack allocation of 4 KiB buffers would cause a stack overflow).
  3. Walks the MDL (Memory Descriptor List) chain to extract up to 4,096 bytes of payload.
  4. Calls send_packet_to_agent() to complete a pending IRP.
  5. Always returns FWP_ACTION_PERMIT (observation-only mode; traffic is never blocked by WFP).

exfil_stream_classify_fn mirrors this for TCP: it reads the same 5-tuple, derives the flow direction from the stream's SEND/RECEIVE flags, walks the stream's NBL chain for payload, and sets countBytesEnforced so WFP does not re-invoke the callout on the same bytes. Both callouts record the untruncated wire_size and release any system mapping they create for an MDL (MmUnmapLockedPages), so they never leak kernel address space.

DKOM Bypass

Windows requires drivers to be EV-signed (Extended Validation) in order to use ObRegisterCallbacks. ExfilGuardian bypasses this requirement via Direct Kernel Object Manipulation:

let ldr_entry = driver.DriverSection as *mut LDR_DATA_TABLE_ENTRY;
(*ldr_entry).Flags |= 0x20; // LDRP_IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY

This sets the IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY flag in the driver's in-memory LDR_DATA_TABLE_ENTRY, which is the same flag that Windows checks before allowing ObRegisterCallbacks registration.

Anti-Kill Protection

The driver protects the agent process from termination by registering an ObRegisterCallbacks pre-operation callback at altitude 321000 (FSFilter Anti-Virus level).

When any process attempts to obtain a handle to the agent's PID with PROCESS_TERMINATE or PROCESS_SUSPEND_RESUME access, the callback silently strips those access rights:

unsafe extern "C" fn pre_operation_callback(
    _ctx: *mut c_void,
    op_info: *mut OB_PRE_OPERATION_INFORMATION,
) -> OB_PREOP_CALLBACK_STATUS {
    let target_pid = PsGetProcessId((*op_info).Object) as usize;

    if target_pid == AGENT_PID.load(Ordering::SeqCst) {
        let flags_to_strip = PROCESS_TERMINATE | PROCESS_SUSPEND_RESUME;
        // Strip from both CreateHandle and DuplicateHandle paths
        (*(*op_info).Parameters)
            .CreateHandleInformation
            .DesiredAccess &= !flags_to_strip;
    }

    OB_PREOP_SUCCESS
}

The agent PID is dynamically registered when it opens the device handle (IRP_MJ_CREATE) and unregistered when it closes the handle (IRP_MJ_CLOSE).

Registry Protection

The driver protects the HKLM\SOFTWARE\ExfilGuardian registry key from tampering via CmRegisterCallbackEx. The callback intercepts five destructive operations:

OperationConstantDescription
Pre-delete keyREG_NT_PRE_DELETE_KEYPrevents deletion of the entire key
Pre-set valueREG_NT_PRE_SET_VALUE_KEYPrevents modifying values (e.g., TargetUrl)
Pre-delete valueREG_NT_PRE_DELETE_VALUE_KEYPrevents removing individual values
Pre-rename keyREG_NT_PRE_RENAME_KEYPrevents renaming the key
Pre-rename valueREG_NT_PRE_RENAME_VALUE_KEYPrevents renaming values

When a modification targeting SOFTWARE\EXFILGUARDIAN is detected, the callback returns STATUS_ACCESS_DENIED, blocking the operation at the kernel level before it reaches the registry.

On this page