原创 | Ripple20:Treck TCP/IP协议栈漏洞分析与验证(附视频)

发布者:ADLab
发布于:2020-07-06 15:43

一、前言


国外安全研究人员在由Treck开发的TCP/IP协议栈中发现了多个漏洞,这一系列漏洞统称为Ripple20。这些漏洞广泛存在于嵌入式和物联网设备中,影响了多个行业领域(包括医疗、运输、能源、电信、工业控制、零售和商业等),涉及了众多供应商(包括HP、Schneider Electric、Intel、Rockwell Automation、Caterpillar、Baxter等)。


这些漏洞源于Ripple20的多个协议(包括IPv4、ICMPv4、IPv6、IPv6OverIPv4、TCP、UDP、ARP、DHCP、DNS或以太网链路层)在处理网络报文发送时存在缺陷,其中包括四个严重漏洞,它们的CVE编号分别为CVE-2020-11896、CVE-2020-11898、CVE-2020-11910、CVE-2020-11911。CVE-2020-11896(CVSS评分10)可导致远程执行代码,CVE-2020-11897(CVSS评分10)可导致越界写入,CVE-2020-11901(CVSS评分9)可导致远程执行代码,CVE-2020-11898(CVSS评分9.1)可导致泄露敏感信息。其它15个Ripple20漏洞的严重程度各异,CVSS评分分别从3.1到8.2。

由于物联网设备供应链的特性,漏洞影响的设备众多,影响范围广且持续时间长,漏洞修复的实施较困难。因此,启明星辰ADLab第一时间对相关漏洞进行了分析并提出了防范建议。


二、协议栈检测


由于采用Treck协议栈的厂家较多,有些厂家是硬件IP核的方式引用了Treck协议栈。单纯通过设备指纹来识别漏洞是不足的,如何检测目标设备是否为Treck协议栈成为资产排查的关键,为此启明星辰ADLab安全研究员对Treck协议栈进行了深入分析,并公开了Treck协议栈指纹检测方法发现漏洞。


Treck协议栈自定义了类型为165(0xa5)的ICMP包,并一旦收到165的ICMP包会回复类型为166的ICMP包响应。如下代码所示:


首先,向目标发送 ICMP请求包,其中type=0xa5,code=0。如下图所示:

然后,接收目标返回的icmp响应包数据,其中type =0xa6,code =0,ICMP报文第9字节后的六个字节为0x01,0x51,0x35,0x28,0x57,0x32(大端)或0x51,0x01,0x28,0x35,0x32,0x57(小端)。


满足上述的条件,则表明目标设备为treck 协议栈。如下图所示:



三、防范建议


1、应用更新

及时更新到Treck TCP/IP协议栈软件的最新稳定版本(6.0.1.67或更高版本)。


2.  阻止异常IP流量

可以通过深度数据包检查来阻止网络攻击,以下是可以适当应用于网络环境中的可能缓解措施,过滤选项包括:

●       如果网络环境不支持,则规范化或拒绝IP分片的数据包(IP分片)

●       如果不需要,请禁用或阻止IP隧道(IPv6-in-IPv4或IP-in-IP隧道)

●       阻止IP源路由和所有不赞成使用IPv6的功能,例如路由标头

●       强制执行TCP检查并拒绝格式错误的TCP数据包

●       阻止未使用的ICMP控制消息,例如MTU更新和地址掩码更新

●       通过安全的递归服务器或应用层防火墙规范DNS

●       确保网络环境中使用的是可靠的OSI第2层设备(以太网)

●       通过DHCP侦听等功能提供DHCP / DHCPv6安全性

●       如果未在交换基础架构中使用,则禁用或阻止IPv6多播。


四、相关概念介绍


1、IP分片

IP分片使得在网络中发送大的IP包成为可能,即使其大小大于网络特定链路中允许的最大值。IP分片技术是一种将数据包分成几个较小的部分以支持通过这些链路和网络传输的技术。该协议支持在发送端进行分片,然后在接收端对分片重新组合。这允许不同的包在网络中零散地传输,并在另一侧正确地重新组装。

