Add keystroke timing obfuscation to the client.
authordjm <djm@openbsd.org>
Mon, 28 Aug 2023 03:31:16 +0000 (03:31 +0000)
committerdjm <djm@openbsd.org>
Mon, 28 Aug 2023 03:31:16 +0000 (03:31 +0000)
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@

usr.bin/ssh/clientloop.c
usr.bin/ssh/misc.c
usr.bin/ssh/misc.h
usr.bin/ssh/packet.c
usr.bin/ssh/packet.h
usr.bin/ssh/readconf.c
usr.bin/ssh/readconf.h
usr.bin/ssh/ssh_config.5

index 67a9f17..6722817 100644 (file)
@@ -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
@@ -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) {
                /*
index 52d79e7..59ee9c9 100644 (file)
@@ -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)
index 0221ce2..5e0c452 100644 (file)
@@ -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>
@@ -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);
index 600d510..0ef57d4 100644 (file)
@@ -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
@@ -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)
 {
index c8817ff..39ee0b5 100644 (file)
@@ -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>
@@ -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 *);
index ed11998..2d85d48 100644 (file)
@@ -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
@@ -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);
index dfe5bab..ce261bd 100644 (file)
@@ -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);
index 1eae6e7..1876917 100644 (file)
@@ -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