EDR tampering is the technique that concerns threat hunters most because it attacks the telemetry pipeline itself. If an attacker successfully tampers with your EDR at the kernel level, they do not just avoid detection for their payload, they remove the evidence collection mechanism entirely, making subsequent activity invisible to everything that depends on that EDR for data. Understanding how EDR products work at the kernel level is essential for understanding where they can be subverted.
How EDR products work at the kernel level
// EDR kernel components register with Windows using callback APIs
// These callbacks are called by the OS kernel when specific events occur
// PsSetCreateProcessNotifyRoutineEx: called when any process is created or exits
// PsSetLoadImageNotifyRoutine: called when any executable image is loaded
// PsSetCreateThreadNotifyRoutine: called when any thread is created
// ObRegisterCallbacks: called when handles to processes/threads are opened
// CmRegisterCallback: called when registry operations occur
// FltRegisterFilter: minifilter for filesystem operations (file create, read, write)
// These callbacks are stored in kernel data structures (arrays and linked lists)
// Removing or overwriting the EDR's entry in these arrays = EDR goes blind
// The EDR process may still be running, but it receives no events
Technique 1: Kernel callback removal without BYOVD
With a kernel write primitive from BYOVD or a kernel exploit, an attacker can walk the notification callback arrays and remove the entries belonging to the EDR driver, without needing to terminate the EDR process at all. The EDR remains running and appears healthy but receives no notifications.
// Detection: compare registered callbacks against expected EDR module presence
// If EDR module is loaded but not in callback array = tampering occurred
// Volatility plugin for callback analysis
vol -f memory.raw windows.callbacks
// Expected output for a healthy CrowdStrike Falcon deployment:
// PspCreateProcessNotifyRoutine:
// [0] 0xfffff806a1234567 CrowdStrike.exe+0x12345
// [1] 0xfffff806b2345678 MsMpEng.exe+0x23456
// etc.
// After callback removal:
// PspCreateProcessNotifyRoutine:
// [0] 0xfffff806b2345678 MsMpEng.exe+0x23456
// CrowdStrike entry is gone, but the module is still loaded
// Automated check: EDR driver present but not in callbacks
vol -f memory.raw windows.modules | grep -i "csagent\|CrowdStrike"
// Module loaded = yes
vol -f memory.raw windows.callbacks | grep -i "csagent\|CrowdStrike"
// Module not in callbacks = tampering detected
// Real-time monitoring via a custom kernel driver or ETW
// The modification of callback arrays generates NtSetSystemInformation events
// or SystemKernelDebuggerInformation events that can be monitored
// Alternative: periodic callback verification script
// Run this on critical systems every few minutes
$edr_modules = @("csagent.sys", "CrowdStrike.sys", "SentinelOne.sys", "MsMpEng.exe")
# Check if EDR processes are running
foreach ($module in $edr_modules) {
$running = Get-Process | Where-Object {$_.Modules.ModuleName -contains $module}
if ($running) {
Write-Host "EDR module running: $module (PID: $($running.Id))"
} else {
Write-Warning "EDR module NOT running: $module - INVESTIGATE IMMEDIATELY"
}
}
Technique 2: ETW provider tampering
// Attackers patch the EtwEventWrite function in memory within PowerShell
// or other processes to prevent ETW events from being written
// This blinds any security tool relying on ETW from that process
// The patch is simple: overwrite the first bytes of EtwEventWrite with a RET instruction
// Result: all ETW events from that process are silently dropped
// Detection: Sysmon Event ID 25 (ProcessTampering)
// Fires when a module in a process has its code replaced in memory
Get-WinEvent -LogName "Microsoft-Windows-Sysmon/Operational" |
Where-Object {$_.Id -eq 25} |
ForEach-Object {
[xml]$data = $_.ToXml()
[PSCustomObject]@{
Time = $_.TimeCreated
Process = $data.Event.EventData.Data | Where-Object {$_.Name -eq "Image"} | Select-Object -ExpandProperty "#text"
Type = $data.Event.EventData.Data | Where-Object {$_.Name -eq "Type"} | Select-Object -ExpandProperty "#text"
}
} | Format-Table -AutoSize
// Sigma rule for ETW tampering
title: ETW Provider Patching Detected via ProcessTampering
logsource:
product: windows
category: process_tampering
detection:
selection:
EventID: 25
Type: "Image is replaced"
Image|endswith:
- '\powershell.exe'
- '\pwsh.exe'
- '\wscript.exe'
- '\cscript.exe'
- '\cmd.exe'
condition: selection
level: critical
Technique 3: Handle table stripping (ObjCallback bypass)
// EDR products use ObRegisterCallbacks to receive notifications when
// processes open handles to protected processes (like LSASS)
// By patching the handle table entry for LSASS in the kernel,
// an attacker can prevent the ObCallback from firing for their handle
// Detection: LSASS access with no Sysmon Event ID 10
// If your Sysmon config logs all LSASS access and you know
// an access occurred (from network forensics or other sources)
// but no Event 10 fired, handle table manipulation may have occurred
// Cross-check: did network telemetry show credential use shortly after
// a period where LSASS access events were sparse?
// Absence of evidence in a place you expect it = evidence of tampering
Technique 4: Minifilter altitude bypass
// EDR filesystem minifilters have an "altitude" that determines their position
// in the filter chain. Higher altitude = earlier in the chain = sees events first
// Attackers can register a minifilter at a higher altitude that intercepts
// and drops I/O requests before the EDR minifilter sees them
// Or simply: unload the EDR minifilter if privileges allow
// Detection: monitor for minifilter unload events
// Windows Event ID 2 in the filter manager log captures filter load/unload
// fltmc.exe list shows currently loaded minifilters
fltmc | Where-Object {$_ -match "Altitude"}
// Run this regularly and compare against baseline
// Any missing minifilter that was previously present = tampering
$baseline_filters = @{
"FileInfo" = "40500"
"WdFilter" = "328010" // Windows Defender
"CsAgent" = "326000" // CrowdStrike (example)
}
$current = fltmc | Select-String "(\w+)\s+(\d+)" |
ForEach-Object {$_.Matches.Groups} |
Group-Object {$_.Groups[1].Value}
foreach ($name in $baseline_filters.Keys) {
if ($name -notin $current.Name) {
Write-Warning "MINIFILTER MISSING: $name (expected altitude $($baseline_filters[$name]))"
}
}
Building a tamper-resistant monitoring architecture
// The problem with all EDR tampering: if the EDR is compromised,
// it cannot report its own compromise. You need out-of-band verification.
// Layer 1: Syslog/SIEM shipping that continues even if EDR stops
// Configure Windows Event Log forwarding directly to your SIEM
// independent of the EDR agent
wecutil qc /q // Enable Windows Event Collection
wecutil cs subscription.xml // Create subscription forwarding to SIEM
// Layer 2: Network-based telemetry (cannot be tampered by host compromise)
// Zeek on a span port sees all traffic regardless of host EDR state
// If a host stops sending EDR telemetry but continues sending network traffic
// = EDR tampered, host still active
// Layer 3: Periodic heartbeat verification
// SIEM alert: "host X has not sent any Sysmon events in 30 minutes
// but is still reachable via network ping"
// This indicates EDR agent failure or tampering
index=sysmon earliest=-30m
| stats max(_time) AS last_event by host
| where last_event < relative_time(now(), "-25m")
| join host [search index=network |
stats count by src_host |
rename src_host AS host |
where count > 0]
// Host has network activity but no Sysmon events = investigate immediately