tcp的半連線攻擊和全連線攻擊--TCP DEFER ACCEPT

滿舅舅發表於2019-02-02
               

半連線攻擊是一種針對協議棧的攻擊,或者說是一中針對主機的攻擊,皮之不存毛將焉附,主機一旦被攻擊而耗盡了記憶體資源,使用者態的應用程式也將無法執行。TCP半連線攻擊可以通過syn cookie機制或者syn中繼機制等進行防範,對於tcp服務來講還有一種可以稱為“全連線攻擊”的攻擊型別,這種攻擊是針對使用者態執行的tcp伺服器的,當然,它可能間接地導致主機癱瘓。所謂的全連線攻擊說的就是客戶端僅僅“連線”到伺服器,然後再也不傳送任何資料,直到伺服器超時後處理或者耗盡伺服器的處理程式。為何不傳送任何資料呢?因為一旦傳送了資料,伺服器檢測到資料不合法後就可能斷開此次連線,如果不傳送資料的話,很多伺服器只能阻塞在recv或者read呼叫上。很多的伺服器架構都是每連線一個程式的方式,這種伺服器更容易受到全連線攻擊,即使是程式池/執行緒池的方式也不例外,症狀就是伺服器主機建立了大量的客戶端處理程式,然後阻塞在recv/read而無所事事,大量的這種連線會耗盡伺服器主機的處理程式。如果處理程式數量達到了主機允許的最大值,那麼就會影響到該主機的正常運作,比如你再也無法ssh到該主機上了。
     半連線攻擊耗盡的是全域性的記憶體,因此可以用不為半連線分配記憶體的方式加以預防--syn cookie,而全連線攻擊耗盡的是主機的處理程式和連線數量,因此可以限制處理程式的建立或者限制預建立的程式池程式的分配,具體到操作上就是隻有到了客戶端真實傳送資料的時候才為其指派處理程式,進一步具體到程式碼運作上的體現就是伺服器的accept在資料到來之前是不返回的,以apache的prefork為例,預先建立了N個處理子程式,每個子程式繼承父程式的偵聽套接字,因此每一個子程式都有權accept,然而一個客戶端要連線的時候,只有在某個子程式accept返回的時候,該子程式才指派給了該客戶端,否則該子程式繼續等待連線,如果一個客戶端僅僅完成了到伺服器的連線而沒有傳送資料,那麼對於伺服器來講,任何子程式的accept都是不會返回的,用netstat察看的話,這種連線處於SYN_RECV狀態,核心協議棧會為這種狀態的完成三次握手的連線保留一段時間,如果這段時間過去了,仍然沒有非握手資料的到來,那麼就會斷開這次連線,如果不限制一個期限的話,雖然防止了ESTABLISHED連線資料的膨脹以及無所事事的處理程式數量的膨脹,但是仍然防止不了SYN_RECV狀態連線資料的膨脹,因此核心協議棧的實現中就增加了這麼一個限制。有了這個機制,apache(的新版本)以及很多基於子程式的伺服器就可以利用它來避免產生大量的無所事事的阻塞在read/recv的程式,核心協議棧保證使用者態的程式在accept返回後就一定有資料可以讀取,一旦處理子程式讀取到了非法的資料的話,伺服器負責斷開此次連線。
     這一切是通過TCP_DEFER_ACCEPT這個套接字引數來實現的,它的介面形式如下:
setsockopt(listen_socket, SOL_TCP, TCP_DEFER_ACCEPT, &val, sizeof(val))
其中val是一個數字,它代表一個時間,字面上理解,在這個時間過去後仍沒有資料到來的話就會在不指派服務程式(accept不返回)的情況下斷開連線,可是這只是一個方面,協議棧的實現中還有另外一個方面,那就是伺服器協議棧會試圖重傳自己的synack好幾次,因此這個限制時間是受到tcp協議棧的synack的重傳次數和defer_accept的值共同決定的。
     在探討defer_accept和synack的重發的關係之前,首先看一下總體的流程。accept函式實際上是很簡單的,每一個偵聽套接字都會有一個accept對列,如果沒有連線到來,呼叫accept的程式將睡眠在該對列上,協議棧的tcp模組負責往這個對列上放入新的客戶端套接字,然後喚醒accept的呼叫程式,accept返回前建立BSD套接字,然後返回使用者空間,每次accept僅僅處理accept對列最前面的一個套接字。在協議棧中tcp_check_req函式是建立accept返回套接字的函式,並且它還負責喚醒accept的呼叫程式,它內部視是否定義defer_accept而採取了不同的行為:
//在定義了defer_accept的情況下,協議棧將不以為握手包的最後一個ack(來自客戶端)的到來為連線的建立,從而不分配accept返回套接字,直接返回NULL。注意,此後客戶端傳送真正資料的時候,由於連線沒有建立(在established連線中找不到),因此還是會呼叫tcp_check_req函式的,此時由於有資料,TCP_SKB_CB(skb)->end_seq == req->rcv_isn+1將不再正確,執行流將繼續往下走。
if (tp->defer_accept && TCP_SKB_CB(skb)->end_seq == req->rcv_isn+1) {
        req->acked = 1;
        return NULL;
}
child = tp->af_specific->syn_recv_sock(sk, skb, req, NULL);
...//將新建的套接字放入到使用者空間accept程式的accept對列中,喚醒該程式,這樣這個請求就指派給該程式了。
tcp_acceptq_queue(sk, req, child);
return child;
     前面提到過,synack的重傳受到核心可調引數sysctl_tcp_synack_retries和defer_accept的共同影響,接下來看一下使用者空間通過setsockopt設定的defer_accept的值是怎麼和synack重傳聯絡在一起的,在setsockopt中為tcp連線的defer_accept欄位賦值:
case TCP_DEFER_ACCEPT:
    tp->defer_accept = 0;
    if (val > 0) { //這個邏輯很簡單,就是將值很“策略”化的轉化成重傳的次數
        while (tp->defer_accept < 32 && val > ((TCP_TIMEOUT_INIT / HZ) <<
             tp->defer_accept))
            tp->defer_accept++;
        tp->defer_accept++;
    }
    break;
在每一個tcp偵聽套接字上都有一個很特殊的timer,這就是tcp_synack_timer,雖然它是在keepalive這個timer的function中呼叫的,但是它卻是很獨立的一個timer,在tcp_synack_timer函式中有下面的程式碼:
if (tp->defer_accept)
    max_retries = tp->defer_accept;
budget = 2*(TCP_SYNQ_HSIZE/(TCP_TIMEOUT_INIT/TCP_SYNQ_INTERVAL));
i = lopt->clock_hand;
do { //針對所有的連線請求進行必要的synack的重傳處理,連線請求之所以還沒有成功有以下幾個原因:
    /*
    1.客戶端的最後一次握手ack還沒有來。
    1.1.伺服器端的synack丟失;
    1.2.客戶端的握手ack丟失;
    2.距離太遠了,ack還在路上。
    3.這是一次syn攻擊,不要指望ack會到來。
    4.ack已經來了,三次握手已經成功,只是設定了defer_accept,協議棧硬是不讓連線成功。
    */
    reqp=&lopt->syn_table[i]; 
    while ((req = *reqp) != NULL) {
        if (time_after_eq(now, req->expires)) {
            if ((req->retrans < thresh ||
                (req->acked && req->retrans < max_retries))
                && !req->class->rtx_syn_ack(sk, req, NULL)) {  //重傳synack
                ... //下一次的重傳間隔會“更長”一些,這也是一種試探策略,既然上次n秒沒回來,這次就試一下比n更大的數。
                timeo = min((TCP_TIMEOUT_INIT << req->retrans), TCP_RTO_MAX);
                req->expires = now + timeo;
                reqp = &req->dl_next;
                continue;
            }
            //這裡丟棄沒有通過上面if的連線請求
        }
        reqp = &req->dl_next;
    }
    i = (i+1)&(TCP_SYNQ_HSIZE-1);
} while (--budget > 0);
     理解了核心的實現原理,下面就剩下測試了,還是用一個簡單的程式和tcpdump來測試,使用者程式的原始碼如下:
int  main (int argc, char **argv)
{
    int err;
    int listen_sd;
    int sd;
    struct sockaddr_in sa_serv;
    struct sockaddr_in sa_cli;
    size_t client_len;
    listen_sd = socket (AF_INET, SOCK_STREAM, 0);  
    memset (&sa_serv, '/0', sizeof(sa_serv));
    sa_serv.sin_family      = AF_INET;
    sa_serv.sin_addr.s_addr = INADDR_ANY;
    sa_serv.sin_port        = htons (6800);        
    int val = 10;
    setsockopt(listen_sd, 1, 2, &val, sizeof(val)) ;  //這就是defer_accept的設定,本機的標頭檔案被偷走了,所以直接用數字
    bind(listen_sd, (struct sockaddr*) &sa_serv, sizeof (sa_serv));                  
    listen (listen_sd, 5);                   
    client_len = sizeof(sa_cli);
    sd = accept (listen_sd, (struct sockaddr*) &sa_cli, &client_len);
      close (listen_sd);
    while (1) {
        read(sd, buf, sizeof(buf) - 1);             
    }
    close (sd);
}
執行之,將synack的核心引數設定為0:
sysctl -w net.ipv4.tcp_synack_retries=0
然後用tcpdump抓包如下:
tcpdump tcp port 6800 and host 192.168.x.y
在另外一臺機器上執行:
telnet 192.168.a.b 6800
不要敲入任何字元,空格也不行...
結果發現,在應用程式val為10的情況下三次握手之外又進行了3次額外的synack重傳,為何是三次呢?看看setsockopt中那個邏輯吧,如果將val設定為1,而將tcp_synack_retries核心引數設定為2的話,則會有兩次重傳,這個也很顯然。接下來最重要的就是核對一下重傳的間隔時間是不是兩倍的往上增長啊,第一次間隔6秒,然後12秒,然後24秒,最終再過48秒後在telnet的機器上敲入一個字元,可悲的是Connection closed by foreign host.映入了眼簾,超時了,伺服器成功的阻止了“全連線”攻擊。

           

再分享一下我老師大神的人工智慧教程吧。零基礎!通俗易懂!風趣幽默!還帶黃段子!希望你也加入到我們人工智慧的隊伍中來!https://blog.csdn.net/jiangjunshow

相關文章