首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
编程技术
发新帖
0
0
[原创]WMI 永久事件处理对象的命令、脚本与日志配置
发表于: 2026-7-19 15:30
698
[原创]WMI 永久事件处理对象的命令、脚本与日志配置
WangWei_CM.
2026-7-19 15:30
698
# WMI 永久事件处理对象的命令、脚本与日志配置 WMI相关功能已经在KswordARK中实现,项目地址:<mark class="encrypted">898K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6w2f1%4N6G2M7X3c8p5c8g2k6Q4x3V1k6w2f1%4N6G2M7X3b7`.</mark>  本篇要完成的是:只读枚举 `ROOT\subscription` 中的四类永久事件处理对象,在控制台输出对象类型、名称及命令、脚本或日志字段。查询或枚举失败时输出 HRESULT,属性不可读时输出占位文本。命令行对象提供命令模板和工作目录。脚本对象提供脚本文本与脚本引擎。日志对象只写文件或事件日志,不能按启动命令解释。 要想完成这项读取,分为四步: 1. 初始化 COM 并设置进程默认安全参数。 2. 连接 `ROOT\subscription` 并设置 WMI 服务代理安全参数。 3. 分别查询四类事件处理对象,限制单次读取的等待时间。 4. 按 `VARIANT` 的真实类型转换属性并输出文本。 这四步从连接环境开始,到属性输出结束。类名、对象名称和每个属性的占位结果需要一同查看,才能分清没有实例、查询失败和属性不可读这三种情况。 ## 一、先理解四步要读取的对象 WMI 是 Windows 的管理对象系统。它把系统状态、配置与管理操作组织成可查询对象。客户端通过 COM 接口连接 WMI 服务,再在指定命名空间中查询类和实例。永久事件订阅是其中一类配置,涉及命名空间、类、实例、属性、提供程序、查询和对象引用。 永久订阅的运行路径由三个对象连接。Filter 描述事件条件。Binding 保存 Filter 与 Consumer 的对象路径引用。Consumer 描述事件匹配后执行命令、运行脚本或写日志等处理方式。 ```text 事件来源或系统状态变化 -> __EventFilter 匹配 WQL 条件 -> __FilterToConsumerBinding 关联对象 -> Consumer 按自身类型处理命令、脚本或日志 ``` 枚举程序只读取事件处理对象的实例及其属性。它不会触发事件,也不能证明 Filter、Binding 已完整配置或 `Consumer` 已成功处理事件。 ```text 初始化 COM -> 连接 ROOT\subscription -> 查询事件处理对象类 -> 读取每个实例的 __CLASS 和属性 -> 按 VARIANT 类型解释 -> 保存对象路径、来源和 HRESULT ``` 命名空间是 WMI 对象的逻辑容器。`ROOT\CIMV2` 保存大量常见的系统管理类,例如进程、服务、磁盘和网络适配器。`ROOT\subscription` 保存永久事件订阅相关的系统类和实例。命名空间路径决定查询范围,同名类在不同命名空间可以表示不同的对象集合。 ```text ROOT CIMV2 Win32_Process 进程实例 Win32_Service 服务实例 其它系统管理类 subscription __EventFilter 永久订阅过滤器 __FilterToConsumerBinding CommandLineEventConsumer 其它永久事件处理对象类 ``` 类描述对象具有哪些属性和方法,实例是类的一条实际记录。`CommandLineEventConsumer` 是命令行事件处理对象类,名称为 `Example` 的一条配置是该类的实例。`Name`、`CommandLineTemplate`、`ExecutablePath`、`WorkingDirectory` 是这个实例的属性。读取时必须保留类名和实例名称,属性名相同并不代表来自同一类。 提供程序负责把某些 WMI 类连接到真实数据或管理功能。WMI 服务接收查询后,根据类所在命名空间调用相应提供程序或读取仓库中的永久对象。客户端无需直接调用驱动、注册表或服务控制器,也不能假设所有类都由同一种底层来源提供。 WQL 是 WMI 查询语言,语法接近 SQL。`SELECT Name FROM CommandLineEventConsumer` 表示从指定类的实例中读取 `Name` 属性。WQL 本身只描述要读取哪些对象和属性,命名空间、连接权限和提供程序可用性仍由 WMI 环境决定。 ```text 连接:ROOT\subscription 查询:SELECT Name, CommandLineTemplate FROM CommandLineEventConsumer 结果:该类的零个或多个实例 属性:Name、CommandLineTemplate 等带类型的值 ``` WMI 属性使用 `VARIANT` 返回。字符串通常为 `VT_BSTR`,整数、布尔值、数组和对象引用使用其它类型。属性缺失、空值和读取失败需要分开表示。把任何属性都当成 C 字符串会丢失类型信息,也可能访问错误的联合成员。 永久事件订阅由三种对象协作。`__EventFilter` 保存事件条件和 WQL。`Consumer` 类实例保存事件发生后的处理方式。`__FilterToConsumerBinding` 保存两者之间的关联。这类处理对象只说明可执行、脚本或写日志等处理端。触发条件来自 Filter,关联关系来自 Binding。 本篇只读取这类对象实例。连接、查询和类型解释都保持只读。对象存在不代表其对应的 Filter、Binding 或实际事件触发已经成功,三个层面需要分别查询。 ## 二、第一步:初始化 COM 与进程安全 ### 1. COM 初始化和安全设置先于 WMI 查询 初始化 COM 的 API 为当前线程建立 WMI 调用环境。`S_OK` 与 `S_FALSE` 表示可继续使用 COM。`RPC_E_CHANGED_MODE` 表示线程已采用另一种 COM 模型,读取时需要避免重复清理。 ```cpp // 意义:为当前线程初始化 COM。 // 返回:S_OK 或 S_FALSE 成功。RPC_E_CHANGED_MODE 表示已有不同模型。 HRESULT CoInitializeEx(LPVOID pvReserved, DWORD dwCoInit); // 意义:撤销当前线程一次成功的 COM 初始化。 // 返回:无。只能与 S_OK 或 S_FALSE 的 CoInitializeEx 对应。 void CoUninitialize(void); // 意义:设置本进程默认 COM 安全级别。 // 返回:S_OK 成功。RPC_E_TOO_LATE 表示安全设置已由更早的调用完成。 HRESULT CoInitializeSecurity( PSECURITY_DESCRIPTOR pSecDesc, LONG cAuthSvc, SOLE_AUTHENTICATION_SERVICE* asAuthSvc, void* pReserved1, DWORD dwAuthnLevel, DWORD dwImpLevel, void* pAuthList, DWORD dwCapabilities, void* pReserved3 ); ``` 正确做法:接受 `RPC_E_TOO_LATE`,并且只在 `CoInitializeEx` 成功时调用 `CoUninitialize`。任务、WMI 和其它 COM 组件可能已经设置过同一进程的安全参数。 ```cpp const HRESULT initStatus = CoInitializeEx(nullptr, COINIT_MULTITHREADED); const bool needUninitialize = SUCCEEDED(initStatus); if (FAILED(initStatus) && initStatus != RPC_E_CHANGED_MODE) { return; } const HRESULT securityStatus = CoInitializeSecurity( nullptr, -1, nullptr, nullptr, RPC_C_AUTHN_LEVEL_DEFAULT, RPC_C_IMP_LEVEL_IMPERSONATE, nullptr, EOAC_NONE, nullptr); if (FAILED(securityStatus) && securityStatus != RPC_E_TOO_LATE) { if (needUninitialize) CoUninitialize(); return; } ``` 错误示例:把 `RPC_E_TOO_LATE` 当成连接失败。这个状态说明安全设置已存在,可以继续使用已有设置。直接终止会遗漏本来可读的对象。 完成第一步后,当前线程已经具备调用 COM 的环境,进程也已有可用的默认安全设置。接下来要连接保存永久订阅对象的命名空间,才能取得实际执行查询的 WMI 服务接口。 ## 三、第二步:连接 ROOT\subscription 并设置代理安全 ### 2. 连接 ROOT\subscription 并取得服务接口 创建 COM 类的 API 根据 CLSID 返回指定接口。`CLSID_WbemLocator` 用于创建 `IWbemLocator`,随后由 locator 连接 WMI 命名空间。接口指针需要调用 `Release`。 ```cpp // 意义:创建 WMI locator COM 对象。 // 返回:S_OK 成功。失败 HRESULT 表示类或接口不可用。 HRESULT CoCreateInstance( REFCLSID rclsid, LPUNKNOWN pUnkOuter, DWORD dwClsContext, REFIID riid, LPVOID* ppv ); // 意义:连接 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 ); // 意义:创建一个由 OLE 自动化分配器管理的 BSTR。 // 返回:成功时为新 BSTR,内存不足时为 nullptr。必须使用下方匹配的释放函数。 BSTR SysAllocString(const OLECHAR* psz); // 意义:释放 SysAllocString 或 COM getter 返回的 BSTR。 // 返回:无。传入 nullptr 安全。 void SysFreeString(BSTR bstrString); // 意义:为已连接的 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 ); ``` 正确做法:使用 `ROOT\subscription`,再给服务代理设置模拟级别。该命名空间保存永久订阅对象。`ROOT\CIMV2` 中的普通系统类不能代替它。 ```cpp IWbemLocator* locator = nullptr; IWbemServices* services = nullptr; if (SUCCEEDED(CoCreateInstance(CLSID_WbemLocator, nullptr, CLSCTX_INPROC_SERVER, IID_IWbemLocator, reinterpret_cast<void**>(&locator)))) { BSTR path = SysAllocString(L"ROOT\\subscription"); const HRESULT connected = locator->ConnectServer( path, nullptr, nullptr, nullptr, WBEM_FLAG_CONNECT_USE_MAX_WAIT, nullptr, nullptr, &services); SysFreeString(path); if (SUCCEEDED(connected)) { CoSetProxyBlanket(services, RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE, nullptr, RPC_C_AUTHN_LEVEL_CALL, RPC_C_IMP_LEVEL_IMPERSONATE, nullptr, EOAC_NONE); } locator->Release(); } ``` 错误示例:省略代理安全设置。服务接口已经返回时,后续查询仍可能因当前令牌的模拟级别不足失败。连接和代理安全是两个独立步骤。 完成第二步后,已经取得了 `ROOT\subscription` 的服务接口,并为这个代理设置了当前令牌所需的安全级别。接下来需要按对象类型执行独立查询,才能避免把日志配置当成命令配置。 ## 四、第三步:分别读取四类对象 ### 3. WQL 查询需要有限等待时间 执行查询的方法返回枚举器。`WBEM_FLAG_FORWARD_ONLY` 降低内存使用,`WBEM_FLAG_RETURN_IMMEDIATELY` 使 WMI 异步准备结果。枚举器的 `Next` 调用需要有限超时,异常 WMI 提供者不能无限阻塞读取线程。 ```cpp // 意义:执行只读 WQL 查询并返回结果枚举器。 // 返回:S_OK 成功。ppEnum 接收 IEnumWbemClassObject,调用方 Release。 HRESULT IWbemServices::ExecQuery( const BSTR strQueryLanguage, const BSTR strQuery, LONG lFlags, IWbemContext* pCtx, IEnumWbemClassObject** ppEnum ); // 意义:从 WMI 枚举器读取至多 lRequested 个对象。 // 返回:WBEM_S_NO_ERROR 成功。WBEM_S_TIMEDOUT 表示等待超时。 HRESULT IEnumWbemClassObject::Next( LONG lTimeout, ULONG uCount, IWbemClassObject** apObjects, ULONG* puReturned ); ``` 正确做法:为每类事件处理对象使用独立 WQL。`CommandLineEventConsumer` 读取 `CommandLineTemplate`、`ExecutablePath`、`WorkingDirectory`。`ActiveScriptEventConsumer` 读取 `ScriptText`、`ScriptingEngine`。`LogFileEventConsumer` 读取 `Filename`、`Text`。`NTEventLogEventConsumer` 读取 `SourceName`、`EventID`、`Category`。 ```cpp const wchar_t* query = L"SELECT Name, CommandLineTemplate, ExecutablePath, WorkingDirectory " L"FROM CommandLineEventConsumer"; // 每次 Next 最多等待五秒。连续超时达到上限时记录类名并继续其它对象类。 HRESULT status = enumerator->Next(5000, 1, &object, &returned); if (status == WBEM_S_TIMEDOUT && returned == 0) { // 记录超时,不把它当作“该类没有对象”。 } ``` 错误示例:把日志处理对象的文件名当作启动映像。`LogFileEventConsumer` 的 `Filename` 是写入目标,`NTEventLogEventConsumer` 的 `SourceName` 是事件来源。这两类对象记录日志,不提供可执行命令。 完成第三步后,已经按类拿到对象实例和对应字段。实例中的每个属性仍是一段带类型的数据,接下来需要检查 `VARIANT`,才能安全取得文本、数值或空值。 ## 五、第四步:按属性类型转换并输出文本 ### 4. WMI 属性使用 VARIANT 返回 读取属性的方法把值写入 `VARIANT`。`VT_BSTR` 保存文本,整数和布尔值使用各自字段。每次读取后必须调用 `VariantClear`,因为属性可能携带 BSTR、接口或数组资源。 ```cpp // 意义:读取 WMI 对象的一个命名属性。 // 返回:S_OK 成功。pVal 接收 VARIANT,调用方必须 VariantClear。 HRESULT IWbemClassObject::Get( LPCWSTR wszName, LONG lFlags, VARIANT* pVal, CIMTYPE* pType, LONG* plFlavor ); // 意义:把 VARIANT 初始化为空值,供 COM 方法写入。 // 返回:无。每个初始化后的 VARIANT 最终都需要清理内部资源。 void VariantInit(VARIANTARG* pvarg); // 意义:释放 VARIANT 内部的 BSTR、数组、接口等资源。 // 返回:S_OK 成功。应在不再读取属性后调用。 HRESULT VariantClear(VARIANTARG* pvarg); ``` 正确做法:初始化 VARIANT、读取属性、按 `vt` 分支解释,再清理资源。空值、缺失字段和查询失败要保留不同状态,不能都替换为空字符串。 ```cpp VARIANT value{}; VariantInit(&value); if (SUCCEEDED(object->Get(L"ScriptText", 0, &value, nullptr, nullptr))) { if (value.vt == VT_BSTR && value.bstrVal != nullptr) { // 保存 ActiveScriptEventConsumer 的原始脚本文本。 } } VariantClear(&value); ``` 错误示例:把 `VARIANT` 强制转换为 `BSTR`。`EventID`、`Category` 等字段可能为整数,直接访问 `bstrVal` 会读取错误的联合成员。 完成第四步后,读取结果已经按属性的真实类型转换为文本。最后还要把对象类别、触发条件和实际执行分别解释,避免从一个静态对象推断事件已经触发。 ## 六、读取结果的解释边界 ### 5. 事件处理对象本身不说明触发条件 对象类型要通过类名记录。命令行对象和脚本对象包含可执行或可解释内容。日志对象只表达记录动作。字段语义由类名决定。 触发条件要与 Filter、Binding 分开读取。处理对象只定义处理方式。`__EventFilter` 定义 WQL 条件。`__FilterToConsumerBinding` 才把两者关联。仅看到处理对象不能推导它何时触发。 结果已经能够说明每个对象属于哪一类、保存了哪些原始属性,以及本次读取是否成功。Filter、Binding 和实际事件处理仍属于另外三类证据,不能用这些对象的枚举结果替代。 完整可运行程序在附件 <mark class="encrypted">c1fK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4j5h3&6Y4N6$3g2A6j5$3#2Q4x3X3g2D9j5h3&6*7L8%4g2#2i4K6u0W2j5$3!0E0i4K6u0r3K9i4A6I4N6W2R3K6P5r3c8W2M7i4S2A6</mark>
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。
上传的附件:
007-WMI永久事件消费者的命令脚本与日志配置.zip
(156.46kb,3次下载)
收藏
・
0
点赞
・
0
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
0
)
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
WangWei_CM.
30
发帖
1
回帖
40
RANK
关注
私信
他的文章
[原创] 进程模块枚举、映像身份与线程启动地址关联
215
[原创]Windows 进程 CPU、内存、I/O 与调度指标采样
981
[原创]跨位数进程的映像、命令行与环境块读取
535
[原创]进程实例身份、父子关系与会话归属
293
[原创]Windows 进程枚举NtQuerySystemInformation(SystemProcessInformation
635
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
谁下载
×
huangyalei
Fzzz
mb_lthgjpwj
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部