Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
djmdjm committed Aug 11, 2023
1 parent 8223756 commit e807526
Show file tree
Hide file tree
Showing 5 changed files with 235 additions and 10 deletions.
204 changes: 202 additions & 2 deletions clientloop.c
Expand Up @@ -106,6 +106,14 @@
#include "ssherr.h"
#include "hostfile.h"

/* Timing attack mitigation parameters; applied only to interactive sessions */
#define SSH_TIMING_QUANTUM_MS 8
#define SSH_TIMING_CHAFF_MIN_MS 2048
#define SSH_TIMING_CHAFF_RNG_MS 8192
#define SSH_TIMING_SMOOTH_TIME_CONSTANT 3 /* seconds */
#define SSH_TIMING_SMOOTH_ALPHA (SSH_TIMING_QUANTUM_MS / \
((double)SSH_TIMING_SMOOTH_TIME_CONSTANT * 1000.0))

/* Permitted RSA signature algorithms for UpdateHostkeys proofs */
#define HOSTKEY_PROOF_RSA_ALGS "rsa-sha2-512,rsa-sha2-256"

Expand Down Expand Up @@ -498,6 +506,195 @@ server_alive_check(struct ssh *ssh)
schedule_server_alive_check();
}

static double
poisson_cdf(double x)
{
size_t i;
const double c[] = { /* coeffs for x^0 .. x^n */
-4.346855093700635e-06, 0.000185762153637246,
0.4980990845127494, -0.3254643199879826,
0.1093803482858701,-0.01795936818674764,
};
double xpow = 1, p = 0;

/*
* Calculating the Poisson CDF requires libm, which we want to avoid.
* Make do with a cubic approximation generated using GNU Octave:
* pkg load statistics
* x=linspace(0, 1, 8192)
* y=1-poisscdf(1,x)
* p=polyfit(x,y,5)
* csvwrite('/dev/stdout', flip(p))
*/
if (x < 0.0)
return 0;
if (x > 1.0)
x = 1.0;
for (i = 0; i < sizeof(c)/sizeof(*c); i++) {
p += c[i] * xpow;
xpow *= x;
}
return p > 0.0 ? p : 0.0;
}

static int
poisson_test(double rate)
{
double p;

/*
* XXX something is broken here. Simply applying the poisson_cdf()
* to the rate yields far too few positive tests. The rate looks
* good, and the CDF approximation is correct, so there is some other
* error somewhere (maybe conceptually?).
* Until figured out; fudge to give a better chaff rate
*/
rate *= 12.5; /* XXX suspiciously like SSH_TIMING_QUANTUM_MS/100 */

p = poisson_cdf(rate);
//debug_f("XXX r=%0.4f = %0.2f kps p=%0.4f", rate, rate * 1000/8, p);
/* We have a probability estimate in p; now roll the dice */
return (arc4random_uniform(1000000000) / 1000000000.0) < p;
}

/* Probabilistically send a dummy keystroke */
static void
maybe_send_chaff(struct ssh *ssh, double keystroke_rate)
{
static u_int XXX;

/* Assume keystrokes follow a Poisson distribution in time */
if (!poisson_test(keystroke_rate))
return;
debug_f("XXX chaff %u", XXX++);
/*
* XXX XXX XXX
* how to do this? need a message that exactly mimics a
* SSH_MSG_CHANNEL_DATA in length and consequence, i.e. echo from
* server, window adjusts etc.
*
* Maybe need protocol extension? Channel, global or transport?
*/
}

/*
* Update a number of timing-attack mitigation timers.
* Sets 't' to the next SSH_TIMING_QUANTUM_MS time after 'now' and
* outputs a smoothed keys/interval rate that is used for deciding whether
* to send chaff packets.
*/
static void
mitigator_update_timers(struct timespec *t, const struct timespec *now,
int just_started, int *keystrokes_last_intervalp, double *keystroke_rate)
{
long long i, n;
struct timespec tmp;
static double rate;

if (just_started) {
/* Start with a random estimate keystroke rate of 0.5-2.5 cps */
rate = (0.5 + (arc4random_uniform(2000) / 1000.0)) *
(SSH_TIMING_QUANTUM_MS / 1000.0);
} else {
/* Calculate number of intervals missed since the last update */
n = (now->tv_sec - t->tv_sec) * 1000000000;
n += now->tv_nsec - t->tv_nsec;
n /= SSH_TIMING_QUANTUM_MS * 1000000;
n = (n < 0) ? 1 : n + 1;

/* Advance to the next interval */
ms_to_timespec(&tmp, SSH_TIMING_QUANTUM_MS * n);
timespecadd(now, &tmp, t);

/* Update moving average rate of keystrokes/interval */
for (i = 0; i < n; i++)
rate = (1.0 - SSH_TIMING_SMOOTH_ALPHA) * rate;
if (rate < 0)
rate = 0;
rate += SSH_TIMING_SMOOTH_ALPHA * *keystrokes_last_intervalp;
}

*keystrokes_last_intervalp = 0;
*keystroke_rate = rate;
}

