<!-- Copyright (c) 2026 defconxt. All rights reserved. -->
<!-- Licensed under AGPL-3.0 — see LICENSE file for details. -->
---
name: deobfuscating-powershell-malware
description: >-
  Deobfuscate and analyze malicious PowerShell scripts including encoded commands,
  string manipulation, invoke-expression chains, and multi-layer obfuscation.
domain: cybersecurity
subdomain: malware-analysis
tags:
  - powershell
  - deobfuscation
  - script-analysis
  - living-off-the-land
  - fileless-malware
  - invoke-expression
version: "1.0"
author: defconxt
license: AGPL-3.0
metadata:
  mitre-attack: ["T1059.001", "T1027"]
---

# Deobfuscating PowerShell Malware

## Overview

PowerShell is the most abused living-off-the-land binary (LOLBin) in enterprise attacks. Threat actors use multiple obfuscation layers — Base64 encoding, string concatenation, format string operators, character code conversion, `Invoke-Expression` chains, AMSI bypass techniques, and compressed/encrypted payloads — to evade detection. Systematic deobfuscation recovers the original payload, reveals C2 infrastructure, and enables detection rule creation. Common delivery vectors include malicious macros, HTA files, LNK shortcuts, and scheduled tasks.

## Prerequisites

- PowerShell 5.1+ or pwsh 7+ (Windows or Linux)
- Python 3.10+ with `base64`, `re`, `zlib`
- PSDecode module or PowerDecode
- ScriptBlock logging enabled for dynamic analysis
- CyberChef for manual decoding chains
- Isolated analysis VM with AMSI enabled for testing

## Key Concepts

### Obfuscation Layers

| Technique | Example | Detection |
|-----------|---------|-----------|
| Base64 `-EncodedCommand` | `powershell -e SQBFAFgA...` | `-e` / `-enc` flag |
| String concatenation | `'Inv'+'oke'+'-Ex'+'pression'` | Fragmented cmdlet names |
| Format operator | `("{2}{0}{1}" -f 'ke-','Ex','Invo')` | `-f` with reordered indices |
| Char code | `[char]73+[char]69+[char]88` | `[char]` casting arrays |
| Backtick escape | `` In`vo`ke-`Exp`ression `` | Random backticks in cmdlets |
| Replace operator | `'REPLACE_IEX' -replace 'REPLACE_',''` | `-replace` building strings |
| Compression | `IEX(New-Object IO.StreamReader(...GZipStream...))` | `GZipStream`/`DeflateStream` |
| SecureString | `ConvertTo-SecureString` AES decryption | `SecureString` conversion |

### AMSI Bypass Patterns

Attackers patch `amsi.dll` in memory to disable scanning. Common patterns include `AmsiScanBuffer` patching, reflection-based field manipulation, and COM hijacking.

## Workflow

### Step 1: Identify Encoding Type

```bash
# Check for -EncodedCommand
echo "$PS_CMD" | grep -oP '(?<=-[eE][nN]?[cC]?\s+)[A-Za-z0-9+/=]+'

# Decode Base64 (UTF-16LE encoded by PowerShell)
echo "SQBFAFgAKAAnAHQAZQBzAHQAJwApAA==" | base64 -d | iconv -f UTF-16LE -t UTF-8

# Check for compressed payload
echo "$PS_CMD" | grep -iE 'frombase64|gzipstream|deflatestream|memorystream'
```

### Step 2: Static Deobfuscation with Python

```python
#!/usr/bin/env python3
"""Multi-layer PowerShell deobfuscator."""
import base64
import re
import zlib

def decode_base64_utf16(encoded: str) -> str:
    """Decode PowerShell Base64 (UTF-16LE)."""
    padded = encoded + "=" * (4 - len(encoded) % 4) if len(encoded) % 4 else encoded
    decoded = base64.b64decode(padded)
    return decoded.decode("utf-16-le", errors="replace")

def resolve_concat(script: str) -> str:
    """Resolve string concatenation: 'a'+'b'+'c' -> 'abc'."""
    return re.sub(
        r"'([^']*)'\s*\+\s*'([^']*)'",
        lambda m: f"'{m.group(1)}{m.group(2)}'",
        script,
    )

def resolve_format_operator(script: str) -> str:
    """Resolve format operator: ("{1}{0}" -f 'b','a') -> 'ab'."""
    pattern = r'\("([^"]+)"\s*-f\s*((?:\'[^\']*\'\s*,?\s*)+)\)'
    def replace_format(m):
        fmt = m.group(1)
        args = re.findall(r"'([^']*)'", m.group(2))
        try:
            result = fmt
            for i, arg in enumerate(args):
                result = result.replace(f"{{{i}}}", arg)
            return f"'{result}'"
        except (IndexError, KeyError):
            return m.group(0)
    return re.sub(pattern, replace_format, script)

def resolve_char_codes(script: str) -> str:
    """Resolve [char] casts: [char]73 -> 'I'."""
    return re.sub(
        r'\[char\]\s*(\d+)',
        lambda m: f"'{chr(int(m.group(1)))}'",
        script, flags=re.IGNORECASE,
    )

def remove_backticks(script: str) -> str:
    """Remove obfuscation backticks from cmdlet names."""
    return re.sub(r'`(?=[a-zA-Z])', '', script)

