|
藍森林 http://www.lslnet.com 2006年6月6日 10:18
剖析SYN Flood攻擊
一、SYN Flood的基本原理
SYN Flood是當前最流行的DoS(拒絕服務攻擊)與DdoS(分佈式拒絕服務攻擊)的方式之一,這是一種利用TCP協議缺陷,發送大量偽造的TCP連接請求,從而使得被攻擊方資源耗盡(CPU滿負荷或內存不足)的攻擊方式。
要明白這種攻擊的基本原理,還是要從TCP連接建立的過程開始說起:
大家都知道,TCP與UDP不同,它是基於連接的,也就是說:為了在服務端和客戶端之間傳送TCP數據,必須先建立一個虛擬電路,也就是TCP連接,建立TCP連接的標準過程是這樣的:
首先,請求端(客戶端)發送一個包含SYN標誌的TCP報文,SYN即同步(Synchronize),同步報文會指明客戶端使用的端口以及TCP連接的初始序號;
第二步,服務器在收到客戶端的SYN報文後,將返回一個SYN+ACK的報文,表示客戶端的請求被接受,同時TCP序號被加一,ACK即確認(Acknowledgement)。
第三步,客戶端也返回一個確認報文ACK給服務器端,同樣TCP序列號被加一,到此一個TCP連接完成。
以上的連接過程在TCP協議中被稱為三次握手(Three-way Handshake)。
問題就出在TCP連接的三次握手中,假設一個用戶向服務器發送了SYN報文後突然死機或掉線,那麼服務器在發出SYN+ACK應答報文後是無法收到客戶端的ACK報文的(第三次握手無法完成),這種情況下服務器端一般會重試(再次發送SYN+ACK給客戶端)並等待一段時間後丟棄這個未完成的連接,這段時間的長度我們稱為SYN Timeout,一般來說這個時間是分鐘的數量級(大約為30秒-2分鐘);一個用戶出現異常導致服務器的一個線程等待1分鐘並不是什麼很大的問題,但如果有一個惡意的攻擊者大量模擬這種情況,服務器端將為了維護一個非常大的半連接列表而消耗非常多的資源----數以萬計的半連接,即使是簡單的保存並遍歷也會消耗非常多的CPU時間和內存,何況還要不斷對這個列表中的IP進行SYN+ACK的重試。實際上如果服務器的TCP/IP棧不夠強大,最後的結果往往是堆棧溢出崩潰---即使服務器端的系統足夠強大,服務器端也將忙於處理攻擊者偽造的TCP連接請求而無暇理睬客戶的正常請求(畢竟客戶端的正常請求比率非常之小),此時從正常客戶的角度看來,服務器失去響應,這種情況我們稱作:服務器端受到了SYN Flood攻擊(SYN洪水攻擊)。
從防禦角度來說,有幾種簡單的解決方法:
第一種是縮短SYN Timeout時間,由於SYN Flood攻擊的效果取決於服務器上保持的SYN半連接數,這個值=SYN攻擊的頻度 x SYN Timeout,所以通過縮短從接收到SYN報文到確定這個報文無效並丟棄改連接的時間,例如設置為20秒以下(過低的SYN Timeout設置可能會影響客戶的正常訪問),可以成倍的降低服務器的負荷。
第二種方法是設置SYN Cookie,就是給每一個請求連接的IP地址分配一個Cookie,如果短時間內連續受到某個IP的重複SYN報文,就認定是受到了攻擊,以後從這個IP地址來的包會被丟棄。
可是上述的兩種方法只能對付比較原始的SYN Flood攻擊,縮短SYN Timeout時間僅在對方攻擊頻度不高的情況下生效,SYN Cookie更依賴於對方使用真實的IP地址,如果攻擊者以數萬/秒的速度發送SYN報文,同時利用SOCK_RAW隨機改寫IP報文中的源地址,以上的方法將毫無用武之地。
二、SYN Flooder源碼解讀
下面我們來分析SYN Flooder的程序實現。
首先,我們來看一下TCP報文的格式:
http://www.yesky.com/20010625/zhang01062519.gif
如上圖所示,一個TCP報文由三個部分構成:20字節的IP首部、20字節的TCP首部與不定長的數據段,(實際操作時可能會有可選的IP選項,這種情況下TCP首部向後順延)由於我們只是發送一個SYN信號,並不傳遞任何數據,所以TCP數據段為空。TCP首部的數據結構為:
http://www.yesky.com/20010625/zhang01062520.gif (圖)
根據TCP報文格式,我們定義一個結構TCP_HEADER用來存放TCP首部:
typedef struct _tcphdr
{
USHORT th_sport; //16位源端口
USHORT th_dport; //16位目的端口
unsigned int th_seq; //32位序列號
unsigned int th_ack; //32位確認號
unsigned char th_lenres; //4位首部長度+6位保留字中的4位
unsigned char th_flag; //2位保留字+6位標誌位
USHORT th_win; //16位窗口大小
USHORT th_sum; //16位校驗和
USHORT th_urp; //16位緊急數據偏移量
}TCP_HEADER;
通過以正確的數據填充這個結構並將TCP_HEADER.th_flag賦值為2(二進制的00000010)我們能製造一個SYN的TCP報文,通過大量發送這個報文可以實現SYN Flood的效果。但是為了進行IP欺騙從而隱藏自己,也為了躲避服務器的SYN Cookie檢查,還需要直接對IP首部進行操作: http://www.yesky.com/20010625/zhang01062521.gif
同樣定義一個IP_HEADER來存放IP首部:
typedef struct _iphdr
{
unsigned char h_verlen; //4位首部長度+4位IP版本號
unsigned char tos; //8位服務類型TOS
unsigned short total_len; //16位總長度(字節)
unsigned short ident; //16位標識
unsigned short frag_and_flags; //3位標誌位
unsigned char ttl; //8位生存時間 TTL
unsigned char proto; //8位協議號(TCP, UDP 或其他)
unsigned short checksum; //16位IP首部校驗和
unsigned int sourceIP; //32位源IP地址
unsigned int destIP; //32位目的IP地址
}IP_HEADER;
然後通過SockRaw=WSASocket(AF_INET,SOCK_RAW,IPPROTO_RAW,NULL,0,WSA_FLAG_OVERLAPPED));
建立一個原始套接口,由於我們的IP源地址是偽造的,所以不能指望系統幫我們計算IP校驗和,我們得在在setsockopt中設置IP_HDRINCL告訴系統自己填充IP首部並自己計算校驗和:
flag=TRUE;
setsockopt(SockRaw,IPPROTO_IP,IP_HDRINCL,(char *)&flag,sizeof(int));
IP校驗和的計算方法是:首先將IP首部的校驗和字段設為0(IP_HEADER.checksum=0),然後計算整個IP首部(包括選項)的二進制反碼的和,一個標準的校驗和函數如下所示:
USHORT checksum(USHORT *buffer, int size)
{
unsigned long cksum=0;
while(size >;1)
{
cksum+=*buffer++;
size -=sizeof(USHORT);
}
if(size ) cksum += *(UCHAR*)buffer;
cksum = (cksum >;>; 16) + (cksum & 0xffff);
cksum += (cksum >;>;16);
return (USHORT)(~cksum);
}
這個函數並沒有經過任何的優化,由於校驗和函數是TCP/IP協議中被調用最多函數之一,所以一般說來,在實現TCP/IP棧時,會根據操作系統對校驗和函數進行優化。
TCP首部檢驗和與IP首部校驗和的計算方法相同,在程序中使用同一個函數來計算。
需要注意的是,由於TCP首部中不包含源地址與目標地址等信息,為了保證TCP校驗的有效性,在進行TCP校驗和的計算時,需要增加一個TCP偽首部的校驗和,定義如下:
struct
{
unsigned long saddr; //源地址
unsigned long daddr; //目的地址
char mbz; //置空
char ptcl; //協議類型
unsigned short tcpl; //TCP長度
}psd_header;
然後我們將這兩個字段複製到同一個緩衝區SendBuf中並計算TCP校驗和:
memcpy(SendBuf,&psd_header,sizeof(psd_header));
memcpy(SendBuf+sizeof(psd_header),&tcp_header,sizeof(tcp_header));
tcp_header.th_sum=checksum((USHORT *)SendBuf,sizeof(psd_header)+sizeof(tcp_header));
計算IP校驗和的時候不需要包括TCP偽首部:
memcpy(SendBuf,&ip_header,sizeof(ip_header));
memcpy(SendBuf+sizeof(ip_header),&tcp_header,sizeof(tcp_header));
ip_header.checksum=checksum((USHORT *)SendBuf, sizeof(ip_header)+sizeof(tcp_header));
再將計算過校驗和的IP首部與TCP首部複製到同一個緩衝區中就可以直接發送了:
memcpy(SendBuf,&ip_header,sizeof(ip_header));
sendto(SockRaw,SendBuf,datasize,0,(struct sockaddr*) &DestAddr,sizeof(DestAddr));
因為整個TCP報文中的所有部分都是我們自己寫入的(操作系統不會做任何干涉),所以我們可以在IP首部中放置隨機的源IP地址,如果偽造的源IP地址確實有人使用,他在接收到服務器的SYN+ACK報文後會發送一個RST報文(標誌位為00000100),通知服務器端不需要等待一個無效的連接,可是如果這個偽造IP並沒有綁定在任何的主機上,不會有任何設備去通知主機該連接是無效的(這正是TCP協議的缺陷),主機將不斷重試直到SYN Timeout時間後才能丟棄這個無效的半連接。所以當攻擊者使用主機分佈很稀疏的IP地址段進行偽裝IP的SYN Flood攻擊時,服務器主機承受的負荷會相當的高,根據測試,一台PIII 550MHz+128MB+100Mbps的機器使用經過初步優化的SYN Flooder程序可以以16,000包/秒的速度發送TCP SYN報文,這樣的攻擊力已經足以拖垮大部分WEB服務器了。
稍微動動腦筋我們就會發現,想對SYN Flooder程序進行優化是很簡單的,從程序構架來看,攻擊時循環內的代碼主要是進行校驗和計算與緩衝區的填充,一般的思路是提高校驗和計算的速度,我甚至見過用彙編代碼編寫的校驗和函數,實際上,有另外一個變通的方法可以輕鬆實現優化而又不需要高深的編程技巧和數學知識,我們仔細研究了兩個不同源地址的TCP SYN報文後發現,兩個報文的大部分字段相同(比如目的地址、協議等等),只有源地址和校驗和不同(如果為了隱蔽,源端口也可以有變化,但是並不影響我們算法優化的思路),如果我們事先計算好大量的源地址與校驗和的對應關係表(如果其他的字段有變化也可以加入這個表),等計算完畢了攻擊程序就只需要單純的組合緩衝區並發送(用指針來直接操作緩衝區的特定位置,從事先計算好的對應關係表中讀出數據,替換緩衝區相應字段),這種簡單的工作完全取決於系統發送IP包的速度,與程序的效率沒有任何關係,這樣,即使是CPU主頻較低的主機也能快速的發送大量TCP SYN攻擊包。如果考慮到緩衝區拼接的時間,甚至可以定義一個很大的緩衝區數組,填充完畢後再發送。 |
剖析SYN Flood攻擊
T/TCP是否對SYN FLOOD會有作用?避免3次握手 |
剖析SYN Flood攻擊
| 剖析SYN Flood攻擊
<<TCP/IP詳解 VOL III>;>; |
| |