/* Copyright (c) 2014 IOnU Security Inc. All rights reserved Created March 2014 by Kendrick Webster K2Client/proxy.c - implementation of proxy.h */ #include #ifndef WIN32 #include #include #include #include #include #include #include #else #include #include #include #pragma comment (lib, "Ws2_32.lib") #endif #include #include #include #include #include #include #include #ifdef __MACH__ #include #else #include #endif //#include "eyeinterface.h" #include "eyeutils.h" #include "constants.h" #include "util.h" #include "platform_binding.h" #include "proxy.h" #ifdef WIN32 #define ssize_t int #define strdup _strdup #else #define SOCKET_ERROR -1 #endif #define RECEIVE_LOOP_CYCLES_PER_SECOND 20 /* affects how quickly proxy_stop() completes, tradeoff with battery life */ #define RECEIVE_TIMEOUT_MICROSECONDS (1000000 / RECEIVE_LOOP_CYCLES_PER_SECOND) #define RECEIVE_BUFFER_SIZE (((K2IPC_MAX_PACKET_SIZE * 8 + 5) / 6) + 512) /* base64 encoding, +512 for HTTP headers */ /* Poll messages (GET /recv/, base64) consist of the following fields: octets description -------------------------------------------------------------------------- 9 nonce (random initialization vector) 4 sequence number (see init message) n client URN (string, no terminator, implicit length) 9 message integrity code (MIC) All fields after the nonce are encrypted using an auto-hashing stream cipher with a hard-coded key. The decrypted MIC will match only for properly encrypted packets. Spurious packets fail the MIC check. This is used to increase the difficulty of DOS attacks. The sequence number is included to prevent replay DOS attacks. The response to a valid poll message is a text document containing the next received packet for the client URN, base64 encoded, followed by a period '.' alone on a line. In case no packet arrives within the poll timeout interval, the response is "timeout". Invalid messages result in a 404 response (delayed to minimize DOS impact). */ /* Send messages (POST to /send) consist of the following fields: octets description -------------------------------------------------------------------------- 9 nonce (random initialization vector) 4 sequence number (see init message) n K2 UDP packet (containing client URN) 9 message integrity code (MIC) Encryption is done in the same manner (and for the same reasons) as for poll messages. The sequence number in send messages is independent from the sequence number in poll messages. The K2 UDP packet must be of a type that contains a client URN. This is the case for all K2 UDP packet types that are sent from a client to the daemon. The response to a valid send message is a text document containing a single line with the text "OK". Invalid messages result in a 404 response (delayed to minimize DOS impact). */ /* Init messages (GET /init/, base64) consist of the following fields: octets description -------------------------------------------------------------------------- 9 nonce (random initialization vector) n client URN (string, no terminator, implicit length) 9 message integrity code (MIC) The response to an init message is a sequence number message: octets description -------------------------------------------------------------------------- 9 nonce (random initialization vector) 4 poll sequence number (randomly initialized) 4 send sequence number (randomly initialized) 9 message integrity code (MIC) The sequence numbers are randomly-generated and stored as part of a session's state (associated with the client URN) when a session is initially established. Upon receiving each poll or send message, the corresponding sequence number is checked, and, if it matches, incremented modulo 2^32. A 'session' remains active as long as there is traffic (i.e. K2IPC hearbeat messages). An absence of traffic causes a session to timeout. Repeated init messages return the current sequence numbers associated with the session. The reply (to all init messages) is delayed a small amount to limit the impact of a replay attack. Although the delay could be selectively applied only for existing sessions, doing so would leak information about who is currently logged in. The delay is thus chosen to be small enough to have a minimal impact on clients while still being large enough to limit traffic and server load in a replay DOS attack. */ #define K2PROXY_NONCE_SIZE 9 #define K2PROXY_MIC_SIZE 9 #define K2PROXY_SEQUENCE_NUMBER_SIZE 4 #define K2PROXY_MESSAGE_OVERHEAD (K2PROXY_NONCE_SIZE + K2PROXY_MIC_SIZE) static const char* const K2PROXY_KEY = "ICIRY4arfFbqGz9YGZaFRvv6+RNnXrsy"; #define K2PROXY_KEY_MIX_CYCLES 32 static const uint8_t K2PROXY_MIC[K2PROXY_MIC_SIZE] = {0x2b, 0x26, 0x21, 0xff, 0xbc, 0x60, 0xc1, 0x4b, 0x14}; static const char* const POLL_TIMEOUT_RESPONSE = "timeout"; static pthread_t sender_thread_id, receiver_thread_id; static volatile int receiver_running; /* TRUE if receiver thread was started and not yet stopped */ static volatile int receiver_end_thread; /* TRUE if receiver thread should exit */ volatile int is_proxy_session_active; /* TRUE after receiver has received "init" response and not yet stopped */ static const char* client_urn; static size_t client_urn_len; static char* proxy_host; static struct sockaddr_in proxy_addr; static void (*on_packet_received)(uint8_t* buf, size_t len); static uint32_t send_sequence; static uint32_t poll_sequence; static const char* const HTTP_GET_MESSAGE = "\ GET /%s/%s HTTP/1.1\r\n\ Host: %s\r\n\ User-Agent: K2Client\r\n\ Connection: close\r\n\ \r\n"; static const char* const SENDER_HTTP_MESSAGE = "\ POST /send HTTP/1.1\r\n\ Host: %s\r\n\ User-Agent: K2Client\r\n\ Connection: close\r\n\ Content-Type: application/x-www-form-urlencoded\r\n\ Content-Length: %lu\r\n\ \r\n\ data=%s\r\n"; /* Mutex for protecting module-scope data that may be accessed by multiple threads. Mutex-protected data: send_sequence - written by receiver thread, read/modified by sender thread All other module-scope data is accessed without the mutex. Most module-scope data is written only when the threads are not running, then read by both threads. Three threads can touch this module's data: 1). platform_binding module's thread (via this module's interface functions) 2). local receiver thread (loops to receive packets) 3). local sender thread (sends a single packet) */ static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; #define LOCK_MUTEX pthread_mutex_lock(&mutex); #define UNLOCK_MUTEX pthread_mutex_unlock(&mutex); /* returns length of an un-padded base64 string representing octets of data */ static size_t b64minlen(size_t bytes) { return ((bytes * 8) + 5) / 6; } /* returns length of a padded base64 string representing octets of data */ static size_t b64len(size_t bytes) { return ((b64minlen(bytes) + 3) / 4) * 4; } /* encodes as a base64 string that is URL-safe, malloc'd here, caller free's */ static char* to_base64_urlsafe(uint8_t *buf, size_t len) { char *base64 = (char*)malloc(b64len(len) + 1); sequencelogic::Base64Encode(buf, len, base64); char *p = base64; char c; while (0 != (c = *p)) { switch (c) { case '+': *p = '-'; break; case '/': *p = '_'; break; case '=': *p = 0; break; } ++p; } return base64; } /* Opens a socket, sets its receive timeout, and connects the socket to the proxy. Returns socket handle if successful, otherwise INVALID_SOCKET. */ static SOCKET get_socket_connected_to_proxy(void) { SOCKET sockfd = socket(AF_INET, SOCK_STREAM, 0); if (INVALID_SOCKET == sockfd) { upcall_errno("socket"); return INVALID_SOCKET; } struct timeval tv; tv.tv_sec = 0; tv.tv_usec = RECEIVE_TIMEOUT_MICROSECONDS; if (0 != setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, (const char *)&tv, sizeof(tv))) { upcall_errno("setsockopt(SO_RCVTIMEO)"); #ifdef WIN32 closesocket(sockfd); #else close(sockfd); #endif return INVALID_SOCKET; } if (0 != connect(sockfd, (const struct sockaddr*)&proxy_addr, sizeof(proxy_addr))) { upcall_errno("connect"); #ifdef WIN32 closesocket(sockfd); #else close(sockfd); #endif return INVALID_SOCKET; } return sockfd; } /* Encrypts a proxy message Adds nonce and MIC fields */ static void encrypt_proxy_message(uint8_t *message, size_t len) { sc_hash_state_t h; memset(&h, 0, sizeof(h)); get_random_bytes(message, K2PROXY_NONCE_SIZE); sc_hash_vector(&h, message, K2PROXY_NONCE_SIZE); sc_hash_vector(&h, K2PROXY_KEY, strlen(K2PROXY_KEY)); sc_hash_mix(&h, K2PROXY_KEY_MIX_CYCLES); memcpy(message + (len - K2PROXY_MIC_SIZE), K2PROXY_MIC, K2PROXY_MIC_SIZE); sc_hash_encrypt(&h, message + K2PROXY_NONCE_SIZE, len - K2PROXY_NONCE_SIZE); } /* copy to big-endian uint32_t */ static void write_uint32_BE(uint8_t *buf, uint32_t val) { buf[0] = (uint8_t)(val >> 24); buf[1] = (uint8_t)(val >> 16); buf[2] = (uint8_t)(val >> 8); buf[3] = (uint8_t)(val); } /* reads a big-endian uint32_t from */ static uint32_t read_uint32_BE(uint8_t *buf) { uint32_t r; r = buf[0]; r <<= 8; r |= buf[1]; r <<= 8; r |= buf[2]; r <<= 8; r |= buf[3]; return r; } /* helper for , no safety checks */ static size_t hexval(char c) { if (c <= '9') { return c - '0'; } else if (c <= 'F') { return c - ('A' - 10); } else { return c - ('a' - 10); } } /* handle "Transfer-Encoding: chunked", replace buffer contents with de-chunked data */ static void de_chunk(char* buf, size_t* len, const char** p_body) { enum { S_HEADERS, S_HEADER_CR, S_HEADER_CRLF, S_HEADER_CRLFCR, S_CHUNK_LENGTH, S_CHUNK_LENGTH_CR, S_CHUNK, } state = S_HEADERS; size_t chunklen = 0, n = *len, outlen = 0; char c, *p = buf, *q = buf; while (n-- && (0 != (c = *p++))) { switch (state) { case S_HEADERS: outlen++; if ('\r' == c) { state = S_HEADER_CR; } break; case S_HEADER_CR: outlen++; switch (c) { case '\n': state = S_HEADER_CRLF; break; case '\r': state = S_HEADER_CR; break; default: state = S_HEADERS; break; } break; case S_HEADER_CRLF: outlen++; switch (c) { case '\r': state = S_HEADER_CRLFCR; break; case '\n': break; default: state = S_HEADERS; break; } break; case S_HEADER_CRLFCR: outlen++; switch (c) { case '\n': chunklen = 0; q = p; if (p_body) { *p_body = q; } state = S_CHUNK_LENGTH; break; case '\r': break; default: state = S_HEADERS; break; } break; case S_CHUNK_LENGTH: if (isxdigit(c)) { chunklen <<= 4; chunklen += hexval(c); } else if ('\r' == c) { if (chunklen) { state = S_CHUNK_LENGTH_CR; } } else if ('\n' != c) { log_error("expected chunk length, invalid char (%d) encountered", c); return; } break; case S_CHUNK_LENGTH_CR: switch (c) { case '\n': state = S_CHUNK; break; case '\r': break; default: log_error("expected chunk length line terminator, invalid char (%d) encountered", c); return; } break; case S_CHUNK: outlen++; *q++ = c; if (0 == --chunklen) { state = S_CHUNK_LENGTH; } break; } } if ((size_t)(q - buf) != outlen) { log_error("chunk decoder output length counter(%lu) differs from pointer offset(%u)", (long unsigned)outlen, (uint32_t)(q - buf)); } else { *q = 0; *len = outlen; } } /* Helper for receive_response() Parses an HTTP response, calls with the message contained in the response. valid responses contain: 1). HTTP headers (content-type: text/plain) 2). a blank line 3). a line of message text 4). a period '.' alone on a line */ static void on_response_received(char* buf, size_t len, void (*handler)(const char* buf, size_t len), const char* context) { if (0 != strstr(buf, "Transfer-Encoding: chunked")) { de_chunk(buf, &len, NULL); } const char* p = strstr(buf, "\r\n\r\n"); if (p) { p += 4; } if (p && (len > 16) && ('\n' == buf[len - 1]) && ('\r' == buf[len - 2]) && ('.' == buf[len - 3]) && ('\n' == buf[len - 4]) && ('\r' == buf[len - 5])) { buf[len - 5] = 0; len -= (p - buf); len -= 5; handler(p, len); } else if (p && (p == strstr(p, POLL_TIMEOUT_RESPONSE))) { log_debug("proxy \"%s\" timeout", context); } else if (p && (p == strstr(p, "404 "))) { log_warn("proxy 404 response for \"%s\" request", context); } else { log_warn("proxy response format unrecognized (for \"%s\" request), %lu bytes:\r\n%s", context, (long unsigned)len, buf); } } /* Receives a proxy response, calls with the message contained in the response. Returns non-zero on error. */ static int receive_response(SOCKET sockfd, void (*handler)(const char* buf, size_t len), const char* context) { char buf[RECEIVE_BUFFER_SIZE]; char *p = buf; size_t s = sizeof(buf); size_t total_len = 0; while (!receiver_end_thread) { #ifdef WIN32 int received = recv(sockfd, p, (int)s, 0); #else ssize_t received = recv(sockfd, p, s, 0); #endif if (SOCKET_ERROR == received) { if (!((EAGAIN == errno) || (EWOULDBLOCK == errno))) { upcall_errno("recv"); return 1; } } else if (0 == received) { total_len = (total_len >= sizeof(buf)) ? (sizeof(buf) - 1) : total_len; buf[total_len] = 0; on_response_received(buf, total_len, handler, context); break; } else { size_t n = (size_t)received; total_len += n; if (n >= s) { upcall_error_string("recv", "packet too large"); return 1; } p += received; s -= received; } } return 0; } /* Sends an HTTP request Returns non-zero on error */ static int send_http(SOCKET sockfd, const char* path, const char* request, size_t len) { #ifdef WIN32 int sent = send(sockfd, request, (int)len, 0); #else ssize_t sent = send(sockfd, request, len, 0); #endif if (SOCKET_ERROR == sent) { upcall_errno("send"); return 1; } if (len != (size_t)sent) { log_error("proxy /%s/ short write: sent %ld bytes of %lu", path, sent, (long unsigned)len); upcall_error_string("send", "short write"); return 1; } return 0; } /* Formats and sends an HTTP GET request containing (cleartext, encrypted here) Returns non-zero on error */ static int send_http_get(SOCKET sockfd, const char* path, uint8_t *message, size_t message_len) { encrypt_proxy_message(message, message_len); char *base64 = to_base64_urlsafe(message, message_len); size_t len1 = strlen(HTTP_GET_MESSAGE) + strlen(proxy_host) + strlen(base64); // larger than needed (not accounting for '%s') char* request = (char*)malloc(len1); size_t len = snprintf(request, len1, HTTP_GET_MESSAGE, path, base64, proxy_host); if (len >= len1) { log_error("proxy /%s/ snprintf truncated from %lu to %lu", path, (long unsigned)len, (long unsigned)len1 - 1); len = len1 - 1; } free(base64); int r = send_http(sockfd, path, request, len); free(request); return r; } /* Creates and sends a 'poll' message (/recv/) Returns non-zero on error */ static int send_poll_message(SOCKET sockfd) { size_t message_len = client_urn_len + K2PROXY_SEQUENCE_NUMBER_SIZE + K2PROXY_MESSAGE_OVERHEAD; uint8_t *message = (uint8_t*)malloc(message_len); uint8_t *p = message + K2PROXY_NONCE_SIZE; write_uint32_BE(p, poll_sequence++); p += K2PROXY_SEQUENCE_NUMBER_SIZE; memcpy(p, client_urn, client_urn_len); int r = send_http_get(sockfd, "recv", message, message_len); free(message); return r; } /* Creates and sends an 'init' message (/init/) Returns non-zero on error */ static int send_init_message(SOCKET sockfd) { size_t message_len = client_urn_len + K2PROXY_MESSAGE_OVERHEAD; uint8_t *message = (uint8_t*)malloc(message_len); uint8_t *p = message + K2PROXY_NONCE_SIZE; memcpy(p, client_urn, client_urn_len); int r = send_http_get(sockfd, "init", message, message_len); free(message); return r; } /* Handles a response to an 'init' message Sets and (module-scope variables) */ static int is_init_response_good; // set to TRUE by on_init_response_received if the response is good, caller initializes to FALSE static void on_init_response_received(const char* buf, size_t len) { uint8_t p[1024]; if (len < 768) { size_t n = sequencelogic::Base64Decode(buf, p); sc_hash_state_t h; memset(&h, 0, sizeof(h)); sc_hash_vector(&h, p, K2PROXY_NONCE_SIZE); sc_hash_vector(&h, K2PROXY_KEY, strlen(K2PROXY_KEY)); sc_hash_mix(&h, K2PROXY_KEY_MIX_CYCLES); sc_hash_decrypt(&h, p + K2PROXY_NONCE_SIZE, len - K2PROXY_NONCE_SIZE); if (0 != memcmp(p + (n - K2PROXY_MIC_SIZE), K2PROXY_MIC, K2PROXY_MIC_SIZE)) { log_error("bad MIC in /init/ response from proxy"); } else { uint8_t *q = p + K2PROXY_NONCE_SIZE; poll_sequence = read_uint32_BE(q); q += K2PROXY_SEQUENCE_NUMBER_SIZE; uint32_t send_seq = read_uint32_BE(q); LOCK_MUTEX { send_sequence = send_seq; } UNLOCK_MUTEX is_init_response_good = TRUE; log_debug("proxy init response: poll_sequence(%u), send_sequence(%u)", poll_sequence, send_seq); } } else { log_error("init response too large (%lu bytes)", (long unsigned)len); } } /* Sends an 'init' message, parses response to , (module-scope variables) Returns non-zero on error */ static int initialize_proxy_session(void) { SOCKET sockfd = get_socket_connected_to_proxy(); if (INVALID_SOCKET == sockfd) { return 1; } log_debug("sending proxy init message"); if (send_init_message(sockfd)) { error_exit: #ifdef WIN32 closesocket(sockfd); #else close(sockfd); #endif return 1; } is_init_response_good = FALSE; if (receive_response(sockfd, on_init_response_received, "init") || !is_init_response_good) { goto error_exit; } #ifdef WIN32 if (SOCKET_ERROR == closesocket(sockfd)) #else if (-1 == close(sockfd)) #endif { upcall_errno("close"); return 1; } return 0; } /* Handles a received K2 UDP packet in base64 encoding */ static void on_packet_received_base64(const char* buf, size_t len) { len; uint8_t p[K2IPC_MAX_PACKET_SIZE]; size_t n = sequencelogic::Base64Decode(buf, p); log_debug("proxy received packet, %d bytes", (int)n); on_packet_received(p, n); } static void* receiver_threadproc(void* arg) { arg; log_debug("proxy receiver thread starting, addr = %s:%d", inet_ntoa(proxy_addr.sin_addr), ntohs(proxy_addr.sin_port)); if (initialize_proxy_session()) { error_exit_1: log_debug("proxy receiver thread exiting due to error"); is_proxy_session_active = FALSE; return NULL; } is_proxy_session_active = TRUE; while (!receiver_end_thread) { SOCKET sockfd = get_socket_connected_to_proxy(); if (INVALID_SOCKET == sockfd) { goto error_exit_1; } if (send_poll_message(sockfd)) { error_exit: #ifdef WIN32 closesocket(sockfd); #else close(sockfd); #endif goto error_exit_1; } if (receive_response(sockfd, on_packet_received_base64, "poll")) { goto error_exit; } #ifdef WIN32 if (SOCKET_ERROR == closesocket(sockfd)) #else if (-1 == close(sockfd)) #endif { upcall_errno("close"); goto error_exit_1; } } is_proxy_session_active = FALSE; log_debug("proxy receiver thread exiting"); return NULL; } /* Helper for Encapsulates a K2IPC UDP into a proxy message, and encodes it as URL-safe base64 text. Returns base64 text, malloc'd here, caller free's */ static char* encode_sender_proxy_message(uint8_t* packet, size_t packet_len) { size_t proxy_message_len = packet_len + K2PROXY_SEQUENCE_NUMBER_SIZE + K2PROXY_MESSAGE_OVERHEAD; uint8_t* proxy_message = (uint8_t*)malloc(proxy_message_len); uint8_t* p = proxy_message + K2PROXY_NONCE_SIZE; uint32_t send_seq; LOCK_MUTEX { send_seq = send_sequence++; } UNLOCK_MUTEX write_uint32_BE(p, send_seq); p += K2PROXY_SEQUENCE_NUMBER_SIZE; memcpy(p, packet, packet_len); encrypt_proxy_message(proxy_message, proxy_message_len); char* r = to_base64_urlsafe(proxy_message, proxy_message_len); free(proxy_message); return r; } /* Helper for , takes a binary K2IPC UDP (with length prepended), adds a proxy message wrapper (with sequence number), encodes base64, and formats into an HTTP POST request message. Returns HTTP POST message text, malloc'd here, caller free's. Sets <*len_out> to length of message text. */ static char* format_sender_http_message(uint8_t* packet, size_t* len_out) { size_t packet_len = read_uint32_BE(packet); char *base64 = encode_sender_proxy_message(packet + sizeof(uint32_t), packet_len); size_t n = strlen(base64); size_t len1 = n + strlen(SENDER_HTTP_MESSAGE) + strlen(proxy_host) + 16; char* message = (char*)malloc(len1); size_t len = snprintf(message, len1, SENDER_HTTP_MESSAGE, proxy_host, n + 5 /* 5 for 'data=' */, base64); if (len >= len1) { log_error("proxy sender snprintf truncated from %lu to %lu", (long unsigned)len, (long unsigned)len1 - 1); len = len1 - 1; } free(base64); *len_out = len; return message; } static void on_sender_response_received(const char* buf, size_t len) { len; log_debug("proxy sender response: %s", buf); } /* sends one base64 encoded packet passed in (malloc'd by caller, free'd here) */ static void* sender_threadproc(void* arg) { if (!is_proxy_session_active) { log_warn("proxy send aborted because a proxy session is not active"); free(arg); return NULL; } SOCKET sockfd = get_socket_connected_to_proxy(); if (INVALID_SOCKET == sockfd) { free(arg); error_exit_1: log_debug("proxy sender thread exiting due to error"); return NULL; } size_t len; char* message = format_sender_http_message((uint8_t*)arg, &len); free(arg); log_debug("proxy send packet, encapsulated as %lu byte HTTP", (long unsigned)len); int r = send_http(sockfd, "send", message, len); free(message); if (r) { error_exit: #ifdef WIN32 closesocket(sockfd); #else close(sockfd); #endif goto error_exit_1; } if (receive_response(sockfd, on_sender_response_received, "send")) { goto error_exit; } #ifdef WIN32 if (SOCKET_ERROR == closesocket(sockfd)) #else if (-1 == close(sockfd)) #endif { upcall_errno("close"); goto error_exit_1; } return NULL; } /* ------------------ startup, shutdown ------------------ */ static void start_receiver_thread(void) { int error = pthread_create(&receiver_thread_id, NULL, receiver_threadproc, NULL); if (error) { upcall_error("pthread_create", error); } else { receiver_running = TRUE; } } static void stop_receiver_thread(void) { receiver_end_thread = TRUE; int error = pthread_join(receiver_thread_id, NULL); if (error) { upcall_error("pthread_join", error); } receiver_running = receiver_end_thread = is_proxy_session_active = FALSE; } /* ----------------------- Module interface functions ----------------------- */ void proxy_start(const struct sockaddr_in* proxy_address, const char* p_host, const char* urn, void (*onPacketReceived)(uint8_t* buf, size_t len)) { proxy_stop(); proxy_addr = *proxy_address; proxy_host = strdup(p_host); client_urn = urn; client_urn_len = strlen(client_urn); on_packet_received = onPacketReceived; start_receiver_thread(); } void proxy_send(const uint8_t* buf, unsigned int len) { uint8_t* packet_copy = (uint8_t*)malloc(len + sizeof(uint32_t)); write_uint32_BE(packet_copy, len); memcpy(packet_copy + sizeof(uint32_t), buf, len); int error = pthread_create(&sender_thread_id, NULL, sender_threadproc, packet_copy); if (error) { upcall_error("pthread_create", error); free(packet_copy); } } void proxy_stop(void) { if (receiver_running) { stop_receiver_thread(); } free(proxy_host); proxy_host = NULL; }