deobfuscating-powershell-obfuscated-malware について
難読化された PowerShell マルウェアを調べるセキュリティ分析向けのスキルです。多層デコード、AST 分析、動的トレース、IOC 抽出の手順を扱います。
主な機能
- 01一般的な難読化手法の特定
- 02IEX の反復置換によるスクリプト展開
- 03AST による式の分析
- 04デコード済みスクリプトからの IOC 抽出
活用シーン
- 01隔離環境でのマルウェアサンプル分析
- 02許可されたテストでの PowerShell スクリプト復元
- 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 と一緒にインストールされる参考資料やスクリプト。
LICENSEassets/template.mdreferences/api-reference.mdstandards.mdworkflows.mdscripts/agent.pyprocess.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、ファイルパスが抽出されている
- 難読化解除済みスクリプトが、サンドボックスで観測されたマルウェアの挙動と一致している