模拟客户端

当用户应用程序通过 WMI 提供程序从系统上的对象请求数据时,模拟表示提供程序提供代表客户端(而不是提供程序)安全级别的凭据。 模拟可防止客户端未经授权访问系统上的信息。

本主题包括以下部分:

WMI 通常使用 LocalServer 安全上下文以较高安全级别作为管理服务运行。 WMI 可以使用管理服务访问特权信息。 当调用某个提供程序来获取信息时,WMI 会将其安全标识符 (SID) 传递给该提供程序,使该提供程序能够以相同的较高安全级别访问信息。

在 WMI 应用程序启动过程中,Windows 操作系统将为 WMI 应用程序提供启动该过程的用户的安全上下文。 该用户的安全上下文通常比 LocalServer 的安全级别低,因此该用户可能无权访问 WMI 可用的所有信息。 当用户应用程序请求动态信息时,WMI 会将该用户的 SID 传递给相应的提供程序。 如果编写得当,提供程序会尝试使用用户 SID 而不是提供程序 SID 来访问信息。

要使提供程序能够成功模拟客户端应用程序,客户端应用程序和提供程序必须满足以下条件:

为模拟注册提供程序

WMI 仅将客户端应用程序的 SID 传递给已注册为模拟提供程序的提供程序。 要使提供程序能够执行模拟,需要修改提供程序注册过程。

以下过程说明如何为模拟注册提供程序。 该过程假设你已了解注册过程。 有关注册过程的详细信息,请参阅注册提供程序

为模拟注册提供程序

  1. 将代表提供程序的 __Win32Provider 类的 ImpersonationLevel 属性设置为 1。

    ImpersonationLevel 属性记录提供程序是否支持模拟。 将 ImpersonationLevel 设置为 0 表示提供程序不模拟客户端,而是在与 WMI 相同的用户上下文中执行请求的所有操作。 将 ImpersonationLevel 设置为 1 表示提供程序使用模拟调用来检查代表客户端执行的操作。

  2. 将同一 __Win32Provider 类的 PerUserInitialization 属性设置为 TRUE。

注意

如果在将 __Win32Provider 属性 InitializeAsAdminFirst 设置为 TRUE 的情况下注册提供程序,则该提供程序仅在初始化阶段使用管理级线程安全令牌。 虽然对 CoImpersonateClient 的调用不会失败,但提供程序使用 WMI 而不是客户端的安全上下文。

 

以下代码示例演示如何为模拟注册提供程序。

instance of __Win32Provider
{
    CLSID = "{FD4F53E0-65DC-11d1-AB64-00C04FD9159E}";
    ImpersonationLevel = 1;
    Name = "MS_NT_EVENTLOG_PROVIDER";
    PerUserInitialization = TRUE;
};

在提供程序中设置模拟级别

如果在将 __Win32Provider 类属性 ImpersonationLevel 设置为 1 的情况下注册提供程序,则 WMI 会调用提供程序来模拟各种客户端。 若要处理这些调用,请在 IWbemServices 接口的实现中使用 CoImpersonateClientCoRevertToSelf COM 函数。

CoImpersonateClient 函数允许服务器模拟发出调用的客户端。 通过将 CoImpersonateClient 调用放入 IWbemServices 实现,提供程序可以设置提供程序的线程令牌以匹配客户端的线程令牌,从而模拟客户端。 如果你不调用 CoImpersonateClient,则提供程序将在管理员安全级别执行代码,从而造成潜在的安全漏洞。 如果提供程序暂时需要充当管理员或手动执行访问检查,则调用 CoRevertToSelf

CoImpersonateClient 不同,CoRevertToSelf 是一个处理线程模拟级别的 COM 函数。 在这种情况下,CoRevertToSelf 会将模拟级别改回原始模拟设置。 一般情况下,提供程序最初充当管理员,并根据它是代表调用方发出调用还是发出自己的调用,在 CoImpersonateClient 和 CoRevertToSelf 之间交替。 提供程序需负责正确发出这些调用,以免向最终用户公开安全漏洞。 例如,提供程序应该只调用模拟代码序列中的本机 Windows 函数。

