從Linux協議棧代碼和RFC看西廂計劃原理
阿新 • • 發佈:2019-02-17
packet 失敗 pat oca last state 分析 ble cep
終於搞定了西廂計劃的方案,由於一直無法下載那個內核模塊,於是也就只能自己寫了,在理解了西廂計劃的原理之後,寫這個模塊並不很費事(其實為了簡單不是寫模塊,而是直接修改內核協議棧代碼),下面先說一下原理,然後再說一下關於內核修改的建議。
具體的技術細節,那就是在TCP的三次握手上大做文章,借用服務器對syn-received狀態處理的特殊性來執行計劃。在TCP的syn-received狀態中,服務器本想接收的是客戶端對其syn-ack的ack,然而此時客戶端並沒有如期發送該ack,而是施行了一個兩階段的“自定義報文”發送,其中第一階段就是發送一個帶有fin標誌且不帶ack標誌的報文;第二階段就是發送一個ack號錯誤的報文。這樣兩個階段就成功欺騙了防火墻,同時又誘使服務器端做了客戶端想讓它做的事情。
除了RFC,最容易得手的就是Linux的源代碼了,其中tcp_rcv_state_process函數可以看出西廂計劃為何會得手,這裏暫且不談防火墻做了什麽,其實我也不知道。所謂的得手,含義是如此折騰服務器的TCP連接,為何連接沒有斷掉。
在Linux的協議棧實現中,tcp_rcv_state_process函數負責處理了TCP連接/釋放的狀態機,由於在連接開始時(三次握手)狀態轉換的特殊性,使得我們最好關註三次握手而不是establish狀態,在establish狀態中,大部分的看似異常報文都是可以通過TCP本身的機制得到修復的,而三次握手過程中則不然,因為TCP的控制塊TCB設施正在構建中,以至於很多機制我們還不能用,因此要折騰就折騰這種狀態的TCP吧,我們先看下tcp_rcv_state_process:
對以上代碼不必做更多的說明了。從該函數的代碼邏輯,可以清晰看出客戶端需要怎麽做,那就是在三次握手的最後一次前,發送兩個壞報文。
可見在syn-received狀態下,沒有明確的“收fin”動作(只有應用程序調用close然後發fin的動作),雖然這涉及到TCP的細節,但是還是可以利用的。大多數的實現中,沒有規定的行為就是簡單的“將包丟棄”。但是要真的想讓服務器丟掉這個fin包,還必須使其不包含ack,這是因為RFC的TCP狀態機說了,只要收到ack,那麽服務器就會進入establish狀態,而這會引起西廂計劃第二階段的失敗(第二階段是引發服務器發送reset,而在establish狀態下,這種reset實在不易引發)。因此我們只需要在客戶端發完syn且收到服務器的syn-ack後,再發送一個不含ack的fin報文即可,該報文過墻時,墻會認為這是個客戶端到服務器方向的終止包,直接放過。
RFC 793 [Page 35]
Reset Generation
2. If the connection is in any non-synchronized state (LISTEN, SYN-SENT, SYN-RECEIVED), and the incoming segment acknowledges something not yet sent (the segment carries an unacceptable ACK), or if an incoming segment has a security level or compartment which does not exactly match the level and compartment requested for the connection, a reset is sent.
那麽如何得知服務器端發送的reset報文就一定能順利到達客戶端並且服務器端還不釋放連接呢?如果嫌RFC實在不好啃,作為程序員,看代碼一定會舒服很多,我們知道Linux協議棧源碼是一個不錯的選擇,它實現了絕大多數的RFC建議。Linux的實現中,reset報文分為auto-reset和active-reset,其中auto-reset僅僅根據引發reset的報文構造一個附帶RST位的TCP回復報文,它並不和任何的socket相關聯,發送了reset報文之後也不會針對本地的連接進行任何操作,這種方式reset報文有一個假設,那就是它將引發auto-reset的“壞報文”的產生歸結為兩點:
正是由於這個auto-reset機制(這也是RFC的意思),西廂計劃才得以成功,西廂計劃正是使用一個壞的報文來使服務器生成一個auto-reset報文,該reset報文過防火墻時,會被認為是由服務器發起到客戶端方向的“終止”報文,然而客戶端是可以忽略該報文的...西廂計劃這樣就成功了另一半。
我們不可能利用active-reset報文,因為該reset報文和一個連接相關聯,一旦發送,將同時釋放本端的TCP連接記錄(TCB)。最終,兩個階段全部完成,一個交互圖如下:
看完《用TCP/IP進行網際互連(第二卷)》,你的思路會煥然一新的,Xinu內核完全是微內核設計,幾乎所有的模塊都是一個獨立的進程,靠IPC進行通信,當然協議棧也不例外。Xinu的IP層由一個IP進程來完成,該IP進程設計的非常統一,使你很容易就能理解IP層的處理,特別是它抽象出了LOCAL接口,這樣本地發到IP層的IP數據報和從物理網卡接收的報文就能使用一種更加統一的處理方式,如果在Xinu上實現Netfilter的話,對於filter表,Xinu一下子就砍掉了INPUT和OUTPUT兩條鏈,因為Xinu並不區分數據報是從哪裏來的,對於Xinu的IP進程,它們都來自於某一個“接口”,如下:
另外一個亮點,那就是其timer-list的設計,以相對時間作為填充,且按照距離表頭的相對時間排序,管理起來很高效,只需要修改表頭即可,也大大增加了cache的命中率。再者,Xinu的TCP狀態機的實現采用了和Linux完全不同的方式。
終於搞定了西廂計劃的方案,由於一直無法下載那個內核模塊,於是也就只能自己寫了,在理解了西廂計劃的原理之後,寫這個模塊並不很費事(其實為了簡單不是寫模塊,而是直接修改內核協議棧代碼),下面先說一下原理,然後再說一下關於內核修改的建議。
序.西廂計劃
所謂西廂計劃是一個借用歷史小說而命名的欺騙GfW的方案,有很多的實現。說實話我真的不知道《西廂記》中的那家夥FQ到底有何與眾不同,有時間一定看一下。而西廂計劃無非就是利用了防火墻的一些弱點而瞞天過海的一個方案。本質上作者是利用對TCP協議規範以及防火墻本身的深入理解來制定這個方案的,正所謂知己知彼。具體的技術細節,那就是在TCP的三次握手上大做文章,借用服務器對syn-received狀態處理的特殊性來執行計劃。在TCP的syn-received狀態中,服務器本想接收的是客戶端對其syn-ack的ack,然而此時客戶端並沒有如期發送該ack,而是施行了一個兩階段的“自定義報文”發送,其中第一階段就是發送一個帶有fin標誌且不帶ack標誌的報文;第二階段就是發送一個ack號錯誤的報文。這樣兩個階段就成功欺騙了防火墻,同時又誘使服務器端做了客戶端想讓它做的事情。
一.從Linux的源碼來看如何做
在Linux的協議棧實現中,tcp_rcv_state_process函數負責處理了TCP連接/釋放的狀態機,由於在連接開始時(三次握手)狀態轉換的特殊性,使得我們最好關註三次握手而不是establish狀態,在establish狀態中,大部分的看似異常報文都是可以通過TCP本身的機制得到修復的,而三次握手過程中則不然,因為TCP的控制塊TCB設施正在構建中,以至於很多機制我們還不能用,因此要折騰就折騰這種狀態的TCP吧,我們先看下tcp_rcv_state_process:
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb,
struct tcphdr *th, unsigned len)
{
struct tcp_opt *tp = tcp_sk(sk);
int queued = 0;
tp->saw_tstamp = 0;
switch (sk->sk_state) {
case TCP_CLOSE:
...
case TCP_LISTEN:
...
case TCP_SYN_SENT:
...
}
...
/* step 1: check sequence number */
if (!tcp_sequence(tp, TCP_SKB_CB(skb)->seq, TCP_SKB_CB(skb)->end_seq)) {
if (!th->rst)
tcp_send_dupack(sk, skb);
goto discard;
}
/* step 2: check RST bit */
if(th->rst) {
tcp_reset(sk);
goto discard;
}
...
/* step 5: check the ACK field */
if (th->ack) { //這是關鍵點
int acceptable = tcp_ack(sk, skb, FLAG_SLOWPATH);
switch(sk->sk_state) {
case TCP_SYN_RECV:
if (acceptable) {
...
} else {
//如果ack序號錯誤,返回1,則要發送auto-reset
return 1;
}
break;
case TCP_FIN_WAIT1:
...
case TCP_CLOSING:
...
case TCP_LAST_ACK:
...
}
} else //如果沒有ack標誌,則丟棄該報文,報文中含有fin與否對於服務器無關緊要,只是為了欺騙防火墻。
goto discard;
/* step 6: check the URG bit */
tcp_urg(sk, skb, th);
/* step 7: process the segment text */
switch (sk->sk_state) {
...
}
/* tcp_data could move socket to TIME-WAIT */
if (sk->sk_state != TCP_CLOSE) {
tcp_data_snd_check(sk);
tcp_ack_snd_check(sk);
}
if (!queued) {
discard:
__kfree_skb(skb);
}
//如果返回0,則正常,如果返回1,則會發送auto-reset,該reset並不會操作本地連接,只是在壞報文到來的反方向默默發送
return 0;
}
對以上代碼不必做更多的說明了。從該函數的代碼邏輯,可以清晰看出客戶端需要怎麽做,那就是在三次握手的最後一次前,發送兩個壞報文。
二.從TCP狀態機理解西廂計劃第一階段
我們先看一下TCP的狀態機,此圖來自RFC,其它的來源都是浮雲:可見在syn-received狀態下,沒有明確的“收fin”動作(只有應用程序調用close然後發fin的動作),雖然這涉及到TCP的細節,但是還是可以利用的。大多數的實現中,沒有規定的行為就是簡單的“將包丟棄”。但是要真的想讓服務器丟掉這個fin包,還必須使其不包含ack,這是因為RFC的TCP狀態機說了,只要收到ack,那麽服務器就會進入establish狀態,而這會引起西廂計劃第二階段的失敗(第二階段是引發服務器發送reset,而在establish狀態下,這種reset實在不易引發)。因此我們只需要在客戶端發完syn且收到服務器的syn-ack後,再發送一個不含ack的fin報文即可,該報文過墻時,墻會認為這是個客戶端到服務器方向的終止包,直接放過。
三.從RFC理解西廂計劃第二階段
既然已經通過自行構造的fin在客戶端到服務器方向騙過了防火墻,那麽下一步就是第二階段的任務了,在服務器到客戶端的方向欺騙防火墻。期望服務器采用正常且優雅的fin方式是不可行的,因為那樣連接真的就斷了,因此就要采用異常的方式,那就是引發服務器發送一個reset報文,而RFC中規定了多種引發reset的方式,西廂計劃明顯采用了下面的方式(要知道為何發送一個reset不會導致自己這邊的連接釋放,請接著往下看):RFC 793 [Page 35]
Reset Generation
2. If the connection is in any non-synchronized state (LISTEN, SYN-SENT, SYN-RECEIVED), and the incoming segment acknowledges something not yet sent (the segment carries an unacceptable ACK), or if an incoming segment has a security level or compartment which does not exactly match the level and compartment requested for the connection, a reset is sent.
那麽如何得知服務器端發送的reset報文就一定能順利到達客戶端並且服務器端還不釋放連接呢?如果嫌RFC實在不好啃,作為程序員,看代碼一定會舒服很多,我們知道Linux協議棧源碼是一個不錯的選擇,它實現了絕大多數的RFC建議。Linux的實現中,reset報文分為auto-reset和active-reset,其中auto-reset僅僅根據引發reset的報文構造一個附帶RST位的TCP回復報文,它並不和任何的socket相關聯,發送了reset報文之後也不會針對本地的連接進行任何操作,這種方式reset報文有一個假設,那就是它將引發auto-reset的“壞報文”的產生歸結為兩點:
1.遠端主機的“異常行為”或者是有人沒有按照TCP規範而有意為之,比如establish狀態時收到一個syn;
2.本端實在沒有可以和該壞報文相關聯的TCP連接,比如連接了一個不存在或未開啟的端口。
對於active-reset,則是在可以將壞報文和既有連接聯系的可預知事件發生時發送的,比如重傳定時器連續超時超過了一定的次數等,當這種active-reset發送之後,本端的連接也隨之煙消雲散。在Linux中,auto-reset是由tcp_v4_send_reset來執行的,我想其註釋已經闡述的很清晰了:/*
* This routine will send an RST to the other tcp.
*
* Someone asks: why I NEVER use socket parameters (TOS, TTL etc.)
* for reset.
* Answer: if a packet caused RST, it is not for a socket
* existing in our system, if it is matched to a socket,
* it is just duplicate segment or bug in other side‘s TCP.
* So that we build reply only basing on parameters
* arrived with segment.
* Exception: precedence violation. We do not implement it in any case.
*/
static void tcp_v4_send_reset(struct sk_buff *skb)
{
...
}
正是由於這個auto-reset機制(這也是RFC的意思),西廂計劃才得以成功,西廂計劃正是使用一個壞的報文來使服務器生成一個auto-reset報文,該reset報文過防火墻時,會被認為是由服務器發起到客戶端方向的“終止”報文,然而客戶端是可以忽略該報文的...西廂計劃這樣就成功了另一半。
我們不可能利用active-reset報文,因為該reset報文和一個連接相關聯,一旦發送,將同時釋放本端的TCP連接記錄(TCB)。最終,兩個階段全部完成,一個交互圖如下:
四.一點擴展
綜上,我們能否使用一個fin包同時引發服務器發送一個reset呢?答案是肯定的,那就是在收到服務器的syn-ack後,發送一個帶有fin且ack序號錯誤的報文,這樣由於:1.syn-received狀態不檢查收到的fin標誌,因此它只是毫無代價欺騙了墻;2.由於ack序號錯誤,因此服務器端會發送一個auto-reset從而在反方向欺騙墻。五.兩本書
最後,如果你覺得RFC不好啃,Linux源碼有很繁雜,那麽推薦一個簡單的協議棧實現,那就是Xinu系統的協議棧實現,代碼很少很清晰。它也是著名的《用TCP/IP進行網際互連(第二卷)》的講解所采用的,看完《用TCP/IP進行網際互連(第二卷)》比看完《TCP/IP祥解(第二卷)》會讓你對協議方面理解更多而不會迷失在茫茫的BSD代碼之中,其實它們說的是一回事。看完《用TCP/IP進行網際互連(第二卷)》,你的思路會煥然一新的,Xinu內核完全是微內核設計,幾乎所有的模塊都是一個獨立的進程,靠IPC進行通信,當然協議棧也不例外。Xinu的IP層由一個IP進程來完成,該IP進程設計的非常統一,使你很容易就能理解IP層的處理,特別是它抽象出了LOCAL接口,這樣本地發到IP層的IP數據報和從物理網卡接收的報文就能使用一種更加統一的處理方式,如果在Xinu上實現Netfilter的話,對於filter表,Xinu一下子就砍掉了INPUT和OUTPUT兩條鏈,因為Xinu並不區分數據報是從哪裏來的,對於Xinu的IP進程,它們都來自於某一個“接口”,如下:
另外一個亮點,那就是其timer-list的設計,以相對時間作為填充,且按照距離表頭的相對時間排序,管理起來很高效,只需要修改表頭即可,也大大增加了cache的命中率。再者,Xinu的TCP狀態機的實現采用了和Linux完全不同的方式。
六.內核協議棧的修改
修改tcp_rcv_state_process的以下代碼段:case TCP_SYN_SENT:
...
在tcp_rcv_synsent_state_process函數中的tcp_send_ack之前發送兩個壞報文即可,至於如何構造這兩個壞報文,前面分析過了。再分享一下我老師大神的人工智能教程吧。零基礎!通俗易懂!風趣幽默!還帶黃段子!希望你也加入到我們人工智能的隊伍中來!https://blog.csdn.net/jiangjunshow
從Linux協議棧代碼和RFC看西廂計劃原理