From: djm Date: Mon, 28 Aug 2023 03:31:16 +0000 (+0000) Subject: Add keystroke timing obfuscation to the client. X-Git-Url: http://artulab.com/gitweb/?a=commitdiff_plain;h=1d1d630434bcb3cb4e4e3e8aa6fdf787da029628;p=openbsd Add keystroke timing obfuscation to the client. 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@ --- diff --git a/usr.bin/ssh/clientloop.c b/usr.bin/ssh/clientloop.c index 67a9f173659..67228179b5e 100644 --- a/usr.bin/ssh/clientloop.c +++ b/usr.bin/ssh/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 * Copyright (c) 1995 Tatu Ylonen , Espoo, Finland @@ -498,6 +498,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). @@ -508,7 +630,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; @@ -528,11 +650,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 @@ -549,7 +674,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) { /* diff --git a/usr.bin/ssh/misc.c b/usr.bin/ssh/misc.c index 52d79e748d9..59ee9c9edfc 100644 --- a/usr.bin/ssh/misc.c +++ b/usr.bin/ssh/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. @@ -2792,24 +2792,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) diff --git a/usr.bin/ssh/misc.h b/usr.bin/ssh/misc.h index 0221ce24e79..5e0c45277ad 100644 --- a/usr.bin/ssh/misc.h +++ b/usr.bin/ssh/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 @@ -212,6 +212,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); diff --git a/usr.bin/ssh/packet.c b/usr.bin/ssh/packet.c index 600d51080fa..0ef57d4e9c4 100644 --- a/usr.bin/ssh/packet.c +++ b/usr.bin/ssh/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 * Copyright (c) 1995 Tatu Ylonen , Espoo, Finland @@ -2060,6 +2060,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) { diff --git a/usr.bin/ssh/packet.h b/usr.bin/ssh/packet.h index c8817ffa21a..39ee0b5ff53 100644 --- a/usr.bin/ssh/packet.h +++ b/usr.bin/ssh/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 @@ -139,6 +139,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 *); diff --git a/usr.bin/ssh/readconf.c b/usr.bin/ssh/readconf.c index ed119981194..2d85d48a3d7 100644 --- a/usr.bin/ssh/readconf.c +++ b/usr.bin/ssh/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 * Copyright (c) 1995 Tatu Ylonen , Espoo, Finland @@ -162,7 +162,7 @@ typedef enum { oFingerprintHash, oUpdateHostkeys, oHostbasedAcceptedAlgorithms, oPubkeyAcceptedAlgorithms, oCASignatureAlgorithms, oProxyJump, oSecurityKeyProvider, oKnownHostsCommand, oRequiredRSASize, - oEnableEscapeCommandline, + oEnableEscapeCommandline, oObscureKeystrokeTiming, oIgnore, oIgnoredUnknownOption, oDeprecated, oUnsupported } OpCodes; @@ -311,6 +311,7 @@ static struct { { "knownhostscommand", oKnownHostsCommand }, { "requiredrsasize", oRequiredRSASize }, { "enableescapecommandline", oEnableEscapeCommandline }, + { "obscurekeystroketiming", oObscureKeystrokeTiming }, { NULL, oBadOption } }; @@ -2257,6 +2258,48 @@ parse_pubkey_algos: 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); @@ -2507,6 +2550,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; } @@ -2701,6 +2745,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); @@ -3243,6 +3291,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); } @@ -3393,6 +3451,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); diff --git a/usr.bin/ssh/readconf.h b/usr.bin/ssh/readconf.h index dfe5bab0a3c..ce261bd6364 100644 --- a/usr.bin/ssh/readconf.h +++ b/usr.bin/ssh/readconf.h @@ -1,4 +1,4 @@ -/* $OpenBSD: readconf.h,v 1.151 2023/07/17 04:08:31 djm Exp $ */ +/* $OpenBSD: readconf.h,v 1.152 2023/08/28 03:31:16 djm Exp $ */ /* * Author: Tatu Ylonen @@ -180,6 +180,7 @@ typedef struct { int required_rsa_size; /* minimum size of RSA keys */ int enable_escape_commandline; /* ~C commandline */ + int obscure_keystroke_timing_interval; char *ignored_unknown; /* Pattern list of unknown tokens to ignore */ } Options; @@ -222,6 +223,11 @@ typedef struct { #define SSH_STRICT_HOSTKEY_YES 2 #define SSH_STRICT_HOSTKEY_ASK 3 +/* ObscureKeystrokes parameters */ +#define SSH_KEYSTROKE_DEFAULT_INTERVAL_MS 20 +#define SSH_KEYSTROKE_CHAFF_MIN_MS 1024 +#define SSH_KEYSTROKE_CHAFF_RNG_MS 2048 + const char *kex_default_pk_alg(void); char *ssh_connection_hash(const char *thishost, const char *host, const char *portstr, const char *user); diff --git a/usr.bin/ssh/ssh_config.5 b/usr.bin/ssh/ssh_config.5 index 1eae6e726ba..18769172d8e 100644 --- a/usr.bin/ssh/ssh_config.5 +++ b/usr.bin/ssh/ssh_config.5 @@ -33,8 +33,8 @@ .\" (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF .\" THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. .\" -.\" $OpenBSD: ssh_config.5,v 1.383 2023/07/17 05:36:14 jsg Exp $ -.Dd $Mdocdate: July 17 2023 $ +.\" $OpenBSD: ssh_config.5,v 1.384 2023/08/28 03:31:16 djm Exp $ +.Dd $Mdocdate: August 28 2023 $ .Dt SSH_CONFIG 5 .Os .Sh NAME @@ -1359,6 +1359,24 @@ or Specifies the number of password prompts before giving up. The argument to this keyword must be an integer. The default is 3. +.It Cm ObscureKeystrokeTiming +Specifies whether +.Xr ssh 1 +should try to obscure inter-keystroke timings from passive observers of +network traffic. +If enabled, then for interactive sessions, +.Xr ssh 1 +will send keystrokes at fixed intervals of a few tens of milliseconds +and will send fake keystroke packets for some time after typing ceases. +The argument to this keyword must be +.Cm yes , +.Cm no +or an interval specifier of the form +.Cm interval:milliseconds +(e.g.\& +.Cm interval:80 for 80 milliseconds). +The default is to obscure keystrokes using a 20ms packet interval. +Note that smaller intervals will result in higher fake keystroke packet rates. .It Cm PasswordAuthentication Specifies whether to use password authentication. The argument to this keyword must be