注意

CoImpersonateClientCoRevertToSelf 的用途是为提供程序设置安全性。 如果你确定模拟失败,则应通过 IWbemObjectSink::SetStatus 向 WMI 返回适当的完成代码。 有关详细信息,请参阅处理提供程序中的拒绝访问消息

 

在提供程序中维护安全级别

提供程序不能在 IWbemServices 的实现中调用 CoImpersonateClient 一次,并假设模拟凭据在提供程序的持续时间内保持不变。 它应在实现过程中调用 CoImpersonateClient 多次,以防止 WMI 更改凭据。

为提供程序设置模拟时的主要忧虑在于可重入性。 在此上下文中,可重入性是指提供程序调用 WMI 以获取信息,然后等待 WMI 回调提供程序。 从本质上讲,执行线程退出提供程序代码的目的仅仅是为了以后可以重新进入代码。 可重入性是 COM 设计的一部分,通常不会造成问题。 但是,当执行线程进入 WMI 时,线程将采用 WMI 的模拟级别。 当线程返回到提供程序时,你必须通过再次调用 CoImpersonateClient 来重置模拟级别。

为了防止你自己受到提供程序中安全漏洞的影响,应该仅在模拟客户端时对 WMI 发出可重入调用。 也就是说,应在调用 CoImpersonateClient 之后、调用 CoRevertToSelf 之前向 WMI 发出调用。 由于 CoRevertToSelf 会导致将模拟设置为运行 WMI 的用户级别(通常是 LocalSystem),在调用 CoRevertToSelf 后对 WMI 发出可重入调用可能会导致为用户和任何被调用提供程序授予的功能比应有的功能要多得多。

注意

如果你调用系统函数或其他接口方法,则无能保证调用上下文能够保留。

 

处理提供程序中的拒绝访问消息

大多数“拒绝访问”错误消息都是在客户端请求它们无权访问的类或信息时出现的。 如果提供程序向 WMI 返回“拒绝访问”错误消息,而 WMI 又将此消息传递给客户端,则客户端可能会推断信息存在。 在某些情况下,这可能是一种安全违规。 因此,提供程序不应将消息传播到客户端。 相反,不应公开提供程序提供的类集。 同样,动态实例提供程序应该调用基础数据源来确定如何处理“拒绝访问”消息。 提供程序需负责将这种理念复制到 WMI 环境中。 有关详细信息,请参阅报告部分实例报告部分枚举

确定提供程序应如何处理“拒绝访问”消息时,必须编写并调试代码。 在调试时,通常可以方便地区分由于低模拟导致的拒绝和由于代码错误导致的拒绝。 可以在代码中使用简单的测试来确定差异。 有关详细信息,请参阅调试拒绝访问代码

报告部分实例

出现“拒绝访问”消息的一种常见原因是 WMI 无法提供所有信息来填充实例。 例如,客户端可能有权查看硬盘驱动器对象,但无权查看硬盘驱动器本身有多少可用空间。 当提供程序由于访问冲突而无法使用属性完全填充实例时,提供程序必须确定如何处理任何情况。

对于对实例拥有部分访问权限的客户端,WMI 不需要获取单个响应。 WMI 版本 1.x 允许提供程序使用以下选项之一:

  • 使用 WBEM_E_ACCESS_DENIED 使整个操作失败且不返回任何实例。

    连同 WBEM_E_ACCESS_DENIED 一起返回一个错误对象,以描述拒绝原因。

  • 返回所有可用属性,并使用 NULL 填充不可用属性。

注意

确保返回 WBEM_E_ACCESS_DENIED 不会在企业中造成安全漏洞。

 

报告部分枚举

发生访问冲突的另一个常见原因是 WMI 无法返回所有枚举。 例如,客户端可能有权查看所有本地网络计算机对象,但无权查看其域外部的计算机对象。 当枚举由于访问冲突而无法完成时,提供程序必须确定如何处理任何情况。

