Switch-Router

理解 TCP 初始序号选择(ISN Selection)

Published at 2020-11-12 | Last Update 2020-11-12

本文将回顾一些 rfc 标准, 再结合 linux 内核实现解释 TCP ISN 选择.

TCP 序号空间是 4G (32 bit), 通信双方在 3-way 握手阶段, 会各自选择一个初始序号填入 SYN / SYN-ACK 报文, 作为数据通信的起始序号.

时间驱动 (clock-driven)

为什么要特意进行 ISN 的选择? 每次都从 0 开始不行吗? 每次都随机可以吗?

TCP原始标准rfc793中这样描述初始序号选择的来历:

“how does the TCP identify duplicate segments from previous incarnations of the connection?” This problem becomes apparent if the connection is being opened and closed in quick succession, or if the connection breaks with loss of memory and is then reestablished.

ISN Selection 的目的就是为了能区分旧报文(identify duplicate segments).

要达到这个效果, 天然的想法就是让新旧连接的序号空间不重叠(overlap), 让 ISN 跟随时钟在序号空间内单调递增.

When new connections are created, an initial sequence number (ISN) generator is employed which selects a new 32 bit ISN. The generator is bound to a (possibly fictitious) 32 bit clock whose low order bit is incremented roughly every 4 microseconds. Thus, the ISN cycles approximately every 4.55 hours.

rfc793给出的方案是设置一个 ISN generator 绑定到一个 32 bit 的时钟上,该时钟按大约每 4 μs 加一的速率递增, 这样算下来大约 4.55 小时会循环整个 32 bit 空间.

为什么是 4 μs (250 kHz)?

其实, 这是以连接速率为 250 KBps (2 Mbps) 为标准设置的. 下面这个图摘自rfc1185 (已废弃), 图中 * 的轨迹就表示 ISN.

        |- 2**32       ISN             ISN
        |              *               *
        |             *               *
        |            *               *
        |           *x              *
        |          o               *
    ^   |         *               *
    |   |        *  x            *
        |       * o             *
    S   |      *o              *
    e   |     o               *
    q   |    *               *
        |   *               *
    #   |  * x             *
        | *o              *
        |o_______________*____________
                         ^         Time -->
                       4.55hrs

     Figure 1.  Clock-Driven ISN  avoiding duplication on
                short-Lived, slow connections.

对于 short (持续时间小于4.55小时)和 slow (速率小于2 Mbps)的连接来说, 250 kHz 增加的 ISN 即足够确保 TCP 能识别出 old 报文. 但对于更 long 和 更 fast 的连接, 就还需要其他机制.

4 μs (250 kHz) 只是一个建议值. 速率越快的连接就需要越快的时钟.

4.55 小时是如何计算的呢?

2**32 / 250 kHz = 17180s = 4.77 hours   看上去似乎对不上

我觉得这里它是这样算的:

250kHz = 1/4 MHz  (按 1M = 1000K 换算)

4G / 250kHz = 4G / (1/4 MHz) = 16K = 16384s = 4.55 hours   (按 1K=1024 换算)

不过 4.55 hours 和 4.77 hours 差别也不大,它们都远远大于 2*MSL = 120s.

ISN 预测攻击

光有时钟还不够, rfc6528 中提到了针对 ISN 生成器的预测攻击.

Unfortunately, the ISN generator described in [RFC0793] makes it trivial for an off-path attacker to predict the ISN that a TCP will use for new connections, thus allowing a variety of attacks against TCP connections [CPNI-TCP]. One of the possible attacks that takes advantage of weak sequence numbers was first described in [Morris1985], and its exploitation was widely publicized about 10 years later [Shimomura1995]. [CERT2001] and [USCERT2001] are advisories about the security implications of weak ISN generators. [Zalewski2001] and [Zalewski2002] contain a detailed analysis of ISN generators, and a survey of the algorithms in use by popular TCP implementations.

简单解释下这种攻击的原理:

client C <-----> Server S
 (IP_C)     ^     (IP_S)
            |
            |
        Attacker X

Client C 和 Server S 原本可以进行正常的 TCP 通信. 现在攻击者 X 希望欺骗 Server, 假借 C 的名义与 S 进行通信.

下面是攻击步骤:

  1. X 对 C 发送大量垃圾报文, 让 C 无法正常发送报文给 S (堵住C的嘴)
  2. X 发送 SYN 报文给 S, SYN 报文中 srcIP = IP_C, seq = ISN_X, 即以 C 的 IP 地址为源地址向 S 建立连接.
  3. S 收到 SYN 后会回复 SYN ACK, seq = ISN_S, dstIP = IP_C. 目标地址为 C. 因此这个报文不会到达 X, 但由于 C 此时处于闭嘴状态, 也不会正确回复 RST
  4. X 预测 ISN\_S , 发送 3-way 握手的 ACK , seq = ISN_S + 1,S 收到后便会以为是 C 发起的连接.

以上的步骤中, 重点就是攻击者 X 是否能正确预测 S 的初始序号 ISN_S.

以上攻击步骤有一个前提: 攻击者 X 并不具备嗅探 C 与 S 的交互报文的条件, 否则它始终能获得 ISN_S

如果 ISN 仅仅与时钟相关, 那么攻击者一旦知道 S 的启动时间, 即可推算 ISN_S.

或者如果 S 没有白名单机制的话, X 也可以事先通过发送 srcIP = IP_X 的 SYN 报文得到 S 的 SYN ACK 响应报文中 ISN_S.

因此,ISN 需要加入除了时钟之外的其他因素

rfc6528 将四元组和一个secretkey加入计算.

TCP SHOULD generate its Initial Sequence Numbers with the expression:

ISN = M + F(localip, localport, remoteip, remoteport, secretkey)

where M is the 4 microsecond timer, and F() is a pseudorandom function (PRF) of the connection-id. F() MUST NOT be computable from the outside, or an attacker could still guess at sequence numbers from the ISN used for some other connection.

这样一来, 即使同一时刻, 每个四元组的都会有不同的 ISN, 只要 secretkey 不被猜到, X 就无法预测 ISN_S.

Linux 内核实现

Linux 内核从很久之前就已经遵从这样的方式. 至少在 2.6 版本就已经是这样了.

此时还是使用没 1μs 时钟增加一.

__u32 secure_tcp_sequence_number(__u32 saddr, __u32 daddr,
				 __u16 sport, __u16 dport)
{
   ...
   seq += tv.tv_usec + tv.tv_sec*1000000;
}

后来也许为了兼容更高速的网络, 将这个值改为了 64ns, 参考4.9.0内核

static u32 seq_scale(u32 seq)
{
	/*
	 *	As close as possible to RFC 793, which
	 *	suggests using a 250 kHz clock.
	 *	Further reading shows this assumes 2 Mb/s networks.
	 *	For 10 Mb/s Ethernet, a 1 MHz clock is appropriate.
	 *	For 10 Gb/s Ethernet, a 1 GHz clock should be ok, but
	 *	we also need to limit the resolution so that the u32 seq
	 *	overlaps less than one time per MSL (2 minutes).
	 *	Choosing a clock of 64 ns period is OK. (period of 274 s)
	 */
	return seq + (ktime_get_real_ns() >> 6);
}