漏洞概述 标题 Client alarm_conditions out-of-bounds read via PublishResponse EventFieldList count mismatch #18 摘要 发现了一个在S2OPC 1.7.3的A&C事件管理包装器路径中的客户端侧越界读取漏洞。恶意OPC UA服务器或中间人可以通过篡改服务器到客户端的PublishResponse流量,增加第一个真实事件的EventFieldList.NotOfEventFields值,并附加一个额外的Null Variant。S2OPC客户端警报包装器随后将远程字段计数用作循环边界,并读取超出本地alarm_selectClauses指针数组的内存。越界值随后被用作字符串路径,并通过事件管理器字典查找路径,导致可重现的客户端崩溃。 版本 受影响版本:1.7.3 观察到的源代码提交:b4c5c7d3c696984615f14d905a7c92b3c377c4 安全影响 控制OPC UA服务器的远程攻击者,或修改有效PublishResponse的中间人,可以在客户端使用官方alarmEvent包装器API时导致S2OPC客户端进程崩溃。当前证据支持可靠的远程DoS。演示的原语包括: 远程控制的循环边界 本地指针数组越界读取 无效指针作为字符串路径重用 下游解引用时崩溃 完整ASan 点击此处查看ASan 受影响路径 主要脆弱逻辑 src/ClientServer/frontend/client_wrapper/alarm_conditions/libs2opc_client_alarm_conditions.c:361 src/ClientServer/frontend/client_wrapper/alarm_conditions/libs2opc_client_alarm_conditions.c:395 src/ClientServer/frontend/client_wrapper/alarm_conditions/libs2opc_client_alarm_conditions.c:396 下游崩溃路径 src/ClientServer/address_space/sopc_event_manager.c:667 src/ClientServer/address_space/sopc_event_manager.c:1053 回调链 src/ClientServer/frontend/client_wrapper/internal/state_machine.c:1867 src/ClientServer/frontend/client_wrapper/internal/state_machine.c:2694 src/ClientServer/frontend/client_wrapper/internal/state_machine.c:1602 样本入口点 samples/ClientServer/client_wrapper/examples/alarm_group_min.c:138 samples/ClientServer/client_wrapper/examples/alarm_group_min.c:149 根本原因 简短形式 该漏洞由信任边界违规引起: 1. 存储在MGroup->alarm_selectClauses中的选择子句路径的本地数量 2. 接收到的PublishResponse中EventNotificationList的remote event->NotOfEventFields值 客户端在创建监控项时构建固定本地指针数组,但后来使用服务器提供的事件字段计数作为循环边界来解码事件字段。代码没有验证event->NotOfEventFields是否保持在alarm_selectClauses_len内。 详细解释 在警报监控项创建期间,包装器在本地构建事件过滤器,并根据本地警报事件模型中的变量数量分配select子句指针数组。相关逻辑如下: 1. 获取本地警报事件实例 2. 计算具有SOPC_Event_GetNbVariables[alarmEvent]的变量数量 3. 分配MGroup->alarm_selectClauses 4. 通过SOPC_Event_ForEachVar(...)填充 5. 将结果长度存储在MGroup->alarm_selectClauses_len 因此,本地数组长度在创建监控项时由客户端模型固定。 后来,当处理发布通知时,包装器回调接收Opcua_EventNotificationList并迭代事件。对于每个事件,它使用远程计数迭代字段: 字段0被特殊处理为ConditionId。对于所有其他字段,字段映射回本地字符串路径使用: 然而,没有边界检查确保: 一旦服务器膨胀NotOfEventFields超出已知选择子句数量加上特殊ConditionId槽,客户端就会读取超出本地指针数组。 漏洞变得外部可见,因为越界读取的值不仅被读取和丢弃。它作为字符串路径传递给事件管理器路径解析代码: 在这一点上,从越界读取获得的垃圾指针被解引用为C字符串,导致崩溃。 观察到的调试器值 在GDB中,在越界读取处捕获了以下值: 这是关键的不匹配: NotOfEventFields = 48 alarm_selectClauses_len = 46 循环索引 i = 47 使用的数组索引是 i - 1 = 46 有效索引仅为 0..45 因此,客户端即将读取 alarm_selectClauses[46],这是一个真正的越界访问。 POC PoC.zip 复现 构建 运行官方警报演示服务器 预期启动输出: 运行最小化变异一个真实PublishResponse的代理 观察到的代理行为: 握手和会话创建正常传递 订阅和监控项创建成功 ConditionRefresh正常发出 代理等待第一个包含事件数据的PublishResponse 然后它增加第一个事件的字段计数并附加一个额外的Null Variant 代表性代理输出: 运行官方客户端包装器样本 观察结果 结果分析 这表明: 1. 官方服务器正常启动 2. 客户端完成正常的包装器工作流 3. 代理仅变异一个携带事件数据的PublishResponse 4. 崩溃发生在包装器的事件通知处理路径中 5. 失败的访问是读取 6. 崩溃的下游路径与垃圾指针被用作字符串键一致 修复建议 修复策略 通知路径不应盲目信任event->NotOfEventFields。鲁棒的修复应同时执行以下操作: 1. 如果event->NotOfEventFields超过本地已知字段预算,则拒绝事件 2. 在访问alarm_selectClauses[iField - 1]之前重新检查数组索引 包装器的有效不变量为: 其中字段0是特殊的ConditionId槽。 建议补丁 以下是针对SOPC_MonitoredAlarm_TriggerSubscriptionNotification()的最小防御性补丁: 结论 这是S2OPC 1.7.3中alarmEvent包装器的一个真正的客户端侧内存安全漏洞。该问题由在映射远程提供的无效事件字段计数到本地分配的select子句指针数组时缺少边界检查引起。结果是在事件路径查找期间可重现的越界读取。当前演示的安全后果是客户端的远程拒绝服务。