OPC UA .NET Standard Stack 资源耗尽漏洞分析-05-26

VSole2023-05-30 09:12:14

漏洞概述

OPC UA .NET Standard Stack是OPC Foundation(OPC基金会)官方维护的OPC UA协议栈的参考实现。该协议栈以.NET语言开发,包含了可移植的OPC UA协议栈和核心库(包含客户端、服务端、配置、复杂类型支持库等),服务端和客户端的参考实现以及客户端和服务端的X.509证书认证等实现。

OPC UA协议是工业控制领域中的一种十分流行的通讯协议。启明星辰ADLab研究员在工控漏洞情报跟踪中发现了OPC UA .NET Standard Reference Server中存在内存资源耗尽漏洞(编号为CVE-2023-27321),并对该漏洞进行了深入分析和验证。

漏洞分析

该漏洞存在于OPC UA .NET Standard Server Stack代码库中。根据官方漏洞公告,远程攻击者可通过发送恶意的请求来耗尽服务器所有可用内存。

图 1、OPC Foundation Security Bulletin关于CVE-2023-27321概述【1】

由官方漏洞公告的描述可以看出,该漏洞存在于OPC UA .NET Standard Reference Server对OPC UA Client请求的处理代码中。OPC UA .NET Standard处理客户端请求的关键代码类位于协议栈代码Stack\Opc.Ua.Core\Stack\Tcp目录下。如下图所示:

图 2、OPC UA .NET Standard处理TCP连接核心代码文件

其中创建OPC UA Server Service的核心入口位于TCPServiceHost类函数CreateServiceHost中。

该函数首先通过Create函数创建一个TCPTransportListener,随后调用ServerBase类函数CreateServiceHostEndpoint启动该监听器:

图 3、TcpServiceHost类成员函数CreateServiceHost调用Create方法创建Tcp监听器

图 4、ServerBase类函数CreateServiceHostEndpoint调用Open方法启动Tcp监听器

CreateServiceHostEndpoint函数调用TCPTransportListener的Open方法启动监听器。在Open函数中,首先通过ChannelQuotas类对OPC UA连接的通道参数进行了一些列的配置,例如MaxBufferSize、MaxMessageSize等。并且实例化了一个用于管理Socket buffer的BufferManager类。然后调用Start函数运行Listener。

图 5、TcpTransportListener类Open函数主要代码

在Start函数中,完成Server Socket的创建工作,同时指定了OnAccept函数来处理OPC UA Client连接:

图 6、TcpServerListener类函数Start创建Server socket

OnAccept函数则建立了一个TcpServerChannel来管理客户端连接。同时设置该channel的各种消息处理回调函数。并调用Attach函数将该TcpServerChannel与Client socket进行关联。

图 7、TcpTransportListener类OnAccept函数创建TcpServerChannel关联Client socket

随后在Attach函数中创建了TcpMessageSocket实例来处理客户端请求数据,使用TcpMessageSocket类的ReadNextMessage方法来处理客户端请求数据。

图 8、TcpServerChannel类函数Attach创建TcpMessageSocket实例处理客户端请求数据

以上便是OPC UA .NET Standard创建Server,接受Client连接和处理Client请求的过程。下面我们着重分析ReadNextMessage()函数,该函数负责处理客户端的请求数据。该函数的实现代码如下所示:

图 9、ReadNextMessage函数代码

该函数是一个双重循环,第一重循环首先通过BufferManager申请一块内存,然后设置m_bytesToReceive为TcpMessageLimits.MessageTypeAndSize(值为8)大小,然后调用ReadNextBlock读取Message数据。

在ReadNextBlock函数中,使用ReceiveAsync函数异步方式接受客户端的请求数据,在ReceiveAsync的接收回调函数OnReadComplete中,通过调用DoReadComplete函数,完整读取一个Message消息的所有内容。然后通过ReadNext函数重新进入ReadNextMessage循环,不断的处理客户端的请求消息。

图 10、ReadNextBlock调用ReceiveAsync异步接收客户端数据

图 11、OnReadComplete通过readState状态确定客户端一个Message是否读取完毕

DoReadComplete函数中首先确保读取到了8字节的Message头部,其中包含了4字节的MessageSize,然后通过该MessageSize大小读取剩余Message消息数据。如果完成一个Message的读取,再通过触发OnMessageReceived函数来处理消息的内容。

图 12、DoReadComplete读取Messsage消息过程

OnMessageReceive函数最终是通过HandleInComingMessage来具体处客户端请求消息内容。在HandleInComingMessage函数中通过判断MessageType来确定不同的Message处理函数(包括ProcessRequestMessage、ProcessHelloMessage、ProcessOpenSecureChannelRequest等)。

图 13、HandleInComingMessage函数处理流程

在消息的具体处理函数ProcessXXX中解析和处理完消息数据后,便会释放存储消息的内存。如下是ProcessRequestMessage函数中释放Message内存的代码:

