如何伪造 TCP 握手?

据我们所知,这种攻击是新发现的。询问一下周围的人,几乎都认为TCP握手会对两侧的IP地址进行验证。这种攻击表明实际上并非如此。

在一个方提斯大学的合作项目中,我和Raoul Houkes研究不同的TCP攻击方式,要不就在协议层面,要不就针对具体的实现。我们发现了一种协议方面的攻击,它可以影响到所有正确的协议实现。

客户端A要与B建立连接,TCP握手的过程是这样的:

A:你好,B。我是A。发送序列号(SYN)是5。
B:你好,A。我是B,确认序列号(ACK)是5,发送序列号(SYN)是3。
A:你好,B。我是A,确认序列号(ACK)是3,发送序列号(SYN)是6。我要请求example.net。
B:你好,A。我是B,确认序列号(ACK)是6.发送序列号(SYN)是4。下面是你请求的结果:……

然后,A和B之间就可以相互通信了。对A或者B而言,每发送一个byte,序列号都会相应地增加。这样就可以跟踪是否所有的数据都被对方接收,保证可靠的传输。

在1981年,这种通讯方式被设计出来的时候,安全并没有那么重要。ARPANET适应单一网络,并且他们需要这样一个发送数据的协议:能够进行出错重传,错误校验,并且可以保持包的有序性等等。TCP解决了所有这些问题。

目前,这两个数字域,分别叫做发送序列号(SYN)和确认序列号(ACK),用来保证安全可靠的传输。但是,同时也存在两个问题:

  1. 这两个数字域都只有32字节,无法记录更大的数值。
  2. 由于它们的双重目的,序号不正确的报文会被丢弃,而不破坏连接。换言之,你可以发送一个ACK不正确的报文,然后再发送一个ACK正确的报文,正确的报文可以被接收。

A向B发送数据包,我们利用上面两个特性来进行攻击,大致过程如下:

A:你好B。我是C。发送序列号(SYN)是5。
B:你好C。我是B。确认序列号(ACK)是5。发送序列号(SYN)是3。
A:你好,B。我是C,确认序列号(ACK)是 1,发送序列号(SYN)是6。我要请求example.net。
B:你好C。我是B,你的报文不正确。请关闭连接。
A:你好,B。我是C,确认序列号(ACK)是2,发送序列号(SYN)是6。我请求example.net。
B:你好C,我是B。这是不正确的。请关闭连接。
A:你好B,我是C,确认序列号(ACK)是3。发送序列号(SYN)是6。我想请求example.net。
B:你好C,我是B,确认序列号(ACK)是6,发送序列号(SYN)是4,以下是你请求的结果……

在上面的案例中,主机A没有收到B的任何消息,B也不知道响应的是一个假的IP地址。主机A伪装成了C的IP地址。

这中攻击的先决条件之一是真正的C没有发送后续报文(或者RST报文),但是这非常容易实现,使用一个并不存在的C(比如0.0.0.0)或者利用防火墙(客户端一般都处于全状态防火墙的后面,或者是NAT, 也或者两者都有)。

B等待C(或者其他客户端)确认消息的时间是有限的。我在Linux4.2内核上做过实验,等待时间是20秒。20秒之后,你需要重新开始(发送下一个SYN),但是这没有什么意义,因为选择的序列号完全是随机的。

这种攻击的成本如何呢?平均而言,平均而言,需要耗费120GB的网络流量(60byte的因特网头,IP头和TCP头的组合)来创建一个虚假连接。也许你很不幸要花费200GB的流量,也可能很幸运只要72GB就够了。

简单调查表明很多VPS系统中1 gbps的带宽花不了多少钱。如果你能充分利用可用带宽,平均而言这种攻击17分钟11秒就够了。

通常你想注入有效载荷,例如,发送一个命令。这个命令需要拼接到已经存在的数据上,使得攻击耗费的流量更大。例如,发送“GET/HTTP/1.0nn”平均下来要花费了152GB或者20分钟。这可以在访问日志中展体现出来,虽然看上去是一个完美的正常连接。

这种攻击的其他案例还包括绕过黑名单或者白名单,比如在某些系统的管理接口上。这从90年代就非常流行,而且现在仍然广泛存在,很多新应用设备还是才用这种方式运行。

概念证明(Proof of concept,简写作PoC)

一个研究项目没有Poc像什么话呢?下面是一些Wireshark的截图,一个dump包,以及用到的代码。

(点击查看大图)

我筛选出了一些192.168.36.17捕获的相关包。第一个包是初始化包,192.168.36.11发送的,伪装的IP是192.168.36.18.。我们的目标主机向虚假IP 192.168.36.18发送响应报文。192.168.36.17开始猜测正确的ACK号。注意,时间从0.x秒增加到8.x秒,这里我把一些尝试过滤掉了。在某一时刻数字从2的32次方(4十多亿)到0,这时因为Wireshark给了我们一个相对的数字。这也意味着我们已经发现了正确的序号。我们向SSH服务器发送确认序列号(ACK)1,服务器收到这个序号后,开始响应。SSH服务器假定拥有的连接都是有效TCP连接。

以下是一些更详细的会话内容:

(点击查看大图)

Server选取的的随机数字是0x0006943f (或者 431167)。

(点击查看大图)

在某一时刻,我们脚本猜测出了0×00069440 (或者 431168),这是正确的数字,因为我们需要发送我们收到的数字加1。

(查看大图)

SSH服务器对此作出了响应,发送了一个标志有效连接开始的标题报文。

原始dump包只有15秒,因为我只在捕获了日志交替之前15秒的事件。感觉15秒时间并不长,但是这有5 544 384(550万)个包,几乎占领十亿字节的一半。如果你想看一下实际效果,可以运行下面提供的攻击代码攻击。

上面的看到的dump包可以点击下面的链接下载:

spoofed-tcp-connection.pcap

最后,进行攻击的代码也可以点击下面的链接下载:

attack-tcp.py

作为一个真正的PoC,以上代码是为了这个目的而写的,没有考虑可维护性。

结论

这种攻击很难避免,因为这是TCP协议的本身的特性造成的。只能漫无目的的猜测可能被注入的无效ACK,然后将它作为一个关闭连接的理由,但即便如此还是很容易受到攻击。

为了验证一次连接的双方,需要用到附加的安全验证方式例如TLS。即使证书没有被授权,一些加密的TLS会话还是要进行,因为有些附加的数据需要传送到客户端。这使得证明变得不可行。

经验总结:不要使用基于IP地址的授权,不要相信IP地址白名单,当需要安全验证(或者不可可抵赖)的时候考虑使用安全协议。
作为一个真正的PoC,以上代码是为了这个目的而写的,没有考虑可维护性。

如何伪造 TCP 握手?,首发于博客 – 伯乐在线