def resolve_replace(script: str) -> str:
    """Resolve -replace operations."""
    pattern = r"'([^']*)'\s*-replace\s*'([^']*)'\s*,\s*'([^']*)'"
    return re.sub(
        pattern,
        lambda m: f"'{m.group(1).replace(m.group(2), m.group(3))}'",
        script,
    )

def deobfuscate(script: str, max_passes: int = 10) -> str:
    """Apply deobfuscation passes until stable."""
    transforms = [
        remove_backticks,
        resolve_concat,
        resolve_format_operator,
        resolve_char_codes,
        resolve_replace,
    ]
    for _ in range(max_passes):
        prev = script
        for transform in transforms:
            script = transform(script)
        # Re-run concat after other transforms may have created new fragments
        script = resolve_concat(script)
        if script == prev:
            break
    return script

# Example usage
sample = """powershell -e SQBFAFgAKAAnAHQAZQBzAHQAJwApAA=="""
match = re.search(r'-[eE][nN]?[cC]?\s+([A-Za-z0-9+/=]+)', sample)
if match:
    decoded = decode_base64_utf16(match.group(1))
    print(f"Decoded: {decoded}")
    print(f"Deobfuscated: {deobfuscate(decoded)}")
```

### Step 3: Decompress GZip/Deflate Payloads

```python
import base64, zlib, io, gzip

def decompress_ps_payload(b64_data: str) -> str:
    """Decompress PowerShell GZip/Deflate compressed payload."""
    raw = base64.b64decode(b64_data)

    # Try GZip first
    try:
        with gzip.open(io.BytesIO(raw)) as f:
            return f.read().decode("utf-8", errors="replace")
    except (gzip.BadGzipFile, OSError):
        pass

    # Try raw Deflate
    try:
        return zlib.decompress(raw, -zlib.MAX_WBITS).decode("utf-8", errors="replace")
    except zlib.error:
        pass

    # Try zlib (with header)
    try:
        return zlib.decompress(raw).decode("utf-8", errors="replace")
    except zlib.error:
        return raw.decode("utf-8", errors="replace")
```

### Step 4: Dynamic Analysis with ScriptBlock Logging

```powershell
# Enable ScriptBlock logging (captures deobfuscated code)
Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging" `
  -Name "EnableScriptBlockLogging" -Value 1

# Execute obfuscated sample (in sandbox!)
# Then retrieve logged scripts:
Get-WinEvent -LogName "Microsoft-Windows-PowerShell/Operational" |
  Where-Object { $_.Id -eq 4104 } |
  Select-Object -ExpandProperty Message |
  Out-File deobfuscated_scriptblocks.txt

# AMSI captures (if not bypassed):
Get-WinEvent -LogName "Microsoft-Windows-Windows Defender/Operational" |
  Where-Object { $_.Id -eq 1116 } |
  Select-Object TimeCreated, Message
```

### Step 5: Extract IOCs from Deobfuscated Script

```python
import re

def extract_ps_iocs(script: str) -> dict:
    """Extract IOCs from deobfuscated PowerShell."""
    return {
        "urls": re.findall(r'https?://[^\s\'">\)]+', script),
        "ips": re.findall(r'\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b', script),
        "domains": re.findall(r'[a-zA-Z0-9][-a-zA-Z0-9]*\.[a-zA-Z]{2,}', script),
        "file_paths": re.findall(r'[A-Z]:\\[^\s\'"]+|/[^\s\'"]+', script),
        "registry_keys": re.findall(r'HKLM:\\[^\s\'"]+|HKCU:\\[^\s\'"]+', script),
        "base64_blobs": re.findall(r'[A-Za-z0-9+/]{40,}={0,2}', script),
        "cmdlets": re.findall(r'(?:Invoke|New|Set|Get)-[A-Za-z]+', script),
    }
```

## Detection

```yaml
title: Deobfuscating Powershell Malware Detection
id: f7ee4872-3510-47e9-bb61-6be4c6226ec9
status: experimental
description: Detects suspicious activity related to deobfuscating powershell malware techniques in malware analysis context
logsource:
  category: process_creation
  product: windows
detection:
  selection:
    CommandLine: "*deobfuscating*powershell*"
  condition: selection
level: high
tags:
  - attack.t1059.001
  - attack.t1027
  - attack.execution
falsepositives:
  - Sandbox detonation environments analyzing submitted samples
```

## Verification

- [ ] All encoding layers identified and decoded
- [ ] Original payload fully recovered
- [ ] C2 URLs/IPs extracted
- [ ] Execution flow documented (download → execute → persist)
- [ ] Detection signatures created (ScriptBlock content, command-line args)
- [ ] AMSI bypass technique documented (if present)

## References

- [Invoke-Obfuscation Framework](https://github.com/danielbohannon/Invoke-Obfuscation)
- [PSDecode - PowerShell Deobfuscator](https://github.com/R3MRUM/PSDecode)
- [MITRE ATT&CK T1059.001 - PowerShell](https://attack.mitre.org/techniques/T1059/001/)
- [MITRE ATT&CK T1027 - Obfuscated Files](https://attack.mitre.org/techniques/T1027/)
- [Microsoft ScriptBlock Logging](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_logging)
