Skip to content

Commit

Permalink
upstream: Add keystroke timing obfuscation to the client.
Browse files Browse the repository at this point in the history
This attempts to hide inter-keystroke timings by sending interactive
traffic at fixed intervals (default: every 20ms) when there is only a
small amount of data being sent. It also sends fake "chaff" keystrokes
for a random interval after the last real keystroke. These are
controlled by a new ssh_config ObscureKeystrokeTiming keyword/

feedback/ok markus@

OpenBSD-Commit-ID: 02231ddd4f442212820976068c34a36e3c1b15be
  • Loading branch information
djmdjm committed Aug 28, 2023
1 parent dce6d80 commit 7603ba7
Show file tree
Hide file tree
Showing 8 changed files with 255 additions and 21 deletions.
133 changes: 129 additions & 4 deletions clientloop.c
@@ -1,4 +1,4 @@
/* $OpenBSD: clientloop.c,v 1.392 2023/04/03 08:10:54 dtucker Exp $ */
/* $OpenBSD: clientloop.c,v 1.393 2023/08/28 03:31:16 djm Exp $ */
/*
* Author: Tatu Ylonen <ylo@cs.hut.fi>
* Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, Espoo, Finland
Expand Down Expand Up @@ -507,6 +507,128 @@ server_alive_check(struct ssh *ssh)
schedule_server_alive_check();
}

/* Try to send a dummy keystroke */
static int
send_chaff(struct ssh *ssh)
{
int r;

if ((ssh->kex->flags & KEX_HAS_PING) == 0)
return 0;
/* XXX probabilistically send chaff? */
/*
* a SSH2_MSG_CHANNEL_DATA payload is 9 bytes:
* 4 bytes channel ID + 4 bytes string length + 1 byte string data
* simulate that here.
*/
if ((r = sshpkt_start(ssh, SSH2_MSG_PING)) != 0 ||
(r = sshpkt_put_cstring(ssh, "PING!")) != 0 ||
(r = sshpkt_send(ssh)) != 0)
fatal_fr(r, "send packet");
return 1;
}

/*
* Performs keystroke timing obfuscation. Returns non-zero if the
* output fd should be polled.
*/
static int
obfuscate_keystroke_timing(struct ssh *ssh, struct timespec *timeout)
{
static int active;
static struct timespec next_interval, chaff_until;
struct timespec now, tmp;
int just_started = 0, had_keystroke = 0;
static unsigned long long nchaff;
char *stop_reason = NULL;
long long n;

monotime_ts(&now);

if (options.obscure_keystroke_timing_interval <= 0)
return 1; /* disabled in config */

if (!channel_still_open(ssh) || quit_pending) {
/* Stop if no channels left of we're waiting for one to close */
stop_reason = "no active channels";
} else if (ssh_packet_is_rekeying(ssh)) {
/* Stop if we're rekeying */
stop_reason = "rekeying started";
} else if (!ssh_packet_interactive_data_to_write(ssh) &&
ssh_packet_have_data_to_write(ssh)) {
/* Stop if the output buffer has more than a few keystrokes */
stop_reason = "output buffer filling";
} else if (active && ssh_packet_have_data_to_write(ssh)) {
/* Still in active mode and have a keystroke queued. */
had_keystroke = 1;
} else if (active) {
if (timespeccmp(&now, &chaff_until, >=)) {
/* Stop if there have been no keystrokes for a while */
stop_reason = "chaff time expired";
} else if (timespeccmp(&now, &next_interval, >=)) {
/* Otherwise if we were due to send, then send chaff */
if (send_chaff(ssh))
nchaff++;
}
}

if (stop_reason != NULL) {
active = 0;
debug3_f("stopping: %s (%llu chaff packets sent)",
stop_reason, nchaff);
return 1;
}

/*
* If we're in interactive mode, and only have a small amount
* of outbound data, then we assume that the user is typing
* interactively. In this case, start quantising outbound packets to
* fixed time intervals to hide inter-keystroke timing.
*/
if (!active && ssh_packet_interactive_data_to_write(ssh)) {
debug3_f("starting: interval %d",
options.obscure_keystroke_timing_interval);
just_started = had_keystroke = active = 1;
nchaff = 0;
ms_to_timespec(&tmp, options.obscure_keystroke_timing_interval);
timespecadd(&now, &tmp, &next_interval);
}

/* Don't hold off if obfuscation inactive */
if (!active)
return 1;

if (had_keystroke) {
/*
* Arrange to send chaff packets for a random interval after
* the last keystroke was sent.
*/
ms_to_timespec(&tmp, SSH_KEYSTROKE_CHAFF_MIN_MS +
arc4random_uniform(SSH_KEYSTROKE_CHAFF_RNG_MS));
timespecadd(&now, &tmp, &chaff_until);
}

ptimeout_deadline_monotime_tsp(timeout, &next_interval);

if (just_started)
return 1;

/* Don't arm output fd for poll until the timing interval has elapsed */
if (timespeccmp(&now, &next_interval, <))
return 0;

/* Calculate number of intervals missed since the last check */
n = (now.tv_sec - next_interval.tv_sec) * 1000 * 1000 * 1000;
n += now.tv_nsec - next_interval.tv_nsec;
n /= options.obscure_keystroke_timing_interval * 1000 * 1000;
n = (n < 0) ? 1 : n + 1;

/* Advance to the next interval */
ms_to_timespec(&tmp, options.obscure_keystroke_timing_interval * n);
timespecadd(&now, &tmp, &next_interval);
return 1;
}

