# Deobfuscating Powershell Obfuscated Malware

> 使用 AST 分析、动态追踪以及 PSDecode 和 PowerDecode 等工具，系统地对多层 PowerShell 恶意软件进行去混淆，以揭示隐藏的载荷和 C2 基础设施。

- Skill: `killvxk/deobfuscating-powershell-obfuscated-malware` (Agent Skill, multi-file: 8 files)
- Install (CLI): `npx skillmds@latest add killvxk/deobfuscating-powershell-obfuscated-malware`
- Raw SKILL.md: https://api.skillmd.com/api/skills/killvxk/deobfuscating-powershell-obfuscated-malware/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: Apache-2.0
- Author: killvxk (https://skillmd.com/u/killvxk)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/killvxk/deobfuscating-powershell-obfuscated-malware

---

# 对混淆 PowerShell 恶意软件进行去混淆

## 概述

PowerShell 因其与 Windows 的深度集成和强大的脚本功能而被恶意软件作者大量滥用。混淆技术包括：字符串拼接、Base64 编码、字符替换、Invoke-Expression 分层、SecureString 滥用、环境变量操控和反引号插入。现代恶意软件使用多层混淆，需要迭代式去混淆。PSDecode、PowerDecode 和 PowerPeeler 等工具可以自动化大部分这一过程，而手动 AST（抽象语法树）分析则可以处理自定义混淆。PowerPeeler 通过对表达式相关 AST 节点进行指令级动态分析，实现了 95% 的去混淆正确率。

## 前置条件

- Python 3.9+，包含 `base64`、`re`、`subprocess` 模块
- PowerShell 5.1+ 或 PowerShell 7+（用于 AST 访问）
- PSDecode（`Install-Module PSDecode`）
- PowerDecode（https://github.com/Malandrone/PowerDecode）
- 用于安全脚本执行的隔离虚拟机或沙箱
- CyberChef，用于手动编码转换
- 理解 PowerShell AST 和 Invoke-Expression 模式

## 核心概念

### 常见混淆技术

PowerShell 恶意软件采用分层混淆来规避静态检测。字符串拼接将命令拆分到多个变量中（`$a='In'+'voke'`）。Base64 编码将整个脚本包装在 `-EncodedCommand` 参数中。字符代码数组使用 `[char]` 转换（`[char[]](73,69,88)|%{$r+=$_}`）。环境变量滥用从 `$env:` 路径中读取子字符串。反引号插入在 PowerShell 会忽略的字符之间添加反引号（``I`nv`oke-Exp`ression``）。SecureString 转换使用 ConvertTo-SecureString 配合嵌入的密钥加密字符串。

### 基于 AST 的去混淆

PowerShell 的抽象语法树暴露了脚本的解析结构，与表面层面的混淆无关。通过遍历 AST 并评估表达式节点，分析人员可以解析拼接的字符串、解码编码值并重建原始命令。PowerPeeler 在指令级别使用这种方法，监控执行过程以将 AST 节点与其评估结果相关联。

### 动态执行追踪

通过将 `Invoke-Expression`（IEX）替换为 `Write-Output`，分析人员可以安全地捕获通常会被执行的去混淆脚本内容。这种技术通过迭代替换 IEX 调用跨越多层工作，直到最终载荷被揭示。

## 操作步骤

### 步骤 1：识别混淆层

```python
#!/usr/bin/env python3
"""识别并分类 PowerShell 混淆技术。"""
import re
import base64
import sys


def analyze_obfuscation(script_content):
    """识别 PowerShell 脚本中使用的混淆技术。"""
    techniques = []

    # 检查 Base64 编码命令
    b64_pattern = re.compile(
        r'-[Ee](?:nc(?:odedcommand)?)\s+([A-Za-z0-9+/=]{20,})',
        re.IGNORECASE
    )
    if b64_pattern.search(script_content):
        techniques.append("Base64 EncodedCommand")

    # 检查 FromBase64String
    if re.search(r'\[Convert\]::FromBase64String', script_content, re.IGNORECASE):
        techniques.append("Base64 FromBase64String")

    # 检查字符串拼接
    concat_count = script_content.count("'+'") + script_content.count('"+"')
    if concat_count > 3:
        techniques.append(f"字符串拼接（{concat_count} 次连接）")

    # 检查字符数组构造
    if re.search(r'\[char\]\s*\d+', script_content, re.IGNORECASE):
        techniques.append("字符代码数组")

    # 检查 Invoke-Expression 变体
    iex_patterns = [
        r'Invoke-Expression',
        r'\bIEX\b',
        r'\.\s*\(\s*\$',
        r'&\s*\(\s*\$',
        r'\|\s*IEX',
        r'\|\s*Invoke-Expression',
    ]
    for pattern in iex_patterns:
        if re.search(pattern, script_content, re.IGNORECASE):
            techniques.append(f"Invoke-Expression 变体：{pattern}")

    # 检查反引号混淆
    tick_count = script_content.count('`')
    if tick_count > 5:
        techniques.append(f"反引号插入（{tick_count} 个反引号）")

    # 检查环境变量滥用
    if re.search(r'\$env:', script_content, re.IGNORECASE):
        env_refs = re.findall(r'\$env:\w+', script_content, re.IGNORECASE)
        if len(env_refs) > 2:
            techniques.append(f"环境变量滥用（{len(env_refs)} 处引用）")

    # 检查 SecureString
    if re.search(r'ConvertTo-SecureString', script_content, re.IGNORECASE):
        techniques.append("SecureString 加密")

    # 检查压缩
    if re.search(r'IO\.Compression|DeflateStream|GZipStream',
                 script_content, re.IGNORECASE):
        techniques.append("压缩（Deflate/GZip）")

    # 检查 XOR 编码
    if re.search(r'-bxor\s+\d+', script_content, re.IGNORECASE):
        techniques.append("XOR 编码")

    # 检查 Replace 链
    replace_count = len(re.findall(r'\.Replace\(', script_content))
    if replace_count > 2:
        techniques.append(f"Replace 链（{replace_count} 次替换）")

    return techniques


def decode_base64_command(script_content):
    """提取并解码 Base64 编码的命令。"""
    b64_match = re.search(
        r'-[Ee](?:nc(?:odedcommand)?)\s+([A-Za-z0-9+/=]{20,})',
        script_content, re.IGNORECASE
    )
    if b64_match:
        encoded = b64_match.group(1)
        try:
            decoded = base64.b64decode(encoded).decode('utf-16-le')
            return decoded
        except Exception:
            return None
    return None


def remove_tick_marks(script_content):
    """移除 PowerShell 反引号混淆。"""
    # 移除非转义序列的反引号
    escape_chars = {'`n', '`r', '`t', '`a', '`b', '`f', '`v', '`0', '``'}
    result = []
    i = 0
    while i < len(script_content):
        if script_content[i] == '`' and i + 1 < len(script_content):
            pair = script_content[i:i+2]
            if pair in escape_chars:
                result.append(pair)
                i += 2
            else:
                # 跳过反引号，保留下一个字符
                result.append(script_content[i+1])
                i += 2
        else:
            result.append(script_content[i])
            i += 1
    return ''.join(result)


def resolve_string_concat(script_content):
    """解析简单的字符串拼接模式。"""
    # 模式：'str1' + 'str2'
    pattern = re.compile(r"'([^']*)'\s*\+\s*'([^']*)'")
    while pattern.search(script_content):
        script_content = pattern.sub(lambda m: f"'{m.group(1)}{m.group(2)}'",
                                      script_content)
    # 模式："str1" + "str2"
    pattern = re.compile(r'"([^"]*)"\s*\+\s*"([^"]*)"')
    while pattern.search(script_content):
        script_content = pattern.sub(lambda m: f'"{m.group(1)}{m.group(2)}"',
                                      script_content)
    return script_content


if __name__ == "__main__":
    if len(sys.argv) < 2:
        print(f"用法：{sys.argv[0]} <powershell_script>")
        sys.exit(1)

    with open(sys.argv[1], 'r', errors='replace') as f:
        content = f.read()

    print("[+] 混淆分析")
    print("=" * 60)
    techniques = analyze_obfuscation(content)
    for t in techniques:
        print(f"  - {t}")

    # 尝试自动去混淆
    print("\n[+] 正在尝试去混淆")
    print("=" * 60)

    # 第 1 层：移除反引号
    deobfuscated = remove_tick_marks(content)

    # 第 2 层：解析字符串拼接
    deobfuscated = resolve_string_concat(deobfuscated)

    # 第 3 层：解码 Base64
    b64_decoded = decode_base64_command(deobfuscated)
    if b64_decoded:
        print("[+] Base64 解码内容：")
        print(b64_decoded[:2000])
        deobfuscated = b64_decoded

    print(f"\n[+] 去混淆后脚本长度：{len(deobfuscated)} 个字符")
    output_file = sys.argv[1] + ".deobfuscated.ps1"
    with open(output_file, 'w') as f:
        f.write(deobfuscated)
    print(f"[+] 已保存到 {output_file}")
```

### 步骤 2：多层 IEX 替换

```python
import subprocess
import tempfile
import os

def iex_replacement_deobfuscate(script_content, max_layers=10):
    """迭代地将 IEX 替换为 Write-Output 以解包各层。"""
    # IEX 替换模式
    replacements = [
        (r'\bInvoke-Expression\b', 'Write-Output'),
        (r'\bIEX\b', 'Write-Output'),
        (r'\|\s*IEX\b', '| Write-Output'),
    ]

    current = script_content
    layers = []

    for layer_num in range(max_layers):
        # 应用 IEX 替换
        modified = current
        for pattern, replacement in replacements:
            modified = re.sub(pattern, replacement, modified, flags=re.IGNORECASE)

        if modified == current and layer_num > 0:
            print(f"  [+] 在第 {layer_num} 层未发现更多 IEX 层")
            break

        # 写入临时文件并在受限的 PowerShell 中执行
        with tempfile.NamedTemporaryFile(mode='w', suffix='.ps1',
                                          delete=False) as tmp:
            tmp.write(modified)
            tmp_path = tmp.name

        try:
            result = subprocess.run(
                ['powershell', '-NoProfile', '-ExecutionPolicy', 'Bypass',
                 '-File', tmp_path],
                capture_output=True, text=True, timeout=30
            )

            output = result.stdout.strip()
            if output and output != current:
                print(f"  [+] 第 {layer_num + 1} 层：解包了 "
                      f"{len(output)} 个字符")
                layers.append({
                    "layer": layer_num + 1,
                    "technique": "IEX 替换",
                    "content_length": len(output),
                })
                current = output
            else:
                break

        except subprocess.TimeoutExpired:
            print(f"  [!] 第 {layer_num + 1} 层：执行超时")
            break
        finally:
            os.unlink(tmp_path)

    return current, layers
```

### 步骤 3：从去混淆后的脚本中提取 IoC

```python
def extract_iocs_from_script(deobfuscated_content):
    """从去混淆后的 PowerShell 中提取攻陷指标。"""
    iocs = {
        "urls": [],
        "ips": [],
        "domains": [],
        "file_paths": [],
        "registry_keys": [],
        "commands": [],
        "base64_blobs": [],
    }

    # URL
    url_pattern = re.compile(
        r'https?://[^\s'"<>)\]]+', re.IGNORECASE
    )
    iocs["urls"] = list(set(url_pattern.findall(deobfuscated_content)))

    # IP 地址
    ip_pattern = re.compile(
        r'\b(?:\d{1,3}\.){3}\d{1,3}\b'
    )
    iocs["ips"] = list(set(ip_pattern.findall(deobfuscated_content)))

    # 文件路径
    path_pattern = re.compile(
        r'[A-Za-z]:\\[^\s'"<>|]+|'
        r'\\\\[^\s'"<>|]+|'
        r'%(?:APPDATA|TEMP|USERPROFILE|PROGRAMFILES)%[^\s'"<>|]*',
        re.IGNORECASE
    )
    iocs["file_paths"] = list(set(path_pattern.findall(deobfuscated_content)))

    # 注册表键
    reg_pattern = re.compile(
        r'(?:HKLM|HKCU|HKCR|HKU|HKCC)(?:\\[^\s'"<>|]+)+',
        re.IGNORECASE
    )
    iocs["registry_keys"] = list(set(reg_pattern.findall(deobfuscated_content)))

    # 可疑命令
    suspicious_cmds = [
        'New-Object Net.WebClient',
        'DownloadString', 'DownloadFile', 'DownloadData',
        'Start-Process', 'Invoke-WebRequest',
        'New-Object IO.MemoryStream',
        'Reflection.Assembly',
        'Add-MpPreference -ExclusionPath',
        'Set-MpPreference -DisableRealtimeMonitoring',
        'New-ScheduledTask', 'Register-ScheduledTask',
    ]
    for cmd in suspicious_cmds:
        if cmd.lower() in deobfuscated_content.lower():
            iocs["commands"].append(cmd)

    return iocs
```

## 验证标准

- 所有混淆层已被正确识别和分类
- Base64 编码的命令已解码为可读的 PowerShell
- 反引号和字符串拼接混淆已解析
- IEX 替换揭示了下一阶段的载荷
- 从最终去混淆阶段提取了 URL、IP 和文件路径
- 去混淆后的脚本与沙箱中观察到的恶意软件行为相符

## 参考资料

- [PSDecode - PowerShell 去混淆](https://github.com/R3MRUM/PSDecode)
- [PowerDecode - 多层去混淆](https://github.com/Malandrone/PowerDecode)
- [PowerPeeler - 指令级去混淆](https://arxiv.org/html/2406.04027v2)
- [SentinelOne - 解构 PowerShell 混淆](https://www.sentinelone.com/blog/deconstructing-powershell-obfuscation-in-malspam-campaigns/)
- [MITRE ATT&CK T1059.001 - PowerShell](https://attack.mitre.org/techniques/T1059/001/)

