/*
 * sshow.c
 *
 * Passive SSH traffic analysis.
 *
 * http://www.openwall.com/advisories/OW-003-ssh-traffic-analysis.txt
 *
 * Copyright (c) 2000-2001 Solar Designer <solar@openwall.com>
 * Copyright (c) 2000 Dug Song <dugsong@monkey.org>
 *
 * $Id: sshow.c,v 1.2 2001/03/19 06:52:15 dugsong Exp $
 */

#include "config.h"

#include <sys/types.h>
#include <sys/times.h>
#include <time.h>

#include <netinet/in_systm.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <err.h>
#include <nids.h>
#include <pcap.h>

#include "pcaputil.h"

#if !defined(NIDS_MAJOR) || (NIDS_MAJOR == 1 && NIDS_MINOR < 15)
#error This program requires libnids 1.15+
#endif

#define HISTORY_SIZE			16

typedef struct {
	u_int min, max;
} range;

typedef struct {
	int direction;			/* 0 for client to server */
	clock_t timestamp;		/* timestamp of this packet */
	u_int cipher_size;		/* ciphertext size */
	range plain_range;		/* possible plaintext sizes */
} record;

struct history {
	record packets[HISTORY_SIZE];	/* recent packets (circular list) */
	int index;			/* next (free) index into packets[] */
	u_int directions;		/* recent directions (bitmask) */
	clock_t timestamps[2];		/* last timestamps in each direction */
};

struct line {
	int input_count;		/* input packets (client to server) */
	int input_size;			/* input size (estimated) */
	int input_last;			/* last input packet size */
	int echo_count;			/* echo packets (server to client) */
};

struct session {
	int protocol;		/* -1 not SSH, 0 unknown, 1 or 2 once known */
	int state;		/* 1 after username, 2 after authentication */
	int compressed;		/* whether compression is known to be used */
	struct history history;	/* session history */
	struct line line;	/* current command line */
};

static int debug = 0;

static clock_t now;

static void
usage(void)
{
	fprintf(stderr, "Usage: sshow [-d] [-i interface | -p pcapfile]\n");
	exit(1);
}

static clock_t
add_history(struct session *session, int direction,
	    u_int cipher_size, range *plain_range)
{
	record *current;
	clock_t delay;

	current = &session->history.packets[session->history.index++];
	session->history.index %= HISTORY_SIZE;

	current->direction = direction;
	current->timestamp = now;
	current->cipher_size = cipher_size;
	current->plain_range = *plain_range;

	session->history.directions <<= 1;
	session->history.directions |= direction;

	delay = now - session->history.timestamps[direction];
	session->history.timestamps[direction] = now;

	return (delay);
}

static record *
get_history(struct session *session, int age)
{
	int index;

	index = session->history.index + (HISTORY_SIZE - 1) - age;
	index %= HISTORY_SIZE;

	return (&session->history.packets[index]);
}

static char *
s_saddr(struct tcp_stream *ts)
{
	static char output[32];

	snprintf(output, sizeof(output), "%s:%u",
		inet_ntoa(*((struct in_addr *)&ts->addr.saddr)),
		ts->addr.source);
	return (output);
}

static char *
s_daddr(struct tcp_stream *ts)
{
	static char output[32];

	snprintf(output, sizeof(output), "%s:%u",
		inet_ntoa(*((struct in_addr *)&ts->addr.daddr)),
		ts->addr.dest);
	return (output);
}

static char *
s_range(range *range)
{
	static char output[32];

	snprintf(output, sizeof(output),
		range->min == range->max ? "%u" : "%u to %u",
		range->min, range->max);
	return (output);
}

static void
print_data(struct half_stream *stream, u_int count)
{
	u_int i;
	int printable;

	printable = 1;
	for (i = 0; i < count; i++) {
		printf("%02x%c", (int)(u_char)stream->data[i],
			i < count - 1 && i % 24 != 23
			? ' ' : '\n');
		printable &=
			isprint(stream->data[i]) ||
			stream->data[i] == '\n';
	}
	if (printable && count >= 4 && !memcmp(stream->data, "SSH-", 4))
		fwrite(stream->data, count, 1, stdout);
}

static u_int
ssh1_plain_size(struct half_stream *stream)
{
	if (stream->count_new < 4) return (0);

	return (u_int)(u_char)stream->data[3] |
		((u_int)(u_char)stream->data[2] << 8) |
		((u_int)(u_char)stream->data[1] << 16) |
		((u_int)(u_char)stream->data[0] << 24);
}

static u_int
ssh1_cipher_size(struct half_stream *stream)
{
	return (4 + ((ssh1_plain_size(stream) + 8) & ~7));
}

static range *
ssh1_plain_range(struct half_stream *stream)
{
	static range output;

	output.min = output.max = ssh1_plain_size(stream) - 5;
	return (&output);
}

