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:
parent
dce6d80d2e
commit
7603ba7126
133
clientloop.c
133
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) {
|
||||
/*
|
||||
|
|
31
misc.c
31
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 */
|
||||
|
|
3
misc.h
3
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);
|
||||
|
|
14
packet.c
14
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)
|
||||
{
|
||||
|
|
3
packet.h
3
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 *);
|
||||
|
|
64
readconf.c
64
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);
|
||||
|
|
|
@ -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);
|
||||
|
|
22
ssh_config.5
22
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
|
||||
|
|
Loading…
Reference in New Issue