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 updated
Implementing AMSI and ETW to catch the implant, and then looking at how to bypass it.
Last updated
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.
Microsoft's documentation describes the ETW architecture as the following:
The Event Tracing API is broken into three distinct components:
Controllers, which start and stop an event tracing session and enable providers
Providers, which provide the events
Consumers, which consume the events
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:
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:
Running this will produce a long list of providers. We will focus on Microsoft-Windows-DotNETRuntime
for now.
Running it again:
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:
Then our Loadee, "Example.exe", which we will compile and the loader will access via reflection:
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:
Running this should ideally return the following:
With our trace created, we can now query it to get its status and configuration using the following command:
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:
Once that's done, our new provider can be added to the controller:
0x2038
is the bitmask of the events shown in Detecting Malicious Use of .NET – Part 2:
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:
And then 145 (MethodJittingStarted_V1
):
To stop this, just run:
Tampering with ETW
In Hiding your .NET ETW by MDSec, xpn states:
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:
Lets update the Loader
:
This has now been converted to a x64
project, and the following function has been added:
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
:
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
:
Now, consider this from Assembly.Load and FileNotFoundException:
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
:
Which can then be used to invoke the assembly from bytes
:
And then unload it:
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:
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 like
NtAllocateVirtualMemory
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.
From Antimalware Scan Interface (AMSI):
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:
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:
This is then triggered with:
Following documentation, AMSI is initialised and a session created:
Once it has been setup, the memory regions are looped over and passed into AmsiScanBuffer
:
However, this seemed to work intermittently; sometimes it would trigger, others it wouldn't. Lets move onto how AMSI is typically used.
In What's new in .NET Framework it states:
Antimalware scanning for all assemblies. In previous versions of .NET Framework, the runtime scans all assemblies loaded from disk using either Windows Defender or third-party antimalware software. However, assemblies loaded from other sources, such as by the Assembly.Load()) method, are not scanned and can potentially contain undetected malware. Starting with .NET Framework 4.8 running on Windows 10, the runtime triggers a scan by antimalware solutions that implement the Antimalware Scan Interface (AMSI).
This means that from .NET 4.8 onwards, AMSI was made apart of the framework. So, when an assembly is loaded, AMSI.DLL is too. This backdates .NET to 4.0 to provide support for AMSI.
If 4.8 is installed, then check the loaded modules. Here is a case for PowerShell:
The same thing will happen with a .NET assembly. This will be a significant consideration if the C2 in question is .NET and is relying on Assembly.Load to perform staging or post exploitation. With that said, there are alternatives to Assembly.Load that carry less risk by muting certain events. That will not be covered here, but see SharpTransactedLoad.
Over the years, AMSI has had its problems with bypasses. Because of that, applications like amsi.fail. Whether the C2 is in .NET, or the implant is able to host a CLR; then AMSI will need to be taken care of. As maelstrom is in neither of these sections, we can skim over some stuff here.
Most commonly, people currently tend to patch AMSI by overwriting the memory AmsiScanBuffer
.
This is documented in Memory Patching AMSI Bypass. In this example, the HRESULT
is updated on the return:
In this example, 0x80070057
is HRESULT: E_INVALIDARG
. So theoretically, this return can be any of those four:
E_ACCESSDENIED
0x80070005
"\xB8\x05\x00\x07\x80\xC3"
E_HANDLE
0x80070006
"\xB8\x06\x00\x07\x80\xC3"
E_INVALIDARG
0x80070057
"\xB8\x57\x00\x07\x80\xC3"
E_OUTOFMEMORY
0x8007000E
"\xB8\x0E\x00\x07\x80\xC3"
However, there is a risk here. If the EDR in question is performing integrity checks on the memory region, then it will notice when it has been changed. In terms of code, its a simple calculation to make.
Assume the patch:
This is 6 bytes long, so read the first 6 or so and store them. Check at some event (time, action, etc) whether the bytes match. Additionally, this could also be done with Kernel Callbacks, ETWTi for memory alterations within AMSI.DLL, and so on. So, the amount of possible detections for altering memory is fairly high. If patching memory is required, it is recommended to read the existing bytes, apply the patch, do something malicious, then reapply the original bytes to handle the integrity checks.
Something we have had a lot of success with is making use of Hardware Breakpoints and Vectored Exception Handlers. This process was documented very well by Ethical Chaos in In-Process Patchless AMSI Bypass. Do remember, though, this is also detectable. A Proof-of-concept for this can be seen in this gist where processes are scanned for breakpoints being set.
We are not going to demonstrate the use of this here, and is left as a task for the reader.
This was a fairly long post given it's on just two native protections. We've tried to provide some clarity into the mechanisms EDRs can use to not only identify malicious activity, but prevent it. Along the way we've discussed common pitfalls and some enhancements that can be made to protect against the bypasses.
Whilst doing this, we've tried to shed more light onto the 'X bypasses EDR' narrative in which, yes, the implant might have comeback but there is likely logs of the activity. As with last week's blog, it is hard to stay completely off the radar of defensive mechanisms, and it's harder still to negate these protections without having the act of negating these protections getting logged. Ultimately, everything an operator can do, broadly speaking, can be logged. It's up to the defender to ensure that these events are captured and linked in to their EDR, their SOC, and their awareness.
Next week we will go back to our implant with a look at improving its static opsec.
Close a session that was opened by .
Remove the instance of the AMSI API that was originally opened by .