Detecting AMSI Bypass
Updated: Jul 21, 2020
AMSI is a Windows feature used by programs such as PowerShell to ask an Anti-Virus engine, while a process is running, “Is this line of code I’m about to run malicious?” It is an effective tool against certain obfuscation and evasion techniques, as AMSI is queried immediately before each line is run, after any deobfuscation performed by the attacker.
AMSI in action
To check “is this data malicious”, PowerShell (or whatever other process chooses to utilise it) needs to specifically ask AMSI to go do its thing. This functionality is provided by amsi.dll. Thus we can tell if a program potentially uses AMSI by the presence of this DLL in its loaded modules.
amsi.dll within the PowerShell process, using the ProcessHacker tool
Any process has full permissions to modify its own internal memory space. As amsi.dll is loaded within the memory space of the PowerShell process, an attacker can use their PowerShell commands to attack AMSI itself.
One technique which we’ve had success with on recent engagements works by finding the in memory location of the AMSI function. The code modifies the memory protections to be read, write, execute, and the function is then re-written to always return a negative (“this is not malware”) result. All future AMSI checks will run this edited code, thus avoiding future AV checks .
We run a malicious-looking command, ‘Invoke-Mimikatz’ and it is blocked. We run the AMSI bypass, and it runs successfully.
We can see this in WinDbg – the initial state of this function looks like this:
A normal function prologue
Following our AMSI patch, WinDbg shows the modified code:
The patched code sets eax (the error code returned by the function) to ERROR_INVALID_PARAMETER and immediately returns.
As part of any offensive exercise, we are always thinking of ways in which we could have been caught; both to improve detection for our customers, as well as to keep our tradecraft fresh. One obvious answer to the question of detecting AMSI bypasses is “better Anti-Virus”: one which could catch us at the moment that we ran the AMSI bypass itself. But this seems fragile; it is a bit of a cat and mouse game, and AMSI bypasses have proven to be relatively easy to come by. So what about detecting it after the fact? What fingerprints does this technique leave for us to detect it?
Detecting the technique
First of all, if we run the above AMSI bypass and then look at the memory in the tool ProcessHacker, we can see the page of RWX memory where the AMSI bypass has occurred:
RWX memory in amsi.dll
This is quite suspicious; but if an attacker were to revert the memory back to just Readable and Executable, then all would appear normal again:
Back to Readable and Executable only
What other fingerprints could be left over from this? Well, if the AMSI code itself has been modified, we could perform an in-memory integrity check against the AMSI binary: check whether the assembly code in memory is identical to what is in the DLL on disk. F-Secure recently released a tool to do exactly this: I highly recommend a read of their blog post, which explains the process of an attacker patching AMSI in far more detail .
Let’s keep the cat-and-mouse game going, though: what could an attacker do to get around this detection? Well, after disabling AMSI, the attacker can run their malicious code: create a new thread in the process to run arbitrary shellcode, and then revert the in-memory AMSI code back to its original state; effectively re-enabling AMSI. They now have a fully featured shell through their separate thread. As long as the developer of the malware doesn’t specifically ask AMSI to scan its own malware, they can continue with impunity, even with AMSI enabled. A defender who looked at the state of memory at this point would see everything exactly as it was in the first place…
…or would they?
(The answer is no, otherwise this would have been a horribly misleading setup).
To get to our detection technique, we first need to recap a fundamental detail of how Windows memory management works behind the scenes.
If two running processes use the same DLL (a very common event; for example, every single process loads ntdll.dll), Windows contains an optimisation to save space in RAM: rather than loading that DLL into RAM dozens of times, it loads it once, and each process refers to that same chunk of RAM.
But what if I, a programmer, edit that code at runtime (like we did above with the AMSI bypass). Since other processes could be referring to that shared physical memory, would their memory be edited as well? Surely that would be a serious security problem if that were the case!
Fortunately, sanity does indeed reign, and editing this shared RAM does not affect other processes. However, it’s interesting to look at how Windows handles this situation. Before Image memory (DLL, EXE, etc.) is edited, Windows takes a copy of this chunk of memory, and makes it private to the process doing the editing. Thus all edits to it only affect that one process: all other processes continue to share memory, but our edited one gets its own separate, private page.
This is known, appropriately, as “Copy on Write” memory: initially it’s shared memory, but upon being written to, our process gets its own personal copy of it, while everyone else keeps using the shared copy.
So with that understanding of the internal mechanics of what’s going on when we patch out the AMSI code, how does this help us with detection?
Well, Windows provides some system calls, allowing us to ask, for a given memory address in a given process, “Is this memory shared?” – or effectively, “Is it copy-on-write?”
If we find the location of the amsi.dll in memory, and find that it is non-shared memory, this is a strong indication that this memory has been modified in the past, indicating that an AMSI bypass has been run.
One problem with this technique is that this system call works by querying the current working set; that is, the memory of a process that is currently in RAM. If the copied page has been paged out to disk, I could not find a way to ask Windows “is this private memory”, other than by accessing kernel internals itself. We can force this memory to be loaded into memory by attempting to read its memory immediately before reading it. This creates a very unlikely race condition: that we load it into RAM, and then before we run the check, it is paged back out to disk; but this seems highly unlikely.
The question then is whether we could revert this memory back to be shared memory? In practice, no: even if the code is reverted to its original state, the page of memory is still marked as non-shareable. In theory Windows could theoretically scan every formerly-copy-on-write page to see if it has been reverted to its original state, and then merge it back to a “shared memory” state; but the amount of processing it would take to save a few kilobytes of RAM doesn’t seem worth it.
In no way is this a perfect detection mechanism. An attacker could edit other code paths that have the same effect, requiring us to look for other patched loctaions. There are other AMSI bypass techniques which only edit R/W memory, which will not cause this signature. And once the process terminates, this signature will be lost. An attacker could launch a new process or inject their malware into a separate process. However, some of these other techniques generate other artifacts (such as ETW events), and the more we can do to force attackers into predictable patterns of behaviour, the better.
We’ve put our proof-of-concept detection code up on GitHub.