WMI事件订阅是目前最隐蔽的 Windows 持久化手法之一。它不写服务、不改注册表 Run 键、不建计划任务,恶意代码挂在 root\subscription 命名空间里,由系统在指定事件发生时主动回调执行。清理时把进程杀掉、文件删掉,重启后照样复活,很多应急人员就是卡在这一步。
本文给出一套完整的 WMI事件订阅排查与清除流程:三张核心表的枚举脚本、基线比对、Sysmon 侧记录、正确的删除顺序,以及删除后防重建的 ACL 加固。全部命令基于系统自带组件,不需要安装第三方工具。
适用场景
遇到下面这些现象,优先怀疑 WMI订阅而不是普通自启动项:
- 删了还会回来:杀掉进程、删掉落盘文件、清空临时目录,几小时或重启后同样行为再次出现,而
Autoruns的服务与计划任务页干干净净。 - 触发有规律但找不到载体:每隔固定时间回连一次 C2,或者每次有进程创建就跟着起一个子进程,找不到对应的定时器或钩子。
- 无文件攻击痕迹:磁盘上扫不出样本,但内存里能抓到脚本片段,常见于用
ActiveScriptEventConsumer执行 VBScript 或 JScript 的样本。
需要注意:合法的 WMI订阅也大量存在。服务器硬件厂商的带外管理代理、部分备份软件、SCCM 客户端都会注册订阅,清理前必须先建基线,否则删掉业务组件会引发二次故障。
前置条件
- Windows Server 2016 / Windows 10 及以上,具备本地管理员权限(读取
root\subscription需要提权)。 - PowerShell 5.1 及以上,模块
CimCmdlets可用;老系统只有Get-WmiObject,语法差异见后文。 - Sysmon v13 及以上,用于把后续新建的订阅记录成事件(19/20/21)。
- 一份已知正常的订阅基线。没有基线就先导出一份,交给业务方确认后再作为基准。
- 处置阶段建议先断网保留现场,再动手删除,否则母体进程会在删除过程中立刻重建。
原理说明
三张表构成一条订阅链
一个可用的订阅必须由三个对象拼起来,缺一不可:
- __EventFilter:定义「什么事件触发」,核心是
Query字段里的 WQL 语句。 - __EventConsumer:定义「触发后做什么」,实际落地为
CommandLineEventConsumer(执行命令行)或ActiveScriptEventConsumer(执行脚本)等子类。 - __FilterToConsumerBinding:把前两者绑定起来的关系对象。只有绑定存在,订阅才会真正生效。
这个结构解释了一个常见误判:只删掉 Filter 或只删掉 Consumer,绑定对象还悬空,系统在解析绑定失败时不会报错,但一旦母体重新写入其中一半,订阅立刻恢复。必须三个对象一起清。
触发方式决定了隐蔽性
最常见的 WQL 有两种写法。一种是定时型:用 __TimerEvent 配合 WITHIN 间隔,比如每 300 秒触发一次,行为上很像计划任务但完全不在任务计划程序里。另一种是事件型:监听 __InstanceCreationEvent,比如「有进程创建就执行」,隐蔽性更强,因为触发时机跟随系统正常活动。
两种写法都无需常驻进程,WMI服务本身就是宿主,这也是传统基于进程树和文件监控的 EDR 容易漏掉它的原因。
日志里能看到什么
Microsoft-Windows-WMI-Activity/Operational 日志里,事件 5861 记录永久事件订阅的注册,内容是完整的订阅定义,包括命令行。这是事后取证最有价值的单一证据源。5857 记录 WMI提供程序启动,5860 记录临时订阅(进程内注册,进程退出即消失)。审计策略里这条日志默认可能未启用,处置前先确认它是开的。
操作步骤
步骤一:枚举三张表并汇总成一张清单
用 Get-CimInstance 而不是 Get-WmiObject,前者在新系统上可用且支持 -Namespace 参数。__EventConsumer 是抽象类,查询它会把所有子类实例一并返回,正好覆盖命令行与脚本两种消费者。
$ns = 'root/subscription'
$result = @()
foreach ($cls in '__EventFilter','__EventConsumer','__FilterToConsumerBinding') {
Get-CimInstance -Namespace $ns -ClassName $cls -ErrorAction SilentlyContinue |
ForEach-Object {
$result += [pscustomobject]@{
Class = $cls
Name = $_.Name
Query = $_.Query
Executable = $_.ExecutablePath
Script = $_.ScriptText
Filter = [string]$_.Filter
Consumer = [string]$_.Consumer
Creator = ($_.CreatorSID | ForEach-Object { $_.Value }) -join ''
}
}
}
$result | Format-List
$result | Export-Csv C:\IR\wmi-subscriptions.csv -NoTypeInformation -Encoding UTF8
看结果时优先盯 Executable 与 Script 两列:任何指向 %TEMP%、%APPDATA%、Public 目录的路径,或者出现 powershell -enc、mshta、rundll32 的命令行,基本可以判定为恶意。Creator 是创建者的 SID 字节串,能反推是哪个账户写进去的。
没有 PowerShell 的老系统用等价语法,注意 Get-WmiObject 需要显式写 -Namespace 且类名区分大小写:
Get-WmiObject -Namespace root\subscription -Class __EventFilter |
Select-Object Name, Query | Format-List
Get-WmiObject -Namespace root\subscription -Class CommandLineEventConsumer |
Select-Object Name, CommandLineTemplate | Format-List
Get-WmiObject -Namespace root\subscription -Class __FilterToConsumerBinding |
Select-Object Filter, Consumer | Format-List
步骤二:与基线做差异比对
把当前清单按「类名 + 名称 + 关键字段」算一个稳定指纹,再和基线比对。指纹不包含时间戳,避免每次比对都全量变化。
function Get-WmiSubFingerprint {
param([string]$Namespace = 'root/subscription')
Get-CimInstance -Namespace $Namespace -ClassName __FilterToConsumerBinding |
ForEach-Object {
$f = [string]$_.Filter -replace '.*Name="([^"]+)".*', '$1'
$c = [string]$_.Consumer -replace '.*Name="([^"]+)".*', '$1'
"{0}|{1}" -f $f, $c
} | Sort-Object
}
# 首次运行:建立基线
Get-WmiSubFingerprint | Set-Content C:\IR\wmi-baseline.txt -Encoding UTF8
# 后续运行:比对,只输出新增项
Compare-Object -ReferenceObject (Get-Content C:\IR\wmi-baseline.txt) `
-DifferenceObject (Get-WmiSubFingerprint) |
Where-Object SideIndicator -eq '=>' | Select-Object -ExpandProperty InputObject
步骤三:让 Sysmon 记录后续新建的订阅
Sysmon 默认不记录 WMI事件,必须在配置里显式打开 WmiEvent 规则。开启后会产生三类事件:19(Filter 注册)、20(Consumer 注册)、21(Binding 注册)。三个都开,否则只能看到一半。
<Sysmon schemaversion="4.90">
<EventFiltering>
<RuleGroup name="WMI-Subscription" groupRelation="or">
<WmiEvent onmatch="include" />
</RuleGroup>
<ProcessCreate onmatch="exclude" />
<FileCreateTime onmatch="exclude" />
</EventFiltering>
</Sysmon>
<!-- 使配置生效(会重启 Sysmon 服务,需在变更窗口内执行) -->
<!-- sysmon64.exe -accepteula -i sysmon-wmi.xml -->
步骤四:按正确顺序删除恶意订阅
顺序必须是先删绑定,再删消费者,最后删过滤器。反过来操作会让绑定在中间态引用不存在的对象,部分系统版本会因此把 WMI仓库写入失败,后续清理更麻烦。删除前先把三个对象的完整定义导出留证。
$ns = 'root/subscription'
$badFilter = 'UpdaterFilter'
$badConsumer = 'UpdaterConsumer'
# 1) 留证:导出完整定义,取证报告需要原始字段
Get-CimInstance -Namespace $ns -ClassName __EventFilter -Filter "Name='$badFilter'" |
Export-Clixml C:\IR\evidence-filter.xml
Get-CimInstance -Namespace $ns -ClassName __EventConsumer -Filter "Name='$badConsumer'" |
Export-Clixml C:\IR\evidence-consumer.xml
# 2) 删绑定(注意用 -Filter 精确匹配,不要用 -Query 模糊匹配)
Get-CimInstance -Namespace $ns -ClassName __FilterToConsumerBinding |
Where-Object { "$($_.Filter)" -match $badFilter } | Remove-CimInstance
# 3) 删消费者
Get-CimInstance -Namespace $ns -ClassName __EventConsumer -Filter "Name='$badConsumer'" |
Remove-CimInstance
# 4) 删过滤器
Get-CimInstance -Namespace $ns -ClassName __EventFilter -Filter "Name='$badFilter'" |
Remove-CimInstance
步骤五:从事件日志确认注册来源
删除只能治标,必须回溯「是谁注册的」。事件 5861 的 Consumer 字段里带着完整命令行,通常能直接看到加载器路径或 C2 地址,再配合 Sysmon 的进程创建记录(事件 1)定位父进程。
# 查最近的永久订阅注册事件
wevtutil qe Microsoft-Windows-WMI-Activity/Operational `
/q:"*[System[(EventID=5861)]]" /c:20 /f:text /rd:true
# 确认该日志本身处于启用状态
wevtutil gl Microsoft-Windows-WMI-Activity/Operational | findstr /i "enabled"
# 关联父进程:找创建了 wmiprvse.exe 子进程的记录
Get-WinEvent -LogName "Microsoft-Windows-Sysmon/Operational" -MaxEvents 500 |
Where-Object { $_.Id -eq 1 -and $_.Message -match 'wmiprvse' } |
Select-Object TimeCreated, Message -First 10
配置验证
四步验证,缺一步都不算清干净:
# 1) 三张表复查,目标名称不应再出现
$ns = 'root/subscription'
foreach ($c in '__EventFilter','__EventConsumer','__FilterToConsumerBinding') {
Get-CimInstance -Namespace $ns -ClassName $c |
Select-Object @{n='Class'; e={$c}}, Name | Format-Table -AutoSize
}
# 2) 差异比对:新增项应为空
Compare-Object -ReferenceObject (Get-Content C:\IR\wmi-baseline.txt) `
-DifferenceObject (Get-WmiSubFingerprint) |
Where-Object SideIndicator -eq '=>'
# 3) 观察期:重启后观察 24 小时,确认行为未复现
Restart-Computer -Force
# 4) 检查 WMI仓库完整性,排除被改坏的衍生问题
winmgmt /verifyrepository
第二步是判断「有没有被重建」的核心动作。如果观察期内 Compare-Object 又输出了新增项,说明母体还在,必须回到步骤五继续回溯,此时应该先把主机隔离,而不是继续删。
常见问题
Q1:删掉之后几分钟又出现了,怎么办
这说明有另一个持久化点在重建订阅,单纯删 WMI是死循环。处置顺序改成「先断网、再找母体、最后清 WMI」:断网阻止下载与回连,母体无法获取新配置,重建行为会停止或报错,此时抓 Sysmon 事件 19/20/21 与 WmiPrvSE.exe 的子进程树,能直接看到是谁在写。
常见的同伙位置包括计划任务、服务 DLL 劫持、以及 svchost 里加载的服务。建议同时跑一遍 Get-ScheduledTask 全量导出、sc query type= service state= all 与注册表 IFEO 项,把同一批 IOC 一次性清完,避免拉锯。
Q2:能不能直接停掉 Winmgmt 服务来阻止执行
不能,代价远大于收益。WMI服务被大量系统组件依赖,停掉会导致网络状态、硬件信息、SCCM 与监控代理、部分安全软件全部失效,服务器基本处于半瘫状态,而恶意订阅并未删除,服务一恢复立刻生效。
正确做法是删订阅 + 修权限。如果确实需要临时阻断执行,用文件系统与注册表权限思路:给 root\subscription 命名空间加限制,撤销普通账户的写入能力,并把可疑 Consumer 指向的可执行文件用 ACL 锁死,这样即使母体尝试重建也会失败。
Q3:怎么区分合法订阅,避免误删业务组件
三条判据按顺序看:创建者(CreatorSID 是否对应安装账户)、落地路径(是否在 Program Files 或厂商目录下)、命令行签名(指向的可执行文件是否有有效数字签名)。三条都正常基本可判为合法,任一条异常进入人工确认。
给一条实操口径:第一次上基线时,把全部订阅导出成表格发给业务与运维确认,确认过的写进白名单文件,后续只对白名单外的增量告警。这比每次靠人工判断可靠,也能应付人员交接。
总结
WMI订阅排查的核心不在工具,而在理解它的三段结构:
- 三张表一条链:Filter 定触发、Consumer 定动作、Binding 定生效,清理必须三个一起动,顺序为绑定、消费者、过滤器。
- 先留存再删除:用
Export-Clixml把原始定义落盘,取证报告需要的是原始字段而不是截图。 - 先断网再清:不清母体只清订阅必然复发,断网能让重建行为暴露出来。
- 靠基线而不是靠眼力:指纹文件 + 白名单 + Sysmon 19/20/21 三件套,才能把一次处置变成可持续的检测能力。
最后提醒一句:这一步做完并不等于主机干净。WMI订阅常与计划任务、服务、注册表自启动组成组合拳,清完务必再做一轮全量自启动面复查,并把 5861 事件接入 SIEM 告警,否则下次换一个名字注册,同样的流程还要再走一遍。