-
-
[原创]WMI 永久订阅的 Filter 与 Consumer 绑定解析
-
发表于: 2天前 757
-
WMI 永久订阅的 Filter 与 Consumer 绑定解析
相关内容实现项目:KswordARK 地址468K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6w2f1%4N6G2M7X3c8p5c8g2k6Q4x3V1k6w2f1%4N6G2M7X3b7`.

文章中内容已经在Ksword中实现,仓库地址:41cK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6w2f1%4N6G2M7X3c8p5c8g2k6Q4x3V1k6w2f1%4N6G2M7X3b7`.
本篇要完成的是:读取 ROOT\subscription 中的永久订阅,列出每个 Filter 的触发条件,再通过 Binding 的对象路径还原它连接的 Filter 和 Consumer。
要完成这项读取,分为四步:
- 初始化 COM 与调用安全状态。
- 连接
ROOT\subscription,取得该命名空间的服务接口。 - 查询
__EventFilter,保存名称、WQL、查询语言和事件命名空间。 - 查询
__FilterToConsumerBinding,解析其中的 Filter 与 Consumer 对象路径。
这四步按顺序提供读取条件、目标命名空间、触发条件和关联关系。最后得到的结果说明某个 Filter 在读取时连接到哪个 Consumer,同时保留无法解析的原始引用和失败状态。
一、对象关系和完整流程
永久订阅的运行路径由 Filter、Binding 和 Consumer 组成。Filter 负责匹配事件。Binding 保存对象引用。Consumer 负责事件匹配后的处理方式。三者都位于 WMI 的 ROOT\subscription 命名空间。
系统状态或事件来源变化 -> __EventFilter 匹配事件 WQL -> __FilterToConsumerBinding 提供对象引用 -> Consumer 执行命令、处理脚本或写日志
初始化 COM -> 连接 ROOT\subscription -> 查询 Filter -> 查询 Binding -> 解析两端对象路径 -> 输出关联关系与读取状态
读取过程需要分别查询 Filter 与 Binding。它以 Binding 的两个对象路径建立关系,不按名称文本猜测归属。Consumer 的类名和名称只在对象路径可解析时读取。
WMI 是按命名空间组织的管理对象系统
Windows Management Instrumentation(WMI)为系统、驱动、服务和应用提供统一的管理对象访问方式。WMI 服务接收查询请求。WMI 提供程序把操作系统中的状态映射成类和实例。客户端通过 COM 接口读取这些对象。
WMI 的基础对象各有职责。命名空间类似逻辑容器,例如 ROOT\CIMV2 包含大量系统管理类。类描述一组属性和方法,例如 Win32_Process。实例表示某一时刻的具体对象,例如某个进程实例。对象路径是能够唯一定位实例的字符串。客户端指定命名空间和查询后,就能通过 IWbemServices 获取实例。
WMI 客户端
COM 调用 / WQL 查询
WMI 服务(winmgmt)
命名空间 ROOT\CIMV2:提供程序与系统管理对象
命名空间 ROOT\subscription:永久订阅对象
命名空间之间相互独立。某个类在 ROOT\CIMV2 存在,不表示相同类名能在 ROOT\subscription 查询到。连接成功也只代表拿到所选命名空间的服务接口。
WQL 描述对象筛选条件,事件 WQL 描述“什么时候”
普通查询与事件查询需要分开理解。WQL(WMI Query Language)是一种面向 WMI 对象的查询语言,语法接近 SQL。普通 WQL 例如 SELECT Name, ProcessId FROM Win32_Process 读取当前实例。事件 WQL 使用 __InstanceCreationEvent、__InstanceDeletionEvent、__InstanceModificationEvent 等事件类,描述对象状态变化时应关注的条件。
一个典型的内在事件查询可能是:
SELECT * FROM __InstanceCreationEvent WITHIN 5
WHERE TargetInstance ISA 'Win32_Process'
WITHIN 5 表示 WMI 按一定间隔检查这类变化,单位是秒。TargetInstance 是事件携带的变化对象。ISA 'Win32_Process' 限制关注的对象类别。查询只表达“匹配什么事件”,并不包含执行命令、写日志或调用脚本的处理细节。
事件还可以由专用提供程序直接报告。无论事件来自轮询还是提供程序,Filter 都需要通过 EventNamespace 指明查询在哪个命名空间解释。相同的 WQL 文本放在不同事件命名空间中,可能指向完全不同的类集合。
临时订阅存在于连接期间,永久订阅保存为 WMI 对象
临时订阅与永久订阅的存续范围不同。临时事件订阅由客户端调用事件查询接口建立,客户端断开后该订阅通常不再保留。永久事件订阅把 Filter、Consumer 和 Binding 实例保存到 ROOT\subscription。WMI 服务可以在随后满足条件时,按保存的关联处理事件。
永久订阅由三个可独立存在的对象组成。__EventFilter 保存事件 WQL、查询语言和事件命名空间。事件处理对象实例保存处理方式。__FilterToConsumerBinding 的 Filter 与 Consumer 属性保存到两端的对象路径引用。Binding 才是条件与处理端之间的连接边。
__EventFilter
Name / Query / QueryLanguage / EventNamespace
__FilterToConsumerBinding
Filter / Consumer / DeliveryQoS / MaintainSecurityContext
事件处理对象实例
__CLASS / Name / 该处理对象类型的特有属性
这三个对象可以形成一对多或多对一关系。一个 Filter 能由多个 Binding 指向不同 Consumer,一个 Consumer 也能被多个 Binding 引用。因此读取过程应为每个对象保留稳定对象路径,并以 Binding 的两个引用建立关联,不能仅按 Name 文本猜测归属。
Consumer 类型决定处理属性的含义
处理对象的属性要先按 WMI 类区分。NTEventLogEventConsumer 用事件日志相关字段描述处理方式。LogFileEventConsumer 用日志文件相关字段描述处理方式。CommandLineEventConsumer 才会出现命令行模板。ActiveScriptEventConsumer 则会涉及脚本引擎和脚本文本等属性。
对象路径类似 CommandLineEventConsumer.Name="Example",它是 WMI 实例定位符,不是可执行文件路径。正确过程是用 GetObject 打开该路径,读取 __CLASS,再按这一类处理对象的文档读取适用字段。未知类别应保留类名、对象路径和原始属性状态,不能把每个 Consumer 都转换为命令文本。
WMI 接口、BSTR 和 VARIANT 有各自的资源责任
WMI 的 COM 自动化对象有明确的所有权。IWbemLocator、IWbemServices、IEnumWbemClassObject 和 IWbemClassObject 都是 COM 接口指针。成功得到接口后,持有者在使用结束时调用 Release。BSTR 是自动化字符串,SysAllocString 成功后由调用方 SysFreeString。不能用 delete[] 或 free 释放。
VARIANT 是可保存字符串、整数、布尔值、接口或数组的联合类型。WMI 属性读取会把真实类型写入 VARIANT。调用方先检查 vt,再读取相应成员,然后调用 VariantClear 释放其中可能存在的 BSTR、接口或数组。把任何属性都强制转换为字符串会破坏数值和对象路径的边界。
永久订阅配置与一次事件处理结果属于不同层次
静态对象与事件执行记录要分开。完整读取 Filter、Binding 和 Consumer,只说明在观察时刻这些 WMI 实例与对象路径可以关联。事件是否发生、WQL 是否匹配、处理对象是否成功处理以及处理产生什么结果,需要从事件日志、WMI 活动记录或独立运行时证据确认。
输出可明确分为三层:配置层记录三类对象和引用关系。解析层记录类名和特有属性。运行时层记录事件时间、匹配结果和处理状态。任何层缺失都保留原始失败 HRESULT,不用另一层的成功替代。
每次读取还应记录命名空间路径、读取时间和调用账户上下文,避免不同用户或不同远程连接得到的结果混在同一批记录中。
二、第一步:初始化 COM 与调用安全状态
这一步让当前线程能够使用 COM 接口,并为后续 WMI 服务调用准备默认安全状态。COM(Component Object Model,组件对象模型)是 Windows 中一套对象调用规则。WMI 客户端、定位器和服务接口都通过这套规则协作。
初始化失败时不能继续读取。默认安全初始化可能返回 RPC_E_TOO_LATE,表示进程中的其它组件已经先设置了 COM 安全参数。此时保持已有设置,继续连接目标命名空间。
完成第一步后,当前线程已经具备调用 WMI COM 接口的前提,但还没有指向任何命名空间。接下来需要连接 ROOT\subscription,取得保存永久订阅对象的服务接口。
三、第二步:连接 ROOT\subscription
Filter 和 Binding 位于永久订阅命名空间
连接 WMI locator 的方法返回命名空间服务接口。ROOT\subscription 保存永久事件订阅,ROOT\CIMV2 通常保存触发事件涉及的系统类。两个命名空间的用途不同。
// 意义:连接指定 WMI 命名空间。
// 返回:S_OK 成功。ppNamespace 接收 IWbemServices,调用方 Release。
HRESULT IWbemLocator::ConnectServer(
BSTR strNetworkResource, BSTR strUser, BSTR strPassword, BSTR strLocale,
LONG lSecurityFlags, BSTR strAuthority, IWbemContext* pCtx,
IWbemServices** ppNamespace
);
// 意义:设置连接后 WMI 服务代理的认证和模拟级别。
// 返回:S_OK 成功。失败 HRESULT 表示代理安全状态不可用。
HRESULT CoSetProxyBlanket(
IUnknown* pProxy, DWORD dwAuthnSvc, DWORD dwAuthzSvc, OLECHAR* pServerPrincName,
DWORD dwAuthnLevel, DWORD dwImpLevel, RPC_AUTH_IDENTITY_HANDLE pAuthInfo,
DWORD dwCapabilities
);
// 意义:从宽字符文本创建 OLE 自动化分配器管理的 BSTR。
// 返回:成功时为新 BSTR,内存不足时为 nullptr。需要下方释放函数。
BSTR SysAllocString(const OLECHAR* psz);
// 意义:释放 SysAllocString 或 COM getter 返回的 BSTR。
// 返回:无。传入 nullptr 安全。
void SysFreeString(BSTR bstrString);
正确做法:以当前令牌连接 ROOT\subscription,再设置 RPC_C_IMP_LEVEL_IMPERSONATE。连接返回服务接口后,代理安全设置仍是独立步骤。
BSTR path = SysAllocString(L"ROOT\\subscription");
IWbemServices* services = nullptr;
if (SUCCEEDED(locator->ConnectServer(path, nullptr, nullptr, nullptr,
WBEM_FLAG_CONNECT_USE_MAX_WAIT, nullptr, nullptr, &services))) {
CoSetProxyBlanket(services, RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE, nullptr,
RPC_C_AUTHN_LEVEL_CALL, RPC_C_IMP_LEVEL_IMPERSONATE, nullptr, EOAC_NONE);
}
SysFreeString(path);
错误示例:从 ROOT\CIMV2 查询 __FilterToConsumerBinding。该类位于永久订阅命名空间,错误命名空间会得到“类不存在”或空结果。
完成第二步后,已经获得只对应 ROOT\subscription 的 IWbemServices 服务接口。这个接口能查询永久订阅对象,但关联的起点仍然未知。接下来先读取 Filter,保留触发条件和稳定对象路径。
四、第三步:查询 __EventFilter
Filter 保存触发条件本身
执行 WQL 的方法返回枚举器。__EventFilter 的 Name 是对象名称,Query 是触发 WQL,QueryLanguage 和 EventNamespace 说明查询解释方式与事件来源。
// 意义:执行只读 WQL 查询并返回对象枚举器。
// 返回:S_OK 成功。ppEnum 接收 IEnumWbemClassObject,调用方 Release。
HRESULT IWbemServices::ExecQuery(
const BSTR strQueryLanguage, const BSTR strQuery,
LONG lFlags, IWbemContext* pCtx, IEnumWbemClassObject** ppEnum
);
正确做法:保留名称、相对路径、WQL、查询语言和事件命名空间。相对路径如 __EventFilter.Name="SCM Event Log Filter" 可与 Binding 的 Filter 引用直接对照。
const wchar_t* query =
L"SELECT Name, Query, QueryLanguage, EventNamespace, __RELPATH "
L"FROM __EventFilter";
// 对每个结果分别保存 Name、Query、QueryLanguage、EventNamespace、__RELPATH。
错误示例:只保存 WQL。相同 WQL 在不同事件命名空间中的含义可能不同。缺少 EventNamespace 时,无法确定查询的事件类来源。
完成第三步后,每个 Filter 都有名称、触发 WQL、事件命名空间和相对路径。此时只知道“什么事件会匹配”,还不知道匹配后连接到哪个处理对象。接下来读取 Binding,并逐条保留它的两个引用。
五、第四步:查询 Binding 并解析两端对象路径
Binding 引用连接 Filter 与 Consumer
Binding 查询要保存两个原始引用。Filter 指向一个 __EventFilter 对象。Consumer 指向 CommandLineEventConsumer、ActiveScriptEventConsumer、日志处理对象或其它处理对象。DeliveryQoS 与 MaintainSecurityContext 是 Binding 自身属性。
const wchar_t* query =
L"SELECT Filter, Consumer, DeliveryQoS, MaintainSecurityContext "
L"FROM __FilterToConsumerBinding";
// 对每个 Binding 保留 Filter 与 Consumer 的原始对象路径,不用名称文本替代引用。
正确做法:通过服务接口读取引用对象。GetObject 接受 WMI 对象路径。返回对象后,读取 __CLASS 与 Name。解析失败时保留原始引用。引用缺失本身也是读取结果。
// 意义:按 WMI 对象路径取得一个对象实例。
// 返回:S_OK 成功。ppObject 接收 IWbemClassObject,调用方 Release。
HRESULT IWbemServices::GetObject(
const BSTR strObjectPath, LONG lFlags, IWbemContext* pCtx,
IWbemClassObject** ppObject, IWbemCallResult** ppCallResult
);
IWbemClassObject* endpoint = nullptr;
if (SUCCEEDED(services->GetObject(bindingConsumerPath, 0, nullptr, &endpoint, nullptr))) {
// 读取 endpoint 的 __CLASS 与 Name,随后 Release。
endpoint->Release();
}
错误示例:把 Consumer 引用当成可执行文件路径。它通常是 NTEventLogEventConsumer.Name="..." 这类 WMI 对象路径,需要先解析类和对象名。是否存在命令文本取决于处理对象类型。
完成第四步后,Filter、Binding 和 Consumer 已能通过对象路径对应起来。读取结果还需要处理超时、属性类型和资源释放,才能区分空结果、等待超时和属性不可读。
六、处理等待与属性资源
枚举器需要有超时边界
读取枚举器的方法每次最多等待指定毫秒数。WBEM_S_TIMEDOUT 且没有对象时表示本次等待结束,不等于类没有实例。连续超时达到上限后记录状态并继续其它类。
// 意义:从 WMI 枚举器读取对象。
// 返回:WBEM_S_NO_ERROR 成功。WBEM_S_TIMEDOUT 等待超时。
HRESULT IEnumWbemClassObject::Next(
LONG lTimeout, ULONG uCount, IWbemClassObject** apObjects, ULONG* puReturned
);
HRESULT status = enumerator->Next(5000, 1, &object, &returned);
if (status == WBEM_S_TIMEDOUT && returned == 0) {
// 增加超时次数。达到上限后结束这一类查询。
}
属性读取后要清理 VARIANT。Filter 的 Query、Binding 的引用通常是 BSTR,数值和布尔字段使用其它联合成员。读取后需要使用下方清理 API,不能把所有属性强制转换成字符串指针。
// 意义:把 VARIANT 初始化为空值,以便 WMI 属性方法写入。
// 返回:无。每个初始化后的 VARIANT 都需要清理内部资源。
void VariantInit(VARIANTARG* pvarg);
// 意义:释放 VARIANT 内部的 BSTR、接口、数组等资源。
// 返回:S_OK 成功。
HRESULT VariantClear(VARIANTARG* pvarg);
完成等待和属性处理后,输出中的每个字段都有明确来源,也能保留读取失败的边界。最后要保持三类对象各自的身份,避免把配置关系误读为一次实际执行结果。
七、保持三类对象的独立身份
Filter、Binding 与 Consumer 不能合成一条记录
对象建模要分别保存 Filter、Binding 和 Consumer。Filter 表示条件,Consumer 表示动作,Binding 表示关联。一个 Filter 可以连接多个 Consumer,一个 Consumer 也可被多个 Binding 引用。
配置与实际触发需要分别判断。读取到完整 Binding,说明三类对象在读取时可以关联。实际事件是否发生、WQL 是否匹配和处理对象是否成功处理,需要事件日志或其它独立证据确认。
完整可运行程序在附件7ccK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4j5h3&6Y4N6$3g2A6j5$3#2Q4x3X3g2D9j5h3&6*7L8%4g2#2i4K6u0W2j5$3!0E0i4K6u0r3K9e0g2H3k6#2R3K6P5r3k6T1P5Y4S2Y4
[招生]科锐逆向工程师培训(2026年7月3日实地,远程教学同时开班, 第56期)!