图 14、ProcessRequestMessage函数释放内存代码

在Server端收到客户端消息时,会根据创建TcpServerChannel时设置的回调函数(参见图7),对客户端进行回复。例如处理客户端request的回调函数是TcpTransportListener类中的OnRequestReceived函数。该函数设置了消息处理完毕的回调函数OnProcessRequestComplete函数,并在此函数中调用SendReponse对客户端消息进行回复。

图 15、客户端Request消息处理的回调函数设置

SendResponse函数则通过WriteSymmetricMessage函数生成Response消息并调用BeginWriteMessage发送给OPC UA Client。

图 16、SendResponse函数关键代码

这里WriteSymmetricMessage函数中依然会通过BufferManager来申请一块内存来存储回复消息数据。申请的内存大小为SendBufferSize。

图 17、WriteSymmetricMessage内存分配关键代码

从上述OPC UA .NET Standard Server处理Client请求的过程可以看出,无论是在接收Client Message消息阶段还是发送Response消息阶段,Server都会申请一块内存来临时存储数据。对于接收阶段,在ReadNextMessage函数中为每个Message申请的内存的大小为m_receiveBufferSize,该变量在TcpServerChannel类中初始化TcpMessageSocket时指定为Quotas.MaxBufferSize(参见图8)。而Quotas.MaxBufferSize是在TcpTransportListener类的Open函数中赋值的(参见图5),其最终来源为ServerBase类函数CreateServiceHostEndpoint中的参数ApplicationConfiguration。对于OPC UA Reference Server应用来说,就是其配置文件(Quickstarts.ReferenceServer.Config.xml)的MaxBufferSize参数,默认值为65535。

图 18、Reference Server配置文件中关于TransportQuotas的参数配置

同样回溯WriteSymmetricMessage函数中SendBufferSize的赋值过程发现,其初始大小也是MaxBufferSize。

图 19、UaSCBinaryChannel类构造函数中对sendBufferSize的初始化

也就是说,对于客户端发送的每个TCP请求,OPC UA Reference Server都会通过BufferManager申请64K的内存来存放Request数据,然后处理完毕后再申请64KB的内存来存放Response数据。

根据BufferManager的实现可知,该类是通过ArrayPool来进行动态内存管理的。

图 20、BufferManager构造函数代码

ArrayPool是.NET框架中的一个类,用于管理和重用数组内存缓冲区。它旨在帮助减少在高性能应用程序中频繁分配和释放大量相同大小的数组时产生的垃圾回收压力。在传统的.NET内存管理中,每次使用new关键字创建数组时,都会在堆上分配一块内存。当这些数组不再使用时,垃圾回收器会负责回收这些内存。这种频繁的内存分配和垃圾回收操作可能会对性能产生负面影响。ArrayPool通过维护一个内部的缓冲区池来解决这个问题。当需要分配一个数组时,可以从池中获取一个可用的数组,而不是每次都分配新的内存。使用完毕后,可以将数组返回到池中以供重用,而不是立即释放内存。

ArrayPool的使用虽然提高了系统进行内存分配和释放的性能,但是对ArrayPool不加限制的不当使用,会导致系统资源被耗尽。

漏洞复现

复现环境

lOPC UA Vulnerable Server

OPC UA .NET Standard Reference Server(Version: UA-.NETStandard-1.4.371.60)

复现过程

首先按照默认配置编译OPC UA .NET Standard Reference Server。然后启动该OPC UA Server。

图 21、OPC UA .NET Standard Reference Server启动界面

运行PoC脚本,脚本中OPC Client连接Reference Server之后将发送大量请求,最终可消耗Reference Server所在主机的所有可用内存。如下图所示:

图 22、OPC UA .NET Standard Reference Server消耗大量内存

下图展示了在调试环境中通过插桩内存分析代码得到的ArrayPool内存占用情况和程序实际内存占用的情况。

补丁分析

根据OPC UA的官方漏洞公告,该漏洞在OPC UA .NET Standard 1.4.371.86版本【2】中修复。通过对该版本代码的分析,我们发现实际上该漏洞在Github库UA-.NET Standard的Commit 67fd91ca993c01c38712a61f0342dfdf3c02f4c5中【3】已被修复。

主要修复的方式如下:

1.限制了服务端RequestQueue队列的大小。

图 23、漏洞补丁(部分)-限制Server端RequestQueue大小

2.增加了判断Client和Server通信的通道是否已满的函数ChannelFull,该函数限制了在一个Channel会话中能保持活跃的最大WriteRequest数量为100。当客户端不再从Server读取数据后,关闭当前Client连接的channel。

图 24、漏洞补丁(部分)-判断Server端Channel WriteRequest数量

上述修复方式的核心是:限制OPC UA Server所能占用的托管内存大小,避免在ArrayPool中分配过多的内存资源。

