ETW for Threat Detection: Building Your Own Windows Telemetry Pipeline
Learn how to harness Event Tracing for Windows (ETW) to detect threats before attackers bypass it π― What You'll Build By the end of this tutorial, you'll have: β A working ETW consumer in C++ β Real-time
Learn how to harness Event Tracing for Windows (ETW) to detect threats before attackers bypass it
π― What You'll Build
By the end of this tutorial, you'll have:
- β A working ETW consumer in C++
- β Real-time PowerShell command monitoring
- β Network connection tracking
- β Process creation telemetry pipeline
- β Understanding of why attackers target ETW (and how)
Difficulty: Intermediate
Time: 45 minutes
Prerequisites: Basic C++ knowledge, Visual Studio installed
π€ Why ETW Matters for Security
ETW is Windows' nervous systemβit captures everything happening on the OS:
- Process creation/termination
- Network connections
- File system operations
- Registry modifications
- PowerShell script execution
- And 100+ other event types
The Problem
Most security tools (EDR, antivirus) rely on ETW for visibility. But here's the catch: attackers know this. That's why ETW bypass is a common technique (we'll cover that in the next article).
This article: Learn to use ETW offensively for defenseβbuild your own monitoring before we explore how attackers break it.
π ETW Architecture Crash Course
βββββββββββββββ
β Providers β (Event sources: kernel, PowerShell, DNS, etc.)
ββββββββ¬βββββββ
β
βΌ
βββββββββββββββ
β Controller β (Start/stop tracing sessions)
ββββββββ¬βββββββ
β
βΌ
βββββββββββββββ
β Consumers β (Read and process events) β We're building this
βββββββββββββββ
Key Concepts:
- Provider: Generates events (identified by GUID)
- Session: Collects events from providers
- Consumer: Processes collected events
- Trace: The data stream
π οΈ Project Setup
1. Create Visual Studio Project
# Create project directory
mkdir ETW-Monitor
cd ETW-Monitor
# We'll use C++ for performance
# File β New β Project β Console App (C++)
2. Link Required Libraries
Add to your project properties:
Additional Dependencies: tdh.lib; advapi32.lib
π Part 1: Building a Basic ETW Consumer
Step 1: Define Event Trace Structure
#include <windows.h>
#include <evntrace.h>
#include <evntcons.h>
#include <tdh.h>
#include <iostream>
#include <string>
#pragma comment(lib, "tdh.lib")
#pragma comment(lib, "advapi32.lib")
// Session name
#define SESSION_NAME L"MySecurityMonitor"
// Event callback function
VOID WINAPI EventRecordCallback(PEVENT_RECORD pEvent) {
// We'll fill this in next
}
Step 2: Start ETW Session
TRACEHANDLE StartTrace() {
// Allocate buffer for trace properties
ULONG bufferSize = sizeof(EVENT_TRACE_PROPERTIES) + sizeof(SESSION_NAME);
PEVENT_TRACE_PROPERTIES pSessionProperties =
(PEVENT_TRACE_PROPERTIES)malloc(bufferSize);
ZeroMemory(pSessionProperties, bufferSize);
pSessionProperties->Wnode.BufferSize = bufferSize;
pSessionProperties->Wnode.Flags = WNODE_FLAG_TRACED_GUID;
pSessionProperties->Wnode.ClientContext = 1; // QPC clock resolution
pSessionProperties->Wnode.Guid = { 0 };
pSessionProperties->LogFileMode = EVENT_TRACE_REAL_TIME_MODE;
pSessionProperties->LoggerNameOffset = sizeof(EVENT_TRACE_PROPERTIES);
// Copy session name
wcscpy_s((LPWSTR)((char*)pSessionProperties +
pSessionProperties->LoggerNameOffset),
sizeof(SESSION_NAME) / sizeof(WCHAR),
SESSION_NAME);
TRACEHANDLE sessionHandle = 0;
ULONG status = StartTraceW(&sessionHandle, SESSION_NAME, pSessionProperties);
if (status != ERROR_SUCCESS) {
std::wcerr << L"StartTrace failed: " << status << std::endl;
free(pSessionProperties);
return 0;
}
free(pSessionProperties);
return sessionHandle;
}
Step 3: Enable Specific Provider
Let's monitor PowerShell activity using the Microsoft-Windows-PowerShell provider:
// PowerShell Provider GUID
// {A0C1853B-5C40-4B15-8766-3CF1C58F985A}
GUID PowerShellProviderGuid = {
0xA0C1853B, 0x5C40, 0x4B15,
{ 0x87, 0x66, 0x3C, 0xF1, 0xC5, 0x8F, 0x98, 0x5A }
};
BOOL EnableProvider(TRACEHANDLE sessionHandle, GUID providerGuid) {
ULONG status = EnableTraceEx2(
sessionHandle,
&providerGuid,
EVENT_CONTROL_CODE_ENABLE_PROVIDER,
TRACE_LEVEL_VERBOSE,
0, // Any keyword
0,
0,
NULL
);
if (status != ERROR_SUCCESS) {
std::wcerr << L"EnableTraceEx2 failed: " << status << std::endl;
return FALSE;
}
return TRUE;
}
Step 4: Process Events
VOID WINAPI EventRecordCallback(PEVENT_RECORD pEvent) {
// Get provider name
LPWSTR providerName = NULL;
ULONG bufferSize = 0;
// Get event metadata
PTRACE_EVENT_INFO pInfo = NULL;
DWORD status = TdhGetEventInformation(pEvent, 0, NULL, pInfo, &bufferSize);
if (status == ERROR_INSUFFICIENT_BUFFER) {
pInfo = (PTRACE_EVENT_INFO)malloc(bufferSize);
status = TdhGetEventInformation(pEvent, 0, NULL, pInfo, &bufferSize);
}
if (status == ERROR_SUCCESS) {
// Extract event details
std::wcout << L"[+] Event Detected" << std::endl;
std::wcout << L" Provider: " << pInfo->ProviderName << std::endl;
std::wcout << L" Event ID: " << pEvent->EventHeader.EventDescriptor.Id << std::endl;
std::wcout << L" Process ID: " << pEvent->EventHeader.ProcessId << std::endl;
std::wcout << L" Thread ID: " << pEvent->EventHeader.ThreadId << std::endl;
// Parse event properties
for (ULONG i = 0; i < pInfo->TopLevelPropertyCount; i++) {
PEVENT_PROPERTY_INFO pProperty = &pInfo->EventPropertyInfoArray[i];
LPWSTR propertyName = (LPWSTR)((PBYTE)pInfo + pProperty->NameOffset);
std::wcout << L" Property: " << propertyName << std::endl;
}
std::wcout << std::endl;
free(pInfo);
}
}
// Consume events
DWORD ConsumeEvents() {
EVENT_TRACE_LOGFILEW trace = { 0 };
trace.LoggerName = (LPWSTR)SESSION_NAME;
trace.ProcessTraceMode = PROCESS_TRACE_MODE_REAL_TIME |
PROCESS_TRACE_MODE_EVENT_RECORD;
trace.EventRecordCallback = EventRecordCallback;
TRACEHANDLE traceHandle = OpenTraceW(&trace);
if (traceHandle == INVALID_PROCESSTRACE_HANDLE) {
std::wcerr << L"OpenTrace failed: " << GetLastError() << std::endl;
return 1;
}
// Process events (blocking call)
ProcessTrace(&traceHandle, 1, NULL, NULL);
CloseTrace(traceHandle);
return 0;
}
Step 5: Main Function
int main() {
std::wcout << L"[*] Starting ETW Security Monitor..." << std::endl;
// Start trace session
TRACEHANDLE sessionHandle = StartTrace();
if (sessionHandle == 0) {
return 1;
}
std::wcout << L"[+] Trace session started" << std::endl;
// Enable PowerShell provider
if (!EnableProvider(sessionHandle, PowerShellProviderGuid)) {
ControlTraceW(sessionHandle, SESSION_NAME, NULL, EVENT_TRACE_CONTROL_STOP);
return 1;
}
std::wcout << L"[+] PowerShell provider enabled" << std::endl;
std::wcout << L"[*] Monitoring events (Ctrl+C to stop)..." << std::endl;
// Consume events
ConsumeEvents();
// Cleanup
ControlTraceW(sessionHandle, SESSION_NAME, NULL, EVENT_TRACE_CONTROL_STOP);
return 0;
}
π Part 2: Advanced Monitoring - Multi-Provider Consumer
Monitor Multiple Threats Simultaneously
// Common provider GUIDs for security monitoring
struct SecurityProviders {
GUID PowerShell = {
0xA0C1853B, 0x5C40, 0x4B15,
{ 0x87, 0x66, 0x3C, 0xF1, 0xC5, 0x8F, 0x98, 0x5A }
};
GUID MicrosoftWindowsTCPIP = {
0x2F07E2EE, 0x15DB, 0x40F1,
{ 0x90, 0xEF, 0x9D, 0x7B, 0xA2, 0x82, 0x18, 0x8A }
};
GUID ProcessCreation = {
0x22FB2CD6, 0x0E7B, 0x422B,
{ 0xA0, 0xC7, 0x2F, 0xAD, 0x1F, 0xD0, 0xE7, 0x16 }
};
GUID FileIO = {
0x90CBDC39, 0x4A3E, 0x11D1,
{ 0x84, 0xF4, 0x00, 0x00, 0xF8, 0x04, 0x64, 0xE3 }
};
};
// Enable all security providers
BOOL EnableSecurityMonitoring(TRACEHANDLE sessionHandle) {
SecurityProviders providers;
BOOL success = TRUE;
success &= EnableProvider(sessionHandle, providers.PowerShell);
success &= EnableProvider(sessionHandle, providers.MicrosoftWindowsTCPIP);
success &= EnableProvider(sessionHandle, providers.ProcessCreation);
success &= EnableProvider(sessionHandle, providers.FileIO);
return success;
}
π― Part 3: Practical Detection - PowerShell Command Monitoring
Detect Suspicious PowerShell Commands
struct SuspiciousPattern {
std::wstring pattern;
std::wstring description;
int severity; // 1-10
};
std::vector<SuspiciousPattern> suspiciousPatterns = {
{ L"Invoke-Mimikatz", L"Credential dumping", 10 },
{ L"Invoke-Expression", L"Dynamic code execution", 7 },
{ L"DownloadString", L"Remote payload download", 8 },
{ L"-enc", L"Base64 encoded command", 6 },
{ L"amsi", L"Potential AMSI bypass", 9 },
{ L"VirtualProtect", L"Memory manipulation", 8 },
{ L"[Ref].Assembly", L"Reflection abuse", 9 }
};
VOID AnalyzeCommandLine(const std::wstring& commandLine) {
std::wcout << L"[*] PowerShell Command: " << commandLine << std::endl;
for (const auto& pattern : suspiciousPatterns) {
if (commandLine.find(pattern.pattern) != std::wstring::npos) {
std::wcout << L"[!] ALERT: " << pattern.description
<< L" (Severity: " << pattern.severity << L"/10)" << std::endl;
// Log to file
LogAlert(commandLine, pattern);
// If critical, take action
if (pattern.severity >= 9) {
std::wcout << L"[!] CRITICAL THREAT DETECTED" << std::endl;
// Could terminate process, isolate host, etc.
}
}
}
}
// Enhanced callback with pattern matching
VOID WINAPI EventRecordCallback(PEVENT_RECORD pEvent) {
// Check if it's PowerShell Script Block event (Event ID 4104)
if (pEvent->EventHeader.EventDescriptor.Id == 4104) {
// Extract script block text
PTRACE_EVENT_INFO pInfo = NULL;
ULONG bufferSize = 0;
TdhGetEventInformation(pEvent, 0, NULL, pInfo, &bufferSize);
if (bufferSize > 0) {
pInfo = (PTRACE_EVENT_INFO)malloc(bufferSize);
TdhGetEventInformation(pEvent, 0, NULL, pInfo, &bufferSize);
// Find "ScriptBlockText" property
for (ULONG i = 0; i < pInfo->TopLevelPropertyCount; i++) {
PEVENT_PROPERTY_INFO pProperty = &pInfo->EventPropertyInfoArray[i];
LPWSTR propertyName = (LPWSTR)((PBYTE)pInfo + pProperty->NameOffset);
if (wcscmp(propertyName, L"ScriptBlockText") == 0) {
// Extract property value
PBYTE pData = (PBYTE)pEvent->UserData + pProperty->Offset;
std::wstring scriptText((LPWSTR)pData);
// Analyze for suspicious patterns
AnalyzeCommandLine(scriptText);
}
}
free(pInfo);
}
}
}
π Part 4: Network Connection Monitoring
Track Suspicious Network Activity
struct NetworkConnection {
DWORD processId;
std::wstring processName;
std::wstring sourceIP;
std::wstring destIP;
USHORT destPort;
ULONGLONG timestamp;
};
std::vector<NetworkConnection> activeConnections;
// Known malicious ports
std::set<USHORT> suspiciousPorts = {
4444, // Metasploit default
5555, // Android Debug Bridge (ADB) - suspicious outbound
6666, // IRC/Beast trojan
8080, // Common C2
31337 // Elite/backdoor
};
VOID MonitorNetworkConnections(PEVENT_RECORD pEvent) {
// Microsoft-Windows-TCPIP provider
// Event ID 1 = TCP connect
if (pEvent->EventHeader.EventDescriptor.Id == 1) {
NetworkConnection conn;
conn.processId = pEvent->EventHeader.ProcessId;
conn.timestamp = pEvent->EventHeader.TimeStamp.QuadPart;
// Parse event properties (simplified)
// In production, use TdhGetProperty for robust parsing
// Check against suspicious ports
if (suspiciousPorts.find(conn.destPort) != suspiciousPorts.end()) {
std::wcout << L"[!] Suspicious network connection detected!" << std::endl;
std::wcout << L" PID: " << conn.processId << std::endl;
std::wcout << L" Destination: " << conn.destIP
<< L":" << conn.destPort << std::endl;
LogNetworkAlert(conn);
}
activeConnections.push_back(conn);
}
}
π Part 5: Process Creation Tracking
Detect Process Injection Attempts
struct ProcessInfo {
DWORD pid;
DWORD parentPid;
std::wstring imagePath;
std::wstring commandLine;
ULONGLONG createTime;
};
std::map<DWORD, ProcessInfo> processTree;
VOID TrackProcessCreation(PEVENT_RECORD pEvent) {
// Kernel Process provider
// Event ID 1 = Process Start
if (pEvent->EventHeader.EventDescriptor.Id == 1) {
ProcessInfo proc;
// Extract process details from event
// (Property parsing omitted for brevity)
processTree[proc.pid] = proc;
// Detection: PowerShell spawning unusual children
if (proc.parentPid != 0) {
auto parent = processTree.find(proc.parentPid);
if (parent != processTree.end()) {
std::wstring parentImage = parent->second.imagePath;
if (parentImage.find(L"powershell.exe") != std::wstring::npos) {
// Check if child is suspicious
if (proc.imagePath.find(L"cmd.exe") != std::wstring::npos ||
proc.imagePath.find(L"rundll32.exe") != std::wstring::npos ||
proc.imagePath.find(L"regsvr32.exe") != std::wstring::npos) {
std::wcout << L"[!] Suspicious process chain detected!" << std::endl;
std::wcout << L" Parent: " << parentImage
<< L" (PID: " << proc.parentPid << L")" << std::endl;
std::wcout << L" Child: " << proc.imagePath
<< L" (PID: " << proc.pid << L")" << std::endl;
}
}
}
}
}
}
πΎ Part 6: Logging and Persistence
Write Events to JSON for SIEM Integration
#include <fstream>
#include <chrono>
void LogToJSON(const std::wstring& eventType,
const std::wstring& details,
int severity) {
std::ofstream logFile("C:\\SecurityLogs\\etw_events.json",
std::ios::app);
auto now = std::chrono::system_clock::now();
auto timestamp = std::chrono::duration_cast<std::chrono::seconds>(
now.time_since_epoch()).count();
logFile << "{" << std::endl;
logFile << " \"timestamp\": " << timestamp << "," << std::endl;
logFile << " \"event_type\": \""
<< std::string(eventType.begin(), eventType.end()) << "\"," << std::endl;
logFile << " \"details\": \""
<< std::string(details.begin(), details.end()) << "\"," << std::endl;
logFile << " \"severity\": " << severity << std::endl;
logFile << "}," << std::endl;
logFile.close();
}
π§ͺ Testing Your Monitor
1. Compile and Run
# Build the project in Visual Studio
# Then run as Administrator (required for ETW)
.\ETW-Monitor.exe
2. Generate Test Events
In another PowerShell window:
# This should trigger detection
Write-Host "Testing ETW monitor"
Invoke-Expression "Get-Process"
[Net.WebClient]::new().DownloadString("https://example.com")
3. Expected Output
[*] Starting ETW Security Monitor...
[+] Trace session started
[+] PowerShell provider enabled
[*] Monitoring events (Ctrl+C to stop)...
[+] Event Detected
Provider: Microsoft-Windows-PowerShell
Event ID: 4104
Process ID: 5432
Thread ID: 6789
[*] PowerShell Command: Invoke-Expression "Get-Process"
[!] ALERT: Dynamic code execution (Severity: 7/10)
[*] PowerShell Command: [Net.WebClient]::new().DownloadString("https://example.com")
[!] ALERT: Remote payload download (Severity: 8/10)
π Understanding ETW Attack Surface
Now that you've built an ETW consumer, you understand why attackers target it:
Attack Vectors (Preview for Next Article)
- Provider Removal
// Attackers disable providers
EnableTraceEx2(session, &guid, EVENT_CONTROL_CODE_DISABLE_PROVIDER, ...);
- ETW Patching
// Patch EtwEventWrite in ntdll.dll
BYTE patch[] = { 0xC3 }; // ret
WriteProcessMemory(hProcess, pEtwEventWrite, patch, 1, NULL);
- Session Tampering
// Kill trace sessions
ControlTrace(hSession, NULL, pProperties, EVENT_TRACE_CONTROL_STOP);
Coming Next: "ETW Bypass Techniques: Evading Windows Telemetry"
π‘οΈ Production Hardening Checklist
Before deploying to production:
- [ ] Run as Windows Service (not console app)
- [ ] Implement proper error handling
- [ ] Add log rotation (prevent disk fill)
- [ ] Integrate with SIEM (Splunk, Elastic, Sentinel)
- [ ] Add signature validation for executables
- [ ] Implement rate limiting (prevent event flood)
- [ ] Add configuration file for patterns
- [ ] Create alerting mechanism (email, Slack, PagerDuty)
π Performance Considerations
ETW Can Be Expensive
// Optimize by filtering early
trace.EnableProperty = EVENT_ENABLE_PROPERTY_PROCESS_START_KEY;
// Use keywords to filter at provider level
ULONGLONG keywords = 0x8000000000000000; // Only security-relevant events
Benchmark Results (My Lab)
| Provider | Events/sec | CPU Usage | Memory |
|---|---|---|---|
| PowerShell only | ~500 | 2-5% | 50MB |
| All providers | ~5000 | 15-20% | 200MB |
| With analysis | ~1000 | 10-15% | 100MB |
π― Key Takeaways
- ETW is incredibly powerful for security monitoring
- Most EDRs rely on it - understand your tools
- Attackers bypass it - that's why we need multiple layers
- Build your own to understand offensive evasion
π Next Steps
- Deploy in lab and monitor your own activity
- Integrate with SIEM for centralized logging
- Purple team exercise: Try to evade your own monitor
- Read next article: ETW Bypass Techniques (coming next week)
π Further Reading
Drop your thoughts in the comments!
Follow me for Part 2: ETW Bypass Techniques - Evading Windows Telemetry
Where we'll explore:
- Patching EtwEventWrite
- Provider removal techniques
- Trace session hijacking
- Anti-ETW detection methods
Tags: #ETW #ThreatDetection #Windows #Cpp #SecurityMonitoring #BlueTeam #PurpleTeam #DefensiveSecurity #Tutorial
Code tested on Windows 11 with Visual Studio 2022. Administrator privileges required.
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.