目录
DDos简介
DDos的前身是 DoS(Denail of Service),即拒绝服务攻击,指利用大连的合理请求,占用过多的目标资源,从而使目标服务器无法响应正常请求
DDos(Distributed Denail of Service),是在Dos的基础上,采用了分布式架构,利用多台主机同时攻击目标主机,这样即使目标服务器部署了网络防御设备,面对大量网络请求时,还是无能为力
目前已知最大流量攻击,是2018年Github遭受的DDos攻击,峰值达到了1.35Tbps,PPS达到了1.2亿
从攻击原理看,DDos分为以下几类
1.耗尽贷款,无论是服务器还是路由器,交换机等网络设备,带宽都有固定上限,带宽耗尽后就会发生网络拥堵,从而无法传输其他正常的网络报文
2.耗尽操作系统的资源,网络服务的正常运行,都需要一定的资源,像CPU,内存等物理资源,以及连接表等软件资源,一旦耗尽,系统就不能处理其他正常的网络连接
3.消耗应用程序的的资源,应用的运行通常需要跟其他资源或系统交互,如应用程序一直忙于处理无效请求,也会导致正常请求的处理变慢,甚至得不到响应
比如构造大量不同的域名来攻击DNS服务器,就会导致DNS服务器不停的执行迭代查询,并更新缓存,这会极大地消耗DNS服务器资源,使DNS响应变慢
案例准备
环境如下
这里只使用了一台机器只是传统的Dos,而非DDos
案例分析
输入下面命令,运行nginx
# 运行Nginx服务并对外开放端口
docker run -itd -p 9527:80 nginx
用curl访问nginx
-w 表示只输出 HTTP 状态码及总时间,-o 表示将响应重定向到 /dev/null
curl -s -w 'Http code: %{http_code}\nTotal time:%{time_total}s\n' -o /dev/null http://【服务端
IP】:9527/
Http code: 200
Total time:0.006s
运行 hping3 命令,来模拟 DoS 攻击:
# -S 参数表示设置 TCP 协议的 SYN(同步序列号),-p 表示目的端口为 80
# -i u50 表示每隔 50 微秒发送一个网络帧
$ hping3 -S -p 9527 -i u50 【服务端IP】
再用curl访问nginx
# --connect-timeout 表示连接超时时间
# 需要把 hping3的 发送时间减小到120-200之间,否则tty终端会卡主
curl -w 'Http code: %{http_code}\nTotal time:%{time_total}s\n' -o /dev/null --connect-timeout 10
http://【服务端IP】:9527/
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- 0:00:10 --:--:-- 0
Http code: 000
Total time:10.002s
curl: (28) Connection timed out after 10001 milliseconds
查看网络流量情况
sar -n DEV 1
01:40:41 PM IFACE rxpck/s txpck/s rxkB/s txkB/s rxcmp/s txcmp/s rxmcst/s
01:40:42 PM docker0 0.00 0.00 0.00 0.00 0.00 0.00 0.00
01:40:42 PM lo 0.00 0.00 0.00 0.00 0.00 0.00 0.00
01:40:42 PM veth0135a4e 0.00 0.00 0.00 0.00 0.00 0.00 0.00
01:40:42 PM eth0 0.00 4829.35 0.00 256.32 0.00 0.00 0.00
01:40:42 PM IFACE rxpck/s txpck/s rxkB/s txkB/s rxcmp/s txcmp/s rxmcst/s
01:40:43 PM docker0 0.00 0.00 0.00 0.00 0.00 0.00 0.00
01:40:43 PM lo 0.00 0.00 0.00 0.00 0.00 0.00 0.00
01:40:43 PM veth0135a4e 0.00 0.00 0.00 0.00 0.00 0.00 0.00
01:40:43 PM eth0 0.00 4812.90 0.00 254.35 0.00 0.00 0.00
(254.35*1024)/4812.9 计算出每个包大概54字节
很明显是有大量的小包
tcpdump抓包分析
tcpdump -i eth0 -n tcp port 9527
13:45:49.182714 IP 【客户端】.9527 > 【服务端】.talarian-mcast3: Flags [S.], seq 2826135571, ack
1762609067, win 64240, options [mss 1460], length 0
13:45:49.182718 IP 【客户端】.9527 > 【服务端】.talarian-mcast4: Flags [S.], seq 586427663, ack
1833631763, win 64240, options [mss 1460], length 0
13:45:49.182724 IP 【客户端】.9527 > 【服务端】.dnox: Flags [S.], seq 1795830526, ack 1902584524,
win 64240, options [mss 1460], length 0
13:45:49.182730 IP 【客户端】.9527 > 【服务端】.nexus-portal: Flags [S.], seq 295657488, ack
197474288, win 64240, options [mss 1460], length 0
这个输出中,Flags [S] 表示这是一个 SYN 包。大量的 SYN 包表明,这是一个 SYN Flood 攻击
实际上,SYN Flood 正是互联网中最经典的 DDoS 攻击方式。从上面这个图,你也可以看到它的原理:
- 即客户端构造大量的 SYN 包,请求建立 TCP 连接;
- 而服务器收到包后,会向源 IP 发送 SYN+ACK 报文,并等待三次握手的最后一次 ACK 报文,直到超时。
- 这种等待状态的 TCP 连接,通常也称为半开连接。由于连接表的大小有限,大量的半开连接就会导致连接表迅速占满,从而无法建立新的 TCP 连接。
下面这张 TCP 状态图,你能看到,此时,服务器端的 TCP 连接,会处于 SYN_RECEIVED 状态:
查看 TCP 半开连接的方法,关键在于 SYN_RECEIVED 状态的连接。我们可以使用 netstat ,来查看所有连接的状态,不过要注意,SYN_REVEIVED 的状态,通常被缩写为 SYN_RECV。
执行下面的 netstat 命令:
# -n 表示不解析名字,-p 表示显示连接所属进程
$ netstat -n -p | grep SYN_REC
tcp 0 0 【服务端】:9527 【客户端】:12503 SYN_RECV -
tcp 0 0 【服务端】:9527 【客户端】:13502 SYN_RECV -
tcp 0 0 【服务端】:9527 【客户端】:15256 SYN_RECV -
tcp 0 0 【服务端】:9527 【客户端】:18117 SYN_RECV -
从结果中,可以发现大量 SYN_RECV 状态的连接,找到源端ip,并将这个ip给禁掉
用iptables把DDoS给禁掉
iptables -I INPUT -s 【DDoS的ip】 -p tcp -j REJECT
再用curl访问,速度快很多了
curl -w 'Http code: %{http_code}\nTotal time:%{time_total}s\n' -o /dev/null --connect-timeout 10
http://【服务端IP】:9527/
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 612 100 612 0 0 100k 0 --:--:-- --:--:-- --:--:-- 119k
Http code: 200
Total time:0.016s
SYN Flood 攻击中的源 IP 并不是固定的。比如,可以在 hping3 命令中,加入 --rand-source 选项,来随机化源 IP。这时,刚才的方法就不适用了。
解决办法如下
# 限制 syn 并发数为每秒 1 次
$ iptables -A INPUT -p tcp --syn -m limit --limit 1/s -j ACCEPT
# 限制单个 IP 在 60 秒新建立的连接数为 10
$ iptables -I INPUT -p tcp --dport 80 --syn -m recent --name SYN_FLOOD --update --seconds 60 --hitcount 10 -j REJECT
如果是多台机器同时发送 SYN Flood,这种方法可能就直接无效了。可能连SSH都登不上了
所以,需要事先对系统做一些 TCP 优化。
比如,SYN Flood 会导致 SYN_RECV 状态的连接急剧增大,可以把默认的半连接数调大
不过,半开状态的连接数是有限制的,可以执行下面命令
# 查看半连接大小
sysctl net.ipv4.tcp_max_syn_backlog
net.ipv4.tcp_max_syn_backlog = 256
# 将半连接大小改为2048个
sysctl -w net.ipv4.tcp_max_syn_backlog=2048
net.ipv4.tcp_max_syn_backlog = 2048
另外,连接每个 SYN_RECV 时,如果失败的话,内核还会自动重试,并且默认的重试次数是 5 次。可以执行下面的命令,将其减小为 1 次:
$ sysctl -w net.ipv4.tcp_synack_retries=1
net.ipv4.tcp_synack_retries = 1
TCP SYN Cookies 也是一种专门防御 SYN Flood 攻击的方法。SYN Cookies 基于连接信息(包括源地址、源端口、目的地址、目的端口等)以及一个加密种子(如系统启动时间),计算出一个哈希值(SHA1),这个哈希值称为 cookie。
然后,这个 cookie 就被用作序列号,来应答 SYN+ACK 包,并释放连接状态。当客户端发送完三次握手的最后一次 ACK 后,服务器就会再次计算这个哈希值,确认是上次返回的 SYN+ACK 的返回包,才会进入 TCP 的连接状态。因而,开启 SYN Cookies 后,就不需要维护半开连接状态了,进而也就没有了半连接数的限制。
注意,开启 TCP syncookies 后,内核选项 net.ipv4.tcp_max_syn_backlog 也就无效了。
通过下面的命令,开启 TCP SYN Cookies:
$ sysctl -w net.ipv4.tcp_syncookies=1
net.ipv4.tcp_syncookies = 1
注意,上述 sysctl 命令修改的配置都是临时的,重启后这些配置就会丢失。所以,为了保证配置持久化,你还应该把这些配置,写入 /etc/sysctl.conf 文件中。比如:
$ cat /etc/sysctl.conf
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_synack_retries = 1
net.ipv4.tcp_max_syn_backlog = 2048
注意,写入 /etc/sysctl.conf 的配置,需要执行 sysctl -p 命令后,才会动态生效。
案例结束后,执行 docker rm -f nginx 命令,清理案例开始时启动的 Nginx 应用。
DDoS 到底该怎么防御
实际上,当 DDoS 报文到达服务器后,Linux 提供的机制只能缓解,而无法彻底解决。即使像是 SYN Flood 这样的小包攻击,其巨大的 PPS ,也会导致 Linux 内核消耗大量资源,进而导致其他网络报文的处理缓慢。
虽然可以调整内核参数,缓解 DDoS 带来的性能问题,却也会像案例这样,无法彻底解决它。
类此C10K,C10M问题,Linux 内核中冗长的协议栈,在 PPS 很大时,就是一个巨大的负担。对 DDoS 攻击来说,也是一样的道理。
所以,当时提到的 C10M 的方法,用到这里同样适合。比如,你可以基于 XDP 或者 DPDK,构建 DDoS 方案,在内核网络协议栈前,或者跳过内核协议栈,来识别并丢弃 DDoS 报文,避免 DDoS 对系统其他资源的消耗。
不过,对于流量型的 DDoS 来说,当服务器的带宽被耗尽后,在服务器内部处理就无能为力了。这时,只能在服务器外部的网络设备中,设法识别并阻断流量(当然前提是网络设备要能扛住流量攻击)。比如,购置专业的入侵检测和防御设备,配置流量清洗设备阻断恶意流量等。
DDoS 并不一定是因为大流量或者大 PPS,有时候,慢速的请求也会带来巨大的性能下降(这种情况称为慢速 DDoS)。
比如,很多针对应用程序的攻击,都会伪装成正常用户来请求资源。这种情况下,请求流量可能本身并不大,但响应流量却可能很大,并且应用程序内部也很可能要耗费大量资源处理。
这时,就需要应用程序考虑识别,并尽早拒绝掉这些恶意流量,比如合理利用缓存、增加 WAF(Web Application Firewall)、使用 CDN 等等。