0. 少说两句
不论是攻击角度还是防御角度,tcp rst都可以发挥他阻断的作用,当然前提是可以有效的阻断TCP连接。在git上有些优秀的开源项目如:tcpkill,tcpwall都实现了tcp rst阻断,能看到这篇文章的小伙伴,我相信大部分是读过这些源码的,这些工具写的很好,其中tcpwall(国内小伙伴,GO语言开发)更胜一筹。虽然他们很好,但是还不够好,任然存在一些小问题,比如有些连接是rst不掉的。这些我会给出一些我的解决方案。
当然,这些工具的 前提是可以抓到TCP流 ,不管使用什么方法,镜像也好串行也罢,这也是我们整体文章大前提。
1. tcp rst流程
tcp rst流程其实很简单,简单到你第一眼看到这几个字,就会感觉这个我会。但是自己写tcp rst工具就会发现,你需要对TCP的 滑动窗口 机制有足够的了解,不然在构造rst包时会很迷茫。
流程如下:
具体可以参考tcpkill代码。
这个流程的关键是 构造RST包,构造RST包的目标是:构造出的RST包可以有效被接受。熟悉TCP协议应知道,只要发送的包序列号在对方接受窗口范围内,都可以被接收。
伪造一个TCP RST包需要:源IP、 目的IP、 源端口、 目的端口、 正确的序列号
五个关键信息,前四个信息通过抓包可以直接获取,源、目的信息调换位置即可,序列号则需要通过简单计算,得到合理的序列号才能使RST包生效。
2. 关键点: 序列号
几个问题:在回复TCP包的时候,seq和ack该如何填写?直接拿收到包的ack作为seq吗?是否需要运算?如何运算?seq和ack又代表着什么?
如果已经了然于胸,请跳过以下内容。
TCP协议主要可以分为两部分:一部分是连接管理,一部分是流量控制。这两部分内容使TCP协议成为实现复杂,但使用简单的协议。
序列号作为TCP数据包的一部分,会连接管理中被使用,但主要用于流量控制的滑动窗口。关于滑动窗口,这个动态展示非常的生动:滑动窗口演示。滑动窗口就是缓存大小的管理方式,它通过控制窗口的大小可以达到控制流量的目的,具体内容这里不展开介绍,演示如下:流量控制演示。
窗口大小和序列号的关系:
所有数据流的序列号需要在窗口的范围内,没有其他的线性关系(通过滑动窗口不能确定序列号的大小),初始序列号的产生是随机的,这样做可以加固TCP传输的安全性。原因可见:链接
可以通过wireshark查看窗口和序列号的关系,位于菜单栏 统计>>流量图 如下图:
wireshark展示的是TCP流的相对序列号,而非绝对序列号,相对序列号/确认号是和TCP会话的初始序列号相关联的,初始序列号在三次握手过程中初始化。通过wireshark的分析流量图可以看到,序列号的增加规律为:每次增加接收包的长度。
我们对发送缓存区的每个字符都标上连续编号,序列号/确认号代表的意思:
数据方向 :A->B
seq (A)(发送):我如果发送数据,起始编号是多少
ack (A)(接收):我已经接收到了 B 哪些编号的数据(值为new_ack = B 的seq + B发送数据的长度)
对于A来说 如果再接收到B的数据,seq小于new_ack的将会被丢弃。
因此,回到tcp rst ,再构造包时发送的rst包的seq应该在接收方的接收范围内。
tcpkill做法为:使用ack值加上窗口值的n倍来增长seq。
...
seq = ntohl(tcp->th_ack);
win = ntohs(tcp->th_win);
for (i = 0; i < 10; i++) {
seq += (i * win);
...
libnet_write(l);
}
...
这样做当然是有效的,但是tcpkill有时会失效。
3. 优化项
失效原因:
1、在使用libpcap抓包时,在ebpf中有缓存,导致发rst包不及时
2、序列号的计算为 窗口*n + ack,但是有些连接中窗口极小,但是window size scaling factor比较大,发送的seq增长过缓,导致其不在接收窗口范围内,rst失败
window size scaling factor是在握手过程中协商的,在发送数据过程中其并不可见。
解决办法:
1、对于第一个问题,在使用libpcap时,使用其无缓存模式。
2、对于第二个问题,当窗口小于某值(如512)时,其最小为512。
4. 参考
TCP-RST 攻击与防御,看这一篇就够了
理解TCP序列号(Sequence Number)和确认号(Acknowledgment Number)
30张图解: TCP 重传、滑动窗口、流量控制、拥塞控制
https://www2.tkn.tu-berlin.de/teaching/rn/animations/gbn_sr/