blob: 8a5f6c368f946d93c6281cca764ae13496ff0c99 [file] [log] [blame] [raw]
/*
* 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 <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]\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:h?")) != -1) {
switch (c) {
case 'd':
debug++;
break;
case 'i':
nids_params.device = 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) {
warnx("listening on %s [%s]", nids_params.device,
nids_params.pcap_filter);
}
else warnx("listening on %s", nids_params.device);
nids_run();
return (0);
}