当用户应用程序通过 WMI 提供程序从系统上的对象请求数据时,模拟意味着提供程序提供表示客户端安全级别的凭据,而不是提供程序的凭据。 模拟可防止客户端获取对系统上信息的未经授权的访问。
本主题将讨论以下部分:
WMI 通常使用 LocalServer 安全上下文在较高安全级别作为管理服务运行。 使用管理服务可让 WMI 访问特权信息。 调用提供程序获取信息时,WMI 会将其安全标识符 (SID) 传递给提供程序,使提供程序能够在同一高级别访问信息。
在 WMI 应用程序启动过程中,Windows作系统为 WMI 应用程序提供开始该过程的用户的安全上下文。 用户的安全上下文通常比 LocalServer 低,因此用户可能无权访问 WMI 可用的所有信息。 当用户应用程序请求动态信息时,WMI 会将用户的 SID 传递给相应的提供程序。 如果编写得适当,提供程序将尝试使用用户 SID 访问信息,而不是使用提供程序 SID。
若要使提供程序成功模拟客户端应用程序,客户端应用程序和提供程序必须满足以下条件:
- 客户端应用程序必须使用 COM 连接安全级别 RPC_C_IMP_LEVEL_IMPERSONATE 或 RPC_C_IMP_LEVEL_DELEGATE调用 WMI。 有关详细信息,请参阅 维护 WMI 安全性。
- 提供程序必须向 WMI 注册为模拟提供程序。 有关详细信息,请参阅 注册用于模拟的提供程序。
- 访问特权信息之前,提供程序必须切换到客户端应用程序的安全级别。 有关详细信息,请参阅 在提供程序中设置模拟级别。
- 如果拒绝访问此信息,提供程序必须正确处理错误条件。 有关详细信息,请参阅 在提供程序中处理访问被拒绝消息。
注册用于身份模拟的提供程序
WMI 仅将客户端应用程序的 SID 传递给已注册为模拟提供程序的提供程序。 使提供程序能够执行身份模拟需要修改提供程序注册过程。
以下过程介绍如何注册服务提供者以进行身份模拟。 此过程假定你已了解注册过程。 有关注册过程的详细信息,请参阅 注册提供程序。
注册用于模拟的提供者
将表示提供程序的 __Win32Provider 类的 ImpersonationLevel 属性设置为 1。
ImpersonationLevel 属性说明提供程序是否支持模拟功能。 将 ImpersonationLevel 设置为 0 表示提供程序不会模拟客户端,并在与 WMI 相同的用户上下文中执行所有请求的作。 将 ImpersonationLevel 设置为 1 表示提供程序使用模拟身份验证调用来检查代表客户端执行的操作。
将同一__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 接口的实现中使用 CoImpersonateClient 和 CoRevertToSelf COM 函数。
CoImpersonateClient 函数允许服务器模拟发出调用的客户端。 在 IWbemServices 的实现中调用 CoImpersonateClient,可以让你的提供程序设置线程令牌,使其匹配客户端的线程令牌,从而模拟客户端。 如果不调用 CoImpersonateClient,提供程序会在管理员级别执行代码,从而创建潜在的安全漏洞。 如果提供商暂时需要充当管理员或手动执行访问检查,请调用 CoRevertToSelf。
与 CoImpersonateClient 相比, CoRevertToSelf 是处理线程模拟级别的 COM 函数。 在这种情况下, CoRevertToSelf 会将模拟级别更改回原始模拟设置。 一般情况下,提供方最初是管理员,并在 CoImpersonateClient 和 CoRevertToSelf 之间切换,这取决于它是进行代表调用者的调用还是进行自己的调用。 提供商有责任正确放置这些调用,以免向最终用户公开安全漏洞。 例如,提供程序应仅调用模拟代码序列中的本机 Windows 函数。
注释
CoImpersonateClient 和 CoRevertToSelf 的目的是为提供程序设置安全性。 如果确定模拟失败,则应通过 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。
提供程序应在枚举期间记录被拒绝的情况,并可以继续提供实例,最后以 nonerror 状态代码结束。 提供者还可以选择在第一次拒绝时终止枚举。 此选项的理由是不同的提供程序具有不同的检索范例。 在发现访问冲突之前,提供程序可能已经传递了实例。 某些提供商可能会选择继续提供其他实例,而另一些提供商可能希望终止。
由于 COM 的结构,除了错误对象之外,无法在错误期间封送回任何信息。 因此,不能同时返回信息和错误代码。 如果选择返回信息,则必须改用 nonerror 状态代码。
调试访问被拒绝的代码
某些应用程序可能使用低于 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);
相关主题