static range *
ssh2_plain_range(struct half_stream *stream)
{
	static range output;

	output.max = stream->count_new - 16;
	/* Assume min padding + 8-byte cipher blocksize */
	output.min = output.max - 7;
	if ((int)output.min < 0) output.min = 0;
	return (&output);
}

static void
client_to_server(struct tcp_stream *ts, struct session *session,
		 u_int cipher_size, range *plain_range)
{
	clock_t delay;
	int payload;

	delay = add_history(session, 0, cipher_size, plain_range);

	if (debug)
		printf("- %s -> %s: DATA (%s bytes, %.2f seconds)\n",
			s_saddr(ts), s_daddr(ts), s_range(plain_range),
			(float)delay / CLK_TCK);
	if (debug > 1)
		print_data(&ts->server, cipher_size);

	payload = plain_range->min;
	if (session->state == 2 && payload > 0) {
		session->line.input_count++;
		session->line.input_last = payload;
		if (session->protocol == 1)
			payload -= 4;
		else {
			payload -= 20 + 1;
			/* Assume several SSH-2 packets in this IP packet */
			if (payload % 44 == 0) {
				session->line.input_count += payload / 44;
				/* One character per SSH-2 packet (typical) */
				payload += payload / 44;
				payload %= 44;
			}
			payload++;
		}
		if (payload <= 0) {
			if (payload < 0 && !session->compressed &&
			    session->protocol == 1) {
				session->compressed = 1;
				printf("+ %s -> %s: Compression detected, "
					"guesses will be much less reliable\n",
					s_saddr(ts), s_daddr(ts));
			}
			payload = 1;
		}
		session->line.input_size += payload;
	}
}