不同的包使用IP头中的标识字段(Identification)进行分组。此标识字段描述分片属于哪个包。同一个包的多个分片的Identification是一样的。IPv4通过Flags及Fragment Offset字段对分片进行管理,Flags由R、DF、MF三部分组成:

●       R(Reserve bit)保留未用

●       DF (Don't Fragment) DF =1:禁止分片 , DF =0:允许分片

●       MF (More Fragment) MF =1:非最后一片, MF =0:最后一片(或未分片)

Fragment Offset(13位):一个IP分组分片封装原IP分组数据的相对偏移量, 片偏移字段以8字节为单位。IP包结构如下图所示:


2、IP隧道技术

IP隧道允许两个独立网络之间的虚拟点到点链路。它是通过将包(可以是IP包)封装在另一个包中来实现的,使得内部包具有与外部包不同的源地址和目标地址。外部包的源地址和目标地址是隧道端点,内部包中的地址用于隧道两端的网络路由。隧道入口点是接收应通过隧道转发的IP数据包的节点。它将此数据包封装在外部IP数据包中。当数据包到达隧道出口点时,它被解封装并转发,就好像它是在目标网络中发送的常规数据包一样。IP-in-IP包如下图所示:

IP隧道技术主要应用在虚拟专用网(VPN)技术中。目前有几种隧道协议,其中最简单和最古老的是IP-in-IP(IP协议编号4)。IP-in-IP是一种IP隧道协议,其中一个IP包通过添加一个外部IP报头(其源地址和目标地址分别等于隧道的入口点和出口点)封装在另一个IP包中。内部数据包未被修改,外部IP头从内部IP头复制一些字段。外部报头的IP协议号为4。IP-in-IP报文示例如下图所示:


五、Treck协议栈


1、协议栈概述


Treck协议栈通过tsPacket结构来描述包结构,通过tsUserPacket结构支持数据包分片。这两个结构体在treck/include/trsocket.h文件中定义。Treck TCP/IP协议栈中的包数据由tsPacket的结构表示。每个包都与一个数据缓冲区相关联,该数据缓冲区保存从接口驱动程序到达的原始数据。tsPacket结构还保存另一个称为ttUserPacket的重要结构,以及指向tsSharedData结构的指针,该结构包括网络协议栈处理数据包时所需的信息(指向套接字结构、src/dst地址或端口等的指针)。定义如下:

struct tsPacket {

ttUserPacket pktUserStruct;

ttSharedDataPtr pktSharedDataPtr;

struct tsPacket * pktChainNextPtr;

struct tsDeviceEntry * pktDeviceEntryPtr;

union anon_union_for_pktPtrUnion pktPtrUnion;

tt32Bit pktTcpXmitTime;

tt16Bit pktUserFlags;

tt16Bit pktFlags;

tt16Bit pktFlags2;

tt16Bit pktMhomeIndex;

tt8Bit pktTunnelCount;

tt8Bit pktIpHdrLen;

tt8Bit pktNetworkLayer;

tt8Bit pktFiller[1];

};

这是包含的ttUserPacket结构(tsUserPacket的typedef),定义如下:

struct tsUserPacket {

void * pktuLinkNextPtr; // Next tsUserPacket for fragmented data

ttUser8BitPtr pktuLinkDataPtr;

ttPktLen pktuLinkDataLength;

ttPktLen pktuChainDataLength;

int pktuLinkExtraCount;

};

pktuLinkNextPtr :用于跟踪数据包中的分片。此字段指向表示下一个分片的另一个tsPacket结构,该tsPacket还保存对下一个分片的引用,如果此链接是最后一个分片,或者数据未被分片,则此字段将为NULL。

pktuLinkDataPtr:指向当前分片的数据缓冲区。当Treck协议栈在不同阶段处理数据包时,数据缓冲区中的确切位置会发生变化,这取决于当前正在处理的数据包所在协议层。例如,当Treck协议栈处理以太网层(在tfEtherRecv()函数中)时,此字段指向以太网报头。

pktuLinkDataLength:pktuLinkDataPtr指向的数据的大小,即单个分片的大小。

pktuChainDataLength:表示包含所有分片的数据包长度,即数据包的总大小。它只为第一个分片设置。如果数据没有分片,则等于pktuLinkDataLength。


2、协议栈处理过程


协议栈中的一个常见模式是在协议栈中的层之间移动时调整pktuLinkDataPtr指针。例如,如果我们的包是一个ICMP回显请求包(ping),它的协议由三层组成:Ethernet、IPv4、ICMP。在这种情况下,当处理以太网层(在tfEtherRecv()函数中)时,pktuLinkDataPtr指向以太网报头的开始,然后在移动到下一层之前,使用以下代码对其进行调整,如下代码所示:


在本例中,0xe(十进制为14)是以太网报头(6(dst MAC)+6(src MAC)+2(etherType))的大小。当tfEtherRecv()函数完成包处理时,它将包转发到下一层处理。支持的以太网类型有ARP、IPv4和IPv6。如下代码所示:

在示例中,当IPv4层接收到数据包(在函数tfIpIncomingPacket()函数中)时,指针pktuLinkDataPtr已经指向IP报头。传入数据由具有相同命名约定tf*IncomingPacket的函数处理,其中*是协议名。对于ICMP包来说,它由三层协议组成(Ethernet/IPv4/ICMP),数据包将由函数tfEtherRecv、tfIpIncomingPacket和tfIcmpIncomingPacket函数分别处理。


3、分片重组

Treck协议栈在tfIpReassemblePacket()函数中处理分片的重组,该函数由tfIpIncomingPacket()调用。每当接收到发往设备的IP分片时,就会调用此函数。如果缺少分片,函数将返回NULL。否则,如果所有分片都到达并且没有空洞,则网络协议栈将使用pktuLinkNextPtr字段将分片链接在一起,然后将数据包传递给下一层进行进一步处理。在此上下文中,“重组”一词并不意味着将数据包复制到连续的存储块,而只是简单地将它们链接到一个链表中。分片数据链表结构如下图所示:


4、tfIpIncomingPacket函数

tfIpIncomingPacket()函数是处理IP包的主要函数,该函数主要流程如下图所示:


tfIpIncomingPacket()首先判断数据包合法性。tfIpIncomingPacket()函数除了验证IP头校验和,它还进行以下验证,如下代码所示:


然后如果所有合法性检查都通过,tfIpIncomingPacket()函数将检查IP报头中TotalLength 是否严格小于数据包的pktuChainDataLength,这表示实际接收的数据比IP报头中声明的数据多。如果是真的,则进行修剪操作,要删除额外的数据,如下代码所示:


再者如果IP数据包的MF为1或者Fragment Offset大于0,则tfIpIncomingPacket()函数就要调用tfIpReassemblePacket()函数进行分片重组。如果IP分片数据接收不完整,则tfIpReassemblePacket()函数返回NULL。如果所有IP分片都到达并且没有错误,则Treck协议栈使用pktuLinkNextPtr字段将这些分片链接在一起,建立链表,并将包传递到下一层进行进一步处理,如下代码所示:


最后如果已经收到完整的IP数据包,则tfIpIncomingPacket()函数根据IP数据包中的协议字段的协议号,调用相应的协议包处理函数进行处理。在下列代码中,当协议号为UDP时,则调用tfUdpIncomingPacket()函数,当数据包协议为IP-in-IP协议(协议号4)时,会递归调用tfIpIncomingPacket()函数,代码实现如下所示:


六、漏洞原理分析


1、CVE-2020-11896


前文已经介绍tfIpIncomingPacket()函数的实现过程,第二步的数据裁剪是漏洞的原因,如下代码所示:

pktuLinkDataLength保留当前分片的大小,pktuChainDataLength保留整个IP数据包的大小。如果执行上述操作,将导致一个不一致性的状态,其中pkt->pktuChainDataLength==pkt->pktuLinkDataLength,但可能有pkt->pktuLinkNextPtr指向其他分片。更进一步的其中链表上分片的总数据大小可能大于存储在pktuChainDataLength变量中的大小。这种操作导致的不一致性将会导致后续报文处理发生异常。


通过简单地设置错误的IP包分片是无法触发漏洞的,因为裁剪过后的分片数据在后续的tfIpReassemblePacket()函数操作中会根据pktuChainDataLength的大小,重新建立分片链表,不会造成不一致的状态。理想的流程是先完成分片链表的建立,再进行链表数据总大小的裁剪流程,这样就会进入不一致的状态。

为了在IP层处理分片数据包并触发执行有问题的流程代码,可以使用IP-in-IP数据包。对于分片的IP-in-IP数据包,tfIpIncomingPacket()函数将至少递归调用两次,一次用于IP隧道包的内层IP数据包,多次用于外层IP数据包(每处理一个外层IP包分片算作一次)。


tfIpIncomingPacket()函数在处理IP隧道数据包的时候将内部IP数据包作为非分片数据包进行处理。内部数据包现在由多个分片组成,但在IP报头中标记为非分片(MF=0),所以它不会再进入tfIpReassemblePacket()函数进行重组。它现在由一个链表中的几个单独的tsPacket链接组成,每个链接都有一个单独的pktuLinkDataLength值。考虑下面的例子,它将有助于理解漏洞的成因:

●       Inner IP packet: IPv4{len=32, proto=17}/UDP{checksum=0, len=12},其中包括1000字节的数据’A’。

●       Outer IP packet (fragment 1): IPv4{frag offset=0, MF=1, proto=4, id=0xabcd} ,其中包括40字节的IP数据。

●        Outer IP packet (fragment 2): IPv4{frag offset=40, MF=0, proto=4, id=0xabcd} ,其中数据负荷为988字节。


为了绕过UDP校验,将校验和字段checksum设置为0。实例中的分片结构如下图所示:


当Treck协议栈处理外部分片时,它使用tsUserPacket结构中的pktuLinkNextPtr字段来链接它们。如前所述,当tfIpIncomingPacket()函数处理内部IP数据包(协议为4,IP-in-IP)时,它已经完成了分片数据的重组(内部IP数据包由链接在一起的两个tsPacket结构表示)。分片数据重组后的链表结构如下图所示:


由于tfIpIncomingPacket()函数在进行有效性判断时,只考虑tsUserPacket中的pktuChainDataLength字段(而不是pktuLinkDataLength),所以在处理内部IP包时将进入错误的链表长度的裁剪流程,从而导致了问题。


内部IP包通过了IP头完整性检查,在该例子中,内部IP包的总长度(32)小于链表数据长度(1000+8+20=1028),因此Treck协议栈将尝试错误地修剪数据包,方法是将字段pktuLinkDataLength和pktuChainDataLength设置为相同的值ipTotalLength(在我们的示例中为32)。这导致内部IP数据包由链接在一起的两个tsPacket结构表示,但它们的数据总长度大于pktuChainDataLength字段(修剪后pktuChainDataLength字段不是1028字节,而是等于32)。经过数据长度裁剪后的链表结构如下图所示:


现在已经使得链表达到了不一致的状态,下面将介绍如何利用这种不一致的状态来导致内存破坏。

在Treck协议栈代码中至少有一个代码路径可以将分片数据复制到单个连续缓冲区中。具体的执行路径为:tfUdpIncomingPacket() ->  tfSocketIncomingPacket() -> tfCopyPacket()。下面的代码是tfSocketIncomingPacket()函数处理UDP数据报的代码的一部分,如下代码所示:


这段代码中tfSocketIncomingPacket()函数调用tfGetSharedBuffer()申请内存,其大小基于pktuChainDataLength字段的值,然后通过tfCopyPacket()函数将数据包的不同分片逐个复制到新分配的内存空间中,发生溢出的代码如下所示:

由于两个分片中的pktuLinkDataLength之和为1000字节,后续的tfCopyPacket函数将会把1000字节的数据拷贝到这段内存中,这将导致堆溢出。


2、CVE-2020-11898


正如前文描述如何触发CVE-2020-11896漏洞那样,Treck TCP/IP协议栈无法正确处理通过IP-in-IP隧道传入的IPv4分片。这也可能允许未经身份验证的攻击者从堆中泄漏内存。可以采用以下示例进行漏洞触发:

●       内部IP数据包:IPv4 {ihl = 0xf,len = 100,proto = 0},有效载荷为'\ x00'* 40 +'\ x41'* 100。

●       外部IP数据包(分片1):IPv4 {frag offset = 0,MF = 1,proto = 4,id = 0xabcd},其中24个字节来自内部IP数据包有效负载。这意味着将复制20个字节的IP标头,外加4个空字节。

●       外部IP数据包(分片2):IPv4 {frag offset = 24,MF = 0,proto = 4,id = 0xabcd},来自内部IP数据包的其余字节作为有效负载。


实例中的分片结构如下图所示:


这里ihl为0xf,表示为最大IP选项,长度为60字节,数据包总长度total_length为100。当网络协议栈收到两个分片时,它将使用tfIpReassemblePacket()函数重新组装它们。分片数据重组后的链表结构如下图所示:


该tfIpReassemblePacket()函数使用tsUserPacket结构中的字段pktuLinkNextPtr链接两个分片。如果启用了IP-in-IP隧道传输,则内部IP数据包将随后由tfIpIncomingPacket()函数中处理,修剪后的pktuChainDataLength字段不是160,而是等于100。经过数据长度裁剪后的链表结构如下图所示:


内部IP数据包通过IP标头完整性检查,因为仅考虑了tsUserPacket的pktuChainDataLength字段(而不考虑pktuLinkDataLength)。因为在标准IP头部(20个字节)之后有4个空字节,并且一个空字节代表IP选项的末尾,IP选项解析通过检查。由于内部IP数据包包含无效的IPv4协议编号(Protocol为0),进入default分支,然后直接进入TM_IP_LOCAL_FLAG分支。如下代码所示:


因此网络协议栈将通过发送类型为3(目标不可达)和代码为2(协议不可达)的ICMP错误消息来拒绝该数据包。如下代码所示:



负责创建错误数据包的是tfIcmpErrPacket()函数。它分配一个新的数据包,初始化一些ICMP头部字段。如下代码所示:


最后从后续数据包(内部IP数据包)中复制一些数据。复制部分如下代码所示:


如代码所见,tfIcmpErrPacket()函数通过获取IP报头长度(以字节为单位加上8,在实际情况下为60 + 8 = 68)与pktuLinkDataLength字段(以及被裁剪为100)之间的最小值来计算要复制的字节数 。由于发送数据包的第一个分片的实际链路数据长度为24(而不是100),因此tfIcmpErrPacket()函数将从堆中复制68-24 = 44字节的额外数据。然后设置v12_icmpErrPacket中相关数据。如下代码所示:


最后调用tfIpSendPacket()函数发送icmp_ErrPacket包到目标地址,这将导致44字节的信息泄露。


3、CVE-2020-11910

CVE-2020-11910是越界读漏洞,该漏洞存在tfIcmpIncomingPacket函数中,该函数主要是处理ICMP包。tfIcmpIncomingPacket函数在处理设备收到类型为3,code为4的ICMP包的时候,代码并没有验证后续数据的长度,直接就访问了对应位置的数据,造成了越界读漏洞。如下代码所示:


4、CVE-2020-11911

CVE-2020-11911是未授权的敏感信息更新漏洞,该漏洞存在tfIcmpIncomingPacket函数中,该函数主要是处理ICMP包。tfIcmpIncomingPacket函数在处理设备收到类型为18(Address mask reply)包的时候,代码并没有验证设备是否发送过类型17(Address mask request)请求,就直接更新了设备的子网掩码。如下代码所示:


七、CVE-2020-11898漏洞验证


远程攻击打印机,验证视频请访问ADLab微信公众号如下:

验证视频


八、参考


1、https://www.ietf.org/rfc/rfc2003.txt

2、https://www.ietf.org/rfc/rfc792.txt

3、https://www.ietf.org/rfc/rfc1853.txt

4、https://www.jsof-tech.com/ripple20/

5、https://kb.cert.org/vuls/id/257161

6、https://www.venustech.com.cn/article/1/11834.html

7、JSOF_Ripple20_Technical_Whitepaper_June20.pdf





声明:该文观点仅代表作者本人,转载请注明来自看雪