Skip to content

Latest commit

 

History

History
228 lines (169 loc) · 7.44 KB

File metadata and controls

228 lines (169 loc) · 7.44 KB

Batch-Layer Obfuscation in malware.bat

This document walks each obfuscation technique in malware.bat. Read this with detection-and-defense.md open — every technique here is judged with a "What this actually defeats" / "What it doesn't" line so you don't walk away thinking batch tricks beat AMSI.

TL;DR: Every layer here operates on the batch / dropper stage. The moment PowerShell decodes the base64 -EncodedCommand, AMSI and ScriptBlock Logging see the plaintext payload regardless of what the batch did.


1. Self-Re-entry Trampoline

if "%~1"=="" set "LEVEL=0" & goto :init
if "%LEVEL%"=="1" goto :real_start
goto :confuse_trace

:init
set "LEVEL=1"
call "%~f0" dummy_param
exit /b

The script re-invokes itself with a sentinel argument so the "real" entry point is reached on the second pass. Naive static analyzers that only follow the first label sequence see a benign confuse_trace exit and stop.

  • Defeats: Trivial line-by-line reading; some signature scanners that fingerprint the first few labels.
  • Doesn't defeat: Anything that actually executes the script (cmd.exe-instrumented sandboxes), Sysmon EID 1 (records both invocations), AMSI on the downstream PowerShell child.

2. Environment Trap (PATH manipulation)

set "PATH_ORIG=%PATH%"
set "PATH=%TEMP%;%PATH%"

Temporarily prepends %TEMP% to PATH. The intent is to confuse tools that resolve binary paths during analysis.

  • Defeats: Almost nothing in 2026. Defenders resolve binaries by image path via ETW / Sysmon, not by PATH lookup at analysis time.
  • Doesn't defeat: Anything monitoring ImageLoaded or ProcessCreate events.

3. Substring Reconstruction

set "str_pool=powershell-encodedCommandNonInteractive"
set "_cmd1=!str_pool:~0,5!"
set "_cmd2=!str_pool:~5,6!"
...

"powershell" and the flag names never appear as contiguous strings in the file. Pieces are spliced out of a pool and reassembled at runtime.

  • Defeats: grep-based hunting and very old AV signatures that look for the literal token powershell -EncodedCommand.
  • Doesn't defeat: Yara rules with nocase or wide patterns, behavioural rules that flag cmd.exe spawning powershell.exe (str_pool itself contains the giveaway).

Note: in this script, exec_cmd is decoy output — the real execution uses the hardcoded c_1 / c_2 strings. The substring layer exists to draw the analyst's eye away.


4. Anti-Emulation Guard

set "g1=%RANDOM%%RANDOM%"
set "g2=%RANDOM%%RANDOM%"
if %g1% NEQ %g2% ( ...real path... ) else ( exit /b )

Two consecutive %RANDOM%%RANDOM% reads should virtually never match. Some naive emulators stub %RANDOM% to a constant — those exit the script immediately and never see the payload.

  • Defeats: Emulators that constant-stub %RANDOM%.
  • Doesn't defeat: Real cmd.exe execution, modern sandbox emulators (which use proper RNG), AMSI on the downstream PowerShell.

5. Time-Based Branching

if %time:~7,1% LSS 5 ( ...path A... ) else ( ...path B... )

Two execution paths that produce identical #part1/#part2/#part3 values but exercise different variables. Pure control-flow obfuscation.

  • Defeats: Coverage-based fuzzers that expect one branch.
  • Doesn't defeat: Anything looking at the final result.

6. Decoy Sub-routines (xor_char, math_decode, init_char_map)

These functions are defined and called in :init_char_map / :init_math_funcs but their outputs are never used in the real payload-assembly path. Their job is to make :strrev, :StrLen, and the math aliases look like load-bearing machinery to a static reader.

  • Defeats: Analysts who follow every function definition expecting it to matter.
  • Doesn't defeat: Anyone who traces from :execute_payload backwards.

The honest lesson here: real-world droppers also pad heavily with decoy routines. Time spent in dead code is the analyst-economics goal.


7. 8-Part Payload Assembly with Indirected Slot Keys

set "p1=cABvAHcAZQByAHMAaABlAGwAbAAgAC0ATgBvAFAAIAAtAE"
...
set "layer1_A=p1"   :: slot key -> variable name
...
for /L %%i in (0,1,7) do (
    set "current=!seq:~%%i,1!"
    for /f "tokens=1,2 delims==" %%a in ('set layer1_!current!') do (
        set "part_value=!%%b!"
        set "final_payload=!final_payload!!part_value!"
    )
)

Each base64 fragment is named p1..p8. A second variable layer maps slot keys A..H to the part names. The reassembly loop walks seq=ABCDEFGH, resolves layer1_<key> to a name, then dereferences the name to its value.

  • Defeats: Yara rules that hunt for contiguous base64 of a known PowerShell-encoded download cradle.
  • Doesn't defeat: Process-tree rules (the resulting powershell.exe still has the long base64 on its command line), AMSI (sees decoded plaintext), Event ID 4104.

Bug history: Earlier versions of this script used random single-digit slot keys (e.g. set "s1=%random:~0,1%") with 8 keys drawn from {0..9}. That caused near-guaranteed collisions and silent payload truncation. Fixed to fixed unique single-character keys A..H. Same idea, no collisions.


8. Decoy Shuffle / Checksum

:shuffle_data            :: circular-rotate #part1/#part2/#part3 sometimes
:verify_checksum         :: trivial sum, always passes

#part1/#part2/#part3 are decoy values; verify_checksum always returns 0. Both functions are pure analyst-bait.


9. Final Indirection

set "wrapper1=c_1"
set "wrapper2=c_2"
set "x=!%wrapper1%!"
set "y=!%wrapper2%!"
...
%x% %y% "%payload%"

Even the executable name (powershell) and flags are one variable-dereference removed from the literal text. The final %x% %y% "%payload%" line is the only real load-bearing line in the script.

  • Defeats: Read-the-last-line analysts who expect to see powershell ... written directly.
  • Doesn't defeat: Process Monitor, Sysmon EID 1, Get-CimInstance Win32_Process — all see the final fully-resolved command line.

Reading Order

  1. Start at :real_start, scroll to the bottom (:execute_payload) and read backwards via prepare_execution → assemble_payload → generate_payload_parts.
  2. Everything between :init_char_map and :shuffle_data is decoy.
  3. The whole script collapses to:
powershell -NoProfile -NonInteractive -EncodedCommand <base64 of cradle>

Knowing that, every other technique in the file is a delay tactic measured in analyst-minutes, not detection events.


The Honest Defense Section

Forget signature evasion. To actually disrupt this chain in production you want:

  1. PowerShell Constrained Language Mode via AppLocker / WDAC — breaks New-Object Net.WebClient and [ScriptBlock]::Create.
  2. ScriptBlock Logging (EID 4104) + central collection — gives you the decoded payload regardless of obfuscation.
  3. AMSI + a modern AV — catches the post-decode PowerShell instantly.
  4. Behavioral rule: cmd.exemshta.exepowershell.exe -EncodedCommand parent chain. Sysmon EID 1 + a Sigma rule, deployed once, ends this entire class of dropper.
  5. Network egress controls on 127.0.0.1 is N/A but in the real-world variant, blocking outbound on PowerShell host (via WFP / EDR) defeats the DownloadString cradle outright.

The obfuscation in this file teaches you what attackers spend time on. The list above is where defenders should spend theirs.