static void
server_to_client(struct tcp_stream *ts, struct session *session,
		 u_int cipher_size, range *plain_range)
{
	clock_t delay;
	int skip;
	range string_range;
	
	delay = add_history(session, 1, cipher_size, plain_range);
	
	if (debug)
		printf("- %s <- %s: DATA (%s bytes, %.2f seconds)\n",
		       s_saddr(ts), s_daddr(ts), s_range(plain_range),
		       (float)delay / CLK_TCK);
	if (debug > 1)
		print_data(&ts->client, cipher_size);
	
/*
 * Some of the checks may want to skip over multiple server responses.
 * For example, there's a debugging packet sent for every option found
 * in authorized_keys, but we can't use those packets in our pattern.
 */
	skip = 0;
	while (((session->history.directions >> skip) & 3) == 3)
		if (++skip > HISTORY_SIZE - 5) break;
	
	if (session->state == 0 &&
	    session->protocol == 1 &&
	    ((session->history.directions >> skip) & 7) == 5 &&
	    plain_range->min == 0 &&
	    get_history(session, skip + 1)->plain_range.min > 4 &&
	    get_history(session, skip + 2)->plain_range.min == 0) {
		session->state = 1;
		string_range = get_history(session, skip + 1)->plain_range;
		string_range.min -= 4; string_range.max -= 4;
		printf("+ %s -> %s: GUESS: Username length is %s\n",
		       s_saddr(ts), s_daddr(ts), s_range(&string_range));
		return;
	}
	
	if (session->state == 1 &&
#ifdef USE_TIMING
	    now - get_history(session, 2)->timestamp >= CLK_TCK &&
#endif
	    session->protocol == 1 &&
	    (session->history.directions & 7) == 5 &&
	    plain_range->min == 0 &&
	    get_history(session, 1)->plain_range.min > 4 &&
	    get_history(session, 2)->plain_range.min == 0) {
		session->state = 2;
		string_range = get_history(session, 1)->plain_range;
		string_range.min -= 4; string_range.max -= 4;
		printf("+ %s -> %s: GUESS: Password authentication, "
		       "password length %s %s%s\n",
		       s_saddr(ts), s_daddr(ts),
		       string_range.min == 32 ? "appears to be" : "is",
		       s_range(&string_range),
		       string_range.min == 32 ? " (padded?)" : "");
	}
	
	if (session->state == 0 &&
	    session->protocol == 2 &&
	    (session->history.directions & 7) == 5) {
		if (plain_range->min == 4 + 9) {
			string_range = get_history(session, 1)->plain_range;
			
			if (string_range.min > 500 && string_range.min < 600) {
				session->state = 2;
				printf("+ %s -> %s: GUESS: DSA "
				       "authentication accepted\n",
				       s_saddr(ts), s_daddr(ts));
			} else
				if (string_range.min > 42 + 9) {
					session->state = 2;
					printf("+ %s -> %s: GUESS: Password "
					       "authentication accepted\n",
					       s_saddr(ts), s_daddr(ts));
				}
		} else if (plain_range->min > 12 + 9 &&
			   plain_range->min < 56 + 9) {
			string_range = get_history(session, 1)->plain_range;
			
			if (string_range.min > 500 && string_range.min < 600)
				printf("+ %s -> %s: GUESS: DSA "
				       "authentication failed\n",
				       s_saddr(ts), s_daddr(ts));
			else if (string_range.min > 42 + 9)
				printf("+ %s -> %s: GUESS: Password "
				       "authentication failed\n",
				       s_saddr(ts), s_daddr(ts));
		}
	}
	
	if (session->state == 1 &&
	    session->protocol == 1 &&
	    (session->history.directions & 3) == 1 &&
	    plain_range->min == 0 &&
	    get_history(session, 1)->plain_range.min == 130) {
		printf("+ %s -> %s: GUESS: RSA authentication refused\n",
		       s_saddr(ts), s_daddr(ts));
	}
	
	if (session->state == 1 &&
	    session->protocol == 1 &&
	    skip >= 1 &&
	    ((session->history.directions >> (skip - 1)) & 037) == 013 &&
	    plain_range->min == 0 &&
	    get_history(session, skip - 1 + 2)->plain_range.min == 16 &&
	    get_history(session, skip - 1 + 3)->plain_range.min == 130 &&
	    get_history(session, skip - 1 + 4)->plain_range.min == 130) {
		char *what;
		
		switch (get_history(session, 1)->plain_range.min - 4) {
		case 28:
			/* "RSA authentication accepted." */
			session->state = 2;
			if (skip > 1 && (what = alloca(64))) {
				snprintf(what, 64, "accepted "
					 "(%d+ authorized_keys option%s)",
					 skip - 1, skip - 1 == 1 ? "" : "s");
				break;
			}
			what = "accepted";
			break;
			
		case 47:
			/* "Wrong response to RSA authentication challenge." */
			what = "failed";
			break;
			
		default:
			what = "???";
		}
		printf("+ %s -> %s: GUESS: RSA authentication %s\n",
		       s_saddr(ts), s_daddr(ts), what);
	}
	
	if (session->state == 2) {
		session->line.echo_count++;
		
		/* Check for backspace */
		if (session->protocol == 1 && !session->compressed &&
		    plain_range->min == 4 + 3 &&
		    session->line.input_size >= 2)
			session->line.input_size -= 2;
		
		if (plain_range->min > 4 + session->line.input_last &&
		    session->line.input_count >= 2 &&
		    session->line.input_size >= 2) {
			int size;
			char *what;
			
			size = session->line.input_size;
			if (session->line.echo_count + 1 >=
			    session->line.input_count &&
			    size <= (session->line.input_count << 2) &&
			    size < 0x100) {
				what = "(command) line";
			}
			else {
				if (session->line.echo_count <= 2 &&
				    size <= (session->line.input_count << 1) &&
				    size >= 2 + 1 && size <= 40 + 1) {
					what = "password";
				}
				else what = NULL;
			}
			if (debug) {
				printf("- %s -> %s: sent %d packets "
				       "(%d characters), seen %d replies\n",
				       s_saddr(ts), s_daddr(ts),
				       session->line.input_count, size,
				       session->line.echo_count);
			}
			if (what) {
				printf("+ %s -> %s: GUESS: "
				       "a %s of %d character%s\n",
				       s_saddr(ts), s_daddr(ts),
				       what, size - 1, size == 2 ? "" : "s");
			}
		}
		if (plain_range->min <= 0 ||
		    plain_range->min > 4 + session->line.input_last) {
			session->line.input_count = 0;
			session->line.input_size = 0;
			session->line.echo_count = 0;
		}
	}
}

