Bypassing ETW For Fun and Profit

EDR products have the option of using multiple sources to collect information on a Widows operating system. One of these log sources is ETW (Event Tracing for Windows). ETW consumers are now integrated into many EDR endpoint agents in order to receive CLR Runtime traces. As opposed to other mainstream threat detection and prevention products that hook commonly abused Windows API calls in userland, ATP has hooks that operate within the kernel. ATP relies heavily on ETW calls.

Event Tracing for Windows (ETW) is an efficient kernel-level tracing facility that lets you log kernel or application-defined events to a log file. You can consume the events in real time or from a log file and use them to debug an application or to determine where performance issues are occurring in the application. 1

ETW vs AMSI
There is a common misconception that ETW is challenging to bypass in a process because it runs in kernel mode, juxtaposed to the AMSI.dll which runs in user mode. However, this is incorrect because the Microsoft .NET runtime provider sends logs in JSON format from the .NET runtime to any subscribers of that provider. A provider can be the Windows OS or any AV/EDR product that can consume the ETW JSON logs. This means that ETW can be bypassed in-process, just like the AMSI. ETW is not strings detection-based, if you’re using C# it’s gathering information like namespace names, class names, and method names. Renaming all of these in your code is a great first step for bypassing ETW.

AMSI (Anti Malware Scan Interface) supports file and memory scanning for known malicious strings – it is built directly into Windows Defender. However, AMSI is agnostic of antimalware vendor, third party vendors can write their own AMSI provider to extend AMSI’s functionality as they see fit. A great example of creating a custom AMSI provider is the SimpleAMSI Provider project from pathtofile. CarbonBlack and SentinelOne both write their own custom AMSI providers. This creates challenges for the attacker when testing payloads locally due to not having that vendor’s custom AMSI.dll, unless you purchase that product.

We’ll begin by opening up ntdll.dll in the disassembler and filtering the exports for ETW functions. The two most common functions that are called by Microsoft security products are EtwEventWrite and EtwEventWriteFull.

Examining the EtwEventWrite API we see that EtwEventWriteFull is called, which in turn calls EtwpEventWriteFull:

Recent research regarding ETW tampering involves patching the EtwEventWrite function by having it return before the function is called.

Adam Chester of MDSEC goes over this method in this blog.

Some common Github tools for patching ETW use this method to patch EtwEventWrite.

From outflankl’s TamperETW project:

From Flangvik’s NetLoader project :

As we all know, offensive cyber is a game of cat and mouse. Companies that make EDR products and are now inspecting these commonly abused ETW functions for evidence of tampering within their address space. If script kiddies compile these projects without renaming functions or obfuscating the code, they will get caught immediately.

Scrolling down within the disassembler we find a function named NtTraceEvent. Windows functions that begin with ‘Nt’ are functions that operate in userland but can call a function in the kernel. This type of function is known as a syscall.

According to MSDN:
The Windows native operating system services API is implemented as a set of routines that run in kernel mode. These routines have names that begin with the prefix Nt or Zw. Kernel-mode drivers can call these routines directly. User-mode applications can access these routines by using system calls. 2

Following the control flow of NtTraceEvent brings us to a syscall. Well, how about that.

This function is the central switching point for writing an event through Event Tracing For Windows (ETW). Both the NtTraceEvent and ZwTraceEvent functions are exported by name from NTDLL. There, in user mode, the functions are aliases for a stub that transfers execution to the

NtTraceEvent implementation in kernel mode such that the execution is recognised as originating in user mode. 3

Diagram of NtTraceEvent/zwTraceEvent transferring control to the kernel-version of NtTraceEvent:

So technically we are not going to touch ETW, we will patch the syscall that ETW uses so that it loses the capability to write ETW events to the file system. We will do this by patching the NtTraceEvent function so that the syscall simply returns. The assembly code in the red box is never going to be called.

We’ll start our PoC by finding the memory address of NtTraceEvent.

We’ll use VirtualProtect 4 to change permissions on this segment in memory. If we set RWX permissions with VirtualProtect, that is usually an EDR trigger. However in this case, we’ll set RWX permissions and then return the permissions to RX. Then memcpy5 will be used to copy the opcode for a return into the buffer where NtTrace Event is located.

We used the disassembler in order to find two pieces in the above code – the opcode for a return instruction (xc3), and the size of the memory region where NtTraceEvent resides.

And the return:

The assembly tells the story: we see the pointer being incremented by 3 to the NtTraceEvent memory region. Then there is a call to memcpy where a return instruction is copied into the region of memory holding NtTraceEvent. And the bottom of the assembler, VirtualProtect isused to return the permissions on the stack to RX.

Immediately following the memcpy function in IDA reveals that the NtTraceEvent function is never called, it immediately returns. Remember the opcode for ret, xc3?

As opposed to patching the high level ETW APIs like EtwEventWrite or EtwEventWriteFull, we shut down the syscall between user mode and kernel mode. Remember that EDRs are monitoring for tampering of commonly abused Windows APIs like those mentioned above.

Obfuscation Method
The other method for shutting ETW abilities is to obfuscate your .NET assembly. If you’re writing a .NET assembly, a common obfuscation technique is to use ConfuserEx. ConfuserEx does most of the leg work for you, obfuscating namespaces, class names, and method names. However, during your prechecks, remember to decompile your .NET assembly in dnSpy or dotPeek to search for items the ETW provider might trigger on. CEX will always leave artifacts behind that need manual obfuscation. We are going to use FuzzySec’s SilkETW tool that he wrote while at Mandiant. It’s meant to abstract away the complexities of ETW and give people a
simple interface to perform research and introspection. SilkETW is excellent for offensive security research due to the instant feedback from the CLI. FuzzySec also added filtering functionality into the tool so that users can filter for the Microsoft-Windows-DotNETRuntime provider – allowing them to see what data is available to security products.

Running an unobfuscated version of Seatbelt shows that 1153 ETW logs were generated.

The JSON output of SilkETW from Seatbelt’s execution:

Ratcheting it up a notch, we’ll use CEX to obfuscate our .NET assembly. ConfuserEx works by modifying .NET assemblies based on an XML template file. It’s an open-source protector for .NET to programmatically: rename resources, add control flow obfuscation, add anti-tamper/anti-debug, and encrypt resources and constants.6

The screenshot below is the output of running the Seatbelt assembly through ConfuserEx:

Keep in mind that your .NET assembly will have a significant increase in size post-obfuscation. The assembly increased in size from 646KB to 1571KB. There are a few inherent OPSEC concerns when using ConfuserEx as well. After choosing ‘Aggressive’ obfuscation in ConfuserEx, the tool still leaves IOCs (indicators of compromise) behind. These IOCs are very commonly found in YARA rules for obfuscated .NET assemblies..

That is the same GUID found in the original Seatbelt AssemblyInfo.cs

To recap, the EtwEventWrite Windows API call has seen an uptick in EDR monitoring recently. Neutering EtwEventWrite is the method that the majority of commodity malware uses. The most challenging, yet most efficient method of bypassing ETW is to stop the syscall from user mode to kernel mode. If that’s not possible, obfuscate the binary/assembly while adhering to OPSEC. Whichever method you choose, run the assembly/binary on the same target architecture while running SilkETW during your red team engagement’s pre-checks to ensure you’re OPSEC safe.

References: