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
This commit is contained in:
djm@openbsd.org 2023-08-28 03:31:16 +00:00 committed by Damien Miller
parent dce6d80d2e
commit 7603ba7126
No known key found for this signature in database
8 changed files with 256 additions and 22 deletions

View 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
@ -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) {
/*

31
misc.c
View 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.
@ -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 */

3
misc.h
View 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>
@ -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);

View 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
@ -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)
{

View 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>
@ -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 *);

View 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
@ -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);

View 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);

View 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
@ -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