与实例提供程序一样,WMI 不需要对部分枚举的单个响应。 WMI 版本 1.x 允许提供程序使用以下选项之一:

  • 为提供程序可访问的所有实例返回 WBEM_S_NO_ERROR。

    如果你使用此选项,用户将不知道某些实例不可用。 许多提供程序(例如使用结构化查询语言 (SQL) 的具有行级安全性的提供程序)使用调用方的安全级别返回成功的部分结果来定义结果集。

  • 使用 WBEM_E_ACCESS_DENIED 使整个操作失败且不返回任何实例。

    提供程序可以选择性地包含一个错误对象用于向客户端描述情况。 请注意,某些提供程序可能会连续访问数据源,并且在枚举进行到一定程度都不会遇到拒绝错误。

  • 返回所有可访问的实例,但同时返回非错误状态代码 WBEM_S_ACCESS_DENIED。

    提供程序应记录枚举期间遇到的拒绝错误,并可以继续提供实例,最后以非错误状态代码结束。 提供程序还可以选择在首次遇到拒绝错误时终止枚举。 此选项的理由是不同的提供程序有不同的检索范式。 提供程序可能在发现访问冲突之前已传递了实例。 有些提供程序可能选择继续提供其他实例,而有些提供程序则希望终止。

由于 COM 的结构,你无法在出错期间封送回任何信息,但错误对象除外。 因此,不能同时返回信息和错误代码。 如果你选择返回信息,则必须改用非错误状态代码。

调试拒绝访问代码

某些应用程序可能使用低于 RPC_C_IMP_LEVEL_IMPERSONATE 的模拟级别。 在这种情况下,提供程序对客户端应用程序发出的大多数模拟调用都会失败。 若要成功设计并实现提供程序,必须记住这一点。

默认情况下,可以访问提供程序的其他模拟级别只有 RPC_C_IMP_LEVEL_IDENTIFY。 如果客户端应用程序使用 RPC_C_IMP_LEVEL_IDENTIFY,则 CoImpersonateClient 不会返回错误代码。 提供程序只会出于标识目的模拟客户端。 因此,提供程序调用的大多数 Windows 方法将返回拒绝访问消息。 这在实践中是无害的,因为不允许用户执行任何不当操作。 但是,在提供程序开发期间了解是否真正模拟客户端可能很有帮助。

代码需要以下引用和 #include 语句才能正确编译。

#define _WIN32_DCOM
#include <iostream>
using namespace std;
#include <wbemidl.h>

以下代码示例演示如何确定提供程序是否成功模拟了客户端应用程序。

DWORD dwImp = 0;
HANDLE hThreadTok;
DWORD dwBytesReturned;
BOOL bRes;

// You must call this before trying to open a thread token!
CoImpersonateClient();

bRes = OpenThreadToken(
    GetCurrentThread(),
    TOKEN_QUERY,
    TRUE,
    &hThreadTok
);

if (bRes == FALSE)
{
    printf("Unable to read thread token (%d)\n", GetLastError());
    return 0;
}

bRes = GetTokenInformation(
    hThreadTok,
    TokenImpersonationLevel, 
    &dwImp,
    sizeof(DWORD),
    &dwBytesReturned
);

if (!bRes)
{
    printf("Unable to read impersonation level\n");
    CloseHandle(hThreadTok);
    return 0;
}

switch (dwImp)
{
case SecurityAnonymous:
    printf("SecurityAnonymous\n");
    break;

case SecurityIdentification:
    printf("SecurityIdentification\n");
    break;

case SecurityImpersonation:
    printf("SecurityImpersonation\n");
    break;

case SecurityDelegation:
    printf("SecurityDelegation\n");
    break;

default:
    printf("Error. Unable to determine impersonation level\n");
    break;
}

CloseHandle(hThreadTok);

开发 WMI 提供程序

设置命名空间安全描述符

保护提供程序