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.rsBuild 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:
- The agent opens a handle to
\\.\ExfilGuardianviaCreateFile. - The agent calls
DeviceIoControl(IOCTL_EXFIL_GET_PACKET)which pends. - The IRP is queued in a kernel-side
LIST_ENTRYprotected by aKSPIN_LOCK. - When WFP intercepts a packet, the driver dequeues an IRP, copies the
ExfilPacketinto the IRP's system buffer, and completes the IRP. - 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:
- FwpmEngineOpen0: Open a dynamic WFP session (
FWPM_SESSION_FLAG_DYNAMIC) - FwpmTransactionBegin0: Start an atomic transaction
- FwpsCalloutRegister3: Register the classify/notify/flow-delete callbacks
- FwpmSubLayerAdd0: Add a sublayer with maximum weight (
0xFFFF) - FwpmCalloutAdd0: Register the callout in the BFE
- FwpmFilterAdd0: Add an inspection filter (
FWP_ACTION_CALLOUT_INSPECTION) with zero conditions (intercept all) - FwpmTransactionCommit0: Commit atomically
Classify Function
The exfil_classify_fn callback is invoked by WFP for every matching packet. It:
- Extracts the 5-tuple (protocol, local/remote IP, local/remote port) from
FWPS_INCOMING_VALUES0. - Allocates an
ExfilPacketon the non-paged pool viaalloc_zeroed(the kernel stack is only 24 KiB; stack allocation of 4 KiB buffers would cause a stack overflow). - Walks the MDL (Memory Descriptor List) chain to extract up to 4,096 bytes of payload.
- Calls
send_packet_to_agent()to complete a pending IRP. - 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_INTEGRITYThis 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:
| Operation | Constant | Description |
|---|---|---|
| Pre-delete key | REG_NT_PRE_DELETE_KEY | Prevents deletion of the entire key |
| Pre-set value | REG_NT_PRE_SET_VALUE_KEY | Prevents modifying values (e.g., TargetUrl) |
| Pre-delete value | REG_NT_PRE_DELETE_VALUE_KEY | Prevents removing individual values |
| Pre-rename key | REG_NT_PRE_RENAME_KEY | Prevents renaming the key |
| Pre-rename value | REG_NT_PRE_RENAME_VALUE_KEY | Prevents 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.