reverse-skills-max: initial public release (restored, redacted, hardened)
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
Apache License 2.0 — see the repository root LICENSE file for the full text.
|
||||
|
||||
Copyright the AnalystAIPack contributors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
@@ -0,0 +1,111 @@
|
||||
---
|
||||
name: deobfuscating-malicious-powershell
|
||||
description: 'Deobfuscates malicious PowerShell by decoding -EncodedCommand, reversing string
|
||||
and format obfuscation, resolving base64/gzip/IEX layers, and recovering the final payload
|
||||
and IOCs. Activates for requests to deobfuscate, decode, or analyze obfuscated PowerShell
|
||||
commands or scripts.'
|
||||
domain: cybersecurity
|
||||
subdomain: malware-analysis
|
||||
tags:
|
||||
- malware
|
||||
- powershell
|
||||
- deobfuscation
|
||||
- base64
|
||||
- script-analysis
|
||||
- windows
|
||||
version: 1.0.0
|
||||
author: analyst-ai-pack
|
||||
license: Apache-2.0
|
||||
mitre_attack:
|
||||
- T1059.001
|
||||
- T1027
|
||||
- T1140
|
||||
d3fend:
|
||||
- D3-FCR
|
||||
- D3-DA
|
||||
references:
|
||||
- 'PowerShell about_PowerShell.exe -EncodedCommand — https://learn.microsoft.com/powershell/'
|
||||
- 'PSDecode — https://github.com/R3MRUM/PSDecode'
|
||||
---
|
||||
|
||||
# Deobfuscating Malicious PowerShell
|
||||
|
||||
## When to Use
|
||||
|
||||
- You captured an obfuscated PowerShell command from a log, maldoc, or phishing payload.
|
||||
- You need to recover the real intent: download URLs, dropped paths, shellcode, or next stage.
|
||||
- You are unrolling layered encoding (base64 → gzip → IEX) to reach the final script.
|
||||
|
||||
**Do not use** `Invoke-Expression` or run the script to "see what it does" on a working host.
|
||||
Decode statically; only execute in the isolated lab with logging if dynamic confirmation is
|
||||
required.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Python (bundled script handles base64/gzip/deflate and common transforms), or a sandbox.
|
||||
- The command/script text captured as a string.
|
||||
|
||||
## Safety & Handling
|
||||
|
||||
- Never pipe attacker PowerShell into a live shell. Replace `IEX`/`Invoke-Expression` with
|
||||
output (print) when transforming.
|
||||
- Treat decoded URLs/IPs as live; defang before sharing.
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Decode -EncodedCommand
|
||||
|
||||
`-EncodedCommand` (`-enc`) is base64 of UTF-16LE. Decode it first:
|
||||
|
||||
```bash
|
||||
python scripts/analyst.py decode --encodedcommand "<base64>"
|
||||
```
|
||||
|
||||
### Step 2: Identify the obfuscation style
|
||||
|
||||
Common patterns:
|
||||
|
||||
```text
|
||||
String reversal : [array]::Reverse / -join with [-1..]
|
||||
Format operator : ("{1}{0}" -f 'b','a') -> 'ab'
|
||||
Char codes : [char]104 + [char]105
|
||||
Concatenation/splits : 'po'+'wer'+'shell', -split / -replace tricks
|
||||
Backtick/casing noise : I`E`X, iEx, &('i'+'ex')
|
||||
```
|
||||
|
||||
### Step 3: Unroll encoding layers
|
||||
|
||||
Resolve nested `FromBase64String`, gzip/deflate (`IO.Compression`), and re-IEX layers until
|
||||
you reach plain script. The script peels base64/gzip layers automatically.
|
||||
|
||||
```bash
|
||||
python scripts/analyst.py deobfuscate payload.ps1
|
||||
```
|
||||
|
||||
### Step 4: Neutralize execution sinks
|
||||
|
||||
Replace `IEX`, `Invoke-Expression`, `&`, and `.Invoke()` with a print so the recovered script
|
||||
is rendered, not run.
|
||||
|
||||
### Step 5: Extract IOCs
|
||||
|
||||
Pull `DownloadString`/`DownloadFile` URLs, C2 hosts, dropped paths, and any embedded
|
||||
base64 shellcode for separate analysis.
|
||||
|
||||
## Validation
|
||||
|
||||
- The fully decoded script is human-readable PowerShell with no remaining base64 blobs.
|
||||
- Execution sinks are neutralized; nothing in your workflow actually ran the payload.
|
||||
- Extracted URLs/paths are consistent with the script's logic.
|
||||
|
||||
## Pitfalls
|
||||
|
||||
- Running the script to decode it — the fastest way to get compromised.
|
||||
- Stopping after one base64 layer when several are nested.
|
||||
- Missing UTF-16LE vs UTF-8 when decoding `-enc` (it is UTF-16LE).
|
||||
- Ignoring `-replace`/`-f` transforms that rebuild commands at runtime.
|
||||
|
||||
## References
|
||||
|
||||
- See [`references/api-reference.md`](references/api-reference.md) for the decoder/deobfuscator.
|
||||
- PowerShell encoded-command docs and PSDecode (linked in frontmatter).
|
||||
@@ -0,0 +1,48 @@
|
||||
# API Reference: Deobfuscating Malicious PowerShell
|
||||
|
||||
## Overview
|
||||
|
||||
`scripts/analyst.py` decodes `-EncodedCommand` (base64 UTF-16LE), peels nested base64 plus
|
||||
gzip/deflate layers, neutralizes execution sinks (`IEX`, `Invoke-Expression`), and extracts
|
||||
URLs. It never runs PowerShell.
|
||||
|
||||
## Dependencies
|
||||
|
||||
| Package | Version | Purpose |
|
||||
|---------|---------|---------|
|
||||
| (stdlib) | — | base64, gzip, zlib, regex |
|
||||
|
||||
## External tools
|
||||
|
||||
| Tool | Purpose |
|
||||
|------|---------|
|
||||
| PSDecode | Emulated unrolling of heavily layered scripts |
|
||||
| CyberChef | Interactive multi-step decoding |
|
||||
|
||||
## Core functions
|
||||
|
||||
### `decode_encodedcommand(b64)`
|
||||
Decodes a `-EncodedCommand`/`-enc` blob (base64 of UTF-16LE) to text.
|
||||
|
||||
### `peel_layers(text, max_depth)`
|
||||
Iteratively decodes the largest base64 blob and attempts gzip/deflate/UTF-16 decompression,
|
||||
returning each layer.
|
||||
|
||||
### `neutralize(text)`
|
||||
Replaces `IEX`/`Invoke-Expression` with output statements so recovered scripts are not run.
|
||||
|
||||
### `extract_iocs(text)`
|
||||
Collects URLs across all decoded layers.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
python scripts/analyst.py decode --encodedcommand "<base64>"
|
||||
python scripts/analyst.py deobfuscate payload.ps1
|
||||
```
|
||||
|
||||
## Sources
|
||||
|
||||
- PowerShell `-EncodedCommand` semantics (UTF-16LE base64) — https://learn.microsoft.com/powershell/
|
||||
- PSDecode — https://github.com/R3MRUM/PSDecode
|
||||
- Common PowerShell obfuscation transforms (format operator, char codes, reversal)
|
||||
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Deobfuscate malicious PowerShell without executing it.
|
||||
|
||||
Decodes ``-EncodedCommand`` (base64 UTF-16LE), peels nested base64 + gzip/deflate layers,
|
||||
applies a few common string transforms, and extracts IOCs. It transforms text only and never
|
||||
invokes PowerShell.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import binascii
|
||||
import gzip
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import zlib
|
||||
from pathlib import Path
|
||||
|
||||
_URL = re.compile(r"https?://[^\s\"'<>)]+", re.I)
|
||||
_B64 = re.compile(r"[A-Za-z0-9+/]{40,}={0,2}")
|
||||
|
||||
|
||||
def decode_encodedcommand(b64: str) -> str:
|
||||
raw = base64.b64decode(b64)
|
||||
# -EncodedCommand payloads are UTF-16LE
|
||||
try:
|
||||
return raw.decode("utf-16-le")
|
||||
except UnicodeDecodeError:
|
||||
return raw.decode("latin-1", "replace")
|
||||
|
||||
|
||||
def _try_decompress(raw: bytes) -> str | None:
|
||||
for fn in (lambda b: gzip.decompress(b),
|
||||
lambda b: zlib.decompress(b),
|
||||
lambda b: zlib.decompress(b, -15)):
|
||||
try:
|
||||
return fn(raw).decode("utf-8", "replace")
|
||||
except (OSError, zlib.error, binascii.Error):
|
||||
continue
|
||||
return None
|
||||
|
||||
|
||||
def peel_layers(text: str, max_depth: int = 6) -> list[str]:
|
||||
"""Return successive decoded layers, peeling base64(+gzip/deflate) blobs."""
|
||||
layers = [text]
|
||||
current = text
|
||||
for _ in range(max_depth):
|
||||
blobs = _B64.findall(current)
|
||||
if not blobs:
|
||||
break
|
||||
blob = max(blobs, key=len)
|
||||
try:
|
||||
raw = base64.b64decode(blob + "=" * (-len(blob) % 4))
|
||||
except binascii.Error:
|
||||
break
|
||||
decoded = _try_decompress(raw)
|
||||
if decoded is None:
|
||||
try:
|
||||
decoded = raw.decode("utf-16-le")
|
||||
except UnicodeDecodeError:
|
||||
decoded = raw.decode("utf-8", "replace")
|
||||
if not decoded or decoded == current:
|
||||
break
|
||||
layers.append(decoded)
|
||||
current = decoded
|
||||
return layers
|
||||
|
||||
|
||||
def neutralize(text: str) -> str:
|
||||
"""Render execution sinks inert for safe display."""
|
||||
text = re.sub(r"\bI`?E`?X\b", "echo", text, flags=re.I)
|
||||
text = re.sub(r"\bInvoke-Expression\b", "Write-Output", text, flags=re.I)
|
||||
return text
|
||||
|
||||
|
||||
def extract_iocs(text: str) -> dict:
|
||||
return {"urls": sorted(set(_URL.findall(text)))}
|
||||
|
||||
|
||||
def deobfuscate(text: str) -> dict:
|
||||
layers = peel_layers(text)
|
||||
final = neutralize(layers[-1])
|
||||
return {
|
||||
"layer_count": len(layers),
|
||||
"final_script": final,
|
||||
"iocs": extract_iocs("\n".join(layers)),
|
||||
}
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(description="Deobfuscate PowerShell")
|
||||
sub = parser.add_subparsers(dest="cmd", required=True)
|
||||
|
||||
p_e = sub.add_parser("decode", help="decode -EncodedCommand")
|
||||
p_e.add_argument("--encodedcommand", required=True)
|
||||
|
||||
p_d = sub.add_parser("deobfuscate", help="peel layers from a script file")
|
||||
p_d.add_argument("file")
|
||||
|
||||
args = parser.parse_args(argv)
|
||||
if args.cmd == "decode":
|
||||
print(decode_encodedcommand(args.encodedcommand))
|
||||
return 0
|
||||
text = Path(args.file).read_text(encoding="utf-8", errors="replace")
|
||||
print(json.dumps(deobfuscate(text), indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user