TCP零窗口攻击?

初识零窗口

  13年时曾经遇到过一个问题(原谅我现在才写这篇文章…):提供下载服务的生产环境上(SUSE Linux,使用Tomcat BIO的Connector提供服务),有大量的状态为ESTABLISHED的连接存在。本来作为下载服务器,有大量连接存在是很正常的事情,但是由于数量远高于平时,引起维护人员的关注并只会到研发这边,我们通过观察这些链接的客户端IP和端口,发现大部分连接一直没有断开的迹象,tomcat创建的大量线程处于阻塞状态(BIO),服务器打开连接句柄过多,都可能会影响新的下载服务请求。情况很像是遇到了DDoS,当然也不排除是下载客户端存在BUG导致。
  通过抓包发现,这些链接无一例外都处于一个无限循环中:每隔一段时间,服务端发送长度为0的KEEP-ALIVE,客户端回复ZeroWindow,如此循环。
TCP零窗口攻击?_第1张图片

TCP滑动窗口

  滑动窗口的概念大家可以自行搜索,这里就不赘述了,大致的概念是发送和接受双方都各有一个发送窗口和一个接受窗口,其主要目的就是为了流量控制,使双方的发送、接收速度尽可能匹配。

零窗口(ZeroWindow)

  当发送方的发送速度大于接收方的处理速度,接收方的缓冲塞满后,就会告诉发送方当前窗口size=0,请停止发送,发送方此时停止发送数据。

坚持定时器(TCP Persist Timer)

  零窗口出现后,如何继续数据的传输呢。我们假设:接收方窗口更新为非0后,发送ACK给发送方,更新window的size,是一种可行的方式。但是如果这个ACK丢掉了(TCP不会给ACK回复ACK),那么发送方和接收方就会处于一种尴尬的局面,一个等着发,一个等着收,直到超时。
  此时引入了坚持定时器的概念,由发送方主动创建,持续不断地询问接收方窗口是否更新,这个询问被称为窗口探测(window probes)。
  终于到了问题的关键,这个定时器没有时间限制,意味着这个TCP连接会永远保持下去。

是不是DDos攻击

google了很久,没有查到各大操作系统对于坚持定时器的时间设置,提到零窗口攻击的信息也很少,截取其中一个明确提到零窗口攻击的页面,来自一个硬件负载均衡的厂商的产品介绍,貌似硬件防火墙或者负载均衡设备应该有相关的防护设置:
原链接
TCP零窗口攻击?_第2张图片

问题重现

  这个现象还是很容易重现的,当时使用IE浏览器(6还是7来着),直接下载一个文件,在弹出确认提示框后不要点击保存或者另存为,放着不动,用wireshark等工具查看,就会发现此时客户端服务端就进入了零窗口探测的状态,且会一直持续下去。
  13年时chrome、firefox浏览器都不能重现,因为他们比较聪明,会自动开始下载,不给你确认的机会…现在19年的IE11也不重现了,确认框还是弹的,但是在我们不确认的情况下,后台已经下载完毕了。
  这也难不倒我们,用Socket写个客户端,模拟建链后不接受数据的情况,服务端就用tomcat,随便在webapps下建个目录放一个zip文件用来下载。代码如下(完整代码点击这里):

public class Client
{
    public static void main(String[] args)
        throws Exception
    {
        Socket client = new Socket("10.253.178.218", 8080);
        client.setSoTimeout(10000);

        StringBuilder sb = new StringBuilder();

        // HTTP协议中的换行符为CRLF,即\r\n,每一行请求消息头都需要以其结尾
        sb.append("GET /fst/aaa.zip HTTP/1.1\r\n");
        sb.append("Host: 10.253.178.218\r\n");
        sb.append("Connection: keep-alive\r\n");
        sb.append("Accept: */*\r\n");
        sb.append("User-Agent: JavaSocket\r\n");

        // 消息头结束需要单独一行CRLF
        sb.append("\r\n");
        OutputStream os = client.getOutputStream();
        InputStream is = client.getInputStream();
        os.write(sb.toString().getBytes("UTF-8"));
        os.flush();

        ......

        // HTTP协议中的换行符为CRLF,即\r\n,读取到单独的一行\r\n意味着消息头读取完毕。
        while (!"\r\n".equals(line))
        {
            line = readLine(is);
            
            // 模拟缓冲满的情况
            Thread.sleep(1000000000000000000L);
            if (line.startsWith("Content-Length"))
            {
                contentLength = Long.parseLong(line.split(":")[1].trim());
            }

            System.out.print(line);
        }
        ......
    }

解决办法

当时的办法

  修改Tomcat源码,将socket对象暴露给业务代码,业务代码将所有socket和已经发送的字节数缓存起来(每次发送数据后即更新对应的字节数),每隔一段时间扫描一遍,将超过阈值时间后发送字节仍然没有变化的socket,强制调用socket.close()将其关闭。

socket.setSoLinger(true, 0); // socket是阻塞IO,数据不发送完默认是关不掉的,需要设置soLinger
socket.close();

新的问题

  写这篇文章时,试了一下NIO的Connector会如何,测试结果也不尽人意,坚持定时器仍然生效,过一会儿连接状态迁移为FIN_WAIT1(抓包里没看到发FIN,奇怪),连接句柄仍然被占用。

恐怖的是,./shutdown.sh关了tomcat,句柄仍不能释放,坚持定时器仍然在持续不断地探测…客户端已经利用TCP的机制,绑架了服务端操作系统层面的句柄资源?

  作者对网络和系统层面了解不多,欢迎大家提出更优雅的解决方法,或者提出指正。

参考
[1]: http://www.pcvr.nl/tcpip/tcp_pers.htm

你可能感兴趣的:(web应用)