CVE-2020-11896 Treck TCP/IP stack漏洞分析

 

Treck介绍

Treck TCP/IP是专门为嵌入式系统和实时操作系统设计的高性能TCP/IP协议套件。旨在提供高性能,可扩展性和可配置性,以便轻松集成到任何环境中,无论处理器,商用RTOS,专有RTOS或者不使用任何RTOS,只要通过API,Treck TCP/IP便可轻松集成到各种嵌入式产品中。

 

漏洞概述

CVE-2020-11896是Treck TCP/IP堆栈中的漏洞。攻击者可以将UDP数据包发送到目标设备的开放端口来执行远程代码。此漏洞的前提条件是设备通过IP隧道支持IP分片。即使在某些条件不满足的情况下,将仍然存在DoS漏洞。

下面来分别介绍下这俩个条件是什么:

 

IP分片

链路层通常对可阐述的每个帧的最大长度有一个上限(MTU),为了保持IP数据报抽象与链路层细节的一致与分离,IP引入了分片重组。当发送数据包大于MTU时,便将数据包分为几个较小的碎片来支持通过链路和网络进行传输。使用IP header中的标识字段来对不同的分片进行分组,用MF位来标志是否为最后一个分片。(若MF=0,即为最后一个分片。)
IPv4数据报如下所图:(MF位用红色标记)

 

IP隧道

IP隧道是指将原始IP包(包含源/目的地址)封装在另一个数据包中进行传输。该数据包用于实际物理网络传递,其中一个隧道协议即为IP-in-IP。

利用下图来说明IP隧道:

图1

外部包的源地址和目的地址为隧道端点,内部包的地址用于隧道两端的网络路由,隧道入口点是接收通过隧道转发的IP包的节点。它将这个包封装在一个外部IP包中。当数据包到达隧道的出口点时,则就如目标网络中发送的正常数据包一样被封装和转发。

 

漏洞分析

关键结构体

分析该漏洞,需要了解几个Treck TCP/IP stack重要的结构体,如下:

tsPacket //Treck TCP/IP堆栈中的数据包
ttUserPacket //一个指向tsSharedData结构的指针,该结构包含协议栈在处理数据包时所需的信息
tsPacket structure (several field)

下面给出各个结构体详细的定义
tsPacket :

图片来源:JSOF_Ripple20_Technical_Whitepaper_June20

Treck TCP/IP堆栈中的数据包由tsPacket表示。每个数据包都与一个数据缓冲区相关联,该缓冲区保存了来自接口驱动程序的原始数据。tsPacket结构还包含另一重要结构:ttUserPacket

ttUserPacket:

图片来源:JSOF_Ripple20_Technical_Whitepaper_June20

字段pktuLinkDataPtr指向当前分片的数据缓冲区。这个数据缓冲区的位置随着协议栈在不同阶段处理数据包而改变,并且取决于当前正在处理的数据包的所在的协议层。例如,当协议栈处理以太网层(在tfEtherRecv中)时,此字段指向以太网头。处理IP层时,指向IP头。 pktuLinkDataLength字段为pktuLinkDataPtr指向的数据大小,即单个IP分片的大小。 pktuLinkNextPtr用于跟踪数据包中的分片。该字段指向下一个分片的tsPacket,该tsPacket进而也包含对下一个分片的引用,依此类推。因此,我们也可以在将此链接列表中的分片称为“链接”。如果是最后一个分片,或者数据未分片,则此字段等于NULL。pktuChainDataLength字段表示分片的总长度。只在第一个分片中设置,如果数据未分片,则它的值等于pktuLinkDataLength

漏洞原因

在理解造成漏洞的根本原因之前先来看下IP header中俩个字段:

Internet 头部长度(IHL):该字段保存IP头部的长度,最小值为5(20字节)。如果存在IP选项,头的长度会变大,最大值为0xf(60字节);
Total Length(总长度):表示整个IP包(或IP分片)的大小,包括报头;

在对传入的数据包进行处理时,传入数据由具有相同命名风格的函数(tf*IncomingPacket)处理,其中*是协议名。在以太网/IPv4/ICMP情况下,数据包将分别由tfEtherRecvtfIpIncomingPackettfIcmpIncomingPacket函数处理。Treck协议栈在tfIpReassemblePacket中对分片进行重组,该函数由tfIpIncomingPacket调用。 每当接收到发往设备的IP分片时,就会调用此过程。 如果存在IP分片缺少时,函数将返回NULL。如果所有分片都到达,则网络堆栈使用字段pktuLinkNextPtr将各个分片链接为数据包,并将它传递给下一层进行进一步处理。上面提到的“重组”并不意味着是将数据包复制到连续的存储块,而是简单地将它们链接在一起形成链表。

传入IP数据时,则会调用tfIpIncomingPacket函数进行完整性检验,验证头部校验和以下字段:
ip_version == 4 && data_available >= 21 && header_length >= 20 && total_length > header_length && total_length <= data_available data_available 即为pktuChainDataLength

