TCP 紧急指针 RCE 漏洞
TL;DR
TCP具有一种名为紧急数据的深奥的机制来传输越界的数据/带外数据(OOBdata)。通过利用这种机制,攻击者可以造成传递给 recv() 系统调用的长度变量下溢,这将导致传递给 recv() 的缓冲区发生攻击者可控的溢出。这可以导致在栈、堆或全局数据部分中分配的缓冲区发生溢出,从而导致远程代码执行。
背景知识 - TCP 速成课程
为了了解在 IPnet 的TCP实现中发现的漏洞,需要一个TCP的快速速成课程。TCP 是一种传输层协议,它允许在不可靠的 IP 层上传输有序的字节流。解释 TCP 协议是如何工作的,即使是粗略的,也远远超出了本文档的范围。但是,必须解释一些细节才能理解下面描述的漏洞。
作为 TCP 会话的一部分通过 IP 层发送的每个数据包称为一个报文段。 下面是一个报文段的结构示意图:
根据其源/目标 IP 和源/目标端口的 4 元组,每个 TCP 报文段都被视为特定 TCP 会话的一部分。
每个段都有一个序列号字段,它使得接收端点确定这个段包含的数据应该出现在字节流中的什么位置。
接收端点接收到的 TCP 连接的第一个报文段包含连接建立时设置的初始序列号 (ISN)。 该段必须设置 SYN 标志。
从作为会话的一部分由该端点接收的报文中的所有其他序列号减去 ISN 是目前已接受的字节流中包含的数据的偏移量。
每个端点的 TCP/IP 堆栈都拥有一个称为 TCP Window 的缓冲区,其中包含作为会话的一部分接收到但尚未由套接字用户处理的所有数据。当用户在套接字上调用 recv() 系统调用时,数据会从 TCP 窗口缓冲区中提取出来。只要有新的(非重复的)TCP 报文段到达,就会向窗口缓冲区中插入数据。为窗口保留下一个有序序列号,如果数据位于流中该偏移量之前,则用户只能接收数据。因此,用户只会读取有序数据。
新到达的报文段可能会补充窗口中先前的无序数据,从而使这些数据中的一些(或全部)有序。因此,下一个顺序序号将进行调整,以反映这一点,现在用户可以接收数据。
TCP 紧急数据说明
TCP的一个鲜为人知的功能是通过现有的TCP连接发送和接收紧急或带外数据。
此功能旨在解决类似以下情况下出现的问题:
- 客户端通过 TCP 连接异步向服务器发送指令。也就是说,发送下一个之前无需等待上一个完成。
- 服务器从它的 TCP 窗口中读取一条指令,并执行它,然后读取下一条,再执行,一直重复此过程。但是,在服务器处理完所有指令之前,客户端决定取消剩余指令的执行。
在只有一个数据流时,不可能在所有先前的请求处理完成之前通知服务器任何新的请求。在现代 7 层协议中,通过显式使用多个数据流来解决此问题。然而,TCP 提供了一种通过相同已建立的第 4 层连接发送此类紧急数据的方法。
几乎所有现代操作系统都提供了一个标准的 MSG_OOB 标志,可以将其传递给 send/recv 系统调用,以通过 TCP 套接字发送和接收这种带外 (OOB) 数据。作为 OOB 数据发送的缓冲区将不会被常规的 recv() 调用(仅接收有序数据)接收,而设置了 MSG_OOB 标志的 recv() 调用也只会接收 OOB 数据。
在 TCP 报文段级别,此功能由每个段中存在的 URG 标志和紧急指针字段来实现。如果设置了 URG 标志,则紧急指针表明紧急数据在所在的流中的偏移量(相对于相对序列号)。 通常紧急数据本身也会出现在同一段中。
这听起来很简单,但是紧急指针的确切含义并没有很好的被定义——不清楚它是指向有序数据的最后一个字节,还是紧急数据的第一个字节。此外,还不清楚如何定义紧急数据的长度,只有一个指针指示它从哪里开始,而不是从哪里结束。
可追溯到 1980 年代初期的多个 RFC 对这些问题给出了相互矛盾的答案:
RFC 793 (1981, page 17) states:
紧急指针指向紧随紧急数据的八位字节的序列号。
However, RFC 1011 (1987, page 8) states:
第 17 页是错误的。 紧急指针指向紧急数据的最后一个八位字节(而不是非紧急数据的第一个八位字节)。
And RFC 1122 (1989, page 84) reinforces this approach:
…紧急指针指向紧急数据序列中最后一个八位字节(不是 LAST+1)的序列号。
Finally, the latest RFC to handle this issue ( RFC 6093 [2011, pages 6-7]) concludes:
考虑到只要 TCP 发送方和 TCP 接收方都为紧急指针实现相同的语义,那么让紧急指针指向“紧急数据之后的八位字节的序列号”与直接指向“紧急数据之后的八位字节的序列号”在功能上没有区别。“紧急数据的最后一个八位字节”,并且所有已知的实现都将紧急指针的语义解释为指向“紧急数据之后的八位字节的序列号”。
因此,由于几乎所有现有的 TCP 实现都将紧急指针作为指向紧随紧急数据的八位字节的序列号处理,因此这应该是所有堆栈的默认行为。
As to the length of the urgent data, RFC 6093 (page 5) also states this:
如果在应用程序读取待处理的 “带外” 字节之前收到 “紧急数据” 的连续指示,则目前的待处理字节将被丢弃(即,被“紧急数据”的新字节覆盖)。
得出的结论是紧急数据应始终为一个八位字节(字节)。
由于紧急指针机制的各种复杂性,一些实现(包括 VxWorks)被迫支持 RFC-1122 兼容模式和非兼容模式——其中紧急数据将指向计算出来的紧急指针的 ±1。
再加上整个功能本身是深奥的,导致它在各种操作系统中的实现和测试都很差。例如,本文档前面描述的 Windows NT/95 WinNuke 错误只需向机器发送任何 OOB 数据即可触发,因为它总是被 TCP 驱动程序错误处理,导致蓝屏死机。
IPnet 堆栈中的 TCP
在 VxWorks 的 IPnet 堆栈中,TCP 协议在 iptcp/src/iptcp.c 中实现。 堆栈接收到的每个 TCP 报文段都由函数 iptcp_input 处理:
函数 iptcp_input 会尝试使用 socklookup 回调函数将每个传入的报文段匹配到现有的 TCP 套接字。如果 4 元组与一个已存在的套接字不匹配,但目标 IP 和端口与绑定的监听套接字匹配,则报文段也可能与监听套接字匹配。在这种情况下,会发生对函数 iptcp_handle_passive_open 的额外调用来执行额外的逻辑,最后为这个连接创建一个新的客户端套接字,它与4元组相匹配。从那时起,该套接字将成为新的匹配套接字。第一个请求建立连接的 SYN 数据包的所有验证都由函数 iptcp_handle_passive_open 完成,例如丢弃所有的既是 FIN 又是 SYN 数据包。
再经过一些额外的检查后,该报文段被传递到函数 iptcp_deliver ,它在 TCP 套接字( tcb )的上下文中处理一个报文段:
首先,该函数检查套接字的状态。如果它刚刚创建,第一个段将由函数 iptcp_deliver_state_listen 或iptcp_deliver_state_syn_sent 以一种特殊的方式处理。来自对等方的Initial Sequence
Number将初始化 tcb->recv.seq_next,这是此套接字的next in-order sequence number。然后再在额外处理(例如处理 URG 和 FIN 标志)之后,该报文段可能会被传递到函数 iptcp_deliver_data中。这是管理 TCP 窗口的地方。一旦报文段中的有序数据可用,它最终将被添加到 sock->rcv_tail 列表中,这是 recv() 调用提取数据的列表。从 iptcp_input 函数开始,到用户应用程序调用 recv()结束的流程概述如下所示:
在用户态代码中,对 recv() 的调用将使用函数 iptcp_usr_get_from_recv_queue 从 sock->rcv_tail 队列中提取数据。然而,在此之前,此函数还通过访问状态变量 tcb->recv.urg_ptr 来处理任何已接收的紧急数据。如上面的反编译片段所示,每次收到带有 URG 标志的段时,都会在 iptcp_deliver 中更新此状态变量。
IPnet 堆栈中的 TCP 紧急数据问题
当函数 iptcp_deliver 接收到设置了 URG 标志的报文段时,tcb -> flags 中的 FLAG_RECEIVER_URG 标志被设置,并且 *tcb->recv.urg_ptr *的值被计算然后作为 TCP 窗口中紧急数据开始位置的偏移量。稍后,当被从 recv() 调用中调用时,函数 iptcp_usr_get_from_recv_queue 会使用此值。此值对于所有 recv 调用很重要,而不仅仅是为了 MSB_OOB 调用,因为段中的任何紧急数据都不会从常规的 recv 调用返回。因此,该值用于了解要从返回缓冲区中丢弃哪些字节。 以下是负责此流程的代码:
此代码更改 len 变量以防紧急数据被插入 TCP 窗口,从而影响当前的 recv () 调用。 如图所示:
recv () 调用请求读取 len 大小的数据,但这将包括紧急数据。上面代码的目的是在这种情况下缩短 len,使用户收到的数据少于请求的数据,而没有任何未请求的 OOB 数据:
我们以后将上述 len 缩短计算称为紧急数据偏移计算。
如前所述,由于紧急指针机制的各种复杂性,VxWorks包括一个RFC-1122兼容模式和一个不兼容模式——其中紧急数据指向计算出的紧急指针的±1。但是,默认情况下,VxWorks 不支持 RFC-1122 兼容模式,因此上述紧急数据偏移计算是从 tcb->recv.urg_ptr 中减去 1 来计算紧急数据开始的位置。
上面代码中引用的所有 3 个状态变量都是无符号的 32 位整数,上面的计算写入的 len 变量也是如此。我们在各种代码流中发现了 4 个不同的漏洞,最终都会造成紧急数据偏移量的计算下溢,从而导致 len 变量变成一个巨大的无符号整数。
这些漏洞中的第一个(紧急指针 = 0)会导致非一的计算,而其他三个变种是各种状态混淆状态引起的,这种状态将会导致 tcb->recv.urg_ptr 和 tcb->recv.seq_next 变量彼此处于矛盾状态。紧急数据偏移计算在以下假设下工作: 紧急指针 (tcb->recv.urg_ptr) 总是在TCP窗口开始的序列号前面,由 tcb->recv.seq_next -sock->ipcom.rcv_bytes 计算。一旦这个假设被打破,计算就会导致整数下溢,并且 len 将成为一个巨大的无符号整数。
由于 len 是用户传递给 recv() 调用的限制,这有效地允许攻击者完全禁用长度检查。这使得在 TCP 套接字上使用 recv 的任何用户空间代码容易受到本地接收缓冲区溢出的影响。 例如:
这种往常只将 1 个字节接收到堆栈变量中的安全代码现在变成了堆栈溢出。溢出长度由攻击者控制,选择要放入 TCP 窗口的数据量。溢出长度由攻击者控制,溢出长度选择要放入 TCP 窗口的数据量。尽管 len 成为一个巨大的数字,但实际写入的数据量仍然会受到窗口中可用数据量的限制。所以,只要传递给recv()的缓冲区小于已建立连接的 TCP 窗口,就会发生溢出。传递给 recv() 的缓冲区可以在堆栈、堆或应用程序的全局数据部分中分配,这意味着一旦触发此缓冲区的溢出将导致不同的效果。这种溢出的利用过程将不得不相应地变化。
发现的每个紧急指针漏洞都会影响一组不同的 VxWorks 版本,但它们的组合跨越了 6.5 及更高版本。以下部分将详细介绍可以触发上述溢出的四种不同代码流。
TCP紧急指针 = 0 整数下溢 (CVE-2019-12255)
如前所示,一个 TCP 连接的紧急指针在代码流 iptcp_deliver 内的变量 tcb->recv.urg_ptr 中设置。在 VxWorks 的 6.9.3 及以下版本中,此代码流如下所示:
如果接收到的 TCP 报文段段头中的 urgent_pointer 字段被设置为 0,tcb->recv.urg_ptr 将等于 p->seg.seq_start,后者是接收到的报文段的序列号。然后,当套接字用户将对套接字执行一个 recv() 操作时,将触发上述部分 (在iptcp_usr_get_from_recv_queue里面的代码) 中呈现的代码。因此,上节所示的函数 iptcp_usr_get_from_recv_queue 中的 If 条件可以通过替换等效值来重写:
注意到对于最后收到的报文段来说 p->seg.seq_start 等于 tcb->recv.seq_next - sock->ipcom.rcv_bytes 。因此,当将紧急指针设置为 0 时在最后一个接收到的报文段中,此条件始终为真。然后,len 变量应该被紧急数据偏移计算缩短(如上一节所述),但是:
该函数尝试求出超出范围 (0) 的紧急数据的偏移量,求出来的结果是 -1。由于 len 变量是一个无符号整数,这意味着它现在将等于 0xffffffff。如上所述,这将会导致用户在 recv() 调用中设置的长度限制被忽略,造成将所有可用数据从 TCP 窗口复制到用户提供的缓冲区。
在 VxWorks 的 6.9.4 版本中,在函数 iptcp_deliver 中添加了对紧急指针的额外验证:
这完全解决了上面详述的问题,防止紧急指针被设置为 0。但是,不幸的是,此更改并未被视为安全修复,也未向后移植到 VxWorks 的先前版本。我们检查过的所有运行 VxWorks 6.9.3 或更早版本的真实世界产品都容易受到此漏洞的影响。
TCP AO 选项格式错误造成的 TCP 紧急指针状态混淆(CVE-2019-12260)
虽然自 VxWorks 版本 6.9.3 以来,上面的 Urgent Pointer = 0 错误已修复,但与此同时 iptcp.c 模块经历了其他重构。添加了一些新功能,例如对新添加的 AO TCP 选项的处理,而这为代码引入了更深层次的错误。
在高于 6.9.3 的 VxWorks 版本中,添加了对 TCP AO 选项(身份验证选项,RFC-5925)的支持。这似乎默认被包含在内,并且确实得到了我们测试过的产品中的 VxWorks 映像的支持。下面给出的漏洞不依赖于实际启用或使用的 TCP AO,因为它总是由 TCP 模块解析。