通过阅读这篇文章,你会了解到这些知识
- ESTABLISHED 状态的连接收到乱序包会回复什么
- Challenge ACK 的概念
- ACK 报文限速是什么鬼
- SystemTap 工具在 linux 内核追踪中的使用
- 包注入神器 scapy 的使用
- RST 攻击的原理
- killcx 等工具利用 RST 攻击的方式来杀掉连接的原理
接下来开始文章的内容。
scapy 实验复现现象
实验步骤如下:
在机器 A(10.211.55.10) 使用 nc 启动一个服务程序,监听 9090 端口,如下所示。
nc -4 -l 9090
机器 A 上同步使用 tcpdump 抓包,其中 -S 表示显示绝对序列号。
sudo tcpdump -i any port 9090 -nn -S
在机器 B 使用 nc 命令连接机器 A 的 nc 服务器,输入 "hello" 。
nc 10.211.55.10 9090
使用 netstat 可以看到此次连接的信息。
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program nametcp 0 0 10.211.55.10:9090 10.211.55.20:50718 ESTABLISHED 9029/nc
在机器 B 上使用 scapy,模拟发送 SYN 包,scapy 脚本如下所示。
send(IP(dst="10.211.55.10")/TCP(sport=50718, dport=9090, seq=10, flags='S'))
源端口号 sport 使用此次连接的临时端口号 50718,序列号随便写一个,这里 seq 为 10。
执行 scapy 执行上面的代码,tcpdump 中显示的包结果如下。
// nc 终端中 hello 请求包18:41:51.956735 IP 10.211.55.20.50718 > 10.211.55.10.9090: Flags [P.], seq 3219267420:3219267426, ack 2848436085, win 229, options [nop,nop,TS val 1094540820 ecr 12823113], length 618:41:51.956787 IP 10.211.55.10.9090 > 10.211.55.20.50718: Flags [.], ack 3219267426, win 227, options [nop,nop,TS val 12827910 ecr 1094540820], length 0// scapy 的 SYN 包18:44:32.373331 IP 10.211.55.20.50718 > 10.211.55.10.9090: Flags [S], seq 10, win 8192, length 018:44:32.373366 IP 10.211.55.10.9090 > 10.211.55.20.50718: Flags [.], ack 3219267426, win 227, options [nop,nop,TS val 12988327 ecr 1094540820], length 0
可以看到,对于一个 SEQ 为随意的 SYN 包,TCP 回复了正确的 ACK 包,其确认号为 3219267426。
从 rfc793 文档中也可以看到:
Linux 内核对于收到的乱序 SYN 报文,会回复一个携带了正确序列号和确认号的 ACK 报文。
这个 ACK 被称之为 Challenge ACK。
我们后面要介绍的杀掉连接工具 killcx 的原理,正是是基于这一点。
原因分析
为了方便说明,我们记发送 SYN 报文的一端为 A,处于 ESTABLISHED 状态接收 SYN 报文的一端为 B,B 对收到的 SYN 包回复 ACK 的原因是想让对端 A 确认之前的连接是否已经失效,以便做出一些处理。
对于 A 而已,如果之前的连接还在,对于收到的 ACK 包,正常处理即可,不再讨论。
如果 A 之前的此条连接已经不在了,此次 SYN 包是想发起新的连接,对于收到的 ACK 包,会立即回复一个 RST,且 RST 包的序列号就等于 ACK 包的序列号,B 收到这个合法的 RST 包以后,就会将连接释放。A 此时若想继续与 B 创建连接,则可以选择再次发送 SYN 包,重新建连,如下图所示。
接下来我们来看内核源码的处理,
内核源码分析
在这之前,我们需要先了解 SystemTap 工具的使用。SystemTap 是 Linux 中非常强大的调试探针工具,类似于 java 中的 javaagent instrument,可以获取一个内核函数运行时的入参变量、返回值、调用堆栈,甚至可以直接修改变量的值。这个工具详细的使用这里不展开,感兴趣的同学可以自行 Google。
接下来我们来使用 SystemTap 这个工具来给内核插入 probe 探针,以 3.10.0 内核为例,内核中回复的 ack 的函数在 net/ipv4/tcp_output.c 的 tcp_send_ack 中实现。我们给这个函数插入调用探针,在端口号为 9090 时打印调用堆栈。新建一个 ack_test.stp 文件,部分代码如下所示。
%{#include #include #include #include %}function tcp_src_port:long(sk:long){return __tcp_sock_sport(sk)}function tcp_dst_port:long(sk:long){return __tcp_sock_dport(sk)}function tcp_src_addr:long(sk:long){return ntohl(__ip_sock_saddr(sk))}function tcp_dst_addr:long(sk:long){return ntohl(__ip_sock_daddr(sk))}function str_addr:string(addr, port) { return sprintf("%d.%d.%d.%d:%d", (addr & 0xff000000) >> 24, (addr & 0x00ff0000) >> 16, (addr & 0x0000ff00) >> 8, (addr & 0x000000ff), port )}probe kernel.function("tcp_send_ack@net/ipv4/tcp_output.c"){ src_addr = tcp_src_addr($sk); src_port = tcp_src_port($sk); dst_addr = tcp_dst_addr($sk); dst_port = tcp_dst_port($sk); if (dst_port == 9090 || src_port == 9090) { printf("send ack : %s:->%s", str_addr(src_addr, src_port), str_addr(dst_addr, dst_port)); print_backtrace(); }}
使用 stap 命令执行上面的脚本
sudo stap -g ack_test.stp
再次使用 scapy 发送一个 syn 包,内核同样会回复 ACK,此时 stap 输出结果如下。
send ack : 10.211.55.10:9090:->10.211.55.20:50718 0xffffffff815d0940 : tcp_send_ack+0x0/0x170 [kernel] 0xffffffff815cb1d2 : tcp_validate_incoming+0x212/0x2d0 [kernel] 0xffffffff815cb44d : tcp_rcv_established+0x1bd/0x760 [kernel] 0xffffffff815d5f8a : tcp_v4_do_rcv+0x10a/0x340 [kernel] 0xffffffff815d76d9 : tcp_v4_rcv+0x799/0x9a0 [kernel] 0xffffffff815b1094 : ip_local_deliver_finish+0xb4/0x1f0 [kernel] 0xffffffff815b1379 : ip_local_deliver+0x59/0xd0 [kernel] 0xffffffff815b0d1a : ip_rcv_finish+0x8a/0x350 [kernel] 0xffffffff815b16a6 : ip_rcv+0x2b6/0x410 [kernel]
可以看到这个 ACK 经过了下面这些函数调用。
tcp_v4_rcv -> tcp_v4_do_rcv -> tcp_rcv_established -> tcp_validate_incoming -> tcp_send_ack
tcp_validate_incoming 函数精简后的部分代码如下所示。
static bool tcp_validate_incoming(struct sock *sk, struct sk_buff *skb, const struct tcphdr *th){// seq 不在窗口内/* Step 1: check sequence number */if (!tcp_sequence(tp, TCP_SKB_CB(skb)->seq, TCP_SKB_CB(skb)->end_seq)) {// RST 标记没有设置if (!th->rst) {if (th->syn)goto syn_challenge;}goto discard;}/* step 4: Check for a SYN。 RFC 5961 4.2 : Send a challenge ack */if (th->syn) {syn_challenge: // 处理 SYN Challenge 的情况tcp_send_challenge_ack(sk, skb); // goto discard;复制代码
tcp_send_challenge_ack 函数真正调用了 tcp_send_ack 函数。 这里的注释提到了 RFC 5961 4.2,说的正是 Challenge ACK 相关的内容。
如果攻击者疯狂发送假的乱序包,接收端也跟着回复 Challenge ACK,会耗费大量的 CPU 和带宽资源。于是 RFC 5961 提出了 ACK Throttling 方案,限制了每秒钟发送 Challenge ACK 报文的数量,这个值由 net.ipv4.tcp_challenge_ack_limit 系统变量决定,默认值是 1000,也就是 1s 内最多允许 1000 个 Challenge ACK 报文。
接下来使用 sysctl 将这个值改小为 1,如下所示。
sudo sysctl -w net.ipv4.tcp_challenge_ack_limit="1"
这样理论上在一秒内多次发送一个 Challenge ACK 包,接下来使用 scapy 在短时间内发送 5 次 SYN 包,看看内核是否只会回复一个 ACK 包,scapy 的脚本如下所示。
send(IP(dst="10.211.55.10")/TCP(sport=50718,dport=9090,seq=10,flags='S'), loop=0, count=5)
tcpdump 抓包结果如下。
03:40:30.970682 IP 10.211.55.20.50718 > 10.211.55.10.9090: Flags [S], seq 10, win 8192, length 003:40:30.970771 IP 10.211.55.10.9090 > 10.211.55.20.50718: Flags [.], ack 3219267426, win 227, options [nop,nop,TS val 45146923 ecr 1094540820], length 003:40:30.974889 IP 10.211.55.20.50718 > 10.211.55.10.9090: Flags [S], seq 10, win 8192, length 003:40:30.975004 IP 10.211.55.20.50718 > 10.211.55.10.9090: Flags [S], seq 10, win 8192, length 003:40:30.978643 IP 10.211.55.20.50718 > 10.211.55.10.9090: Flags [S], seq 10, win 8192, length 003:40:30.981987 IP 10.211.55.20.50718 > 10.211.55.10.9090: Flags [S], seq 10, win 8192, length 0
可以看到确实是只对第一个 SYN 包回复了一个 ACK 包,其它的四个 SYN 都没有回复 ACK。
RST 攻击
RST 攻击也称为伪造 TCP 重置报文攻击,通过伪造 RST 报文来关闭掉一个正常的连接。
源 IP 地址伪造非常容易,不容易被伪造的是序列号,RST 攻击最重要的一点就是构造的包的序列号要落在对方的滑动窗口内,否则这个 RST 包会被忽略掉,达不到攻击的效果。
下面我们用实验演示不在滑动窗口内的 RST 包会被忽略的情况,完整的代码见:rst_out_of_window.pkt https://github.com/arthur-zhang/tcp_ebook/blob/master/tcp_rst/rst_out_of_window.pkt
+0 < S 0:0(0) win 32792 +0 > S. 0:0(0) ack 1 <...>+.1 < . 1:1(0) ack 1 win 65535 +0 accept(3, ..., ...) = 4// 不在窗口内的 RST+.010 < R. 29202:29202(0) ack 1 win 65535// 如果上面的 RST 包落在窗口内,连接会被重置,下面的写入不会成功+.010 write(4, ..., 1000) = 1000 // 断言服务端会发出下面的数据包+0 > P. 1:1001(1000) ack 1 <...>
执行上面的脚本,抓包的结果如下,完整的包见:rst_out_of_window.pcap https://github.com/arthur-zhang/tcp_ebook/blob/master/tcp_rst/rst_out_of_window.pcap
抓包文件中的第 5 个包可以看到,write 调用成功,1000 字节发送成功,write 调用并没有收到 RST 包的影响。
下面来介绍两个工具,利用 RST 攻击的方式来杀掉一条连接。
工具一:tcpkill 工具使用及原理介绍
Centos 下安装 tcpkill 命令步骤如下
yum install epel-release -yyum install dsniff -y
实验步骤: 1、机器 c2(10.211.55.10) 启动 nc 命令监听 8080 端口,充当服务器端,记为 B
nc -l 8080
2、机器 c2 启动 tcpdump 抓包
sudo tcpdump -i any port 8080 -nn -U -vvv -w test.pcap
3、本地机器终端(10.211.55.2,记为 A)使用 nc 与 B 的 8080 端口建立 TCP 连接
nc c2 8080
在服务端 B 机器上可以看到这条 TCP 连接
netstat -nat | grep -i 8080tcp 0 0 10.211.55.10:8080 10.211.55.2:60086 ESTABLISHED
4、启动 tcpkill
sudo tcpkill -i eth0 port 8080
注意这个时候 tcp 连接依旧安然无恙,并没有被杀掉。
5、在本地机器终端 nc 命令行中随便输入一点什么,这里输入hello,发现这时服务端和客户端的 nc 进程已经退出了
下面来分析抓包文件,这个文件可以从我的 github 下载 tcpkill.pcap
可以看到,tcpkill 假冒了 A 和 B 的 IP发送了 RST 包给通信的双方,那问题来了,伪造 ip 很简单,它是怎么知道当前会话的序列号的呢?
tcpkill 的原理跟 tcpdump 差不多,会通过 libpcap 库抓取符合条件的包。 因此只有有数据传输的 tcp 连接它才可以拿到当前会话的序列号,通过这个序列号伪造 IP 发送符合条件的 RST 包。
原理如下图所示
可以看到 tcpkill 对每个端发送了 3 个RST 包,这是因为在高速数据传输的连接上,根据当前抓的包计算的序列号可能已经不再 TCP 连接的窗口内了,这种情况下 RST 包会被忽略,因此默认情况下 tcpkill 未雨绸缪往后计算了几个序列号。还可以指定参数-n指定更多的 RST 包,比如tcpkill -9
根据上面的分析 tcpkill 的局限还是很明显的,无法杀掉一条僵死连接,下面我们介绍一个新的工具 killcx,看看它是如何来处理这种情况的。
工具二:killcx
killcx 是一个用 perl 写的在 linux 下可以关闭 TCP 连接的脚本,无论 TCP 连接处于什么状态。
下面来做一下实验,实验的前几步骤跟第一个例子中一模一样
1、机器 c2(10.211.55.10) 启动 nc 命令监听 8080 端口,充当服务器端,记为 B
nc -l 8080
2、机器 c2 启动 tcpdump 抓包
sudo tcpdump -i any port 8080 -nn -U -vvv -w test.pcap
3、本地机器终端(10.211.55.2,记为 A)使用 nc 与 B 的 8080 端口建立 TCP 连接
nc c2 8080
在服务端 B 机器上可以看到这条 TCP 连接
netstat -nat | grep -i 8080tcp 0 0 10.211.55.10:8080 10.211.55.2:61632 ESTABLISHED
4、客户端 A nc 命令行随便输入什么,这一步也完全可以省略,这里输入"hello"
5、执行 killcx 命令,注意 killcx 是在步骤 4 之后执行的
sudo ./killcx 10.211.55.2:61632
可以看到服务端和客户端的 nc 进程已经退出了。
抓包的结果如下
前 5 个包都很正常,三次握手加上一次数据传输,有趣的事情从第 6 个包开始
- 第 6 个包是 killcx 伪造 IP 向服务端 B 发送的一个 SYN 包
- 第 7 个包是服务端 B 回复的 ACK 包,里面包含的 SEQ 和 ACK 号
- 第 8 个包是 killcx 伪造 IP 向服务端 B 发送的 RST 包
- 第 9 个包是 killcx 伪造 IP 向客户端 A 发送的 RST 包
整个过程如下图所示
小结
这篇文章介绍了为什么 ESTABLISHED 状态连接的需要对 SYN 包做出响应,Challenge ACK 是什么,使用 scapy 复现了现象,演示了 SystemTap 内核探针调试工具的使用,最后通过修改系统变量复现了 ACK 限速。
文章的最后介绍了杀掉 TCP 连接的两个工具 tcpkill 和 killcx:
- tcpkill 采用了比较保守的方式,抓取流量等有新包到来的时候,获取 SEQ/ACK 号,这种方式只能杀掉有数据传输的连接
- killcx 采用了更加主动的方式,主动发送 SYN 包获取 SEQ/ACK 号,这种方式活跃和非活跃的连接都可以杀掉
作者:挖坑的张师傅