In this blog post, we introduce a technique that can help attackers run malicious code over Microsoft Windows 10 (Version 1607) using PowerShell (version 5). CyberArk alerted Microsoft to the weakness, and while Microsoft issued a patch in version 1709, organizations that haven’t implemented the fix remain at risk.
The technique can be carried out on unpatched systems by running code straight from memory while bypassing the Microsoft AMSI (Antimalware Scan Interface) protection giving attackers the ability to run malicious code over a victim’s machine without being detected.
As described in the Microsoft Developer Network (MSDN), AMSI is a generic interface standard that allows applications and services to integrate with any antimalware product present on a machine. It provides enhanced malware protection for users and their data, applications and workloads.
See Figure 0 for details on where AMSI sits.
Figure 0- AMSI Architecture Courtesy of MSFT
AMSI is antimalware vendor agnostic, designed to allow for the most common malware scanning and protection techniques provided by today’s antimalware products that can be integrated into applications. It supports a calling structure allowing for file and memory or stream scanning, content source URL/IP reputation checks, and other techniques.
By default, AMSI works with Microsoft Defender to scan relevant data. Windows Defender will unregister itself from being an “AMSI Provider” and shut itself down when another AV engine registers as an “AMSI Provider.”
In this research, the bypass technique exploits the fact that AMSI’s protection is provided at the same level on which the threat operates. AMSI is implemented as a Dynamic-link library (DLL) that is loaded into every PowerShell session. In the same level of this session, a potentially malicious code (AMSI’s bypass code) can be executed.
AMSI & PowerShell
Starting with Windows 10, AMSI by default provides protection to PowerShell, which is a very strong system tool used by both system administrators and attackers.
A few important things to note:
- AMSI protects PowerShell by loading AMSI’s DLL (amsi.dll) into the PowerShell’s memory space.
- AMSI protection does not distinguish between a simple user with low privileges and a powerful user, such as an admin. AMSI loads its DLL for any PowerShell instance.
- AMSI scans the PowerShell console input by using Windows Defender to determine whether to block the payload operation or allow it to continue.
API monitoring in figure 1 shows the AMSI behavior behind the scenes:
- The string that was submitted to the PowerShell console (“echo ‘Avi-G’”).
- The AmsiScanString() function (under API monitor) which has been automatically invoked with the new input string insertion.
Figure 1- AmsiScanString
Bypassing AMSI General Flow
In our research, we were able to bypass the PowerShell AMSI protection of a simple user with low privileges. Malwares can use the same technique to run their malicious payloads above any kind of user.
We used the following components to perform the bypass:
Figure 2- POC components
Obtainer- a simple C# code script that is crafted as a PowerShell module, responsible for obtaining our AmsiDumpsi.dll.
Operator– AmsiDumpsi.dll is responsible for patching the real amsi.dll->AmsiScanString() function.
In Figure 3, you can see the complete process:
Figure 3- Bypassing Flow
It’s worth mentioning that the first AMSI bypass attempt was to simply unload the Amsi.dll by calling to the FreeLibrary() Api. The module was successfully unloaded, but PowerShell crashed because the process kept using the handle to the Amsi.dll.
Deep Diving into the POC code
Let’s take a short look at the original AmsiScanString() function:
Figure 4- Original AmsiScanString() Function
As you can see at 7fff9c1b2530 – 7fff9c1b2560, AmsiScanString() verifies the argument’s integrity. Right after that, the function initializes the user arguments to be transferred to the real scan. AmsiScanBuffer() treats the user console input string as a buffer to be scanned.
Our AmsiDumpsi.dll patches the original AmsiScanString() function straight in the memory. Here you can see the function at runtime after the patch:
Figure 5- AmsiScanString() After Patching
By changing the second function line, we’re zeroing one of the given arguments (rdx) and causing an error. For that reason, the function will jump straight to the end (instead of scanning the string with AmsiScanBuffer() ) in order to store the error code in the eax register and to return it to the caller function (see address- 00007fff9c1b2579).
By changing the 00007fff9c1b2579 line, we’ve changed the error code to be zero, so now eax will contain 0 [move eax,0] (instead of the original instruction [move eax,0x80070057h]) and the function returns 0.
As we can see in Microsoft’s documentation, returning 0 is equal to S_OK. S_OK means that the function successfully “scanned the payload” (bypassed the scan) and we can keep going.
Figure 6- AmsiScanString() Documentation
Now let’s look at the Obtainer code:
Figure 7- Obtainer loads AmsiDumpsi.dll
As you can see, we have a simple C# code, which was crafted into a PowerShell module by using the Add-Type cmdlet. This module loads the AmsiDumpsi.dll.
Here we can see the patch function in the AmsiDumpsi.dll:
Figure 8- Patch Function
As you can see, AmpsiDumpsiAttached() performs the following steps:
- Get a Pointer to the real amsi->AmsiScanString() function.
- Look for the original function error code (0x80070057) that the function returns in case of an error.
- Enable writing into the required memory address by setting the PAGE_EXECUTE_READWRITE permission.
- Patch the second AmsiScanString line by submitting the 4831d2 opcodes [xor rdx,rdx].
- Set the AmsiScanString error_code to 0.
Here’s a video that demonstrates this:
Let’s see what happens when we try to obtain a malicious Mimikatz payload into the PowerShell session by using the Net.Webclient->DownloadString method and the iex (Invoke-expression) cmdlet, which invokes the downloaded string into the PowerShell session:
Figure 9- AMSI and Defender protects against the new malicious payload submissions
As you can see, the Defender pops up and blocks the string (payload) from being invoked. If we try to look for the obtained Mimikatz function (by using the get-item function), we can’t find it.
After loading our AmpsiDumpsi.dll using the Obtainer, we can see the obtained Mimikatz function, while no Defender alerts have popped up:
Figure 10- Bypassing the AMSI protection, new Mimikatz payload submitted into the process memory
This research demonstrates how a bypass can be utilized on unpatched systems via PowerShell, regardless of a user’s privileges.
The advantages of the technique presented here are that amsi.dll is loaded in every PowerShell process; the API call for the AmsiScanString is performed regularly; and AMSI seems to be working correctly. Because of this, you’re only able to see that it actually doesn’t operate as it should if you protect the DLL in memory or examine its code at runtime.
For this reason, it’s important that organizations push this patch to all systems to avoid unnecessary risk.
(Editor’s Note: Microsoft has since changed the way AMSI handles PowerShell sessions. Please read our more recent blog for an update on this topic.)