[openssh-commits] [openssh] 04/04: upstream: Add keystroke timing obfuscation to the client.

git+noreply at mindrot.org git+noreply at mindrot.org
Mon Aug 28 13:34:44 AEST 2023


This is an automated email from the git hooks/post-receive script.

djm pushed a commit to branch master
in repository openssh.

commit 7603ba71264e7fa938325c37eca993e2fa61272f
Author: djm at openbsd.org <djm at openbsd.org>
Date:   Mon Aug 28 03:31:16 2023 +0000

    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
---
 clientloop.c | 133 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
 misc.c       |  31 +++++++++-----
 misc.h       |   3 +-
 packet.c     |  14 ++++++-
 packet.h     |   3 +-
 readconf.c   |  64 +++++++++++++++++++++++++++-
 readconf.h   |   8 +++-
 ssh_config.5 |  22 +++++++++-
 8 files changed, 256 insertions(+), 22 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 at cs.hut.fi>
  * Copyright (c) 1995 Tatu Ylonen <ylo at 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) {
 		/*
diff --git a/misc.c b/misc.c
index 95658703..42582c61 100644
--- a/misc.c
+++ b/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.
@@ -2901,22 +2901,33 @@ ptimeout_deadline_ms(struct timespec *pt, long ms)
 	ptimeout_deadline_tsp(pt, &p);
 }
 
+/* Specify a poll/ppoll deadline at wall clock monotime 'when' (timespec) */
+void
+ptimeout_deadline_monotime_tsp(struct timespec *pt, struct timespec *when)
+{
+	struct timespec now, t;
+
+	monotime_ts(&now);
+
+	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 now, t;
+	struct timespec 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);
-		ptimeout_deadline_tsp(pt, &t);
-	}
+	ptimeout_deadline_monotime_tsp(pt, &t);
 }
 
 /* Get a poll(2) timeout value in milliseconds */
diff --git a/misc.h b/misc.h
index f9bdc6eb..4f941597 100644
--- a/misc.h
+++ b/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 at 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);
diff --git a/packet.c b/packet.c
index 77e5c57b..52017def 100644
--- a/packet.c
+++ b/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 at cs.hut.fi>
  * Copyright (c) 1995 Tatu Ylonen <ylo at 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)
 {
diff --git a/packet.h b/packet.h
index 176488b1..11925a27 100644
--- a/packet.h
+++ b/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 at 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 *);
diff --git a/readconf.c b/readconf.c
index 0d50e89b..131c24f5 100644
--- a/readconf.c
+++ b/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 at cs.hut.fi>
  * Copyright (c) 1995 Tatu Ylonen <ylo at 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);
diff --git a/readconf.h b/readconf.h
index dfe5bab0..ce261bd6 100644
--- a/readconf.h
+++ b/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 <ylo at 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

-- 
To stop receiving notification emails like this one, please contact
djm at mindrot.org.


More information about the openssh-commits mailing list