如果所有检验通过,该函数会检验IP报头中指定的总长度是否小于数据包的pktuChainDataLength。若是,则表明实际收到的数据比IP报头中声明的要多。则进行调整并删除额外的数据。

图片来源:JSOF_Ripple20_Technical_Whitepaper_June20

pktuLinkDataLength是当前分片的大小,而pktuChainDataLength是整个数据包的大小。如果进行了上述操作,则会产生问题,其中pkt->pktuChainDataLength == pkt->pktuLinkDataLength,但是pkt-> pktuLinkNextPtr可能指向其他分片,因此会产生链表上的分片的总大小大于存储在pktuChainDataLength中的大小的情况。

接下来看看处理UDP数据报部分代码,可以看到将分片数据复制到了一个连续的缓冲区中。该流程的内部包括分配的新包(tfGetSharedBuffer),大小由pktuChainDataLength决定,然后依次将包的不同分片复制到tfGetSharedBuffer中。

图片来源:JSOF_Ripple20_Technical_Whitepaper_June20

可以看到函数tfCopyPacket没有考虑到写入缓冲区的长度。它简单地获取每个链接的源数据包(数据分片),并将其该数据复制到tfGetSharedBuffer中。而此处分配的缓冲区可能会比经处理后失效的包中所有单个链接数据总长度小而存在堆溢出。

触发漏洞

上述存在漏洞的代码是处理IP分片的程序流,要想触发该漏洞,则必须使分片后数据包又在IP层进行处理,这样才会触发到存在问题的数据修剪的部分。

正常对于IPv4层来讲,每收到一个分片都会调用tfIpIncomingPacket函数,然后该函数又调用tfIpReassemblePacket进行处理。tfIpReassemblePacket负责将分片插入到上面提到的链表中。它不会将分片复制到连续的内存缓冲区中。收到所有分片后,tfIpReassemblePacket将完整的数据包作为分片链表返回,在下一个协议层上进行进一步处理。而重组操作是在存在问题的修剪操作之后进行的。重组操作完成后,tfIpIncomingPacket将返回或转发数据包到下一个协议层(例如UDP)进行处理。下一层不会再调用tfIpIncomingPacket,而这将不会触发存在问题的程序流。也就是说,以正常IP包执行的话,则并不会触发漏洞。

为了使分片在IP层被处理并触发漏洞,我们使用IP隧道。上面已经说过IP隧道是将原始IP包(包含源/目的地址)封装在另一个数据包中进行传输,并且IP隧道允许内部IP包作为一个非分片包被tfIpIncomingPacket处理。函数tfIpIncomingPacket则将被递归调用两次,一次用于IP隧道的内层,多次用于外层(每个分片都会调用一次)。首先,tfIpIncomingPacket将接收来自外层的所有分片,会在每个分片上调用tfIpReassemblePacket,一旦它们全部被接收,则会传递到下一个协议层,这种情况下,下个协议层还是为IPv4,因此又会由tfIpIncomingPacket调用tfIpIncomingPacket来处理内部IP层。

JSOF_Ripple20_Technical_Whitepaper_June20中举了个例子,数据包如下所示:

图片来源:JSOF_Ripple20_Technical_Whitepaper_June20

当协议栈处理外部IP包时,它将使用tsUserPacket结构中的pktuLinkNextPtr字段将其链接。之后会调用tfIpIncomingPacket来处理内部包,处理传入的分片数据,内部IP包由链接在一起的两个分片组成,每个分片都有单独的pktuLinkDataLength值。但在IP头中却被标记为非分段(MF = 0)。(内部IP数据包通过了IP包头完整性检查,因为仅仅考虑了tsUserPacketpktuChainDataLength字段而忽略pktuLinkDataLength字段。)示例中,内部IP数据包的总长度(32)小于链数据长度(1000 + 8 + 20 = 1028)因此Treck堆栈将通过同时设置两个字段pktuLinkDataLengthpktuChainDataLength为相同的值,即都为IP长度大小(示例中为32)来错误的修剪数据。这样连接在一起的两个分片表示的内部包的总大小明显大于pktuChainDataLength字段,而修剪后pktuChainDataLength字段仅为32。通过构造这个数据包这样便可触发堆溢出。

 

缓解措施

1.设置防火墙策略对数据包进行过滤;
2.最小化关键设备的网络暴露,非必要情况下,防止受影响的设备能够被Internet访问;
3.将受影响设备与核心业务网进行隔离;
4.更新到最新的Treck堆栈;

 

参考

https://www.jsof-tech.com/wp-content/uploads/2020/06/JSOF_Ripple20_Technical_Whitepaper_June20.pdf
https://tools.ietf.org/html/rfc791
https://en.wikipedia.org/wiki/IP_tunnel

(完)