COM安全的基础是Windows安全,在Windows中,安全的最小边界是进程,所以进程内组件通常不需要考虑安全,但是如果编写进程外组件,客户是否有权访问进程外的组件,就需要通过安全检查。
COM安全主要包括认证、访问控制和令牌管理:
检验发送消息者的身份的确就是他所声称的那个身份,并且消息也的确是他发送的。
我们可以选择认证协议,可以选择认证级别。下面的代码片断选择了Kerberos认证协议,并选择了在第一个方法调用时客户的安全凭证需要在服务器上认证
SOLE_AUTHENTICATION_SERVICE AuthSvc[1];
AuthSvc[0].dwAuthnSvc=RPC_C_AUTHN_GSS_KERBEROS;
AuthSvc[0].dwAuthzSvc=NULL;
AuthSvc[0].pPrincipalName=NULL;
hr=::CoInitializeSecurity(pAccessCtl.p,1,AuthSvc,NULL,RPC_C_AUTHN_LEVEL_CONNECT,RPC_C_IMP_LEVEL_IDENTIFY ,NULL,EOAC_ACCESS_CONTROL,NULL);
1) 允许谁启动服务器进程
在注册表中组件的APPID键中有一项LaunchPermission,记录了系统中可以启动服务器进程的账号的二进制码,通常我们创建了EXE形式的组件后,注册表中并没有这一项,需要通过DCOMNCNFG.EXE来设置。如果启动进程时找不到这一项,会从HKEY_LOCAL_MACHINE/SOFTWARE/Microsoft/Ole/中寻找DefaultLaunchPermission的值。如果连DefaultLaunchPermission都找不到,系统将拒绝任何激活请求。
2) 检验访问者是否具有权限访问服务器的对象
在注册表中组件的APPID键中有一项AccessPermission,记录了系统中可以访问服务器进程的账号的二进制码。同样我们可以通过DCOMNCNFG.EXE进行设置。如果跨进程调用时COM没有找到AccessPermission,就会查找机器范围内有无默认设置,通常找不到,这时,COM会创建一个新的访问控制列表,只允许SYSTEM账号访问。
启动服务器的进程权限检查总是通过注册表来设置,而访问进程的权限也可以通过调用
CoInitializeSecurity函数来解决。我们可以通过将IAccessControl接口作为第一个参数传递给该函数,同时指定dwCapabilities参数为EOAC_ACCESS_CONTROL。
负责控制方法内部执行和启动服务进程时采用谁的令牌。扮演的实质就是服务器是否能够使用客户的令牌对系统资源进行调用。
主要包括:
1) 认证服务----使用什么安全软件包进行安全检查
可以同时使用多个安全包,也可以为不同的接口代理配置不同的安全包
目前的Windows平台上提供的安全包参考MSDN链接:ms-help://MS.MSDNQTR.2003FEB.2052/com/htm/cmf_m2z_22yg.htm
2) 授权服务
参考ms-help://MS.MSDNQTR.2003FEB.2052/com/htm/cmf_m2z_77js.htm
3) 主要名称
1)---3)项都包含在SOLE_AUTHENTICATION_SERVICE结构中,该结构作为参数传递给CoInitializeSecurity函数。
4) 认证级别
ms-help://MS.MSDNQTR.2003FEB.2052/com/htm/cmf_m2z_3lo8.htm
5) 扮演级别
ms-help://MS.MSDNQTR.2003FEB.2052/com/htm/cmf_m2z_2jco.htm
1) 注册表设置(可以通过DCOMCNFG.EXE程序调整)
组件的APPID健,比如我们创建了一个EXE形式的组件Server,注册表中APPID默认是没有安全设置,如果我们使用了DCOMCNFG.EXE进行配置,就可以看到注册表中多出了一项AuthenticationLevel,值为0x00000002,身份验证级别为连接。
通过DCOMCFNG.EXE我们还可以设置访问权限、启动权限、进程用什么身份令牌运行,如图:
MSDN: ms-help://MS.VSCC.2003/MS.MSDNQTR.2003FEB.2052/com/htm/security_1xbb.htm
2) 调用API函数设置
将忽略组件注册表中保存的APPID值对应的安全设置
安全协商机制是COM在创建一个新的代理时,为该代理进行安全设置的一个过程。
(参见ms-help://MS.MSDNQTR.2003FEB.2052/com/htm/cmf_m2z_22yg.htm)
客户和服务器都可以通过调用CoInitializeSecurity函数进行自己的安全设置。CoInitializeSecurity用来设置进程内的默认的安全设置。注意:这里的客户和服务器是相对的,一个组件当它调用另一个进程外的组件的时候,它是客户,当它被另一个程序调用时它又是服务器。
客户可以设置的包括:
an authentication level 认证级别
an impersonation level 扮演级别
the authentication identity 认证身份
capabilities
服务器可以设置的包括:
an ACL 安全描述表
a list of authentication service/authorization service/principal name tuples
一组 认证服务/授权服务/主要名称
and an authentication level 认证级别
当COM对客户和服务器的安全设置进行协商时,COM会选择客户和服务器的认证服务中相同的作为认证服务,同时COM选择的认证级别是两者的设置中最高的,扮演级别取决于客户端的设置。当COM选定了适合客户和服务器的安全设置后,新创建的客户端的接口代理就继承了这个安全设置,但是客户端仍然可以调用IClientSecurity::SetBlanket对接口代理的安全设置进行修改。
参考MSDN:ms-help://MS.MSDNQTR.2003FEB.2052/com/htm/cmf_a2c_8ayh.htm
例一:服务器安全设置
extern "C" int WINAPI _tWinMain(HINSTANCE /*hInstance*/, HINSTANCE /*hPrevInstance*/,
LPTSTR /*lpCmdLine*/, int nShowCmd)
{
//访问控制设置:允许cs账号访问组件,但是拒绝guest账号
CComPtr<IAccessControl> pAccessCtl;
HRESULT hr=pAccessCtl.CoCreateInstance(CLSID_DCOMAccessControl);
ACTRL_ACCESS_ENTRYW rgaae[]=
{
{ {0,NO_MULTIPLE_TRUSTEE,TRUSTEE_IS_NAME,TRUSTEE_IS_USER,L"FREEBIRD//guest"},ACTRL_ACCESS_DENIED,COM_RIGHTS_EXECUTE,0,NO_INHERITANCE,0},
//{ {0,NO_MULTIPLE_TRUSTEE,TRUSTEE_IS_NAME,TRUSTEE_IS_GROUP,L"FREEBIRD//Administrators"},ACTRL_ACCESS_ALLOWED,COM_RIGHTS_EXECUTE,0,NO_INHERITANCE,0},
{ {0,NO_MULTIPLE_TRUSTEE,TRUSTEE_IS_NAME,TRUSTEE_IS_USER,L"FREEBIRD//cs"},ACTRL_ACCESS_ALLOWED,COM_RIGHTS_EXECUTE,0,NO_INHERITANCE,0}
};
ACTRL_ACCESS_ENTRY_LISTW aael={sizeof(rgaae)/sizeof(*rgaae),rgaae};
ACTRL_PROPERTY_ENTRYW ape={0,&aael,0};
ACTRL_ACCESSW aa={1,&ape};
hr=pAccessCtl->SetAccessRights(&aa);
/////////////////////////////
SOLE_AUTHENTICATION_SERVICE AuthSvc[1];
AuthSvc[0].dwAuthnSvc=RPC_C_AUTHN_GSS_KERBEROS;
AuthSvc[0].dwAuthzSvc=NULL;
AuthSvc[0].pPrincipalName=NULL;
//注意:传递RPC_C_IMP_LEVEL_DEFAULT会出错
hr=::CoInitializeSecurity(pAccessCtl.p,-1,NULL,NULL,RPC_C_AUTHN_LEVEL_CONNECT,RPC_C_IMP_LEVEL_IDENTIFY ,NULL,EOAC_ACCESS_CONTROL,NULL);
return _AtlModule.WinMain(nShowCmd);
}
如果我们把{ {0,NO_MULTIPLE_TRUSTEE,TRUSTEE_IS_NAME,TRUSTEE_IS_USER,L"FREEBIRD//cs"},ACTRL_ACCESS_ALLOWED,COM_RIGHTS_EXECUTE,0,NO_INHERITANCE,0}中的红色部分
修改成ACTRL_ACCESS_DENIED,这样当我们通过cs登陆windows,然后执行一个客户程序,而这个客户程序没有显式调用CoInitializeSecurity函数的情况下,创建进程外组件将会遇到E_ACCESSDENIED。这里连激活权限都已经禁止了,我认为ACTRL_ACCESS_DENIED中的ACCESS用词不当。客户端代码如下:
#include "stdafx.h"
#import "Server.tlb" no_namespace raw_interfaces_only named_guids
#include <iostream>
using namespace std;
int _tmain(int argc, _TCHAR* argv[])
{
::CoInitialize(NULL);
CComPtr<IA> pA;
HRESULT hr=pA.CoCreateInstance(__uuidof(A));
if(hr==E_ACCESSDENIED)
{
cout<<"激活请求被拒绝"<<endl;
pA.Release();
::CoUninitialize();
return 1;
}
hr=pA->f();
if(hr!=S_OK)
{
cout<<"调用f方法时被拒绝"<<endl;
pA.Release();
::CoUninitialize();
return 1;
}
pA.Release();
::CoUninitialize();
return 0;
}
// Client.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#import "Server.tlb" no_namespace raw_interfaces_only named_guids
#include <iostream>
#include <iaccess.h>
//#include <objidl.h>
#include <objbase.h>
using namespace std;
#pragma comment(lib,"ole32.lib")
HRESULT SetSecurity();
int _tmain(int argc, _TCHAR* argv[])
{
::CoInitializeEx(NULL,COINIT_APARTMENTTHREADED);
HRESULT hr=SetSecurity();
if(hr!=S_OK)
{
cout<<"设置客户端安全出错"<<endl;
return 1;
}
CComPtr<IA> pA;
hr=pA.CoCreateInstance(__uuidof(A));
if(hr==E_ACCESSDENIED)
{
cout<<"激活请求被拒绝"<<endl;
pA.Release();
::CoUninitialize();
return 1;
}
hr=pA->f();
if(hr!=S_OK)
{
cout<<"调用f方法时被拒绝"<<endl;
pA.Release();
::CoUninitialize();
return 1;
}
pA.Release();
::CoUninitialize();
return 0;
}
HRESULT SetSecurity()
{
SOLE_AUTHENTICATION_SERVICE AuthSvc[1];
AuthSvc[0].dwAuthnSvc=RPC_C_AUTHN_GSS_KERBEROS;
AuthSvc[0].dwAuthzSvc=NULL;
AuthSvc[0].pPrincipalName=NULL;
//注意:传递RPC_C_AUTHN_LEVEL_NONE以外的参数会出错
//注意:传递非NULL的IAccessControl接口指针作为第一个参数会出错
HRESULT hr=::CoInitializeSecurity(NULL,1,AuthSvc,NULL,RPC_C_AUTHN_LEVEL_NONE,RPC_C_IMP_LEVEL_IDENTIFY,NULL,EOAC_ACCESS_CONTROL,NULL);
return hr;
}
该函数用于激活指定计算机上的组件,多用于客户端。
该函数在CoInitializeSecurity函数后面调用。具体使用方法可以参考下面的函数:
SetClientDefaultSecurity(感谢高岿、程悟、张亮的整理)
// 设置客户方默认安全级别
HRESULT SetClientDefaultSecurity(
LPOLESTR wszUser, // 用户名
LPOLESTR wszDomain, // 服务器名
LPOLESTR wszPassword, // 密码
REFCLSID rclsid, // 组件GUID
MULTI_QI* mqi, // 多接口数组
int nItf // 数组中接口数
)
{
HRESULT hr = 0;
IUnknown* pIUnk = 0;
//
// The COAUTHIDENTITY structure represents a username and password
//
COAUTHIDENTITY auid;
auid.User = wszUser;
auid.UserLength = wcslen(wszUser);
auid.Domain = wszDomain;
auid.DomainLength = wcslen(wszDomain);
auid.Password = wszPassword;
auid.PasswordLength = wcslen(wszPassword);
auid.Flags = SEC_WINNT_AUTH_IDENTITY_UNICODE;
//
// The COAUTHINFO structure specifys the authentication settings
// used while making a remote activation request from the client
// machine to server machine.
//
COAUTHINFO auinfo;
auinfo.dwAuthnSvc = RPC_C_AUTHN_WINNT;
auinfo.dwAuthzSvc = RPC_C_AUTHZ_NONE;
auinfo.pwszServerPrincName = NULL;
auinfo.dwAuthnLevel = RPC_C_AUTHN_LEVEL_CONNECT;
auinfo.dwImpersonationLevel = RPC_C_IMP_LEVEL_IMPERSONATE;
auinfo.pAuthIdentityData = &auid;
auinfo.dwCapabilities = EOAC_NONE;
//
// Indentifies a remote machine resource to the new or enhanced
// activation functions.
//
COSERVERINFO svrinfo;
svrinfo.dwReserved1 = 0;
svrinfo.dwReserved2 = 0;
svrinfo.pAuthInfo = &auinfo;
svrinfo.pwszName = wszDomain;
hr = CoCreateInstanceEx(rclsid, 0, CLSCTX_ALL, &svrinfo, nItf, mqi);
if ( FAILED(hr) )
return hr;
pIUnk = (IUnknown*) mqi[0].pItf;
if ( pIUnk == NULL )
return hr;
IClientSecurity* pcs = 0;
hr = pIUnk->QueryInterface(IID_IClientSecurity, (void**) &pcs);
if ( FAILED(hr) )
return hr;
for (int i=0; i < nItf; i++)
{
hr = pcs->SetBlanket(mqi[i].pItf,
RPC_C_AUTHN_WINNT,
RPC_C_AUTHZ_NONE,
NULL,
RPC_C_AUTHN_LEVEL_PKT,
RPC_C_IMP_LEVEL_IMPERSONATE,
&auid,
EOAC_NONE);
if ( FAILED(hr) )
{
pcs->Release();
return hr;
}
}
pcs->Release();
return S_OK;
}
该函数主要分为两个部分:第一部分是调用CoCreateInstanceEx函数在指定计算机上创建组件对象,复杂在于传递的参数,但是只要搞清楚了什么是认证、扮演等COM安全术语,就没有多大的问题了;第二部分是调用IClientSecurity::SetBlancket对接口代理进行有别于进程的安全设置。