OPC服务器到底安不安全?

本文深入探讨了OPC服务器的安全性问题,分析了基于COM和.NET的OPC服务器接口定义,并提出了通过实施IOPCSecurityPrivate接口增强安全性的方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

上篇写的是客户端的应用,这次谈谈服务端,服务器的安全性。你要写一个基于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()接口,这种情况怎么办?这种情况比较复杂,需要借助工业防火墙的帮助。如果在创建实例前没有侦测到该接口下的函数应该把TCP的连接重置以断开进一步的访问。关于OPC包深度解析及在工业防火墙下应用的博文在此,继续前行进入下一篇。看完请留言哦!

 

### 配置 OPC UA 服务器以启用远程连接和访问 为了确保 OPC UA 服务器能够支持远程访问,需要对服务器进行多方面的配置,包括网络、安全、地址空间以及服务端点等。OPC UA 的优势在于其基于 TCP/IP 的通信机制,无需依赖 DCOM,从而简化了远程连接的配置过程[^1]。 #### 网络配置 确保 OPC UA 服务器所在的主机与客户端处于同一网络环境中,或至少可以通过 IP 地址相互访问。服务器的 IP 地址或主机名应被正确解析,且服务器的防火墙应允许 OPC UA 所需的端口(默认为 TCP 4840)通过。如果服务器运行在非标准端口上,需确保客户端使用相同的端口号进行连接。 #### 服务器名称与端点配置 OPC UA 服务器通常需要配置一个唯一的服务器名称(Server Name),该名称将用于客户端建立连接。例如,在某些 OPC UA 实现中,服务器名称可以是 `opc.tcp://<server-ip>:4840`,其中 `<server-ip>` 是服务器的 IP 地址[^2]。此外,服务器应配置多个端点以支持同的安全策略和加密方式,如无加密、Basic128Rsa15、Aes128Sha256RsaOaep 等。 #### 安全性配置 OPC UA 支持多种安全机制,包括用户身份验证、加密和访问控制。服务器应配置适当的用户权限,确保只有授权用户可以访问数据。例如,可以启用用户名/密码认证或使用基于证书的身份验证机制。此外,建议启用安全策略以保护数据的完整性和机密性。 #### 地址空间配置 OPC UA 服务器的地址空间定义了客户端可以访问的数据节点。在配置地址空间时,应确保所有需要远程访问的数据节点都已正确添加到服务器的地址空间中。例如,某些 OPC UA 服务器允许通过配置文件或图形界面添加设备节点及其属性,如模拟数据(Counter、Random 等)。 #### 服务端点配置 OPC UA 服务器通常会暴露多个端点(Endpoints),每个端点代表一种通信方式和安全策略。客户端应连接到与自身安全配置兼容的端点。例如,若客户端支持加密通信,则应选择支持 Basic128Rsa15 或 Aes128Sha256RsaOaep 加密的端点。 #### 示例代码:使用 Python 连接 OPC UA 服务器 以下是一个使用 Python 的 `opcua` 库连接到远程 OPC UA 服务器并读取数据的示例: ```python from opcua import Client # 创建 OPC UA 客户端并连接到远程服务器 client = Client("opc.tcp://<server-ip>:4840") client.connect() # 获取根节点并浏览地址空间 root = client.get_root_node() print("Root node is: ", root) # 读取特定节点的值 node = client.get_node("ns=2;s=Channel1.Device1.Tag1") value = node.get_value() print("Value of Tag1: ", value) # 断开连接 client.disconnect() ``` #### 示例代码:使用 C# 连接 OPC UA 服务器 以下是一个使用 C# 连接 OPC UA 服务器并读取数据的示例: ```csharp using System; using Opc.Ua.Client; using Opc.Ua; class Program { static void Main() { // 创建 OPC UA 客户端并连接到远程服务器 ApplicationConfiguration config = new ApplicationConfiguration(); config.ApplicationName = "OPC UA Client"; config.SecurityConfiguration = new SecurityConfiguration(); config.CertificateValidator = new CertificateValidator(); config.CertificateValidator.Initialize(); Session session = Session.Create( config, new ConfiguredEndpoint(null, new Uri("opc.tcp://<server-ip>:4840")), false, true, "OPC UA Client", 15000, null, null ).Result; // 读取特定节点的值 NodeId nodeId = new NodeId("Channel1.Device1.Tag1", 2); DataValue value = session.Read(nodeId, 0, TimestampsToReturn.Both); Console.WriteLine("Value of Tag1: " + value.Value); // 断开连接 session.Close(); } } ``` #### 服务端配置注意事项 OPC UA 服务器的配置工具通常提供图形界面,用于设置端点、地址空间、用户权限等。例如,某些 OPC UA 服务器允许通过“属性配置相对路径”来定义节点与设备之间的关系,使得客户端可以通过标准路径访问数据。此外,部分 OPC UA 服务器支持批量修改功能,便于在大批量数据节点的情况下快速调整配置[^4]。 #### 跨平台支持 OPC UA 的一大优势是其平台无关性,支持在多种操作系统上运行,包括 Windows、Linux 和 Unix 等。因此,OPC UA 服务器可以在同平台上部署,客户端也可以使用同平台进行访问,极大地提高了系统的灵活性和兼容性[^1]。 #### 网关与协议转换 在某些工业场景中,OPC UA 可以作为网关,将其他协议(如 PLC 通信协议)转换为 OPC UA 协议,从而实现与 MES、ERP 等系统的无缝对接。例如,通过配置 OPC UA 网关,可以将三菱 PLC 的数据通过 OPC UA 接口提供给上层系统[^3]。
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值