Ripple20是一系列影响数亿台设备的0day(19个),是JSOF研究实验室在Treck TCP/IP协议栈中发现的【1】,该协议栈广泛应用于嵌入式和物联网设备。而由于这些漏洞很底层,并且流传版本很广(6.0.1.67以下),通过供应链的传播,使得这些漏洞影响十分广泛和深远。这也是这个漏洞代称Ripple的由来:The damaging effects of a these vulnerabilities has been amplified like a ripple
effect to a dramatic extent due to the supply chain factor.
(Ripple意思就是涟漪,连锁作用)
我们在本文中,重点分析了Ripple20中的CVE-2020-11896(CVSS V3 10.0)漏洞原理和利用。
背景知识
IP 分片
IP分片的概念是为了解决数据包最大长度的限制,例如以太网最大传输单元MTU为1500字节。那么如果发送的IP packet超过这个长度,就需要将数据包分为小的片段fragments。分片之后,想要将分片的数据包,在接收端重组,还需要一些标志位,用于表示接下来是否还有分片,该分片在整个数据包的偏移offset和是否是同一数据包等信息。在IP头部有4个字节(4-8字节)是用于存储这些分片信息的:
其中Flags字段有3部分:Reverse位、DF(Don't Fragment)和MF(More Fragments)。
IP隧道
IP隧道是将IP报文封装在另一个IP报文中的技术,如果外层和内存协议都是IP协议,那么我们将之称为ip-in-ip,结构如下图所示:
Treck TCP/IP的内部实现
Treck TCP/IP中,通过tsPacket结构表示数据包Packet:
struct tsPacket {
ttUserPacket pktUserStruct;
ttSharedDataPtr pktSharedDataPtr; // Point to corresponding sharable ttSharedData
struct tsPacket * pktChainNextPtr; // Next packet (head of a new datagram in a queue)
struct tsDeviceEntry * pktDeviceEntryPtr; // pointer to network Device struct
union anon_union_for_pktPtrUnion pktPtrUnion;
tt32Bit pktTcpXmitTime;
tt16Bit pktUserFlags;
tt16Bit pktFlags;
tt16Bit pktFlags2;
tt16Bit pktMhomeIndex;
tt8Bit pktTunnelCount; // Number of times this packet has been decapsulated. Initially setto zero.
tt8Bit pktIpHdrLen; // Number of bytes occupied by the IP header.
tt8Bit pktNetworkLayer; // Specifies the network layer type of this packet (IPv4, IPv6, ARP, etc).
tt8Bit pktFiller[1];
};
该结构重要字段包括:tsShareData字段:指向存储处理数据包所需信息的Buffer的指针pktChainNextPtr字段:指向下一个PacketpktUserStruct字段:表示数据包分片pktUserStruct字段是另外一个重要的数据结构
ttUserPacket(typedef struct tsUserPacket):
struct tsUserPacket {
void * pktuLinkNextPtr; // Next tsUserPacket for fragmented data 指向下一个分片
ttUser8BitPtr pktuLinkDataPtr; // Pointer to data
ttPktLen pktuLinkDataLength; // Size of data pointed by pktuLinkDataPtr 当前buffer的大小
ttPktLen pktuChainDataLength; // Total packet length (of chained fragmented data). Valid in first link only. 整个packet的长度,如果没有分片,等于pktuLinkDataLength
int pktuLinkExtraCount; // Number of links linked to this one (not including this one). Valid in first link only.
};
该结构重要字段包括:pktuLinkNextPtr字段:指向下一个分片的指针pktuLinkDataPtr字段:指向数据buffer的指针pktuLinkDataLength字段:表示当前分片长度pktuChainDataLength字段:表示整个Packet长度,当不存在分片的时候,这个长度与pktuLinkDataLength长度相等
数据包在不同层级中进行处理的的时候,需要调整pktuLinkDataPtr指针。例如一个ICMP请求包,该数据包有3层:Ethernet、IPv4和ICMP。在以太网层处理过程中(tfEtherRecv),pktuLinkDataPtr字段指向的是以太网头部,当下一层处理的时候,该字段和一些其他字段将会做以下调整:
在这个例子中,以太网包头长度为0xE,所以pktuLinkDataPtr指针向前调整0xE,而长度字段则减少0xE。在接下来,IPv4层(tfIpIncomingPacket)开始处理数据包,pktuLinkDatatr此时指向的是IP头部。Treck协议栈通过在tfIpIncomingPacket调用tfIpReassemblePacket函数来重组分片,每接受到一个分片,该函数将该分片插入到链表中,链表之间通过pktuLinkNextPtr指针连接起来:
如果分片有遗漏,那么最终返回Null;如果没有,那么该函数将会把分片链表交给下一层去处理。
CVE-2020-11896漏洞原理
下图是IP头部的结构:
其中4字节到8字节表示的是IP分片相关信息,包括表示,Flags和Fragment Offset。而前4字节则有两个重要字段:IP Header Length:IP头部长度Total Length:IP数据包总长度
接着输入理解一下tfIpIncomingPacket函数的实现,该函数首先对数据包进行一些简单的check:
接下来检查IpTotalLength是否小于等于pktuChainDataLength字段,这意味着实际接收到的数据比IP头部标识的IP数据包总长度要长,在这种情况下,就对多余数据进行裁剪:
裁剪(trimming)的方式很简单,就是将pktuChainDataLength和pktuLinkDataLength设置为ipTotalLength的长度。注意,漏洞就在这个裁剪这里。回顾一下先前所说:pktuChainDataLength代表的是整个数据包的长度,pktuLinkDataLength代表的是当前分片的长度,如果上述操作成功,那么就会出现:pktuLinkDataLength = pktuChainDataLength = IpTotalLength可是如果这个时候,如果pktuLinkNextPtr还是指向其他的分片呢,那么就会存在不一致现象,这种不一致的现象将会导致后续处理数据包产生错误。但是还是有问题,因为在tfIpIncomingPacket函数处理中,首先进行的是trimming操作,接着调用tfIpReassemblePacket对根据pktuChainDataLength大小,建立分片链表,而在tfIpReassemblePacket函数中并不会将分片数据包复制到buffer中。也就是说,先进行trimming操作,接着调用tfIpReassemblePacket建立分片链表,最终将分片链表返回给下一层进行处理,而下一层并不会再次调用tfIpIncomingPacket。这种情况下,上述不一致现象实际上并不能利用。
使用IP隧道
为了使得分片在IP层被处理,并且可以到达脆弱代码位置,我们使用IP隧道。tfIpIncomingPacket函数在处理内层IP数据包时,将之作为没有分片的数据包进行处理,也就是说MF标志位=0,此时将不会调用tfIpReassemblePacket函数进行处理。这个tfIpIncomingPacket函数将会在两个地方被调用,一次是在内层的IP packet(没有分片,只调用一次),一次是在外层的ip packet(多次,每个分片调用一次)。在这个处理过程中,tfIpIncomingPacket首先将会接收所有的外层ip分片,对于每个分片,都会调用tfIpReassemblePacket函数。在接收全部的分片之后,将会进入下一网络层的处理,这里由于ip-in-ip,tfIncomingPacket函数将会被本身所递归调用,去处理内层ip数据。ip-in-ip结构如下图所示:
考虑如下这个场景:内层IP数据包:IPv4{len=32, proto=17}/UDP{checksum=0, len=12} payload为:’A’*1000外层IP数据包(分片1):IPv4{frag offset=0, MF=1, proto=4, id=0xabcd} 外层IP数据包(分片2):IPv4{frag offset=40, MF=0, proto=4, id=0xabcd}整体数据包及分片情况如下图所示:
当tfIpIncomingPacket函数(该函数被处理外层数据包的tfIpIncomingPacket所递归调用)处理内层IP数据包的时候,此时已经完成了分片的重组,这两个ip分片被tsUserPacket->pktuLinkDataPtr所连接起来。那么接下来在该函数接下来的流程中,内层IP数据包的total length(32),是小于pktuChainDataLength(1000+ 8 + 20 = 1028)的,进入trimming分支进行裁剪操作,将pktuChainDataLength设置为32。
if ((uint)ipTotalLength <= pkt->pktuChainDataLength) {
if ((uint)ipTotalLength != pkt->pktuChainDataLength) {
pkt->pktuChainDataLength = (uint)ipTotalLength;
pkt->pktuLinkDataLength = (uint)ipTotalLength;
}
}
现在我们新的问题出现了:如何将这个不一致问题(inconsistency)转为一个内存破坏(memory corruption)?
UDP 2.3.2中的heap overflow
在UDP数据处理中,可以确定的是,至少有一条代码路径是将IP分片复制到自定义的buffer里面,那么就存在漏洞的可能性。这个过程需要malloc一个堆空间,堆空间的大小取决于pktuChainDataLength字段,然后将ip分片复制到heap中。做复制这个事情的函数是tfCopyPacket,该函数逻辑抽象如下:
可以看到,这个memcpy的过程并不考虑长度。而堆块本身的大小是取决于pktuChainDataLength,这个字段在先前漏洞trimming被触发后,实际上是小于实际IP数据包总大小的,heap overflow就这样出现了。接下来是一些UDP本身字段的校验,这部分就不再详细叙述了,为了解决接收队列非空的要求,还需要快速的发数据包保持接受队列中存在数据包。
CVE-2020-11896漏洞利用
接下来作者利用Digi Connect ME 9210进行了验证,证明可以做到远程代码执行。这个设备如下图所示:
这是一个极小的嵌入式设备,用于完成串口到以太网的转化,串口常包括SPI、I2C和CAN等,里面有嵌入式的ARM CPU,有一个NET+OS操作系统。本次实验的目标是通过远程执行shellcode,将开发板上面的LED灯点亮。
exploit编写策略
heap overflow的利用大概有两种方式:
1.覆盖堆中的meta-data信息。所谓meta-data就是堆中的元信息,包括堆块的大小、空闲位的标志等等堆本身自带的信息,这种利用方式和libc pwn中的off by one原理类似,溢出到下一个堆块的重要信息。
2.覆盖堆上面的数据结构。这种攻击方式是希望堆上面存着一些数据结构,该结构中存着一些函数指针可以被覆盖。
第二种利用方式与特定应用程序相关,第一种则适用面更加广泛,本文选择第一种利用方式。
理解Treck heap的内部实现
Treck中实现了一套自己的堆分配机制,在内部使用的是固定大小的堆分配模式,一个分配单元被称之为bucket。在Treck实现中,有如下几种固定大小:
而释放后的bucket,有自己对应的free buckets list,每次释放一个bucket后,就将其插入到对应大小list的头部,这里可以类比于ptmalloc中的tcache机制。在Treck协议栈中通过tfGetRawBuffer,该函数参数为一个4字节大小的size,返回一个指向分配内存的指针,这个分配的内存我们称之为raw buffer,raw buffer的释放通过tfFreeRawBuffer。分配的过程就是从对应空闲链表中取出一个堆块,然后转为ttRawBufferPtr类型指针返回给用户:
如果空闲链表中没有空闲的bucket,那么就调用tfBufferDoubleMalloc分配一个新堆块返回给用户:
释放的时候将其插入到对应的空闲链表中去
可以看到,空闲bucket插入到空闲链表后,将会利用rawNextPtr指针链接起来,rawNextPtr指向上一个空闲bucket,结构如下图所示:
到目前为止,我们搞清楚了Trec中的堆结构,堆的分配和释放过程,特别是空闲bucket也是类似ptmalloc一样通过链表利用堆中的指针链接起来的。那么我们自然可以想到,如果可以覆盖掉rawNexPtr
指针为可控值,就可以做到任意地址分配了:
就像上图所示,通过两次分配之后,我们就可以在栈上分配一个可控的堆块。这种攻击方式,在libc Pwn中是非常常见的,例如fastbin attack打malloc_hook改为one_gadget。这个嵌入式设备是ARM v9,并且这个堆实现过程基本没有check,甚至没有开NX,最后白皮书中采取的方法也是通过ROP跳到shellcode来做到RCE的。目前笔者手头还没有实际运行Treck的设备,所以没法实际动手调试和写exp,运行效果可以看演示效果【1】【2】。
总结
1.Ripple 20存在于Treck TCP/IP协议栈中,该协议栈广泛存在于嵌入式设备中,波及的行业包括:工业设备、电力设备、企业网络设备、交通、能源等等,而上述设施我们称之为:关键基础设施
2.Ripple 20不好修补,首先由于供应链的广泛传播,漏洞存在形式又很底层,所以排查设备是否运行Treck TCP/IP协议栈容易疏漏。其次虽然Treck官方已经提供了最新稳定版本6.0.1.67,但是这种基础协议栈软件的更新还是比较麻烦的
3.从白皮书披露来看,漏洞利用技术并不十分复杂,所以漏洞利用门槛可能不会很高,例如这个漏洞,即使无法做到RCE,至少远程令设备crash是比较容易做到的,而关键基础设施的crash也已经算是大问题了
引用:【1】https://www.jsof-tech.com/ripple20/
【2】https://mp.weixin.qq.com/s/2F1-35HIk126crowAh9LLw
相关实验:TCP攻击实例分析
(通过该实验了解SYN-Flooding攻击,RST攻击,TCP会话劫持,并通过会话劫持拿到服务器shell 权限。)