summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authordjm@openbsd.org <djm@openbsd.org>2023-08-28 03:31:16 +0000
committerDamien Miller <djm@mindrot.org>2023-08-28 13:34:10 +1000
commit7603ba71264e7fa938325c37eca993e2fa61272f (patch)
treee0b2fecdb68f63bdafd81eee605f5e3be9c0177a
parentdce6d80d2ed3cad2c516082682d5f6ca877ef714 (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.c133
-rw-r--r--misc.c29
-rw-r--r--misc.h3
-rw-r--r--packet.c14
-rw-r--r--packet.h3
-rw-r--r--readconf.c64
-rw-r--r--readconf.h8
-rw-r--r--ssh_config.522
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) {
/*
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,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)
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@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@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)
{
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@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@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);
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@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