Dissect ActiveX Control Safety
1、介绍
如果你曾经在网页或者ASP中使用过com对象,你可能会发现,有时候会出现这样讨厌的对话框

这是因为你的控件没有被标记为安全的,对于初始化不安全或者对于脚本不安全,甚至兼而有之。你每打开一次这样的网页,这种情况就会发生一次,你怎么办?当然,这可以通过设置IE本身的安全等级为low来解决这样的问题,但是如果你要制作一个可发布的控件,你能想象到每一位用户在使用你制作的控件时都要且列抱怨这种强制行为;或者如果你是其中一个使用者,当你同样遇到这种情况之后不得不将自己的IE安全等级设置为low,与此同时你平时上网过程中那些行为不轨的控件有时也会悄然而至,你的灾难来了!作为一个聪明的程序员,你不能要求任何用户做一些不切实际或者不够安全的改变来适应你的产品,因为你知道那只会使你的控件被逐渐打入冷宫,到最后销声匿迹,那不是你想要的。我们要消灭掉这样的对话框而且还不让用户发现丝毫安全上和使用上的失望。本文就是横向探讨在C++编程环境下如何消除这些问题的。
2、原理
ActiveX控件是一种极其危险的提供功能的方法(目前正在被MS逐渐冷落),因为它是一种组建对象模型(COM)的对象,只要电脑的用户可以完成的任务,它都可以完成。比如它可以存取注册表,可以随意访问本地文件系统等等。一个网页上面的控件一般有2种不安全的状态,一种是脚本的不安全,一种是初始化的不安全。当用户将一个压缩解压缩控件指向一个远程被压缩的包含特洛伊木马的系统文件并且需要控件来解压缩这个文件时,系统安全会被打破。这个状态就是初始化的不安全。从代码的角度来讲,如果控件从IPersist派生,也就是说控件实现了永久性,那么就会触发unsafe for initializing。而在脚本程序安全执行以前,一个控件依赖于特定的系统设置,那么在允许这段代码运行之前,控件的开发人员需要提供一些必要的代码。从代码的角度来讲,如果控件从IDispatch派生,也就是说控件支持脚本,那么就会触发unsafe for scripting。
从用户下载一个ActiveX控件开始,这个控件甚至可能很容易被攻击,因为网络上任何网络程序都可以使用它,无论是出于友好的目的还是恶意的目的。因此IE浏览器(本文只探讨IE内核的浏览器)总是试图弹出一个对话框来告诉你,这个控件可能是不安全的。这几乎总是一个很好的预防网络攻击的好方法,但是对于那些我们认为总是安全的控件,我们仍然要总是接受这种IE产生的干扰,这就使人厌烦了。其实当身为程序员的你写这样的安全控件时,这样的问题是很容易解决的。
3、解决方法
目前,对这个问题的解决方法主要有几种:使用数字签名,继承IObjectSafety接口,修改注册表等。
3.1、使用数字签名
数字签名是一种使控件足够安全的方法,它通过特定的密钥来加密控件的使用,使得使用控件的对象能够根据密钥是否相符来检测控件是否足够安全。通常,拥有自己的可发布的数字签名是要Money的,本文是一篇技术文章,对此并不深入探讨,下面主要介绍代码方面的安全化。
3.2、继承IObjectSafety接口
IObjectSafety接口是在头文件"objsafe.h"中定义的接口,定义如下:
IObjectSafety : public IUnknown 

{
public:
virtual HRESULT STDMETHODCALLTYPE GetInterfaceSafetyOptions( 
/**//* [in] */ REFIID riid, 
/**//* [out] */ DWORD *pdwSupportedOptions, 
/**//* [out] */ DWORD *pdwEnabledOptions) = 0;
virtual HRESULT STDMETHODCALLTYPE SetInterfaceSafetyOptions( 
/**//* [in] */ REFIID riid, 
/**//* [in] */ DWORD dwOptionSetMask, 
/**//* [in] */ DWORD dwEnabledOptions) = 0;
}; 
其中参数意义如下:
//riid Interface identifier for the object to be made safe.
//
//dwOptionSetMask Options to be changed.
//
//dwEnabledOptions Settings for the options that are to be changed. This can be one of the following values.
// INTERFACESAFE_FOR_UNTRUSTED_CALLER
// Indicates the interface identified by riid should be made safe for scripting.
// INTERFACESAFE_FOR_UNTRUSTED_DATA
// Indicates the interface identified by riid should be made safe for untrusted data during initialization. 
这个接口允许容器询问控件是否安全或者改变控件的安全属性。目前IObjectSafety支持四种安全属性,但是一般我们只使用前两个:脚本安全和初始化安全。这些属性定义如下:
// Option bit definitions for IObjectSafety:
#define INTERFACESAFE_FOR_UNTRUSTED_CALLER 0x00000001 // Caller of interface may be untrusted
#define INTERFACESAFE_FOR_UNTRUSTED_DATA 0x00000002 // Data passed into interface may be untrusted
#define INTERFACE_USES_DISPEX 0x00000004 // Object knows to use IDispatchEx
#define INTERFACE_USES_SECURITY_MANAGER 0x00000008 // Object knows to use IInternetHostSecurityManager
3.2.1、实现方法
首先包含头文件
#include "objsafe.h"
然后是自己的控件类继承IObjectSafetyImpl
class YourClass :
public IObjectSafetyImpl<YourClass, INTERFACESAFE_FOR_UNTRUSTED_CALLER | INTERFACESAFE_FOR_UNTRUSTED_DATA> 

