# 如何伪造 TCP 握手？

- URL: https://wuming.si/blog/1149/
- Author: Aaron
- Published: 2016-03-14
- Language: zh-Hans
- Categories: Uncategorized
- Originally published at: https://blog.wo.ai/archives/1149

据我们所知，这种攻击是新发现的。询问一下周围的人，几乎都认为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包，以及用到的代码。

[![](https://blog.wo.ai/wp-content/plugins/wp-o-matic/cache/f0dd5adbfe_7cc829d3jw1f1wstnzg2pj20sr07279p.jpg)](https://blog.wo.ai/wp-content/plugins/wp-o-matic/cache/f0dd5adbfe_7cc829d3jw1f1wstnzg2pj20sr07279p.jpg)

[（点击查看大图）](https://blog.wo.ai/wp-content/plugins/wp-o-matic/cache/f0dd5adbfe_7cc829d3jw1f1wstnzg2pj20sr07279p.jpg)

我筛选出了一些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连接。

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

[![](https://blog.wo.ai/wp-content/plugins/wp-o-matic/cache/6819109349_7cc829d3jw1f1wstp8isbj20ul0ecdq5.jpg)](https://blog.wo.ai/wp-content/plugins/wp-o-matic/cache/6819109349_7cc829d3jw1f1wstp8isbj20ul0ecdq5.jpg)

[（点击查看大图）](https://blog.wo.ai/wp-content/plugins/wp-o-matic/cache/6819109349_7cc829d3jw1f1wstp8isbj20ul0ecdq5.jpg)

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

[![](https://blog.wo.ai/wp-content/plugins/wp-o-matic/cache/6819109349_7cc829d3jw1f1wstpv9vrj20uj0frk16.jpg)](https://blog.wo.ai/wp-content/plugins/wp-o-matic/cache/6819109349_7cc829d3jw1f1wstpv9vrj20uj0frk16.jpg)

[（点击查看大图）](https://blog.wo.ai/wp-content/plugins/wp-o-matic/cache/6819109349_7cc829d3jw1f1wstpv9vrj20uj0frk16.jpg)

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

[![](https://blog.wo.ai/wp-content/plugins/wp-o-matic/cache/66a1facd7d_7cc829d3jw1f1wstqvm2ij20uk0grdq7.jpg)](https://blog.wo.ai/wp-content/plugins/wp-o-matic/cache/66a1facd7d_7cc829d3jw1f1wstqvm2ij20uk0grdq7.jpg)

[（查看大图）](https://blog.wo.ai/wp-content/plugins/wp-o-matic/cache/66a1facd7d_7cc829d3jw1f1wstqvm2ij20uk0grdq7.jpg)

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

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

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

[spoofed-tcp-connection.pcap](http://lgms.nl/files/spoofed-tcp-connection.pcap)

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

[attack-tcp.py](http://lgms.nl/files/attack-tcp.py)

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

## 结论

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

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

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

### 相关文章

  * [从TCP协议的原理来谈谈RST复位攻击](http://blog.jobbole.com/72910/)
  * [Redis 未授权访问缺陷可轻易导致系统被黑](http://blog.jobbole.com/94518/)
  * [Nginx 的 TCP 负载均衡介绍 ](http://blog.jobbole.com/91757/)
  * [通信协议：HTTP、TCP、UDP](http://blog.jobbole.com/84429/)
  * [理解 TCP/IP 网络栈 & 编写网络应用](http://blog.jobbole.com/86656/)
  * [Wireshark 基本介绍和学习 TCP 三次握手](http://blog.jobbole.com/94996/)
  * [互联网协议入门（二）](http://blog.jobbole.com/77931/)
  * [互联网协议入门（一）](http://blog.jobbole.com/77851/)
  * [HTTP 长连接和短连接](http://blog.jobbole.com/93960/)
  * [C#高性能 TCP 服务的多种实现方式](http://blog.jobbole.com/97902/)

[如何伪造 TCP 握手？](http://blog.jobbole.com/98768/)，首发于[博客 – 伯乐在线](http://blog.jobbole.com)。
