深入浅出TCP中的SYN-Cookies

_____egon新书来袭请看:https://egonlin.com/book.html

深入浅出TCP中的SYN-Cookies

本文渐进地介绍TCP中的syn-cookie技术,包括其由来、原理、实例测试。

SYN Flood 攻击

TCP连接建立时,客户端通过发送SYN报文发起向处于监听状态的服务器发起连接,服务器为该连接分配一定的资源,并发送SYN+ACK报文。对服务器来说,此时该连接的状态称为半连接(Half-Open),而当其之后收到客户端回复的ACK报文后,连接才算建立完成。在这个过程中,如果服务器一直没有收到ACK报文(比如在链路中丢失了),服务器会在超时后重传SYN+ACK

如果经过多次超时重传后,还没有收到, 那么服务器会回收资源并关闭半连接,仿佛之前最初的SYN报文从来没到过一样!

这看上一切正常,但是如果有坏人故意大量不断发送伪造的SYN报文,那么服务器就会分配大量注定无用的资源,并且从backlog的意义 中可知,服务器能保存的半连接的数量是有限的!所以当服务器受到大量攻击报文时,它就不能再接收正常的连接了。换句话说,它的服务不再可用了!这就是SYN Flood攻击的原理,它是一种典型的DDoS攻击。

连接请求的关键信息

Syn-Flood攻击成立的关键在于服务器资源是有限的,而服务器收到请求会分配资源。通常来说,服务器用这些资源保存此次请求的关键信息,包括请求的来源和目(五元组),以及TCP选项,如最大报文段长度MSS、时间戳timestamp、选择应答使能Sack、窗口缩放因子Wscale等等。当后续的ACK报文到达,三次握手完成,新的连接创建,这些信息可以会被复制到连接结构中,用来指导后续的报文收发。

那么现在的问题就是服务器如何在不分配资源的情况下

  1. 验证之后可能到达的ACK的有效性,保证这是一次完整的握手
  2. 获得SYN报文中携带的TCP选项信息

SYN cookies 算法

SYN Cookies算法可以解决上面的第1个问题以及第2个问题的一部分

我们知道,TCP连接建立时,双方的起始报文序号是可以任意的。SYN cookies利用这一点,按照以下规则构造初始序列号:

  • t为一个缓慢增长的时间戳(典型实现是每64s递增一次)
  • m为客户端发送的SYN报文中的MSS选项值
  • s是连接的元组信息(源IP,目的IP,源端口,目的端口)和t经过密码学运算后的Hash值,即s = hash(sip,dip,sport,dport,t)s的结果取低 24 位

则初始序列号n为:

  • 高 5 位为t mod 32
  • 接下来3位为m的编码值
  • 低 24 位为s

当客户端收到此SYN+ACK报文后,根据TCP标准,它会回复ACK报文,且报文中ack = n + 1,那么在服务器收到它时,将ack - 1就可以拿回当初发送的SYN+ACK报文中的序号了!服务器巧妙地通过这种方式间接保存了一部分SYN报文的信息。

接下来,服务器需要对ack - 1这个序号进行检查:

  • 将高 5 位表示的t与当前之间比较,看其到达地时间是否能接受。
  • 根据t和连接元组重新计算s,看是否和低 24 一致,若不一致,说明这个报文是被伪造的。
  • 解码序号中隐藏的mss信息

到此,连接就可以顺利建立了。

SYN Cookies 缺点

既然SYN Cookies可以减小资源分配环节,那为什么没有被纳入TCP标准呢?原因是SYN Cookies也是有代价的:

  1. MSS的编码只有3位,因此最多只能使用 8 种MSS
  2. 服务器必须拒绝客户端SYN报文中的其他只在SYNSYN+ACK中协商的选项,原因是服务器没有地方可以保存这些选项,比如WscaleSACK
  3. 增加了密码学运算

Linux 中的 SYN Cookies

Linux上的SYN Cookies实现与wiki中描述的算法在序号生成上有一些区别,其SYN+ACK的序号通过下面的公式进行计算:

内核编译需要打开 CONFIG_SYN_COOKIES
seq hash(saddr, daddr, sport, dport, 00req.th.seq << 24 (hash(saddr, daddr, sport, dport, t, 1mss_ind) 0x00FFFFFF

其中,req.th.seq表示客户端的SYN报文中的序号,mss_ind是客户端通告的MSS值得编码,它的取值在比较新的内核中有 4 种(老的内核有 8 种), 分别对应以下 4 种值

static __u16 const msstab[] = {
    536,
    1300,
    1440,    /* 1440, 1452: PPPoE */
    1460,
};

感兴趣的可以顺着以下轨迹浏览调用顺序

tcp_conn_request
  |-- cookie_init_sequence
     |-- cookie_v4_init_sequence
        |-- __cookie_v4_init_sequence
           |-- secure_tcp_syn_cookie

SYN Cookies 与时间戳

如果服务器和客户端都打开了时间戳选项,那么服务器可以将客户端在SYN报文中携带了TCP选项的使能情况暂时保存在时间戳中。当前使用了低 6 位,分别保存WscaleSACKECN

客户端会在ACKTSecr字段,把这些值带回来。

实验

Linux中的/proc/sys/net/ipv4/tcp_syncookies是内核中的SYN Cookies开关,0表示关闭SYN Cookies1表示在新连接压力比较大时启用SYN Cookies,2表示始终使用SYN Cookies

本实验是在4.4.0内核运行的,服务端监听50001端口,backlog参数为3(该参数意义)。同时,模拟不同的客户端注入SYN报文。

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netinet/ip.h>
#include <netinet/tcp.h>
#include <net/if.h>
#include <sys/ioctl.h>
#include <linux/if_tun.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <pthread.h>
 
#define PCKT_LEN 1024
#define BACKLOG 3
#define TUN_ADDR "192.168.2.1"
#define SPOOF_NET "192.168.3.0"
#define SPOOF_PREFIX "192.168.3."
 
#define COUNT 8
 
const char* spoof_ip_list[COUNT] = {"192.168.3.1",
         "192.168.3.2",
         "192.168.3.3",
         "192.168.3.4",
         "192.168.3.5",
         "192.168.3.6",
         "192.168.3.7",
         "192.168.3.8"}; 
 
const uint16_t spoof_mss[COUNT] = {536, 1300, 1440, 1460, 536, 1300, 1440, 1460};
 
const uint32_t spoof_tsp[COUNT] = {1000, 2000, 3000, 4000, 5000,6000, 7000, 8000};
 
const uint8_t spoof_wscale[COUNT] = {1, 2, 3, 4, 1, 2, 3, 4};
 
#define TUN_PORT 50001
 
struct psdhdr{
 uint32_t saddr;
 uint32_t daddr;
 char zero;
 char protocol;
 uint16_t tcplen;
};
 
struct mss_opt{
 uint8_t kind; // = 2
 uint8_t length; // = 4
 uint16_t mss;  
}__attribute__((packed));
 
struct tstamp_opt{
 uint8_t kind; // = 8 
 uint8_t length; // = 10
 uint32_t tsval;  
 uint32_t tsecr;
 uint8_t nop[2];
}__attribute__((packed));
 
struct wscale_opt{
 uint8_t kind; // = 3 
 uint8_t length; // = 3
 uint8_t scale;  
 uint8_t nop;
}__attribute__((packed));
 
uint16_t calc_cksm(void *pkt, int len)
{
    uint16_t *buf = (uint16_t*)pkt;
    uint32_t cksm = 0;
    while(len > 1)
    {
        cksm += *buf++;
        cksm = (cksm >> 16) + (cksm & 0xffff);
        len -= 2;
    }
    if(len)
    {
        cksm += *((uint8_t*)buf);
        cksm = (cksm >> 16) + (cksm & 0xffff);
    }
    return (uint16_t)((~cksm) & 0xffff);
} 
 
 
unsigned short tcp_checksum (struct iphdr *ip, struct tcphdr* th, char* opt, int optlen)
{
        uint16_t sum = 0;
        char buf[PCKT_LEN];
        int chksumlen = 0;
        struct psdhdr psdhdr;
 
        memset(buf, 0, PCKT_LEN);
 
        psdhdr.saddr = ip->saddr;
        psdhdr.daddr = ip->daddr;
        psdhdr.zero = 0;
        psdhdr.protocol = ip->protocol;
        psdhdr.tcplen = htons(sizeof(struct tcphdr) + optlen);
 
        memcpy(&buf[0], &psdhdr, sizeof(struct psdhdr));
 
        chksumlen += sizeof(struct psdhdr);
 
        memcpy(&buf[chksumlen], th, sizeof(struct tcphdr));
 
        chksumlen += sizeof(struct tcphdr);
 
        if (optlen > 0)
        {
                memcpy(&buf[chksumlen], opt, optlen);
                chksumlen += optlen;
        }
 
        sum = calc_cksm(buf, chksumlen);
 
        return sum; 
}
 
 
int tun_create(int flags)
{
    int fd, err;
    struct ifreq ifr;
 
    if ((fd = open("/dev/net/tun", O_RDWR)) < 0){
        return fd;
    }
 
    memset(&ifr, 0, sizeof(ifr));
    ifr.ifr_flags = flags;
 
    if ((err = ioctl(fd, TUNSETIFF, (void*)&ifr)) < 0 )
    {
        close(fd);
        return err;
    }
 
    if (strcmp(ifr.ifr_name, "tun0")) {
        close(fd);
        return -1;
    }
 
    return fd;
} 
 
int tun_setup(char* tundev)
{
    struct ifreq ifr;
    int sockfd;
    int err;
    
    memset(&ifr, 0, sizeof(ifr));
    snprintf(ifr.ifr_name, (sizeof(ifr.ifr_name) - 1), "%s", tundev);
    
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        return err;
    }
        
    if((err = ioctl(sockfd, SIOCGIFFLAGS, (void *)&ifr)) < 0 ) 
    {
        return err;
    }
 
    ifr.ifr_flags |= IFF_UP;
    if((err = ioctl(sockfd, SIOCSIFFLAGS, (void *)&ifr)) < 0 ) 
    {
        return err;
    }
 
    close(sockfd);
 
    return 0;
} 
 
/* Configure a local IPv4 address and netmask for the device */
int tun_set_address(const char* dev,
                     const char* ip,
                      int prefix_len)
{
    char command[128];
 
    memset(command, 0, sizeof(command));
    
    sprintf(command, "ip addr add %s/%d dev %s > /dev/null 2>&1", ip, prefix_len, dev);
 
    int result = system(command);
    
    return result;
} 
 
int tun_set_route()
{
    char command[128];
 
    memset(command, 0, sizeof(command));
 
    sprintf(command,
           "ip route add %s/24 via %s > /dev/null 2>&1", // ip -4 route add 
            SPOOF_NET, TUN_ADDR);
 
    int result = system(command);
    
    return result;
} 
 
void* server_thread(void* args)
{
        int listenfd;
        struct sockaddr_in servaddr;
 
        listenfd = socket(PF_INET, SOCK_STREAM, 0);
 
        bzero(&servaddr, sizeof(servaddr));
        servaddr.sin_family = AF_INET;
        servaddr.sin_addr.s_addr = inet_addr(TUN_ADDR);
        servaddr.sin_port = htons(TUN_PORT);
 
        bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
 
        listen(listenfd, BACKLOG);
        while(1)
        {
                sleep(1);
        }
 
        return NULL;
}
 
int server_setup()
{
        pthread_t thread;
        if (pthread_create(&thread, NULL, server_thread, NULL) != 0) {
                perror("pthread error");
                return -1;
        }       
}
 
 
void syn_send(int tun_fd, int i)
{
        char buffer[PCKT_LEN];
        struct iphdr *ip = (struct iphdr *) buffer;
        struct tcphdr *tcp = (struct tcphdr *)(buffer + sizeof(struct iphdr));
        uint16_t tot_len = sizeof(struct iphdr) + sizeof(struct tcphdr);
        uint16_t opt_len = 0;
        char* opt = (char*)(buffer + tot_len); // TCP option 
        memset(buffer, 0, PCKT_LEN);    
 
        if (spoof_mss[i] != 0)
        {
                struct mss_opt mss_opt;
 
                memset(&mss_opt, 1, sizeof(mss_opt));
 
                mss_opt.kind = 2;
                mss_opt.length = 4;
                mss_opt.mss = htons(spoof_mss[i]);
 
                memcpy(&opt[opt_len], &mss_opt, sizeof(mss_opt));  
 
                // if we have mss option
                tot_len += sizeof(mss_opt);
                opt_len += sizeof(mss_opt);
        }
 
        if (spoof_tsp[i] != 0)
        {
                struct tstamp_opt ts_opt;
 
                memset(&ts_opt, 1, sizeof(ts_opt));
 
                ts_opt.kind = 8;
                ts_opt.length = 10;
                ts_opt.tsval = htonl(spoof_tsp[i]);
                ts_opt.tsecr = 0;
 
                memcpy(&opt[opt_len], &ts_opt, sizeof(ts_opt));
                tot_len += sizeof(ts_opt);
                opt_len += sizeof(ts_opt);
        }
 
        if (spoof_wscale[i] != 0)
        {
                struct wscale_opt wscale_opt;
 
                memset(&wscale_opt, 1, sizeof(wscale_opt));
 
                wscale_opt.kind = 3;
                wscale_opt.length = 3;
                wscale_opt.scale = spoof_wscale[i];
                
                memcpy(&opt[opt_len], &wscale_opt, sizeof(wscale_opt));
                tot_len += sizeof(wscale_opt);
                opt_len += sizeof(wscale_opt);
        }
 
        ip->ihl = 5;
        ip->version = 4;
        ip->tos = 16;
        ip->tot_len = htons(tot_len);
        ip->id = htons(60000 + i);
        ip->frag_off = 0;
        ip->ttl = 64;
        ip->protocol = 6; // TCP 
        ip->saddr = inet_addr(spoof_ip_list[i]);
        ip->daddr = inet_addr(TUN_ADDR);
        ip->check = calc_cksm((unsigned short *)buffer,sizeof(struct iphdr));
 
        tcp->th_sport = htons(60000 + i);
        tcp->th_dport = htons(TUN_PORT);
        tcp->th_seq = htonl(1);
        tcp->th_ack = 0;
        tcp->th_off = (sizeof(struct tcphdr) + opt_len + sizeof(uint32_t) - 1) / sizeof(uint32_t);
        tcp->th_flags = TH_SYN;
        tcp->th_win = htons(4096);
        tcp->th_urp = 0;
        tcp->th_sum = 0;
        tcp->th_sum = tcp_checksum(ip, tcp, opt, opt_len);  
 
        if(write(tun_fd, buffer, tot_len) < 0)
        {
                perror("write() error");
                exit(-1);
        }
        else
        {
                printf("send packet %d\n", i);
        }
 
        return;
}
 
int main(int argc, char *argv[])
{
    int tun_fd, err;
 
    
    struct sockaddr_in sin, din;
    int one = 1;
    const int *val = &one;
 
    
    tun_fd = tun_create(IFF_TUN | IFF_NO_PI);
    if (tun_fd < 0)
    {
        perror("tun_create");
        return 0;
    }
    
    if (tun_setup("tun0") < 0)
    {
        perror("tun_setup");
        return 0;
    } 
    
    if (tun_setup("tun0") < 0)
    {
        perror("tun_setup");
        return 0;
    }
 
    if (tun_set_address("tun0", TUN_ADDR, 24) < 0)
    {
        perror("set address");
        return 0;
    }
 
    if (tun_set_route() < 0)
    {
        perror("set address");
        return 0;
    }
 
        server_setup();
 
    sleep(5);
 
        for(int i = 0; i < COUNT; i++)
    {
        syn_send(tun_fd, i);
                
        usleep(10000);
    }
 
        sleep(5);
        
    close(tun_fd);
 
    return 0; 
}
 
测试代码

不开启 SYN Cookies

echo 0 > /proc/sys/net/ipv4/tcp_syncookies

可以看到,在收到3SYN报文后,服务器不再响应新的连接请求了,这也就是SYN-Flood的攻击方式。

有条件使用 SYN Cookies

echo 1 > /proc/sys/net/ipv4/tcp_syncookies

由于服务器的backlog参数为3,因此图中的从第4SYN+ACK(#8报文)开始使用SYN Cookies

从时间戳可以看出,#8报文(44167748)比 #6号报文(44167796)还要小。

44167748 = 0x2A1F244 ,最后低6位是 0b000100 ,与SYN报文中 wscale = 4 是相符的

小结

SYN Cookie技术可以让服务器在收到客户端的SYN报文时,不分配资源保存客户端信息,而是将这些信息保存在SYN+ACK的初始序号和时间戳中。对正常的连接,这些信息会随着ACK报文被带回来。

posted @ 2020-11-11 17:33  linhaifeng  阅读(457)  评论(0编辑  收藏  举报