{ 
其中INTERFACESAFE_FOR_UNTRUSTED_CALLER | INTERFACESAFE_FOR_UNTRUSTED_DATA 代表默认这个控件是脚本安全而且初始化安全的。然后在com接口表中添加接口名
BEGIN_COM_MAP(CCGrid)
COM_INTERFACE_ENTRY(IObjectSafety)
END_COM_MAP() 
OK,大功告成。这也是一种很简单的方法。
3.3、修改注册表
修改注册表项使控件支持安全类别的原理,归根到底其实就是下面的第三个方法,另外两个方法都是对这个方法的外围包装和更加安全的处理。修改注册表在不同的工程中有着不同的表现:
3.3.1、用于ATL属性工程的com接口
因为VC的属性工程已经加入了安全控件的注册表支持,并且直接加入了关键字implements_category,因此我们仅仅象下面这样既可完成安全属性设置:
[
coclass/progid/vi_prgid, //必须至少有其中一个
implements_category("CATID_SafeForScripting"),
implements_category("CATID_SafeForInitializing"),
] 
不过要注意的是,对于类似下面的代码(往往是非属性工程的)是不能添加的:
[
uuid(
),
helpstring("")
]
coclass YourCtrl 

{
[default] interface
,
[default,source] dispinterface
} 
因为这种coclass是IDL的属性,而IDL本身并没有安全功能。
而上面的那种coclass是VC的属性,MS提供了对安全控件的支持。
3.3.2、主要用于非属性工程的通用方法
在类声明文件中加入头文件
#include "objsafe.h"
在类声明中添加如下代码:
BEGIN_CATEGORY_MAP( YourCtrlClassName )
IMPLEMENTED_CATEGORY( CATID_SafeForScripting )
IMPLEMENTED_CATEGORY( CATID_SafeForInitializing )
END_CATEGORY_MAP()
即可使其支持safety属性,当然你也可以选择性的只支持一种安全属性。
3.3.3、主要用于MFC ActiveX Control的通用方法
在$project.cpp文件中添加以下代码:
#include "comcat.h"
#include "Objsafe.h"
// 控件的CLSID,注册表用(一定要是实际使用的控件的) 
const GUID CDECL CLSID_SafeItem =
{ 0x7AE7497B, 0xCAD8, 0x4E66,
{ 0xA5,0x8B,0xDD,0xE9,0xBC,0xAF,0x6B,0x61 } };
// 版本控制
const WORD _wVerMajor = 1;
// 次版本号
const WORD _wVerMinor = 0;

/**///////////////////////////////////////////////////////////////////////
// 创建组件种类
HRESULT CreateComponentCategory(CATID catid, WCHAR* catDescription) 

{
ICatRegister* pcr = NULL ;
HRESULT hr = S_OK ;
hr = CoCreateInstance(CLSID_StdComponentCategoriesMgr, NULL, CLSCTX_INPROC_SERVER, IID_ICatRegister, (void**)&pcr);
if (FAILED(hr))
return hr;
// Make sure the HKCR\\Component Categories\\{..catid
}
// key is registered.
CATEGORYINFO catinfo;
catinfo.catid = catid;
catinfo.lcid = 0x0409 ; // english
// Make sure the provided description is not too long.
// Only copy the first 127 characters if it is.
int len = wcslen(catDescription);
if (len>127)
len = 127;
wcsncpy(catinfo.szDescription, catDescription, len);
// Make sure the description is null terminated.
catinfo.szDescription[len] = ‘\\0‘;
hr = pcr->RegisterCategories(1, &catinfo);
pcr->Release();
return hr;
}
// 注册组件种类
HRESULT RegisterCLSIDInCategory(REFCLSID clsid, CATID catid) 

{
// Register your component categories information.
ICatRegister* pcr = NULL ;
HRESULT hr = S_OK ;
hr = CoCreateInstance(CLSID_StdComponentCategoriesMgr, NULL, CLSCTX_INPROC_SERVER, IID_ICatRegister, (void**)&pcr);
if (SUCCEEDED(hr)) 
{
// Register this category as being implemented by the class.
CATID rgcatid[1] ;
rgcatid[0] = catid;
hr = pcr->RegisterClassImplCategories(clsid, 1, rgcatid);
}
if (pcr != NULL)
pcr->Release();
return hr;
}
// 卸载组件种类
HRESULT UnRegisterCLSIDInCategory(REFCLSID clsid, CATID catid) 

{
ICatRegister* pcr = NULL ;
HRESULT hr = S_OK ;
hr = CoCreateInstance(CLSID_StdComponentCategoriesMgr, NULL, CLSCTX_INPROC_SERVER, IID_ICatRegister, (void**)&pcr);
if (SUCCEEDED(hr)) 
{
// Unregister this category as being implemented by the class.