diff options
author | djm@openbsd.org <djm@openbsd.org> | 2023-08-28 03:31:16 +0000 |
---|---|---|
committer | Damien Miller <djm@mindrot.org> | 2023-08-28 13:34:10 +1000 |
commit | 7603ba71264e7fa938325c37eca993e2fa61272f (patch) | |
tree | e0b2fecdb68f63bdafd81eee605f5e3be9c0177a | |
parent | dce6d80d2ed3cad2c516082682d5f6ca877ef714 (diff) |
upstream: 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@
OpenBSD-Commit-ID: 02231ddd4f442212820976068c34a36e3c1b15be
-rw-r--r-- | clientloop.c | 133 | ||||
-rw-r--r-- | misc.c | 29 | ||||
-rw-r--r-- | misc.h | 3 | ||||
-rw-r--r-- | packet.c | 14 | ||||
-rw-r--r-- | packet.h | 3 | ||||
-rw-r--r-- | readconf.c | 64 | ||||
-rw-r--r-- | readconf.h | 8 | ||||
-rw-r--r-- | ssh_config.5 | 22 |
8 files changed, 255 insertions, 21 deletions
diff --git a/clientloop.c b/clientloop.c index 99846a97..94ff2cb9 100644 --- a/clientloop.c +++ b/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 @@ -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). @@ -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; @@ -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 @@ -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) { /* @@ -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. @@ -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) @@ -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> @@ -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); @@ -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 @@ -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) { @@ -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> @@ -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 *); @@ -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 @@ -178,7 +178,7 @@ typedef enum { oFingerprintHash, oUpdateHostkeys, oHostbasedAcceptedAlgorithms, oPubkeyAcceptedAlgorithms, oCASignatureAlgorithms, oProxyJump, oSecurityKeyProvider, oKnownHostsCommand, oRequiredRSASize, - oEnableEscapeCommandline, + oEnableEscapeCommandline, oObscureKeystrokeTiming, oIgnore, oIgnoredUnknownOption, oDeprecated, oUnsupported } OpCodes; @@ -327,6 +327,7 @@ static struct { { "knownhostscommand", oKnownHostsCommand }, { "requiredrsasize", oRequiredRSASize }, { "enableescapecommandline", oEnableEscapeCommandline }, + { "obscurekeystroketiming", oObscureKeystrokeTiming }, { NULL, oBadOption } }; @@ -2280,6 +2281,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); @@ -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; } @@ -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); @@ -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); } @@ -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); @@ -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 <ylo@cs.hut.fi> @@ -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/ssh_config.5 b/ssh_config.5 index ab8d1021..5364ae85 100644 --- a/ssh_config.5 +++ b/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 @@ -1358,6 +1358,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 |