/*
* Waits until the client can do something (some data becomes available on
* one of the file descriptors).
Expand All @@ -517,7 +639,7 @@ client_wait_until_can_do_something(struct ssh *ssh, struct pollfd **pfdp,
int *conn_in_readyp, int *conn_out_readyp)
{
struct timespec timeout;
int ret;
int ret, oready;
u_int p;

*conn_in_readyp = *conn_out_readyp = 0;
Expand All @@ -537,11 +659,14 @@ client_wait_until_can_do_something(struct ssh *ssh, struct pollfd **pfdp,
return;
}

oready = obfuscate_keystroke_timing(ssh, &timeout);

/* Monitor server connection on reserved pollfd entries */
(*pfdp)[0].fd = connection_in;
(*pfdp)[0].events = POLLIN;
(*pfdp)[1].fd = connection_out;
(*pfdp)[1].events = ssh_packet_have_data_to_write(ssh) ? POLLOUT : 0;
(*pfdp)[1].events = (oready && ssh_packet_have_data_to_write(ssh)) ?
POLLOUT : 0;

/*
* Wait for something to happen. This will suspend the process until
Expand All @@ -558,7 +683,7 @@ client_wait_until_can_do_something(struct ssh *ssh, struct pollfd **pfdp,
ssh_packet_get_rekey_timeout(ssh));
}

ret = poll(*pfdp, *npfd_activep, ptimeout_get_ms(&timeout));
ret = ppoll(*pfdp, *npfd_activep, ptimeout_get_tsp(&timeout), NULL);

if (ret == -1) {
/*
Expand Down
29 changes: 20 additions & 9 deletions misc.c
@@ -1,4 +1,4 @@
/* $OpenBSD: misc.c,v 1.186 2023/08/18 01:37:41 djm Exp $ */
/* $OpenBSD: misc.c,v 1.187 2023/08/28 03:31:16 djm Exp $ */
/*
* Copyright (c) 2000 Markus Friedl. All rights reserved.
* Copyright (c) 2005-2020 Damien Miller. All rights reserved.
Expand Down Expand Up @@ -2901,24 +2901,35 @@ ptimeout_deadline_ms(struct timespec *pt, long ms)
ptimeout_deadline_tsp(pt, &p);
}

/* Specify a poll/ppoll deadline at wall clock monotime 'when' */
/* Specify a poll/ppoll deadline at wall clock monotime 'when' (timespec) */
void
ptimeout_deadline_monotime(struct timespec *pt, time_t when)
ptimeout_deadline_monotime_tsp(struct timespec *pt, struct timespec *when)
{
struct timespec now, t;

t.tv_sec = when;
t.tv_nsec = 0;
monotime_ts(&now);

if (timespeccmp(&now, &t, >=))
ptimeout_deadline_sec(pt, 0);
else {
timespecsub(&t, &now, &t);
if (timespeccmp(&now, when, >=)) {
/* 'when' is now or in the past. Timeout ASAP */
pt->tv_sec = 0;
pt->tv_nsec = 0;
} else {
timespecsub(when, &now, &t);
ptimeout_deadline_tsp(pt, &t);
}
}

/* Specify a poll/ppoll deadline at wall clock monotime 'when' */
void
ptimeout_deadline_monotime(struct timespec *pt, time_t when)
{
struct timespec t;

t.tv_sec = when;
t.tv_nsec = 0;
ptimeout_deadline_monotime_tsp(pt, &t);
}

/* Get a poll(2) timeout value in milliseconds */
int
ptimeout_get_ms(struct timespec *pt)
Expand Down
3 changes: 2 additions & 1 deletion misc.h
@@ -1,4 +1,4 @@
/* $OpenBSD: misc.h,v 1.104 2023/08/18 01:37:41 djm Exp $ */
/* $OpenBSD: misc.h,v 1.105 2023/08/28 03:31:16 djm Exp $ */

/*
* Author: Tatu Ylonen <ylo@cs.hut.fi>
Expand Down Expand Up @@ -214,6 +214,7 @@ struct timespec;
void ptimeout_init(struct timespec *pt);
void ptimeout_deadline_sec(struct timespec *pt, long sec);
void ptimeout_deadline_ms(struct timespec *pt, long ms);
void ptimeout_deadline_monotime_tsp(struct timespec *pt, struct timespec *when);
void ptimeout_deadline_monotime(struct timespec *pt, time_t when);
int ptimeout_get_ms(struct timespec *pt);
struct timespec *ptimeout_get_tsp(struct timespec *pt);
Expand Down
14 changes: 13 additions & 1 deletion packet.c
@@ -1,4 +1,4 @@
/* $OpenBSD: packet.c,v 1.311 2023/08/28 03:28:43 djm Exp $ */
/* $OpenBSD: packet.c,v 1.312 2023/08/28 03:31:16 djm Exp $ */
/*
* Author: Tatu Ylonen <ylo@cs.hut.fi>
* Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, Espoo, Finland
Expand Down Expand Up @@ -2083,6 +2083,18 @@ ssh_packet_not_very_much_data_to_write(struct ssh *ssh)
return sshbuf_len(ssh->state->output) < 128 * 1024;
}

/*
* returns true when there are at most a few keystrokes of data to write
* and the connection is in interactive mode.
*/

int
ssh_packet_interactive_data_to_write(struct ssh *ssh)
{
return ssh->state->interactive_mode &&
sshbuf_len(ssh->state->output) < 256;
}

void
ssh_packet_set_tos(struct ssh *ssh, int tos)
{
Expand Down
3 changes: 2 additions & 1 deletion packet.h
@@ -1,4 +1,4 @@
/* $OpenBSD: packet.h,v 1.94 2022/01/22 00:49:34 djm Exp $ */
/* $OpenBSD: packet.h,v 1.95 2023/08/28 03:31:16 djm Exp $ */

/*
* Author: Tatu Ylonen <ylo@cs.hut.fi>
Expand Down Expand Up @@ -145,6 +145,7 @@ int ssh_packet_write_poll(struct ssh *);
int ssh_packet_write_wait(struct ssh *);
int ssh_packet_have_data_to_write(struct ssh *);
int ssh_packet_not_very_much_data_to_write(struct ssh *);
int ssh_packet_interactive_data_to_write(struct ssh *);

int ssh_packet_connection_is_on_socket(struct ssh *);
int ssh_packet_remaining(struct ssh *);
Expand Down
64 changes: 62 additions & 2 deletions readconf.c
@@ -1,4 +1,4 @@
/* $OpenBSD: readconf.c,v 1.380 2023/07/17 06:16:33 djm Exp $ */
/* $OpenBSD: readconf.c,v 1.381 2023/08/28 03:31:16 djm Exp $ */
/*
* Author: Tatu Ylonen <ylo@cs.hut.fi>
* Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, Espoo, Finland
Expand Down Expand Up @@ -178,7 +178,7 @@ typedef enum {
oFingerprintHash, oUpdateHostkeys, oHostbasedAcceptedAlgorithms,
oPubkeyAcceptedAlgorithms, oCASignatureAlgorithms, oProxyJump,
oSecurityKeyProvider, oKnownHostsCommand, oRequiredRSASize,
oEnableEscapeCommandline,
oEnableEscapeCommandline, oObscureKeystrokeTiming,
oIgnore, oIgnoredUnknownOption, oDeprecated, oUnsupported
} OpCodes;

Expand Down Expand Up @@ -327,6 +327,7 @@ static struct {
{ "knownhostscommand", oKnownHostsCommand },
{ "requiredrsasize", oRequiredRSASize },
{ "enableescapecommandline", oEnableEscapeCommandline },
{ "obscurekeystroketiming", oObscureKeystrokeTiming },

{ NULL, oBadOption }
};
Expand Down Expand Up @@ -2280,6 +2281,48 @@ process_config_line_depth(Options *options, struct passwd *pw, const char *host,
intptr = &options->required_rsa_size;
goto parse_int;

case oObscureKeystrokeTiming:
value = -1;
while ((arg = argv_next(&ac, &av)) != NULL) {
if (value != -1) {
error("%s line %d: invalid arguments",
filename, linenum);
goto out;
}
if (strcmp(arg, "yes") == 0 ||
strcmp(arg, "true") == 0)
value = SSH_KEYSTROKE_DEFAULT_INTERVAL_MS;
else if (strcmp(arg, "no") == 0 ||
strcmp(arg, "false") == 0)
value = 0;
else if (strncmp(arg, "interval:", 9) == 0) {
if ((errstr = atoi_err(arg + 9,
&value)) != NULL) {
error("%s line %d: integer value %s.",
filename, linenum, errstr);
goto out;
}
if (value <= 0 || value > 1000) {
error("%s line %d: value out of range.",
filename, linenum);
goto out;
}
} else {
error("%s line %d: unsupported argument \"%s\"",
filename, linenum, arg);
goto out;
}
}
if (value == -1) {
error("%s line %d: missing argument",
filename, linenum);
goto out;
}
intptr = &options->obscure_keystroke_timing_interval;
if (*activep && *intptr == -1)
*intptr = value;
break;

case oDeprecated:
debug("%s line %d: Deprecated option \"%s\"",
filename, linenum, keyword);
Expand Down Expand Up @@ -2530,6 +2573,7 @@ initialize_options(Options * options)
options->known_hosts_command = NULL;
options->required_rsa_size = -1;
options->enable_escape_commandline = -1;
options->obscure_keystroke_timing_interval = -1;
options->tag = NULL;
}

Expand Down Expand Up @@ -2731,6 +2775,10 @@ fill_default_options(Options * options)
options->required_rsa_size = SSH_RSA_MINIMUM_MODULUS_SIZE;
if (options->enable_escape_commandline == -1)
options->enable_escape_commandline = 0;
if (options->obscure_keystroke_timing_interval == -1) {
options->obscure_keystroke_timing_interval =
SSH_KEYSTROKE_DEFAULT_INTERVAL_MS;
}

/* Expand KEX name lists */
all_cipher = cipher_alg_list(',', 0);
Expand Down Expand Up @@ -3273,6 +3321,16 @@ lookup_opcode_name(OpCodes code)
static void
dump_cfg_int(OpCodes code, int val)
{
if (code == oObscureKeystrokeTiming) {
if (val == 0) {
printf("%s no\n", lookup_opcode_name(code));
return;
} else if (val == SSH_KEYSTROKE_DEFAULT_INTERVAL_MS) {
printf("%s yes\n", lookup_opcode_name(code));
return;
}
/* FALLTHROUGH */
}
printf("%s %d\n", lookup_opcode_name(code), val);
}

Expand Down Expand Up @@ -3423,6 +3481,8 @@ dump_client_config(Options *o, const char *host)
dump_cfg_int(oServerAliveCountMax, o->server_alive_count_max);
dump_cfg_int(oServerAliveInterval, o->server_alive_interval);
dump_cfg_int(oRequiredRSASize, o->required_rsa_size);
dump_cfg_int(oObscureKeystrokeTiming,
o->obscure_keystroke_timing_interval);

/* String options */
dump_cfg_string(oBindAddress, o->bind_address);
Expand Down

0 comments on commit 7603ba7

Please sign in to comment.