关于 CVE-2020-11896和CVE-2020-11898 的学习笔记
CVE-2020-11896
IPv4 分片
IP分片使得即使在IP分组的大小大于网络的特定链路中允许的最大大小的情况下,也可以在网络中发送IP分组。IP分段是一种将分组分成几个较小部分(“片段”)以支持在这些链路和网络上传输的技术。该协议支持TH分组的分段然后重组。
使用IP报头中的标识字段将不同的片段分组。该标识字段描述片段属于哪个分组。这允许不同的数据包在网络中分段传输,并在另一端正确重组。最后一个片段的MF(更多片段)位标志设置为0,而所有其他片段的MF=1。
网络堆栈负责对大型数据包进行分段,并通过网络发送多个分段。请求发送大型数据报的UDP应用程序就是一个例子。网络堆栈还负责在接收到分段的数据包时对其进行重组。
如果只有部分数据包碎片到达,网络堆栈最终会丢弃这些碎片。在大多数实现中,当处理任何片段时,网络堆栈启动计时器。当该计时器到期时,网络堆栈丢弃属于同一标识组的所有片段。
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包结构如下图所示:
1 | 0 1 2 3 |
IP 隧道
IP隧道允许两个独立网络之间的虚拟点对点链路。它是通过将一个数据包(可以是IP数据包)封装在另一个数据包中来实现的,从而使内部数据包具有与外部数据包不同的源地址和目的地址。
外部数据包的源地址和目的地址是隧道端点,内部数据包中的地址用于隧道两端的网络路由。
隧道入口点是接收应该通过隧道转发的IP分组的节点。它将此数据包封装在外部IP数据包中。当数据包到达隧道出口点时,会将其解封并转发,就好像它是在目标网络中发送的常规数据包一样。
隧道使用的一个主要示例是虚拟专用网(VPN)技术。
有几种隧道协议,最简单、最古老的协议之一是IP-in-IP(IP协议号4)。
IP-in-IP
IP-in-IP是一种IP隧道协议,在该协议中,通过添加具有分别等于隧道入口点和出口点的源地址和目的地址的外部IP报头,将一个IP数据包封装在另一个IP数据包中。
内部数据包未修改,外部IP报头从内部IP报头复制一些字段。外部标头的IP协议号为4。
Treck TCP/IP
在 Treck TCP/IP 中,有个结构体用来描述其 TCP/IP栈,称为tsPacket。
1 | struct tsPacket { |
这是包含的ttUserPacket 结构(tsUserPacket的typedef ):
1 | struct tsUserPacket { |
pktuLinkDataPtr 指向当前片段的数据缓冲区。随着网络堆栈在不同阶段处理数据包并取决于当前正在处理的数据包层 ,此数据缓冲区内的确切位置会发生变化。 对于 例如,当网络栈处理所述以太网层(在tfEtherRecv ),该字段指向以太网报头。
pktuLinkDataLength字段指定pktuLinkDataPtr指向的数据的大小,即单个片段的大小。
pktuLinkNextPtr用于跟踪数据包中的片段。此字段指向表示下一个片段的另一个tsPacket,该片段又包含对下一个片段的引用,依此类推。因此,我们也可以在此链表中将片段称为“链接”。如果此链接是最后一个片段,或者如果数据未分段,则此字段将等于NULL。
pktuChainDataLength字段表示包括所有片段的分组长度,即分组的总大小。它只为第一个片段设置,如果数据没有分段,则等于pktuLinkDataLength。
堆栈中的一种常见模式是在堆栈中的各层之间移动时调整pktuLinkDataPtr指针。例如,如果我们的数据包是ICMP回应请求数据包(PING),则它将由三层组成:以太网,然后是IPv4,最后是ICMP。在这种情况下,当以太网层被处理时(在函数tfEtherRecv中),pktuLinkDataPtr指向以太网头的开始,然后在移动到下一层之前,使用以下代码对其进行调整:
1 | pkt->pktuLinkDataPtr = pkt->pktuLinkDataPtr + 0xe; |
在本例中,0xE(十进制14)是以太网头的大小(6(Dst MAC)+6(Src MAC)+2(EtherType))。
当tfEtherRecv完成数据包处理时,它会使用代表下一层协议的EtherType字段将数据包转发到下一层处理。遇到的支持的以太网类型有ARP、IPv4和IPv6。
在此的示例中,当IPv4层接收到数据包(在函数tfIpIncomingPacket中)时,指针pktuLinkDataPtr已经指向以太网头,因此可以安全地假设pktuLinkDataPtr指向的数据是IPv4头。
传入的数据由具有相同命名约定TFIncomingPacket的函数处理(正如我们已经看到的),其中是协议名称。在以太网/IPv4/ICMP的情况下,包将由函数tfEtherRecv、tfIpIncomingPacket处理。
和tfIcmpIncomingPacket。
Treck堆栈处理从tfIpIncomingPacket调用的过程tfIpReAssemblePacket中的片段重组。每当接收到发往设备的IP片段时,都会调用此过程。如果缺少片段,则函数返回NULL。否则,如果所有片段都到达并且没有漏洞,则网络堆栈使用pktuLinkNextPtr字段将片段链接在一起,并传递数据包以供下一层进一步处理。在此上下文中的单词“重组”并不意味着将分组复制到连续的存储块,而是简单地将它们链接在一个链表中。
漏洞原因
为了了解漏洞的根本原因,我们将快速查看IP标头中的两个字段:
• IHL (4个比特):该尺寸的所述IP 报头中的双字。最低值是5 (20 个字节)。如果有IP选项,头长度变大,最多值的0xf(60个字节)。
• 总长度(2个字节):整个IP数据包的大小,以字节(或IP片段,如果是分段的)为单位,包括报头。
函数tfIpIncomingPacket 从一些基本的健全性检查开始。除了验证标头校验和之外,它还验证:
1 | ip_version == 4 && data_available >= 21 && header_length >= 20 && total_length > header_length && total_length <= data_available |
“可用数据”是使用字段pktuChainDataLength测量的。
如果所有健全性检查都通过,该函数将检查IP报头中指定的总长度是否严格小于数据包的pktuChainDataLength,表明实际接收的数据多于IP报头中所述的数据。如果为真,则执行修剪操作以删除额外数据:
1 | if ((uint)ipTotalLength <= pkt->pktuChainDataLength) { if ((uint)ipTotalLength != pkt->pktuChainDataLength) { |
这就是漏洞所在。回想一下,pktuLinkDataLength保存当前片段的大小,pktuChainDataLength保存整个数据包的大小。如果执行上述操作,则会创建不一致,其中pkt->pktuChainDataLength。
==pkt->pktuLinkDataLength,但可能有其他片段指向。
pkt->pktuLinkNextPtr.。另一种方式是将其视为一种虚构的不一致状态,其中链表上片段的总大小大于pktuChainDataLength中存储的大小。
由弱修剪操作产生的不一致对于处理的其余部分来说不是好兆头。然而,我们还有另一个挑战需要克服。每次使用一个接收到的片段调用tfIpIncomingPacket函数,并调用tfIpReAssemblePacket来处理它。tfIpReAssemblePacket负责将片段插入到上述链表中。它不会将片段复制到连续的内存缓冲区。收到所有片段后,tfIpReAssemblePacket以片段链接列表的形式返回完整的数据包,以便在下一个协议层进行进一步处理。该重组操作在易受攻击的修剪操作之后执行。一旦可靠的操作完成,tfIpIncomingPacket将返回或转发数据包,以便在下一网络层(例如:UDP)进行处理。这会阻止我们执行利用漏洞攻击,因为我们需要分段的数据包才能达到不一致的状态。换句话说,易受攻击的代码应该只在每个片段的基础上执行(或在单个片段的数据包上执行)。如果以这种方式执行,它实际上并不容易受到攻击。
那么,我们如何才能用传入的碎片数据触发易受攻击的修剪流,从而实现上述不一致呢?
在IP层处理分段数据包
为了使分段的数据包在IP层得到处理并到达易受攻击的流,我们使用隧道。
隧道允许tfIpIncomingPacket将内部IP数据包作为非分段数据包进行处理。函数tfIpIncomingPacket将被递归调用两次,一次用于IP隧道的内层,多次用于外层(每个片段一次)。首先,tfIpIncomingPacket将接收来自外层的所有片段,在每个片段上调用tfIpReAssemblePacket,一旦它们都被接收,它将把执行传递到下一个协议层,在本例中再次是IPv4,因此将从具有内部IP层的tfIpIncomingPacket调用tfIpIncomingPacket。
对外部IP分组进行分段会导致使用内部分组调用tfIpIncomingPacket,该内部分组现在由几个分段组成,但在IP报头中标记为未分段(MF=0)。就描述数据包的数据结构而言,它现在由链接列表中的几个单独的链接组成,每个链接都有一个单独的pktuLinkDataLength值。
让我们说得更具体些。请考虑下面的示例,它将伴随我们完成本文:
(我们将 checksum 字段设置为0,因为这将导致跳过UDP校验和验证。)。
当网络堆栈处理外部片段时,它使用字段将它们链接起来。
如前所述,tsUserPacket结构中的pktuLinkNextPtr。当函数tfIpIncomingPacket处理内部IP包(由于协议=4)时,它正在处理传入的分片数据(内部IP包由两个链接在一起的tsPacket结构表示),但仍会调用易受攻击的流,从而解决了我们的挑战。
此外,内部IP分组通过IP报头健全性检查,因为只考虑tsUserPacket的pktuChainDataLength字段(而不是pktuLinkDataLength)。在我们的示例中,内部IP数据包(32)的总长度较小。
超过链数据长度(1000+8+20=1028),因此Treck堆栈将通过将字段pktuLinkDataLength和pktuChainDataLength设置为相同的值-总IP长度(在我们的示例中为32),来尝试不正确地修剪数据包。这导致内部IP分组由链接在一起的两个tsPacket结构表示,但是它们的总CUSIZE大于pktuChainDataLength字段(pktuChainDataLength字段在修整后等于32,而不是1028字节)。
利用UDP实现堆溢出
既然我们已经达到了不一致的状态,我们就面临着另一个问题–我们如何利用这种不一致来获得内存损坏原语?
原来,至少有一条代码路径将碎片数据复制到单个连续缓冲区中。这是处理UDP数据报的代码的一部分。该流的内部逻辑由正在分配的新分组(使用tfGetSharedBuffer)组成,其大小基于pktuChainDataLength字段,随后是分组的不同片段的副本逐个进入新分配的分组。
负责执行复制的函数是tfCopyPacket,它按顺序接受源包和目的包。以下是片段复制代码的摘录:
1 | i = 0; |
如您所见,函数tfCopyPacket没有考虑它写入的缓冲区的长度。它只是从src包(我们的分段包)中提取每个链接,并将其数据复制到目标包中。目标数据包是根据pktuChainDataLength分配的,因此如果之前触发了该漏洞,则在我们的无效之后,分配的缓冲区可能小于数据包中所有单个链接的总和-因此,会发生堆溢出。
还有一件事需要描述,那就是我们如何触发这一流程。
如果应用程序正在侦听任何UDP端口,则发往该端口的UDP数据包将被传递给套接字处理函数tfSocketIncomingPacket。其任务是将数据包附加到套接字接收队列(稍后由应用层轮询)。
在我们的研究中,我们发现当UDP数据包的套接字接收队列非空并且有新的数据包到达时,上述包含堆溢出的流是可以实现的。请看tfSocketIncomingPacket的以下摘录:
1 | ocal_10 = pkt; |
我们看到,为了到达tfGetSharedBuffer,我们需要绕过涉及到ocRecvCopyFraction的检查。我们不知道它的确切用途,但通过调试和实验,我们发现它的值是4(在我们的情况下)。
在我们反复出现的示例中,我们的第一个数据包链路的缓冲区大小很小,因此SizzeOfPacketBuffer。
相对较小(大约10s字节)。
但是当我们到达该流时,pkt->pktuChainDataLength等于4(修剪后为32,然后在处理IP层时递减20(IP报头的大小),然后再次递减8(UDP报头的大小))。因此,4*4=16小于sizeOfPacketBuffer,我们通过此检查。
我们需要确保的最后一件事是UDP数据包的接收队列是非空的(否则无法到达此流)。在理论上,有几种方法可以做到这一点。在我们的攻击中,我们发现将多个UDP数据包足够快地发送到同一端口就可以做到这一点。然而,要让这一部分可靠地工作是很棘手的。该漏洞是用Python编写的,使用的Scapy对于我们的目的来说太慢了。为了克服这个障碍,我们使用了Scapy的L3Socket对象,并实例化了一堆线程,这些线程只会用良性的UDP数据包淹没设备,因此套接字接收队列将是非空的。用C或GO编写代码可能也可以。根据要利用的设备和侦听服务器,可以对此部分进行其他改进。
另一个障碍是,在我们到达发生溢出的tfSocketIncomingPacket之前,易受攻击的数据包通过tfUdpIncomingPacket。此函数包含一些与UDP相关的健全性检查,因此我们还需要通过这些检查:
1 | udpLen = udpHdr->udpLength >> 8 | udpHdr->udpLength << 8; |
正如我们所看到的,通过确保UDP长度字段等于pktuChainDataLength字段减去内部IP报头的大小,我们可以避免这种类型的修剪(不要与易受攻击的流混淆)。
总而言之:如果我们的设备上有UDP端口在监听,我们可以快速发送数据包,这样套接字接收队列就不会为空。同时,我们将发送会触发该漏洞的零碎UDP数据包,并勾选几个复选框。我们预期的结果是使用tfGetSharedBuffer在堆上分配一个小缓冲区,然后tfCopyPacket会使其溢出。
CVE-2020-11898
前面提到了,Treck TCP/IP不能正确处理IP-in-IP隧道上传入的IPv4片段。这还可能允许未经验证的攻击者从 heap 中泄漏内存。
如果 tfIcmpErrPacket 将越界数据复制到错误数据包中,则可作为信息泄漏漏洞来被利用。
参考如上实例:
当网络堆栈接收到这两个片段时,它会使用tfIpReAssemblePacket重新组装它们。此函数使用tsUserPacket结构中的字段pktuLinkNextPtr链接两个片段。如果启用了隧道,则IP层接下来将在函数tfIpIncomingPacket中处理内部IP数据包。
内部IP分组通过IP报头健全性检查,因为只考虑tsUserPacket的pktuChainDataLength字段(而不是pktuLinkDataLength)。此外,由于在标准IP报头(20字节)之后有4个空字节,并且空字节表示选项列表的末尾(见https://tools.ietf.org/html/rfc791),因此IP选项解析通过。
如果内部IP数据包的IP报头中的总长度字段严格小于链数据长度,则网络堆栈将尝试修剪数据包。如前文中所述,修剪是通过将字段pktuLinkDataLength和pktuChainDataLength设置为相同的值,即总长度字段(在我们的示例中为100)来实现的。
由于内部IP数据包包含无效的IPv4协议号(协议0),因此网络堆栈将通过发送类型为3(目的地不可达)代码为2(协议不可达)的ICMP错误消息来拒绝该数据包。
负责创建错误数据包的函数是tfIcmpErrPacket。它会分配一个新数据包,初始化一些ICMP字段,并最终从违规数据包(内部IP数)
1 | length = (packetPtr->pktUserStruct).pktuLinkDataLength; if (headerLengthInBytes + 8 <= length) { |
正如我们所看到的,tfIcmpErrPacket通过取IP报头长度(以字节为单位)加8(在我们的示例中,60+8=68)和pktuLinkDataLength字段(在本例中修剪为100)之间的最小值来计算要复制的字节数。由于违规数据包的第一个片段的实际链接数据长度为24(不是100),tfIcmpErrPacket将复制68−24=44字节从堆中泄漏的数据。
此漏洞可用于在启用漏洞缓解(如ASLR)时,以及在没有调试器的情况下,利用CVE2020-11896和其他RCE漏洞进行攻击。
2020-7-2 更新
试了下文章写的 poc 发现并没有任何返回,但是机器直接打崩了。等个可以调试的设备。
关于 treck 协议栈扫描
看到 启明星辰 ADLAB 的公众号提到了 Treck协议栈自定义了类型为165(0xa5)的ICMP包,并一旦收到165的ICMP包会回复类型为166的ICMP包响应。
由于手头没有相应的设备,以及查下公司的打印机相关设备,似乎都没有在官方公告的影响范围内,所以用这个方法跑了下,就到目前写这篇文章为止了,大概跑了1000多个 IP ,没有任何返回,目前猜测,公网 scan 的 话可能会被网关给 drop 掉。
2020-7-2 更新:
昨天发现上海公司有一台设备,测试的时候发现 scapy 写的扫描是有问题的,scapy 本身似乎对包进行了判断,导致拿不到回包,所以 github 上公开的扫描应该是不行,另外一点 ttl 如果太小似乎也会被drop掉(在多层路由的情况下)
这里贴一下我用 socket 写的脚本
设置 ICMP_ECHO_REQUEST 为0xa5 ,然后再收包判断 type
例:
参考链接
https://www.jsof-tech.com/wp-content/uploads/2020/06/JSOF_Ripple20_Technical_Whitepaper_June20.pdf