如何还原混淆的 PowerShell 脚本
最后更新:
还原混淆的 PowerShell 脚本,就是一层一层地撤销攻击者为隐藏脚本真实行为而做的变换——而且全程绝不运行这个脚本。一段可疑的单行命令,披着一堵 Base64 的墙,或缠着字符码和字符串拼接而来,底下往往是一段再普通不过的 PowerShell 命令:一次下载、一段解码后的载荷、一段只在内存里运行、从不落盘的命令。混淆不是为了炫技,它是为了绕过特征扫描、让命令行一眼看不懂、让分析人员在看到关键部分之前就放弃。本文讲清每一层是做什么的、如何把它剥离成惰性文本、以及你找到的那些片段实际意味着什么。整个流程可以在PowerShell 还原工具里走完——逐层解码,全程在浏览器本地完成,不执行任何东西,也不让任何东西离开你的机器。
攻击者为什么混淆 PowerShell——以及为什么编码层是首先要剥掉的东西
PowerShell 预装在每一台 Windows 机器上,能从互联网下载并执行代码而不往磁盘写文件,还带着启动它的人的权限运行。这种组合使它成为 MITRE ATT&CK 框架里被滥用最多的工具之一,编号 T1059.001,也使攻击者拿到一段恶意 PowerShell 命令后做的第一件事就是把它藏起来。一段明文命令,比如 DownloadString 后面跟 Invoke-Expression,检测起来毫不费力:任何扫描器、任何日志复查、任何瞥一眼进程命令行的人都会发现它。而经过编码、压缩、切碎之后,同一段命令变成了一串匹配不到任何已知特征、人类读者也看不懂的字符。
这也正是编码层必须最先剥掉的原因。在解码之前,你根本不知道眼前的是安装程序、恶作剧,还是窃取凭据的工具——其余一切判断都只是猜测。好消息是,混淆几乎总能通过静态还原逆转:载荷必须能被运行它的机器解开,所以每一层都有确定的逆操作。Base64 能解码、gzip 能解压、字符码能映射回字母、拼接的字符串能接回去。这项工作是机械的,不是神秘的,却是分流一段可疑脚本时价值最高的步骤:一次解码就把“看不懂的乱码”变成一段可以真正推理的命令。
-EncodedCommand 与 UTF-16LE:看起来像乱码的 Base64 为什么通常是可读文本
你会遇到的最常见的 PowerShell 混淆形式只有一个开关:powershell.exe -EncodedCommand,后面跟一长串 Base64。它看起来像随机字符,但几乎总是一段普通的脚本,只是编码方式会让第一次见到的人吃惊。PowerShell 的 -EncodedCommand 要求 Base64 解码后是 UTF-16LE——每个字符两个字节,每个 ASCII 字母后面跟一个零字节——因为这就是 Windows 上 .NET 字符串的编码。所以无害命令 Write-Host "hello" 的 Base64 以 SQB3AHIAaQB0AGUALQBIAG8AcwB0ACA… 开头,而反复出现的字母 "A" 就是那些零字节留下的痕迹。
知道这一点,你读编码串的方式就变了。一段解码成 UTF-8 乱码的 Base64,很可能根本不是 UTF-8:先试 UTF-16LE,“乱码”就会变成可读的 PowerShell。没有 -EncodedCommand 开关的裸 Base64 串也一样——解码后是可打印文本的长串合法 Base64,就是同一个花招,只是没带开关。实用的规则很简单:在 PowerShell 命令行里看到长串 Base64,就先假定它是一段脚本,直到解码证明不是;而且要用一个从不执行它的工具把它当作纯文本惰性地解码,而不是粘贴到聊天窗口或终端里看看会发生什么。
层层叠加:压缩、字符码、拼接、-f、反引号
真实的载荷很少只停在一层。-EncodedCommand 解开之后,底下的脚本往往自己又被编码了一次:一段 gzip 压缩后又包了一层 Base64,或者一串像 [char]68+[char]105 这样的字符码、一个数字一个数字地拼出命令,或者一段被切成碎片再用加号粘回去的字符串,或者一个 -f 格式操作符在运行时把参数填进格式串,或者关键字里撒了反引号,让 Do`wnloadString 躲过对 DownloadString 的简单搜索。每一层存在的理由各不相同。压缩把长载荷压成短短的一行,并彻底躲过文本搜索。字符码和拼接让可疑的词从不以字面形式出现在文件里,从而躲过特征匹配。-f 操作符和反引号在保持脚本语法有效的前提下躲过简单的字符串搜索。
对付它们的办法都一样:按顺序一层一层剥,直到没有哪一层还能剥。解开 Base64,再检查结果是不是压缩过的并解压,再解析字符码、接合碎片、代入格式参数、去掉反引号——每一步产出的都是下一步能处理的纯文本。一个把每层都做成可见时间线卡片的工具——每层一张卡,写着剥离前与剥离后——比一个只默默输出最终结果的工具更有价值,因为时间线本身就是证据:它说明存在过哪些层;而当还原提前停下——“无法继续还原”——那是一条发现,不是失败。它告诉你载荷里有静态分析够不到的东西:动态的、加密的,或只在运行时才拼出来的内容。
脚本块日志 4104:长脚本如何被拆开、又如何拼回去
有一种还原后的 PowerShell 完全不需要解码:机器自己给你的。当脚本块日志(Script Block Logging)开启后,Windows 会把 PowerShell 处理的每个脚本块的内容记为 Microsoft-Windows-PowerShell/Operational 日志里的 4104 事件——而且记的是 PowerShell 引擎看到的代码,也就是 -EncodedCommand 和混淆层已经被引擎展开之后的样子。一段以 Base64 形式到达的可疑单行命令,在 4104 事件里就是它实际运行的可读脚本。这就是为什么这个功能是防御者能开启的最有价值的 PowerShell 日志设置,同时也是为什么它默认是关的:它很啰嗦,日志里还可能含有凭据和其他敏感数据,所以微软建议除诊断用途外配合受保护事件日志(Protected Event Logging)一起使用。
麻烦在于,Windows 会把长的脚本块拆成多个 4104 事件。每个事件带着同一个 ScriptBlockId、一个 MessageNumber 和一个 MessageTotal——比如 5 段里的第 2 段——完整的脚本只有把这些事件按 ID 分组、按消息号排序、拼接起来才存在。单独看一个事件,可能在一个词中间就断了;只读第一段的搜索,会漏掉载荷末尾做的事。所以拼装是一个机械步骤,只有一件事要检查:有没有缺段。如果日志说 3 段里有 2 段,而第 2 段始终没到——因为日志轮转了,或者被清空了——拼出来的文本就是不完整的,基于它得出的任何结论都只是暂时的。同样的分组逻辑也是EVTX 查看器在导出的 .evtx 文件里标记 4104 事件时用的办法;另见配套文章在没有 Windows 机器时读取 .evtx 文件。
读懂指标:下载摇篮、AMSI 绕过、执行策略绕过
脚本变得可读之后,下一步不是宣布它恶意——而是把里面的指标当作待验证的线索来读。下载摇篮——DownloadString、DownloadFile、Net.WebClient、Invoke-WebRequest、bitsadmin、certutil 从某个 URL 拉文件——告诉你这个脚本会从某处取代码,下一个问题就是从哪里取、那个目的地在你的网络里是否正常。AMSI 绕过——amsiInitFailed、AmsiUtils,或给反恶意软件扫描接口打补丁的代码——告诉你作者希望脚本在不被扫描的情况下运行,这是一个强信号,但不是定论:渗透测试人员和一些合法工具也会这么做。执行策略绕过(-ExecutionPolicy Bypass、-ep bypass)告诉你作者不想让 PowerShell 自带的防护拦住脚本,而这在恶意软件和运行未签名脚本的日常自动化里都很常见。
纪律在于把每个指标当作问题,而不是答案。下载摇篮加隐藏窗口(-WindowStyle Hidden)再加编码命令,是一个值得上报的模式;而其中任何一个单独出现,都只是值得检查的模式。把线索变成定论的是上下文:是谁启动了这个进程、它的父进程是什么、它联系的 URL 是否已知、同样的脚本是否出现在别的机器上。一个列出指标、贴出匹配片段、并写着“请结合上下文验证”的还原工具,做的是诚实的工作;而一个仅凭模式列表就给脚本盖上“恶意”或“干净”印章的工具,则夸大了静态分析能知道的东西。反过来也一样:没有发现任何指标的脚本并没有被证明安全——它可能只是用了指标列表没覆盖的技术,或者在运行时动态拼装载荷。
安全工作流:不要粘贴到聊天里,不要运行,惰性解码
处理可疑 PowerShell 脚本最重要的一条规则是:绝不要运行它来看看它会做什么,也不要把它粘贴到聊天窗口里问 AI。运行它就是执行载荷——而这正是你要避免的结果。粘贴到聊天里会把一段可能恶意的脚本发给第三方服务,留在你无法控制的对话记录里,而且常常还会遭到拒绝,因为 AI 系统通常拒绝还原可能恶意的脚本。安全的工作流是故意做得很无聊的:把乱码复制到浏览器本地的还原工具里,或者一台沙箱分析机上,把解码输出当作惰性文本来读,绝不把它当作代码来执行。
接下来的流程是一张很短的清单。第一,逐层解码,直到文本不再变化,并记下还原是在哪里停下的。第二,读懂指标,把每一条如果属实意味着什么写下来,再逐一对照上下文验证——父进程、目标 URL、它来自哪台机器。第三,把结果交给为下一步而设的人和系统:你的 EDR 控制台、一次沙箱引爆、你的事件响应团队。还原工具的工作到可读文本和诚实的指标为止;定论属于沙箱和分析人员,不属于解码工具。如果脚本来自你管理的机器,检查一下脚本块日志是否开着——如果开着,4104 事件就是它实际运行内容的真相,值得在日志轮转之前先收集起来。