/*
* Performs keystroke timing attack mitigations. Returns non-zero if the
* output fd should be polled.
*/
static int
timing_attack_mitigator(struct ssh *ssh, struct timespec *timeout)
{
static int active, keystroke_last_interval;
static double keystroke_rate;
static struct timespec next_quantum, chaff_until;
struct timespec now, tmp;
int just_started = 0, had_keystroke = 0;

/*
* 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, arrange to send packets on fixed
* time intervals to hide inter-keystroke timing.
*/
monotime_ts(&now);
if (active && (!channel_still_open(ssh) || quit_pending)) {
debug3_f("stopping: no active channels");
active = 0;
} else if (!active && ssh_packet_interactive_data_to_write(ssh) &&
!ssh_packet_is_rekeying(ssh) && channel_still_open(ssh) &&
!quit_pending) {
debug3_f("starting");
just_started = had_keystroke = active = 1;
ms_to_timespec(&tmp, SSH_TIMING_QUANTUM_MS);
timespecadd(&now, &tmp, &next_quantum);
} else if (active && !ssh_packet_interactive_data_to_write(ssh) &&
ssh_packet_have_data_to_write(ssh)) {
/*
* Output buffer contains more than just a few keystrokes,
* so stop quantising.
*/
debug3_f("stopping: output buffer filling");
active = 0;
} else if (active && !ssh_packet_have_data_to_write(ssh)) {
/* no keystrokes in obuf */
if (timespeccmp(&now, &chaff_until, >=)) {
debug3_f("stopping: chaff time expired");
active = 0;
} else if (timespeccmp(&now, &next_quantum, >=)) {
/* Send a chaff packet */
maybe_send_chaff(ssh, keystroke_rate);
}
} else if (active) {
/* Still in active mode and have a keystroke queued. */
had_keystroke = 1;
}

if (had_keystroke) {
/*
* Arrange to send chaff packets for a random interval after
* the last keystroke was sent.
*/
ms_to_timespec(&tmp, SSH_TIMING_CHAFF_MIN_MS +
arc4random_uniform(SSH_TIMING_CHAFF_RNG_MS));
timespecadd(&now, &tmp, &chaff_until);
keystroke_last_interval = 1;
}

if (active) {
ptimeout_deadline_monotime_tsp(timeout, &next_quantum);
if (just_started || timespeccmp(&now, &next_quantum, >)) {
/* Timer has expired; reset it and prepare to poll */
mitigator_update_timers(&next_quantum, &now,
just_started, &keystroke_last_interval,
&keystroke_rate);
return 1;
}
}
/* If mitigator isn't active, or we're rekeying then prepare to poll */
return !active || ssh_packet_is_rekeying(ssh);
}

/*
* Waits until the client can do something (some data becomes available on
* one of the file descriptors).
Expand All @@ -508,7 +705,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;
Expand All @@ -528,11 +725,14 @@ client_wait_until_can_do_something(struct ssh *ssh, struct pollfd **pfdp,
return;
}

oready = timing_attack_mitigator(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
Expand Down
27 changes: 19 additions & 8 deletions misc.c
Expand Up @@ -2774,24 +2774,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)
Expand Down
1 change: 1 addition & 0 deletions misc.h
Expand Up @@ -211,6 +211,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);
Expand Down
12 changes: 12 additions & 0 deletions packet.c
Expand Up @@ -2041,6 +2041,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) < 128;
}

void
ssh_packet_set_tos(struct ssh *ssh, int tos)
{
Expand Down
1 change: 1 addition & 0 deletions packet.h
Expand Up @@ -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 *);
Expand Down

0 comments on commit e807526

Please sign in to comment.