Analyzing Windows Kernel Callbacks for EDR Evasion
The architectural battlefield between Endpoint Detection and Response (EDR) solutions and advanced persistent threats (APTs) has shifted from the user-mode API hooks of the past to the kernel-mode callbacks of the present. As EDR vendors moved away from fragile `ntdll.dll` hooking-which was easily bypassed via direct syscalls-they migrated toward the Windows Kernel's native notification mechanisms.
While these callbacks provide a more robust telemetry stream, they also introduce a single point of failure: if an attacker can manipulate the kernel's registration lists, they can effectively "blind" the EDR without ever touching user-mode code. This post explores the mechanics of kernel callbacks, the methodology of callback unregistration, and the escalating difficulty of bypassing modern telemetry.
The Mechanics of Kernel Callbacks
Windows provides several specialized routines within `ntoskrnl.exe` that allow drivers to register "callback" functions. These functions are triggered by the kernel whenever specific system events occur. For an EDR, these are the primary sensors for detecting malicious activity.
The most critical callback types include:
- Process Creation/Termination (`PsSetCreateProcessNotifyRoutine`): Notifies the driver when a process is created or exits. This is the first line of defense for detecting suspicious process trees.
- Thread Creation (`PsSetCreateThreadNotifyRoutine`): Allows monitoring of thread injection and remote thread execution.
- Image Loading (`PsSetLoadImageNotifyRoutine`): Triggers when a driver or DLL is mapped into memory, essential for detecting reflective DLL injection or malicious driver loading.
- Registry Operations (`CmRegisterCallbackEx`): Enables the monitoring of registry modifications, such as the creation of persistence keys in `Run` or `Services`.
- Object Callbacks (`ObRegisterCallbacks`): Monitors handle creation and duplication, specifically targeting attempts to obtain `PROCESS_ALL_ACCESS` handles to sensitive processes like `lsass.exe`.
When an EDR driver calls `PsSetCreateProcessNotifyRoutine`, the kernel adds a pointer to the EDR's callback function into an internal, non-exported array. When a new process starts, the kernel iterates through this array and executes every registered routine.
The Evasion Strategy: Callback Unregistration
The fundamental goal of kernel-level evasion is to remove the EDR's callback pointer from these internal arrays. If the EDR's routine is no longer in the list, the kernel simply skips it during the event cycle, leaving the EDR unaware of the malicious event.
Identifying the Target Arrays
The primary challenge for an attacker is that the arrays containing these callback pointers (e.เน `PspCreateProcessNotifyRoutine`, `PspCreateThreadNotifyRoutine`, etc.) are not exported by `ntoskrnl.exe`. An attacker cannot simply call `GetProcAddress` to find them.
To locate these arrays, an attacker must employ one of the following techniques:
- Pattern Scanning: Scanning `ntoskrnl.exe` in memory for the opcodes associated with the `PsSet...` functions. Since these functions must reference the array to add elements, the array's address is often embedded in the function's prologue or an adjacent instruction.
- Structure Traversal: Finding the `PsSetCreateProcessNotifyRoutine` function and tracing the `mov` instruction that references the internal `PspCreateProcessNotifyRoutine` array.
The Logic of Unregistration
Once the base address of the array is located, the evasion process follows a predictable logic. The attacker must iterate through the array and identify which entry belongs to the EDR.
```c
// Conceptual pseudo-code for callback unregistration
void UnregisterEDRCallback(PVOID arrayBase, PVOID edrDriverBase, PSIZE_T arraySize) {
for (SIZE_T i = 0; i < *arraySize; i++) {
// The array contains pointers to EX_CALLBACK_ROUTINE_BLOCK structures
PVOID entry = (PVOID)((PUCHAR)arrayBase + (i sizeof(PVOID)));
// Check if the entry points into the EDR driver's memory range
if (IsAddressInDriverRange(entry, edrDriverBase)) {
// Zero out the entry to 'blind' the EDR
*entry = NULL;
DbgPrint("EDR Callback Removed at index %zu\n", i);
}
}
}
```
Note: In modern Windows versions, the array does not store raw function pointers but rather pointers to `EX_CALLBACK_ROUTINE_BLOCK` structures. The logic must account for this indirection.
Implementation Considerations: The BYOVD Vector
Performing these operations requires kernel-mode execution. Since modern Windows environments enforce Driver Signature Enforcement (DSE), an attacker cannot simply load an unsigned driver to perform the unregistration.
This has led
Conclusion
As shown across "The Mechanics of Kernel Callbacks", "The Evasion Strategy: Callback Unregistration", "Implementation Considerations: The BYOVD Vector", a secure implementation for analyzing windows kernel callbacks for edr evasion depends on execution discipline as much as design.
The practical hardening path is to enforce host hardening baselines with tamper-resistant telemetry, protocol-aware normalization, rate controls, and malformed-traffic handling, and behavior-chain detection across process, memory, identity, and network telemetry. This combination reduces both exploitability and attacker dwell time by forcing failures across multiple independent control layers.
Operational confidence should be measured, not assumed: track detection precision under peak traffic and adversarial packet patterns and time from suspicious execution chain to host containment, then use those results to tune preventive policy, detection fidelity, and response runbooks on a fixed review cadence.