半連接配接攻擊是一種針對協定棧的攻擊,或者說是一中針對主機的攻擊,皮之不存毛将焉附,主機一旦被攻擊而耗盡了記憶體資源,使用者态的應用程式也将無法運作。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.映入了眼簾,逾時了,伺服器成功的阻止了“全連接配接”攻擊。
本文轉自 dog250 51CTO部落格,原文連結:http://blog.51cto.com/dog250/1271184