DEV Community

Cover image for Decoding a PowerShell -EncodedCommand During Incident Response (the UTF-16 gotcha)
Peter Anderson
Peter Anderson

Posted on • Originally published at base64.dev

Decoding a PowerShell -EncodedCommand During Incident Response (the UTF-16 gotcha)

You're triaging an alert. Scheduled task, weird parent process, and a command line that looks like this:

powershell.exe -nop -w hidden -enc JABjACAAPQAg...
Enter fullscreen mode Exit fullscreen mode

You know the drill: grab the Base64 blob, decode it, read the script. So you paste it into a decoder and get back this:

$ c   =   " h t t p : / / ...
Enter fullscreen mode Exit fullscreen mode

Garbage. A space (or a null) between every single character. First instinct is that the payload is doubly-encoded or encrypted. It isn't. This is the single most common gotcha with -EncodedCommand, and once you know it, it takes ten seconds to fix.

Why it looks garbled

powershell.exe -enc (short for -EncodedCommand) expects Base64 of UTF-16LE (little-endian Unicode) bytes — not UTF-8. That's mandated by PowerShell itself, not a choice the attacker made.

In UTF-16LE, every ASCII character is stored as two bytes: the character followed by a 0x00 null byte. So the letter c isn't 0x63, it's 0x63 0x00. When you Base64-decode the blob and then read it as UTF-8, every one of those null bytes renders as a space or an invisible control character. Hence the h t t p spacing.

Text:      c        =        "
UTF-16LE:  63 00    3D 00    22 00
UTF-8 view: c  ␀     =  ␀     "  ␀     <- the null shows up as a "space"
Enter fullscreen mode Exit fullscreen mode

Decode it as UTF-16LE instead and the nulls disappear, because that's what they were: the high byte of each 16-bit code unit.

Decode it correctly

In PowerShell itself — the encoding is literally called Unicode in .NET, which means UTF-16LE:

$enc = 'JABjACAAPQAg...'
[System.Text.Encoding]::Unicode.GetString([System.Convert]::FromBase64String($enc))
Enter fullscreen mode Exit fullscreen mode

In Python — decode the bytes, then read them as utf-16-le:

import base64
enc = "JABjACAAPQAg..."
print(base64.b64decode(enc).decode("utf-16-le"))
Enter fullscreen mode Exit fullscreen mode

In CyberChef — build the recipe From Base64Decode text (UTF-16LE). Or From Base64 then Remove null bytes for a quick-and-dirty look.

Any of these turns the spaced-out mess back into readable PowerShell.

The encode direction (for building test cases)

If you're writing detections or a lab sample, this is how the blob is produced — same Unicode/UTF-16LE contract in reverse:

$cmd = 'Write-Host "hello from encoded command"'
[System.Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($cmd))
Enter fullscreen mode Exit fullscreen mode

Feed that output back to powershell -enc and it runs. Legit automation uses this all the time — it's a clean way to pass a multi-line script through a single argument without quoting nightmares. That's exactly why malware hides in the same crowd.

The second layer: it's often gzip too

Sometimes you decode correctly and still get binary garbage. That's the next common trick: the real script is gzip-compressed, then Base64'd. The outer PowerShell you just decoded is a tiny loader whose whole job is to inflate the inner payload in memory:

# telltale pattern inside the decoded stub
$s = New-Object IO.MemoryStream(,[Convert]::FromBase64String("H4sIA..."))
IO.StreamReader(New-Object IO.Compression.GzipStream($s,[IO.Compression.CompressionMode]::Decompress)).ReadToEnd()
Enter fullscreen mode Exit fullscreen mode

The tell is the inner Base64 starting with H4sI — that's the gzip magic number (1f 8b) in Base64. When you see it, decode Base64 again and gunzip to reach the actual script. Payloads can nest two or three layers deep this way.

Is it safe to decode a suspicious sample?

Yes. Decoding is not executing. Converting Base64 back to text and inflating gzip are pure data transformations — no powershell.exe runs, nothing touches the script block, nothing hits the network. The danger is only if you run the decoded command. So decode freely and read; just don't pipe the result into a shell.

For fast triage there's a free browser-based tool that decodes -enc / -EncodedCommand for you — it handles the UTF-16LE conversion and auto-inflates nested gzip layers, and it runs entirely client-side so it's safe to paste suspicious samples into (nothing is uploaded): PowerShell Encoded Command Decoder.

Analyst checklist vs. the layers

What you see after decoding What it means Next step
Space between every char You decoded as UTF-8 Re-decode as UTF-16LE
Clean, readable PowerShell Done Read it, extract IOCs
Binary garbage, starts H4sI Inner gzip payload Base64-decode again, then gunzip
Another -enc / FromBase64String Nested loader Repeat the whole process on the inner blob

TL;DR

  • powershell -enc <base64> is Base64 of UTF-16LE, not UTF-8 — that's why naive decoding shows a space/null between every character.
  • Decode it right with [System.Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($enc)), or in Python with base64.b64decode(enc).decode("utf-16-le").
  • If the result is still binary and starts with H4sI, it's gzip inside Base64 — inflate it to reach the real script.
  • Decoding never executes the command, so it's safe to analyze suspicious samples locally.

Seen a -enc payload with an interesting nesting trick? Share the pattern in the comments.

Top comments (0)