| name | deobfuscating-powershell-obfuscated-malware |
| description | 使用 AST 分析、动态追踪以及 PSDecode 和 PowerDecode 等工具,系统地对多层 PowerShell 恶意软件进行去混淆,以揭示隐藏的载荷和 C2 基础设施。 |
| domain | cybersecurity |
| subdomain | malware-analysis |
| tags | ["powershell","deobfuscation","malware-analysis","scripting","obfuscation","ast-analysis","incident-response"] |
| version | 1.0 |
| author | mahipal |
| license | Apache-2.0 |
对混淆 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:识别混淆层
"""识别并分类 PowerShell 混淆技术。"""
import re
import base64
import sys
def analyze_obfuscation(script_content):
"""识别 PowerShell 脚本中使用的混淆技术。"""
techniques = []
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")
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("字符代码数组")
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()
tick_count > :
techniques.append()
re.search(, script_content, re.IGNORECASE):
env_refs = re.findall(, script_content, re.IGNORECASE)
(env_refs) > :
techniques.append()
re.search(, script_content, re.IGNORECASE):
techniques.append()
re.search(,
script_content, re.IGNORECASE):
techniques.append()
re.search(, script_content, re.IGNORECASE):
techniques.append()
replace_count = (re.findall(, script_content))
replace_count > :
techniques.append()
techniques
():
b64_match = re.search(
,
script_content, re.IGNORECASE
)
b64_match:
encoded = b64_match.group()
:
decoded = base64.b64decode(encoded).decode()
decoded
Exception:
():
escape_chars = {, , , , , , , , }
result = []
i =
i < (script_content):
script_content[i] == i + < (script_content):
pair = script_content[i:i+]
pair escape_chars:
result.append(pair)
i +=
:
result.append(script_content[i+])
i +=
:
result.append(script_content[i])
i +=
.join(result)
():
pattern = re.()
pattern.search(script_content):
script_content = pattern.sub( m: ,
script_content)
pattern = re.()
pattern.search(script_content):
script_content = pattern.sub( m: ,
script_content)
script_content
__name__ == :
(sys.argv) < :
()
sys.exit()
(sys.argv[], , errors=) f:
content = f.read()
()
( * )
techniques = analyze_obfuscation(content)
t techniques:
()
()
( * )
deobfuscated = remove_tick_marks(content)
deobfuscated = resolve_string_concat(deobfuscated)
b64_decoded = decode_base64_command(deobfuscated)
b64_decoded:
()
(b64_decoded[:])
deobfuscated = b64_decoded
()
output_file = sys.argv[] +
(output_file, ) f:
f.write(deobfuscated)
()
步骤 2:多层 IEX 替换
import subprocess
import tempfile
import os
def iex_replacement_deobfuscate(script_content, max_layers=10):
"""迭代地将 IEX 替换为 Write-Output 以解包各层。"""
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):
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
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=
)
output = result.stdout.strip()
output output != current:
(
)
layers.append({
: layer_num + ,
: ,
: (output),
})
current = output
:
subprocess.TimeoutExpired:
()
:
os.unlink(tmp_path)
current, layers
步骤 3:从去混淆后的脚本中提取 IoC
def extract_iocs_from_script(deobfuscated_content):
"""从去混淆后的 PowerShell 中提取攻陷指标。"""
iocs = {
"urls": [],
"ips": [],
"domains": [],
"file_paths": [],
"registry_keys": [],
"commands": [],
"base64_blobs": [],
}
url_pattern = re.compile(
r'https?://[^\s\'"<>)\]]+', re.IGNORECASE
)
iocs["urls"] = list(set(url_pattern.findall(deobfuscated_content)))
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', ,
, ,
,
,
,
,
, ,
]
cmd suspicious_cmds:
cmd.lower() deobfuscated_content.lower():
iocs[].append(cmd)
iocs
验证标准
- 所有混淆层已被正确识别和分类
- Base64 编码的命令已解码为可读的 PowerShell
- 反引号和字符串拼接混淆已解析
- IEX 替换揭示了下一阶段的载荷
- 从最终去混淆阶段提取了 URL、IP 和文件路径
- 去混淆后的脚本与沙箱中观察到的恶意软件行为相符
参考资料