static void
process_data(struct tcp_stream *ts, struct session *session)
{
	u_int have, need;
	char *lf;
	
	if (session->protocol < 0) return;
	
	if (ts->client.count_new &&
	    (have = ts->client.count - ts->client.offset)) {
		switch (session->protocol) {
		case 1:
			if (have < (need = ssh1_cipher_size(&ts->client))) {
				if (debug) {
					printf("- %s <- %s: got %u of "
					       "%u bytes\n", s_saddr(ts),
					       s_daddr(ts), have, need);
				}
				nids_discard(ts, 0);
				return;
			}
			if (have != need && debug) {
				printf("- %s <- %s: left %u bytes\n",
				       s_saddr(ts), s_daddr(ts),
				       have - need);
			}
			nids_discard(ts, need);
			server_to_client(ts, session, need,
				ssh1_plain_range(&ts->client));
			return;
			
		case 2:
			server_to_client(ts, session, have,
					 ssh2_plain_range(&ts->client));
			return;
			
		default:
			break;
		}
	}
	if (ts->server.count_new &&
	    (have = ts->server.count - ts->server.offset)) {
		if (!session->protocol) {
			lf = (char *)memchr(ts->server.data, '\n', have);
			if (have < 7 || (!lf && have < 0x100)) {
				nids_discard(ts, 0);
				return;
			}
			if (lf && !memcmp(ts->server.data, "SSH-", 4))
				session->protocol = ts->server.data[4] - '0';
			/* some clients announce SSH-1.99 instead of SSH-2.0 */
			if (session->protocol == 1 &&
			    ts->server.data[5] == '.' &&
			    ts->server.data[6] == '9') {
				session->protocol = 2;
			}
			if (session->protocol != 1 && session->protocol != 2) {
				session->protocol = -1;
				if (debug) {
					printf("- %s -> %s: not SSH\n",
					       s_saddr(ts), s_daddr(ts));
				}
				return;
			}
			need = lf - ts->server.data + 1;
			nids_discard(ts, need);
			printf("+ %s -> %s: SSH protocol %d\n",
			       s_saddr(ts), s_daddr(ts), session->protocol);
			if (debug)
				print_data(&ts->server, have);
			return;
		}
		
		switch (session->protocol) {
		case 1:
			if (have < (need = ssh1_cipher_size(&ts->server))) {
				if (debug) {
					printf("- %s -> %s: got %u of "
					       "%u bytes\n", s_saddr(ts),
					       s_daddr(ts), have, need);
				}
				nids_discard(ts, 0);
				return;
			}
			if (have != need && debug) {
				printf("- %s -> %s: left %u bytes\n",
				       s_saddr(ts), s_daddr(ts),
				       have - need);
			}
			nids_discard(ts, need);
			client_to_server(ts, session, need,
					 ssh1_plain_range(&ts->server));
			return;
			
		case 2:
			client_to_server(ts, session, have,
					 ssh2_plain_range(&ts->server));
		}
	}
}

static void
process_event(struct tcp_stream *ts, struct session **session)
{
	struct tms buf;
	char *what;
	
	now = times(&buf);
	what = NULL;
	
	switch (ts->nids_state) {
	case NIDS_JUST_EST:
		ts->client.collect = 1;
		ts->server.collect = 1;
		if (debug) {
			printf("- %s -> %s: ESTABLISHED\n",
			       s_saddr(ts), s_daddr(ts));
		}
		if (!(*session = calloc(1, sizeof(**session)))) {
			err(1, "calloc");
		}
		(*session)->history.timestamps[0] = now;
		(*session)->history.timestamps[1] = now;
		return;
		
	case NIDS_CLOSE:
		what = "CLOSED";
		
	case NIDS_RESET:
		if (!what) what = "RESET";
		
	case NIDS_TIMED_OUT:
		if (!what) what = "TIMED OUT";
		if ((*session)->protocol > 0) {
			printf("+ %s -- %s: %s\n",
			       s_saddr(ts), s_daddr(ts), what);
		}
		else if (debug) {
			printf("- %s -- %s: %s\n",
			       s_saddr(ts), s_daddr(ts), what);
		}
		free(*session);
		return;
		
	case NIDS_DATA:
		process_data(ts, *session);
		return;
	}
}

static void
null_syslog(int type, int errnum, struct ip *iph, void *data)
{
}

static void
cleanup(int signum)
{
	exit(0);	/* Just so that atexit(3) jobs are called */
}

int
main(int argc, char *argv[])
{
	extern char *optarg;
	extern int optind;
	int c;
	
	while ((c = getopt(argc, argv, "di:p:h?")) != -1) {
		switch (c) {
		case 'd':
			debug++;
			break;
		case 'i':
			nids_params.device = optarg;
			break;
		case 'p':
			nids_params.filename = optarg;
			break;
		default:
			usage();
			break;
		}
	}
	argc -= optind;
	argv += optind;
	
	signal(SIGTERM, cleanup);
	signal(SIGINT, cleanup);
	signal(SIGHUP, cleanup);
	
	setlinebuf(stdout);
	
	if (argc > 0) {
		nids_params.pcap_filter = copy_argv(argv);
	}
	else nids_params.pcap_filter = "tcp";
	
	nids_params.syslog = null_syslog;
	nids_params.scan_num_hosts = 0;
	nids_params.one_loop_less = 1;
	
	if (!nids_init())
		errx(1, "nids_init: %s", nids_errbuf);
	
	nids_register_tcp(process_event);

        if (nids_params.pcap_filter != NULL) {
                if (nids_params.filename == NULL) {
                        warnx("listening on %s [%s]", nids_params.device,
                              nids_params.pcap_filter);
                }
                else {
                        warnx("using %s [%s]", nids_params.filename,
                              nids_params.pcap_filter);
                }
        }
        else {
                if (nids_params.filename == NULL) {
                    warnx("listening on %s", nids_params.device);
                }
                else {
                    warnx("using %s", nids_params.filename);
                }
        }

	nids_run();
	
	return (0);
}
