tcp的半串連攻擊和全串連攻擊–TCP_DEFER_ACCEPT

來源:互聯網
上載者:User

轉載:http://blog.csdn.net/dog250/article/details/5955094

半串連攻擊是一種針對協議棧的攻擊,或者說是一中針對主機的攻擊,皮之不存毛將焉附,主機一旦被攻擊而耗盡了記憶體資源,使用者態的應用程式也將無法運行。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.映入了眼帘,逾時了,伺服器成功的阻止了“全串連”攻擊。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.