安全建议

鉴于该漏洞无需认证便可从网络侧发动针对OPC UA服务器的拒绝服务攻击,我们建议使用了OPC UA .NET Standard代码的相关OPC UA产品及时更新OPC UA .NET Standard代码到版本1.4.371.86, 或者将引用的代码版本更新到修复了该漏洞的commit版本。

函数调用opc
本作品采用《CC 协议》,转载必须注明作者和本文链接
OPC UA协议是工业控制领域中的一种十分流行的通讯协议。漏洞分析该漏洞存在于OPC UA .NET Standard Server Stack代码库中。根据官方漏洞公告,远程攻击者可通过发送恶意的请求来耗尽服务器所有可用内存。同时设置该channel的各种消息处理回调函数。并调用Attach函数将该TcpServerChannel与Client socket进行关联。图 12、DoReadComplete读取Messsage消息过程OnMessageReceive函数最终是通过HandleInComingMessage来具体处客户端请求消息内容。
1 赛题回顾 2 最终排名(部分) 3 启发与思路 4 算法与模型 函数名(CG图) 复赛模型融合 Section信息 字符匹配 Yara匹配 Opcode 4. 其他布尔信息 灰度图 直方图 PE静态特征模型 特征工程 5 结果与改进 复...
莫过于去花之后替换进apk中,依然正常运行,这对汇编功底无疑是一种挑战。今天就献丑拿某流量第一的APK样本做一下IDA脚本一键去花指令分析。◆上IDA脚本,此代码为片段代码ARM64是ARM架构的64位版本。ARM64的调用约定定义了函数如何传递参数和返回结果。前八个浮点型参数通过寄存器V0至V7传递。这意味着如果函数修改了这些寄存器的值,那么它需要在返回前恢复它们的原始值。
静态分析法是在不执行代码文件的情形下,对代码进行静态分析的一种方法。静态分析时并不执行代码,而是观察代码文件的外部特征,获取文件的类型(EXE、DLI、DOC、ZIP等)、大小、PE头信息、Import/ExportAPI内部字符串、是否运行时解压缩、注册信息、调试信息、数字证书等多种信息。
MTCTF-2022 部分WriteUp
2022-11-23 09:35:37
MTCTF 本次比赛主力输出选手Article&Messa&Oolongcode,累计解题3Web,2Pwn,1Re,1CryptoWeb★easypickle题目给出源码:。import base64import picklefrom flask import Flask, sessionimport osimport random. @app.route('/')def hello_world(): if not session.get: session['user'] = ''.join return 'Hello {}!\x93作用同c,但是将从stack中出栈两元素分别导入的模块名和属性名:此外对于蓝帽杯WP还存在一个小问题,原题采用_loads函数加载pickle数据但本题是loads,在opcodes处理上会有些微不通具体来说就是用loads加载时会报错误如下:对着把传入参数换成元组就行,最终的payload如下
2008年在安全社区中所知道的windows恶意可执行软件大约有1000多万个,2013年这个数字达到了1亿,2020年安全社区已知的windows恶意可执行软件数量已经超过5亿[1],这个数字还在持续增长。
VMPWN的入门系列-2
2023-08-03 09:29:42
解释器是一种计算机程序,用于解释和执行源代码。与编译器不同,解释器不会将源代码转换为机器语言,而是直接执行源代码。即,这个程序接收一定的解释器语言,然后按照一定的规则对其进行解析,完成相应的功能,从本质上来看依然是一个虚拟机。总的来说,如果输入字符数小于0x10,string类的大概成员应该如下struct?
假如想在x86平台运行arm程序,称arm为source ISA, 而x86为target ISA, 在虚拟化的角度来说arm就是Guest, x86为Host。这种问题被称为Code-Discovery Problem。每个体系结构对应的helper函数在target/xxx/helper.h头文件中定义。
虚拟机检测技术整理
2023-05-11 09:15:35
第一次尝试恶意代码分析就遇到了虚拟机检测,于是就想着先学习一下检测的技术然后再尝试绕过。学习后最终发现,似乎最好的方法不应该是去patch所有检测方法,而是直接调试并定位检测函数再绕过。但既然已经研究了两天,索性将收集到的资料整理一下,方便后人查找。恶意软件可以搜索这些文件、目录或进程的存在。VMware 虚拟机中可能会有如下的文件列表:C:\Program Files\VMware\
Webshell 检测综述
2022-12-15 09:45:32
通过Webshell,攻击者可以在目标服务器上执行一些命令从而完成信息嗅探、数据窃取或篡改等非法操作,对Web服务器造成巨大危害。Webshell恶意软件是一种长期存在的普遍威胁,能够绕过很多安全工具的检测。许多研究人员在Webshell检测领域进行了深入研究,并提出了一些卓有成效的方法。本文以PHP Webshell为例。
VSole
网络安全专家