The Antimalware Scan Interface (AMSI) assists antivirus programs in detecting “script-based attacks” – e.g., malicious PowerShell or Microsoft Office macros. Even if the script used were heavily obfuscated, there will come a point where the plain un-obfuscated code has to be supplied to the scripting engine. In this instance, AMSI can be called upon to intercept.
However, like all great walls of defences, AMSI is not infallible. In May 2018, CyberArk released a proof-of-concept (POC) code to disable AMSI ( https://www.cyberark.com/threat-research-blog/amsi-bypass-redux/). In fact, this code has been weaponized by threat actors, as shown from a recent post by Bromium (https://www.bromium.com/disabling-anti-malware-scanning-amsi/). As defenders, we want the ability to detect potential indicators left by this POC, which is why we’ve written this post.
But what is AMSI?
Essentially, AMSI is an interface on which applications or services (third-party included) are able to scan a script’s content for malicious usage. If a signature in the script is registered by the AMSI antimalware service provider (Windows Defender by default), it will be blocked.
To put this into context, consider the following steps PowerShell takes to integrate with AMSI:
- When a PowerShell process is created, AMSI.DLL is loaded from disk into its address space.
- Within AMSI.DLL, there’s a function known as AmsiScanBuffer(), essentially the function used to scan a script’s content.
- In the PowerShell command prompt, any supplied content would first be sent to AmsiScanBuffer(), before any execution takes place.
- Subsequently, AmsiScanBuffer() would check with the registered anti-virus to determine if any signatures have been created.
- If the content is deemed malicious, it would be blocked.
This example below shows AMSI in action.
You can find out more about AMSI from an excellent Microsoft blogpost here (https://docs.microsoft.com/en-us/windows/desktop/amsi/how-amsi-helps).
Can the signatures be bypassed?
A script’s content is only deemed as malicious if the appropriate signature has been logged. However, bypassing a logged signature is relatively easy; all a threat actor needs to do is simply change the payload, as shown below.
Effectively, this becomes a cat and mouse game, as it would be up to antivirus providers to update their own signatures. AMSI can’t do much in this scenario, as it is mainly an interface for antivirus programs. We will not be diving into this too deeply, as signature bypassing on its own is a huge topic.
How about other bypasses?
Other than bypassing signatures, there are other ways in which a threat actor can bypass AMSI. To name a few:
- In 2016, Matt Graeber tweeted an AMSI bypass, which assigned amsiInitFailed with a “True” value, thereby causing AMSI initialization to fail.To combat this, antivirus providers created signatures to detect and prevent this bypass from executing. However, as mentioned above, it is relatively trivial to bypass signatures.
- At BlackHat Asia 2018, Tal Liberman revealed a simple technique: if a threat actor sets the registry key “HKCU\Software\Microsoft\Windows Script\Settings\AmsiEnable” to 0, AMSI is disabled. From a defensive perspective, this can be circumvented by hunting for registry keys that are set with a value of 0.
- On May 2018, CyberArk released a POC code to bypass AMSI by patching one of its functions, namely AmsiScanBuffer(). Essentially, this is what we are interested in detecting, which we will now explore.
Bypass through AmsiScanBuffer()
The image below shows the parameters within the AmsiScanBuffer() function.
One of the parameters, “length”, is essentially the crux of the bypass. This parameter contains the length of the string to be scanned. If the parameter is set to a constant value of 0 by some means, AMSI would effectively be bypassed because the AmsiScanBuffer function would assume that any subsequent strings to be scanned would all have a length of 0.
This is achieved by patching the opcode of AMSI.dll during runtime. Specifically, the opcode to change lies in the AmsiScanBuffer pointer address at an offset of 27 as illustrated below.
Here, the general purpose register – r8d – holds the value of the “length” parameter. The said value would then be copied over to the EDI register for further processing. However, if the opcode is changed as below…
…what happens next is interesting. The patched instruction, “xor edi edi”, would result in the EDI register being set to zero instead of it holding the “length” parameter value. This would result in AMSI being disabled as it would assume that any strings send to AmsiScanBuffer() would have a length of zero.
Locating the address to patch is easy, however. As shown in CyberArk’s post, the “GetProcAddress()” function can be used to grab a handle towards AmsiScanBuffer().
There are other ways to achieve this as well. A recent blog post by SecureYourIt (https://secureyourit.co.uk/wp/2019/05/10/dynamic-microsoft-office-365-amsi-in-memory-bypass-using-vba/), showed a different method in locating “AmsiScanBuffer()”. Instead of setting a handle towards “AmsiScanBuffer()” directly, the handle was set towards “AmsiUacInitialize()” first. Subtracting the value 256 from the handle would subsequently cause the handle to point towards “AmsiScanBuffer()”.
In the above example, the handle was set towards “AmsiUacInitialize()”,although you could technically use any functions within Amsi.dll. Such a method would be useful in cases where signatures have been created towards “AmsiScanBuffer()”. You can read more from the post by SecureYourIt here. (https://secureyourit.co.uk/wp/2019/05/10/dynamic-microsoft-office-365-amsi-in-memory-bypass-using-vba/)
Let’s look at detection?
Since this bypass technique works by changing the opcode of AMSI.dll at a specific address, one way to detect the bypass is to scan that address for any possible tampering. The following code snippets below demonstrate how a PowerShell process infected with the above bypass can be detected.
Firstly, a process handle would be opened towards the infected PowerShell Process.
With a handle opened, we can start querying for the address of the loaded AMSI.dll.
Once the address has been identified, a handle to it is created.
With the handle set, we can thereafter locate and scan the patched address. The instructions “xor edi edi” and “nop” correspond to “0x31, 0xff, 0x90” in hex, which translates to “49, 255, 144” in decimal. With that, a check for these values at the patched address can easily determine if a bypass occurred.
I’ll patch it differently then!
The detection logic above can be easily circumvented if a different instruction is used. For example, if the patched instruction was changed to “sub edi, edi” (this set edi to 0 as well), the above detection method won’t work. An easy way to prevent this is to set a check for any tampering of the original instruction.
However, this detection logic can be easily circumvented as well! What if the code was patched in a way such that it does not call AmsiScanBuffer at all? There are potentially a multitude of ways in which a threat actor is able to patched Amsi.dll to prevent it from executing properly.
Enter integrity checking
Instead of having detection rules that scan a specific address in Amsi.dll, an alternative is to check the hash integrity of the loaded Amsi.dll module. Unfortunately, this is not easily achieved. When a DLL is loaded into a process by the Windows loader, relocations potentially occur – this may result in the DLL’s section being loaded in different places, thereby changing its hash as there may be more “empty space” in between the sections. This makes it difficult for us to determine if the hash is legitimate as it would be different from the one on the disk.
Fortunately, there is a way to circumvent this. Instead of hashing the entire Amsi.dll module, we can instead hash the module’s code section. Amsi.dll code section is marked read-only, which means its contents should not change upon loading into memory (as illustrated below).
This is useful information to us for the following reasons.
- The code section should not have any modification when loaded into memory. This means that its hash should match the code section of the on-disk Amsi.dll module.
- Most attempts made by a threat actor to patch Amsi.DLL would likely be made to its code section, thereby resulting in a different hash. As such, we can check the code section hash for its legitimacy.
To aid us in this, we used a PE header parsing script written by John Stewien(https://gist.github.com/caioproiete/b51f29f74f5f5b2c59c39e47a8afc3a3). Utilizing this script would enable us to parse the Amsi.dll section headers, thereby allowing us to locate the pointer address of the code section. This can be illustrated in the following code snippets.
Firstly, we will parse the section headers of the Amsi.dll module that is located on the disk.
With the section headers, we are then able to parse out the base address of the module’s code section, as well as the size of it.
With this information, we would thereby be able to carve the code section out into a byte area and hash it.
Next, we will attempt to do the same for the Amsi.dll module that is loaded within an infected PowerShell Process.
As before, we would parse the base address of the module’s code section. In this scenario however, we would be using the VirtualAddress section header as the module is in memory space.
Subsequently, we carve the code section out of the specified memory region and hash it.
Finally, we can simply set a comparison of both hashes to check the legitimacy of the Amsi.dll module that was loaded into PowerShell memory region.
Putting the code into production
Now that we have the detection logic in place, the next step is to run it on any newly spawned PowerShell process. One simple way to achieve this is by setting a listener on every process creation.
When a new process is created, we can just set a simple check to determine if the process is PowerShell. If determined to be, we can thereafter point our detection logic towards it.
And with that, we can detect the bypass.
Although the examples shown above were targeted towards PowerShell, the same detection logic can be applied for other processes (such as Wscript, Cscript) that load Amsi.dll as well. You can find out more about our detection POC on our GitHub repo here.
Ultimately, the success of this detection hinges on its scanning frequency. There are a multitude of ways in which an adversary can evade the above detection mechanism. For instance, an adversary can set a spawn PowerShell process to sleep for a few seconds before patching Amsi.dll, thereby circumventing the above detection, which only scans upon a process creation.
This is relatively trivial for us to prevent as well. Instead of only scanning on every process creation, we could store the Process IDs of newly created processes in a linked list. Thereafter, we can scan these processes at intervals as illustrated below.
Likewise, an adversary can combat this as well. Instead of patching Amsi.dll once, an adversary can patch it, run his malicious code, and then unpatched it back to its unaltered format. An adversary can repeat this process a few time in an attempt to evade the scanning intervals. Correspondingly, the scanning intervals can be increased to combat the adversary.
Adversaries are constantly upskilling themselves and will always inevitably find new ways to circumvent defences, with AMSI being one of them. In this post, we have shown how it’s possible to combat these adversaries by introducing our POC detection mechanism. As defenders, it’s crucial for us to adopt an offensive mindset and constantly research and create new detection strategies to combat these adversaries.