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. 02IEX の反復置換によるスクリプト展開
  3. 03AST による式の分析
  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で難読化されたマルウェアの難読化解除を伴う、許可されたセキュリティテストを実施する場合
  • 管理された環境でマルウェアのサンプルや攻撃の痕跡を分析する場合
  • レッドチーム演習やペネトレーションテストを実施する場合
  • 攻撃手法の理解に基づいて検出機能を構築する場合

前提条件

  • base64、re、subprocessモジュールを含むPython 3.9+
  • PowerShell 5.1+またはPowerShell 7+(ASTへのアクセス用)
  • PSDecode(Install-Module PSDecode)
  • PowerDecode(https://github.com/Malandrone/PowerDecode)
  • スクリプトを安全に実行するための隔離VMまたはサンドボックス
  • エンコード変換を手動で行うための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、ファイルパスが抽出されている
  • 難読化解除済みスクリプトが、サンドボックスで観測されたマルウェアの挙動と一致している

参考資料

コメント