mcpField
D
deobfuscating-powershell-obfuscated-malware

deobfuscating-powershell-obfuscated-malware

github.com/mukul975安全35.2k129 次安装8 周前更新
代码仓库

关于 deobfuscating-powershell-obfuscated-malware

这是一项面向安全分析人员的 PowerShell 恶意软件混淆分析技能,介绍分层解码、AST 分析和动态跟踪方法,并包含提取 IOC 的流程。

主要功能

  1. 01识别常见混淆方式
  2. 02迭代替换 IEX 以展开脚本
  3. 03使用 AST 分析表达式
  4. 04从解码脚本提取 IOC

适用场景

  1. 01分析受控环境中的恶意软件样本
  2. 02在授权测试中还原 PowerShell 脚本
  3. 03提取攻击产物中的 IOC

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/mukul975/anthropic-cybersecurity-skills --skill deobfuscating-powershell-obfuscated-malware

安装后在 Claude Code 中输入/deobfuscating-powershell-obfuscated-malware确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

LICENSE
assets/
template.md
references/
api-reference.md
standards.md
workflows.md
scripts/
agent.py
process.py

详细介绍

去混淆 PowerShell 混淆恶意软件

概述

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

何时使用

  • 在涉及去混淆 PowerShell 混淆恶意软件的授权安全测试中
  • 在受控环境中分析恶意软件样本或攻击痕迹时
  • 在开展红队演练或渗透测试项目时
  • 在基于对攻击技术理解来构建检测能力时

前提条件

  • 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 忽略的字符之间添加反引号(Invoke-Expression`)。SecureString 转换使用带有内嵌密钥的 ConvertTo-SecureString 对字符串进行加密。

基于 AST 的去混淆

PowerShell 的抽象语法树会暴露脚本的解析结构,而不受表层混淆的影响。通过遍历 AST 并求值表达式节点,分析人员可以解析拼接的字符串、解码编码后的值,并重建原始命令。PowerPeeler 在指令级使用这种方法,监控执行过程以将 AST 节点与其求值结果关联起来。

动态执行跟踪

通过将 Invoke-Expression(IEX)替换为 Write-Output,分析人员可以安全地捕获本应被执行的去混淆脚本内容。该技术通过迭代替换 IEX 调用,可跨多个层工作,直到揭示最终载荷。

工作流

步骤 1:识别混淆层
#!/usr/bin/env python3
"""Identify and classify PowerShell obfuscation techniques."""
import re
import base64
import sys


def analyze_obfuscation(script_content):
    """Identify obfuscation techniques used in PowerShell script."""
    techniques = []

    # Check for Base64 encoded command
    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")

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

    # Check for string concatenation
    concat_count = script_content.count("'+'") + script_content.count('"+"')
    if concat_count > 3:
        techniques.append(f"String Concatenation ({concat_count} joins)")

    # Check for char array construction
    if re.search(r'\[char\]\s*\d+', script_content, re.IGNORECASE):
        techniques.append("Character Code Array")

    # Check for Invoke-Expression variants
    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 variant: {pattern}")

    # Check for tick-mark obfuscation
    tick_count = script_content.count('`')
    if tick_count > 5:
        techniques.append(f"Tick-mark Insertion ({tick_count} backticks)")

    # Check for environment variable abuse
    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"Environment Variable Abuse ({len(env_refs)} refs)")

    # Check for SecureString
    if re.search(r'ConvertTo-SecureString', script_content, re.IGNORECASE):
        techniques.append("SecureString Encryption")

    # Check for compression
    if re.search(r'IO\.Compression|DeflateStream|GZipStream',
                 script_content, re.IGNORECASE):
        techniques.append("Compression (Deflate/GZip)")

    # Check for XOR encoding
    if re.search(r'-bxor\s+\d+', script_content, re.IGNORECASE):
        techniques.append("XOR Encoding")

    # Check for Replace chain
    replace_count = len(re.findall(r'\.Replace\(', script_content))
    if replace_count > 2:
        techniques.append(f"Replace Chain ({replace_count} replacements)")

    return techniques


def decode_base64_command(script_content):
    """Extract and decode Base64 encoded commands."""
    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):
    """Remove PowerShell tick-mark obfuscation."""
    # Remove backticks that are not escape sequences
    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:
                # Skip the backtick, keep the next char
                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):
    """Resolve simple string concatenation patterns."""
    # Pattern: '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)
    # Pattern: "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"Usage: {sys.argv[0]} <powershell_script>")
        sys.exit(1)

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

    print("[+] Obfuscation Analysis")
    print("=" * 60)
    techniques = analyze_obfuscation(content)
    for t in techniques:
        print(f"  - {t}")

    # Attempt automatic deobfuscation
    print("\n[+] Attempting Deobfuscation")
    print("=" * 60)

    # Layer 1: Remove tick marks
    deobfuscated = remove_tick_marks(content)

    # Layer 2: Resolve string concatenation
    deobfuscated = resolve_string_concat(deobfuscated)

    # Layer 3: Decode Base64
    b64_decoded = decode_base64_command(deobfuscated)
    if b64_decoded:
        print("[+] Base64 decoded content:")
        print(b64_decoded[:2000])
        deobfuscated = b64_decoded

    print(f"\n[+] Deobfuscated script length: {len(deobfuscated)} chars")
    output_file = sys.argv[1] + ".deobfuscated.ps1"
    with open(output_file, 'w') as f:
        f.write(deobfuscated)
    print(f"[+] Saved to {output_file}")
步骤 2:多层 IEX 替换
import subprocess
import tempfile
import os

def iex_replacement_deobfuscate(script_content, max_layers=10):
    """Iteratively replace IEX with Write-Output to unwrap layers."""
    # IEX replacement patterns
    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):
        # Apply IEX replacements
        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"  [+] No more IEX layers found at layer {layer_num}")
            break

        # Write to temp file and execute in constrained 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 {layer_num + 1}: Unwrapped "
                      f"{len(output)} chars")
                layers.append({
                    "layer": layer_num + 1,
                    "technique": "IEX replacement",
                    "content_length": len(output),
                })
                current = output
            else:
                break

        except subprocess.TimeoutExpired:
            print(f"  [!] Layer {layer_num + 1}: Execution timeout")
            break
        finally:
            os.unlink(tmp_path)

    return current, layers
步骤 3:从去混淆脚本中提取 IOC
def extract_iocs_from_script(deobfuscated_content):
    """Extract indicators of compromise from deobfuscated PowerShell."""
    iocs = {
        "urls": [],
        "ips": [],
        "domains": [],
        "file_paths": [],
        "registry_keys": [],
        "commands": [],
        "base64_blobs": [],
    }

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

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

    # File paths
    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)))

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

    # Suspicious commands
    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 和文件路径
  • 去混淆脚本与沙箱中观察到的恶意软件行为一致

参考资料

评论

支持 Markdown