上篇写的是客户端的应用,这次谈谈服务端,服务器的安全性。你要写一个基于COM的OPC服务器,一般要先定义执行的接口见下(摘自OpcDa30Server.idl),
[
uuid(ED0E3ADE-0CF5-47cd-A1AB-522E5E75D5CD),
helpstring("OPC Data Access 3.00 Source Server")
]
coclass OpcDa30Server
{
[default] interface IOPCServer;
[source] interface IOPCShutdown;
};
如果你要写一个基于.NET的OPC服务器,一般会采用OPC基金会的1.06版SDK,最泛化的是如下定义(摘自Opc.IServer.cs),
public interface IServer : IDisposable
{
event ServerShutdownEventHandler ServerShutdown;
string GetLocale();
string SetLocale(string locale);
string[] GetSupportedLocales();
string GetErrorText(string locale, ResultID resultID);
}
进一步的具体执行的服务器定义如下(摘自Opc.Da.IServer.cs),
public interface IServer : Opc.IServer
{
int GetResultFilters();
void SetResultFilters(int filters);
ServerStatus GetStatus();
ItemValueResult[] Read(Item[] items);
IdentifiedResult[] Write(ItemValue[] values);
ISubscription CreateSubscription(SubscriptionState state);
void CancelSubscription(ISubscription subscription);
BrowseElement[] Browse(ItemIdentifier itemID, BrowseFilters filters, out BrowsePosition position);
BrowseElement[] BrowseNext(ref BrowsePosition position);
ItemPropertyCollection[] GetProperties(ItemIdentifier[] itemIDs,PropertyID[] propertyIDs, bool returnValues);
}
看到这里,看官脑海里可能会问这样定义有什么错吗?哪里不安全呢?
答案是depends。对于一个布置在微软系统下的OPC服务器及使用微软的网络,使用的是微软的域名账号(如corp\user)。如果你的域名帐户没有被攻陷,OPC服务器是安全的,如果攻陷了就哈哈了。大家还记得2015年发生的蜻蜓工控攻击事件么?它就是采用水坑攻击获得了在工控网里使用的域名账号和密码,进而在微软网络下通过遍历找到了OPC服务器,调用OPC的接口获得了相应点的信息,进而可以随意读写。这样的OPC服务器安全吗?当然不是。有没有正解?
其实OPC基金会给出了关于安全方面的接口,但是大家都不执行,觉得已经在使用了微软的网络被攻破的风险很小。相关的COM接口如下(摘自OpcSec.idl),
[
object,
uuid(7AA83A02-6C77-11d3-84F9-00008630A38B),
pointer_default(unique)
]
interface IOPCSecurityPrivate : IUnknown
{
HRESULT IsAvailablePriv([out] BOOL* pbAvailable);
HRESULT Logon([in, string] LPCWSTR szUserID, [in, string] LPCWSTR szPassword);
HRESULT Logoff(void);
};
在C#里的相应接口(摘自Security.cs),
[ComImport]
[GuidAttribute("7AA83A02-6C77-11d3-84F9-00008630A38B")]
[InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
public interface IOPCSecurityPrivate
{
void IsAvailablePriv([Out][MarshalAs(UnmanagedType.I4)] out int Available);
void Logon([MarshalAs(UnmanagedType.LPWStr)] string szUserID,
[MarshalAs(UnmanagedType.LPWStr)] string szPassword);
void Logoff();
};
可见OPC基金会预见了可能的安全问题,给出了自己的答案。问题是——你的OPC服务器端执行了吗?显而易见,包括OPC自己的样本服务端程序都没有执行安全接口,更遑论那些以SDK为基础的二次开发商了。
那怎样补救呢?在以上的COM定义的类里加上IOPCSecurityPrivate,
[
uuid(ED0E3ADE-0CF5-47cd-A1AB-522E5E75D5CD),
helpstring("OPC Data Access 3.00 Source Server")
]
coclass OpcDa30Server
{
[default] interface IOPCServer;
[source] interface IOPCShutdown;
interface IOPCSecurityPrivate;
};
当执行IOPCSecurityPrivate下的Logon时基金会给出的样本如下(摘自COpcCommon.cpp)。当然你可以把其中的m_cUserName换成窗口的安全识别(如定义一个DWORD dwUserSecurityID,然后调用窗口相应的身份API来获得返回的dwUserSecurityID)
HRESULT COpcCommon::Logon(LPCWSTR szUserID, LPCWSTR szPassword)
{
if (szUserID == NULL || szUserID[0] == 0)
{
m_cUserName.Empty();
return S_OK;
}
if (szPassword == NULL || szPassword[0] == 0)
{
return E_FAIL;
}
m_cUserName = szUserID;
return S_OK;
}
这样当我们执行IOPCServer下的AddGroup()函数时加上对身份的检测(摘自COpcDaServer.cpp),如不符立即返回如下,
HRESULT COpcDaServer::AddGroup(LPCWSTR szName, BOOL bActive,DWORD dwRequestedUpdateRate, OPCHANDLE hClientGroup, LONG* pTimeBias, FLOAT* pPercentDeadband, DWORD dwLCID, OPCHANDLE* phServerGroup, DWORD* pRevisedUpdateRate, REFIID riid, LPUNKNOWN* ppUnk)
{
if (m_cUserName.IsEmpty())
return;
COpcLock cLock(*this);
COpcDaGroup* pGroup = NULL;
类似地在C#里(摘自Opc.Da.Server.cs),加上OpcRcw.Security.IOPCSecurityPrivate和它的接口函数,然后当其它的接口比如AddGroup()被调用时可以做类似的如上处理(如定义一个类似m_cUserName或dwUserSecurityID的变量然后检查它的值,如空立即返回)
public class Server : Opc.Da.IServer, OpcRcw.Security.IOPCSecurityPrivate
{
#region OpcRcw.Security.IOPCSecurityPrivate
public void IsAvailablePriv([MarshalAs(UnmanagedType.I4), Out] out int pbAvailable) {
/* 执行自己的逻辑看 m_cUserName 或者 dwUserSecurityID 是否还有效*/
}
public void Logon([MarshalAs(UnmanagedType.LPWStr)] string szUserID, [MarshalAs(UnmanagedType.LPWStr)] string szPassword) {
/* 执行自己的逻辑来获得 m_cUserName 或者 dwUserSecurityID */
}
public void Logoff() {
/* 执行自己的逻辑清除 m_cUserName 或者 dwUserSecurityID*/
}
#endregion
总结一下,如果用户使用的是微软域下的账号,为了在用户账号沦陷下的情况下保证OPC服务器的安全,需要创立独立的供OPC服务器登录使用的账号,而且此账号不作他用。在向OPC服务器发送请求时客户端使用的第一个OPC接口应该是Logon(),然后才是搜寻OPC服务器及后面相应的接口调用。除了如上的OPC服务器本身的安全特性,在配置方面如DCOM上也可以做些安全方面的硬化,在此不再赘述。有些看客会问,我已经有了第三方的OPC服务器运行而且没有使用Logon()接口,这种情况怎么办?这种情况比较复杂,如果大家感兴趣的话以后再写一篇关于OPC包深度解析及在工业防火墙下应用的博文,请留言哦!
本人的下一篇在此