diff --git a/CMakeLists.txt b/CMakeLists.txt index 38aef723..09e366ff 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -786,6 +786,13 @@ if (NOT LWS_WITHOUT_TESTAPPS) "test-server/test-server-dumb-increment.c" "test-server/test-server-mirror.c" "test-server/test-server-echogen.c") + if (UNIX) + create_test_app(test-fuzxy "test-server/fuzxy.c" + "" + "" + "" + "") + endif() if (UNIX AND NOT ((CMAKE_C_COMPILER_ID MATCHES "Clang") OR (CMAKE_CXX_COMPILER_ID MATCHES "Clang"))) create_test_app(test-server-pthreads "test-server/test-server-pthreads.c" diff --git a/test-server/fuzxy.c b/test-server/fuzxy.c new file mode 100644 index 00000000..9ef6a477 --- /dev/null +++ b/test-server/fuzxy.c @@ -0,0 +1,683 @@ +/* + * fuzzing proxy - network-level fuzzing injection proxy + * + * Copyright (C) 2016 Andy Green + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation: + * version 2.1 of the License. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + * + * + * fuzxy is designed to go on the client path + * + * [ client <-> fuzxy ] <-> server + * + * you can arrange that with, eg, + * + * http_proxy=localhost:8880 + * + * env var before starting the client. + * + * Even though he is on the client side, he is able to see and change traffic + * in both directions, and so fuzz both the client and the server. + */ + +#if defined(_WIN32) && defined(EXTERNAL_POLL) +#define WINVER 0x0600 +#define _WIN32_WINNT 0x0600 +#define poll(fdArray, fds, timeout) WSAPoll((LPWSAPOLLFD)(fdArray), (ULONG)(fds), (INT)(timeout)) +#endif + +#include "lws_config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "../lib/libwebsockets.h" + +#ifdef _WIN32 +#include +#include "gettimeofday.h" +#else +#include +#include +#include +#endif + + +enum types { + FZY_S_DEAD = 0, + FZY_S_LISTENING = 1, + FZY_S_ACCEPTED = 2, + FZY_S_PROXIED = 3, + FZY_S_ONWARD = 4, +}; + +enum proxy_parser_states { + FZY_PP_CONNECT = 0, + FZY_PP_ADDRESS = 1, + FZY_PP_PORT = 2, + FZY_PP_CRLFS = 3, +}; + +enum fuzzer_parser_states { + FZY_FP_SEARCH = 0, + FZY_FP_SEARCH2 = 1, + FZY_FP_INJECT = 2, + FZY_FP_PENDING = 3, +}; + +struct ring { + char buf[4096]; + int head; + int tail; +}; + +struct state { + enum types type; + enum proxy_parser_states pp; + int ppc; + + struct ring in; + + char address[256]; + int port; + + enum fuzzer_parser_states fp; + int fuzc; + int pending; + + int twin; /* must be fixed up when arrays lose guys */ + unsigned int outbound:1; /* from local -> remote */ +}; + + +int force_exit = 0; + +static const int ring_size(struct ring *r) +{ + return sizeof(r->buf); +} +static const int ring_used(struct ring *r) +{ + return (r->head - r->tail) & (ring_size(r) - 1); +} +static const int ring_free(struct ring *r) +{ + return (ring_size(r) - 1) - ring_used(r); +} +static const int ring_get_one(struct ring *r) +{ + int n = r->buf[r->tail] & 255; + + if (r->tail == r->head) + return -1; + + r->tail++; + if (r->tail == ring_size(r)) + r->tail = 0; + + return n; +} + +void sighandler(int sig) +{ + force_exit = 1; +} + +static struct option options[] = { + { "help", no_argument, NULL, 'h' }, + { "debug", required_argument, NULL, 'd' }, + { "port", required_argument, NULL, 'p' }, + { "ssl", no_argument, NULL, 's' }, + { "allow-non-ssl", no_argument, NULL, 'a' }, + { "interface", required_argument, NULL, 'i' }, + { "closetest", no_argument, NULL, 'c' }, + { "libev", no_argument, NULL, 'e' }, +#ifndef LWS_NO_DAEMONIZE + { "daemonize", no_argument, NULL, 'D' }, +#endif + { "resource_path", required_argument, NULL, 'r' }, + { NULL, 0, 0, 0 } +}; + +static struct pollfd pfd[128]; +static struct state state[128]; +static int pfds = 0; + +static void close_and_remove_fd(int index) +{ + int n; + + lwsl_notice("%s: closing index %d\n", __func__, index); + close(pfd[index].fd); + pfd[index].fd = -1; + + n = state[index].twin; + if (n) { + assert(state[n].twin == index); + } + state[index].type = FZY_S_DEAD; + + if (index == pfds - 1) { + if (state[index].twin) + state[state[index].twin].twin = 0; + state[index].twin = 0; + goto bail; + } + + /* swap the end guy into the deleted guy and trim back one */ + + if (state[pfds - 1].twin) { + state[state[pfds - 1].twin].twin = index; + if (n && n == pfds - 1) + n = index; + } + + /* swap the last guy into dead guy's place and trim by one */ + pfd[index] = pfd[pfds - 1]; + state[index] = state[pfds - 1]; + + if (n) { + pfds--; + state[n].twin = 0; + close_and_remove_fd(n); + return; + } + +bail: + pfds--; +} + +static void construct_state(int n, enum types s, int flags) +{ + memset(&state[n], 0, sizeof state[n]); + state[n].type = s; + pfd[n].events = flags | POLLHUP; +} + +static int +fuzxy_listen(const char *interface_name, int port, int *sockfd) +{ + struct sockaddr_in serv_addr4; + socklen_t len = sizeof(struct sockaddr); + struct sockaddr_in sin; + int n, opt = 1; + + *sockfd = socket(AF_INET, SOCK_STREAM, 0); + + if (*sockfd == -1) { + lwsl_err("ERROR opening socket\n"); + goto bail1; + } + + if (setsockopt(*sockfd, SOL_SOCKET, SO_REUSEADDR, + (const void *)&opt, sizeof(opt)) < 0) { + lwsl_err("unable to set listen socket options\n"); + goto bail2; + } + + bzero((char *) &serv_addr4, sizeof(serv_addr4)); + serv_addr4.sin_addr.s_addr = INADDR_ANY; + serv_addr4.sin_family = AF_INET; + + if (interface_name[0] && + lws_interface_to_sa(0, interface_name, (struct sockaddr_in *) + (struct sockaddr *)&serv_addr4, + sizeof(serv_addr4)) < 0) { + lwsl_err("Unable to find interface %s\n", interface_name); + goto bail2; + } + + serv_addr4.sin_port = htons(port); + + n = bind(*sockfd, (struct sockaddr *)&serv_addr4, + sizeof(serv_addr4)); + if (n < 0) { + lwsl_err("ERROR on binding to port %d (%d %d)\n", + port, n, errno); + goto bail2; + } + + if (getsockname(*sockfd, (struct sockaddr *)&sin, &len) == -1) + lwsl_warn("getsockname: %s\n", strerror(errno)); + else + port = ntohs(sin.sin_port); + + listen(*sockfd, SOMAXCONN); + + return 0; +bail2: + close(*sockfd); +bail1: + return -1; +} + +struct fuzxy_rule { + const char *s[3]; + int len[3]; + int inject_len; +}; + +struct fuzxy_rule r = { + { "Host:", "\x0d", " \x0d" }, + { 5, 1, 2 }, + 65536 +}; + +static int fuzz(int n, char *out, int len) +{ + struct state *s = &state[n]; + int m = 0; + int c; + + while (m < len) { + switch (s->fp) { + case FZY_FP_SEARCH: + c = ring_get_one(&state[s->twin].in); + if (c < 0) + return m; + if (c == r.s[0][s->fuzc++]) { + if (s->fuzc == r.len[0]) { + s->fuzc = 0; + s->fp = FZY_FP_SEARCH2; + } + } else + s->fuzc = 0; + out[m++] = c; + break; + + case FZY_FP_SEARCH2: + c = ring_get_one(&state[s->twin].in); + if (c < 0) + return m; + if (c == r.s[1][s->fuzc++]) { + if (s->fuzc == r.len[1]) { + lwsl_notice("+++++++fuzzer hit...\n"); + s->fuzc = 0; + s->fp = FZY_FP_INJECT; + s->pending = c; + goto inject; + } + } else + s->fuzc = 0; + out[m++] = c; + break; + case FZY_FP_INJECT: +inject: + out[m++] = r.s[2][s->fuzc++ % r.len[2]]; + if (s->fuzc == r.inject_len) + s->fp = FZY_FP_PENDING; + break; + + case FZY_FP_PENDING: + out[m++] = s->pending; + s->fp = FZY_FP_SEARCH; + s->fuzc = 0; + break; + } + } + + return m; +} + +static int +handle_accept(int n) +{ + struct addrinfo ai, *res, *result; + struct sockaddr_in serv_addr4; + struct state *s = &state[n]; + void *p = NULL; + int m, sockfd; + + while (1) { + m = ring_get_one(&s->in); + if (m < 0) + return 0; + + switch (s->pp) { + case FZY_PP_CONNECT: + if (m != "CONNECT "[s->ppc++]) { + lwsl_notice("failed CONNECT match\n"); + return 1; + } + if (s->ppc == 8) { + s->pp = FZY_PP_ADDRESS; + s->ppc = 0; + } + break; + case FZY_PP_ADDRESS: + if (m == ':') { + s->address[s->ppc++] = '\0'; + s->pp = FZY_PP_PORT; + s->ppc = 0; + break; + } + if (m == ' ') { + s->address[s->ppc++] = '\0'; + s->pp = FZY_PP_CRLFS; + s->ppc = 0; + break; + } + + + s->address[s->ppc++] = m; + if (s->ppc == sizeof(s->address)) { + lwsl_notice("Failed on address length\n"); + return 1; + } + break; + case FZY_PP_PORT: + if (m == ' ') { + s->pp = FZY_PP_CRLFS; + s->ppc = 0; + break; + } + if (m >= '0' && m <= '9') { + s->port *= 10; + s->port += m - '0'; + break; + } + return 1; + + case FZY_PP_CRLFS: + if (m != "\x0d\x0a\x0d\x0a"[s->ppc++]) + s->ppc = 0; + if (s->ppc != 4) + break; + s->type = FZY_S_PROXIED; + + memset (&ai, 0, sizeof ai); + ai.ai_family = PF_UNSPEC; + ai.ai_socktype = SOCK_STREAM; + ai.ai_flags = AI_CANONNAME; + + if (getaddrinfo(s->address, NULL, &ai, &result)) { + lwsl_notice("failed to lookup %s\n", + s->address); + return 1; + } + + res = result; + while (!p && res) { + switch (res->ai_family) { + case AF_INET: + p = &((struct sockaddr_in *)res-> + ai_addr)->sin_addr; + break; + } + + res = res->ai_next; + } + + if (!p) { + lwsl_notice("Failed to get address result %s\n", + s->address); + freeaddrinfo(result); + return 1; + } + + serv_addr4.sin_family = AF_INET; + serv_addr4.sin_addr = *((struct in_addr *)p); + serv_addr4.sin_port = htons(s->port); + bzero(&serv_addr4.sin_zero, 8); + freeaddrinfo(result); + + lwsl_err("Conn %d req '%s' port %d\n", n, + s->address, s->port); + /* we need to open the associated onward connection */ + sockfd = socket(AF_INET, SOCK_STREAM, 0); + + if (connect(sockfd, (struct sockaddr *)&serv_addr4, + sizeof(struct sockaddr)) == -1 || + errno == EISCONN) { + lwsl_err("proxied onward connection failed\n"); + return 1; + } + s->twin = pfds; + construct_state(pfds, FZY_S_ONWARD, + POLLOUT | POLLIN | POLLERR); + state[pfds].twin = n; + lwsl_notice("binding conns %d and %d\n", n, pfds); + state[pfds].outbound = s->outbound; + state[pfds].ppc = 0; + pfd[pfds++].fd = sockfd; + + lwsl_notice("onward connection in progress\n"); + if (ring_used(&s->in)) + pfd[s->twin].events |= POLLOUT; + if (write(pfd[n].fd, + "HTTP/1.0 200 \x0d\x0a\x0d\x0a", 17) < 17) + return 1; + } + } + + return 0; +} + +static void sigpipe_handler(int x) +{ +} + +int +main(int argc, char **argv) +{ + char interface_name[128] = "", interface_name_local[128] = "lo"; + int port_local = 8880, accept_fd; + struct sockaddr_in cli_addr; + int debug_level = 7; + socklen_t clilen; + struct state *s; + char out[4096]; + int opts = 0; + int n = 0, m; + +#ifndef _WIN32 + int syslog_options = LOG_PID | LOG_PERROR; +#endif +#ifndef LWS_NO_DAEMONIZE + int daemonize = 0; +#endif + signal(SIGPIPE, sigpipe_handler); + + while (n >= 0) { + n = getopt_long(argc, argv, "eci:hsap:d:Dr:", options, NULL); + if (n < 0) + continue; + switch (n) { + case 'e': + opts |= LWS_SERVER_OPTION_LIBEV; + break; +#ifndef LWS_NO_DAEMONIZE + case 'D': + daemonize = 1; + #ifndef _WIN32 + syslog_options &= ~LOG_PERROR; + #endif + break; +#endif + case 'd': + debug_level = atoi(optarg); + break; + case 'p': + port_local = atoi(optarg); + break; + case 'i': + strncpy(interface_name, optarg, sizeof interface_name); + interface_name[(sizeof interface_name) - 1] = '\0'; + break; + case 'h': + fprintf(stderr, "Usage: libwebsockets-test-fuzxy " + "[--port=

] [--ssl] " + "[-d ] " + "[--resource_path ]\n"); + exit(1); + } + } + +#if !defined(LWS_NO_DAEMONIZE) && !defined(WIN32) + /* + * normally lock path would be /var/lock/lwsts or similar, to + * simplify getting started without having to take care about + * permissions or running as root, set to /tmp/.lwsts-lock + */ + if (daemonize && lws_daemonize("/tmp/.lwsts-lock")) { + fprintf(stderr, "Failed to daemonize\n"); + return 1; + } +#endif + + signal(SIGINT, sighandler); + +#ifndef _WIN32 + /* we will only try to log things according to our debug_level */ + setlogmask(LOG_UPTO (LOG_DEBUG)); + openlog("fuzxy", syslog_options, LOG_DAEMON); +#endif + + /* tell the library what debug level to emit and to send it to syslog */ + lws_set_log_level(debug_level, lwsl_emit_syslog); + + lwsl_notice("%s\n(C) Copyright 2016 Andy Green - " + "licensed under LGPL2.1\n", argv[0]); + + /* listen on local side */ + + if (fuzxy_listen(interface_name, port_local, &pfd[pfds].fd)) { + lwsl_err("Failed to listen on local side\n"); + goto bail1; + } + construct_state(pfds, FZY_S_LISTENING, POLLIN | POLLERR); + pfds++; + + lwsl_notice("Local side listening on %s:%u\n", + interface_name_local, port_local); + + while (!force_exit) { + + m = poll(pfd, pfds, 50); + if (m < 0) + continue; + for (n = 0; n < pfds; n++) { + s = &state[n]; + if (s->type == FZY_S_LISTENING && + (pfd[n].revents & POLLIN)) { + /* first do the accept entry */ + + clilen = sizeof(cli_addr); + accept_fd = accept(pfd[0].fd, + (struct sockaddr *)&cli_addr, &clilen); + if (accept_fd < 0) { + if (errno == EAGAIN || + errno == EWOULDBLOCK) + continue; + + lwsl_warn("ERROR on accept: %s\n", + strerror(errno)); + continue; + } + construct_state(pfds, FZY_S_ACCEPTED, + POLLIN | POLLERR); + state[pfds].outbound = n == 0; + state[pfds].pp = FZY_PP_CONNECT; + state[pfds].ppc = 0; + pfd[pfds++].fd = accept_fd; + lwsl_notice("new connect accepted\n"); + continue; + } + if (pfd[n].revents & POLLIN) { + assert(ring_free(&s->in)); + m = (ring_size(&s->in) - 1) - + s->in.head; + if (s->in.head == ring_size(&s->in) - 1 && + s->in.tail) + m = 1; + m = read(pfd[n].fd, s->in.buf + s->in.head, m); +// lwsl_notice("read %d\n", m); + if (m <= 0) + goto drop; + s->in.head += m; + if (s->in.head == ring_size(&s->in)) + s->in.head = 0; + + switch (s->type) { + case FZY_S_ACCEPTED: /* parse proxy CONNECT */ + if (handle_accept(n)) + goto drop; + break; + case FZY_S_PROXIED: + case FZY_S_ONWARD: + if (ring_used(&s->in)) + pfd[s->twin].events |= POLLOUT; + break; + default: + assert(0); + break; + } + if (s->in.head == s->in.tail) { + s->in.head = s->in.tail = 0; + pfd[n].events |= POLLIN; + } + if (!ring_free(&s->in)) + pfd[n].events &= ~POLLIN; + } + if (pfd[n].revents & POLLOUT) { + switch (s->type) { + case FZY_S_PROXIED: + case FZY_S_ONWARD: + /* + * draw down enough of the partner's + * in ring to either exhaust it + * or fill an output buffer + */ + m = fuzz(n, out, sizeof(out)); + if (m) { + m = write(pfd[n].fd, out, m); + if (m <= 0) + goto drop; + } else + pfd[n].events &= ~POLLOUT; + + if (ring_free(&state[s->twin].in)) + pfd[s->twin].events |= POLLIN; + + break; + default: + break; + } + } + + continue; +drop: + close_and_remove_fd(n); + n--; /* redo this slot */ + } + } + +bail1: + lwsl_notice("%s exited cleanly\n", argv[0]); + +#ifndef _WIN32 + closelog(); +#endif + + return 0; +} +