From the CHANGES file, Fixes: Add "Strict KEX" support. This mitigates a SSH protocol flaw which lets a MITM attacker silently remove packets immediately after the first key exchange. At present the flaw does not seem to reduce Dropbear's security (the only packet affected would be a server-sig-algs extension, which is used for compatibility not security). For Dropbear, chacha20-poly1305 is the only affected cipher. Both sides of the connection must support Strict KEX for it to be used. The protocol flaw is tracked as CVE-2023-48795, details at https://terrapin-attack.com . Thanks to the researchers Fabian Bäumer, Marcus Brinkmann, and Jörg Schwenk. Thanks to OpenSSH for specifying strict KEX mode. diff --git a/cli-session.c b/cli-session.c index 5981b247..d261c8f8 100644 --- a/cli-session.c +++ b/cli-session.c @@ -46,6 +46,7 @@ static void cli_finished(void) ATTRIB_NORETURN; static void recv_msg_service_accept(void); static void cli_session_cleanup(void); static void recv_msg_global_request_cli(void); +static void cli_algos_initialise(void); struct clientsession cli_ses; /* GLOBAL */ @@ -117,6 +118,7 @@ void cli_session(int sock_in, int sock_out, struct dropbear_progress_connection } chaninitialise(cli_chantypes); + cli_algos_initialise(); /* Set up cli_ses vars */ cli_session_init(proxy_cmd_pid); @@ -487,3 +489,12 @@ void cli_dropbear_log(int priority, const char* format, va_list param) { fflush(stderr); } +static void cli_algos_initialise(void) { + algo_type *algo; + for (algo = sshkex; algo->name; algo++) { + if (strcmp(algo->name, SSH_STRICT_KEX_S) == 0) { + algo->usable = 0; + } + } +} + diff --git a/common-algo.c b/common-algo.c index 378f0ca8..f9d46ebb 100644 --- a/common-algo.c +++ b/common-algo.c @@ -307,6 +307,12 @@ algo_type sshkex[] = { /* Set unusable by svr_algos_initialise() */ {SSH_EXT_INFO_C, 0, NULL, 1, NULL}, #endif +#endif +#if DROPBEAR_CLIENT + {SSH_STRICT_KEX_C, 0, NULL, 1, NULL}, +#endif +#if DROPBEAR_SERVER + {SSH_STRICT_KEX_S, 0, NULL, 1, NULL}, #endif {NULL, 0, NULL, 0, NULL} }; diff --git a/common-kex.c b/common-kex.c index ac884424..8e33b12a 100644 --- a/common-kex.c +++ b/common-kex.c @@ -183,6 +183,10 @@ void send_msg_newkeys() { gen_new_keys(); switch_keys(); + if (ses.kexstate.strict_kex) { + ses.transseq = 0; + } + TRACE(("leave send_msg_newkeys")) } @@ -193,7 +197,11 @@ void recv_msg_newkeys() { ses.kexstate.recvnewkeys = 1; switch_keys(); - + + if (ses.kexstate.strict_kex) { + ses.recvseq = 0; + } + TRACE(("leave recv_msg_newkeys")) } @@ -550,6 +558,10 @@ void recv_msg_kexinit() { ses.kexstate.recvkexinit = 1; + if (ses.kexstate.strict_kex && !ses.kexstate.donefirstkex && ses.recvseq != 1) { + dropbear_exit("First packet wasn't kexinit"); + } + TRACE(("leave recv_msg_kexinit")) } @@ -859,6 +871,18 @@ static void read_kex_algos() { } #endif + if (!ses.kexstate.donefirstkex) { + const char* strict_name; + if (IS_DROPBEAR_CLIENT) { + strict_name = SSH_STRICT_KEX_S; + } else { + strict_name = SSH_STRICT_KEX_C; + } + if (buf_has_algo(ses.payload, strict_name) == DROPBEAR_SUCCESS) { + ses.kexstate.strict_kex = 1; + } + } + algo = buf_match_algo(ses.payload, sshkex, kexguess2, &goodguess); allgood &= goodguess; if (algo == NULL || algo->data == NULL) { diff --git a/kex.h b/kex.h index 77cf21a3..7fcc3c25 100644 --- a/kex.h +++ b/kex.h @@ -83,6 +83,9 @@ struct KEXState { unsigned our_first_follows_matches : 1; + /* Boolean indicating that strict kex mode is in use */ + unsigned int strict_kex; + time_t lastkextime; /* time of the last kex */ unsigned int datatrans; /* data transmitted since last kex */ unsigned int datarecv; /* data received since last kex */ diff --git a/process-packet.c b/process-packet.c index 94541602..133a152d 100644 --- a/process-packet.c +++ b/process-packet.c @@ -44,6 +44,7 @@ void process_packet() { unsigned char type; unsigned int i; + unsigned int first_strict_kex = ses.kexstate.strict_kex && !ses.kexstate.donefirstkex; time_t now; TRACE2(("enter process_packet")) @@ -54,22 +55,24 @@ void process_packet() { now = monotonic_now(); ses.last_packet_time_keepalive_recv = now; - /* These packets we can receive at any time */ - switch(type) { - case SSH_MSG_IGNORE: - goto out; - case SSH_MSG_DEBUG: - goto out; + if (type == SSH_MSG_DISCONNECT) { + /* Allowed at any time */ + dropbear_close("Disconnect received"); + } - case SSH_MSG_UNIMPLEMENTED: - /* debugging XXX */ - TRACE(("SSH_MSG_UNIMPLEMENTED")) - goto out; - - case SSH_MSG_DISCONNECT: - /* TODO cleanup? */ - dropbear_close("Disconnect received"); + /* These packets may be received at any time, + except during first kex with strict kex */ + if (!first_strict_kex) { + switch(type) { + case SSH_MSG_IGNORE: + goto out; + case SSH_MSG_DEBUG: + goto out; + case SSH_MSG_UNIMPLEMENTED: + TRACE(("SSH_MSG_UNIMPLEMENTED")) + goto out; + } } /* Ignore these packet types so that keepalives don't interfere with @@ -98,7 +101,8 @@ void process_packet() { if (type >= 1 && type <= 49 && type != SSH_MSG_SERVICE_REQUEST && type != SSH_MSG_SERVICE_ACCEPT - && type != SSH_MSG_KEXINIT) + && type != SSH_MSG_KEXINIT + && !first_strict_kex) { TRACE(("unknown allowed packet during kexinit")) recv_unimplemented(); diff --git a/ssh.h b/ssh.h index 1b4fec65..ef3efdca 100644 --- a/ssh.h +++ b/ssh.h @@ -100,6 +100,10 @@ #define SSH_EXT_INFO_C "ext-info-c" #define SSH_SERVER_SIG_ALGS "server-sig-algs" +/* OpenSSH strict KEX feature */ +#define SSH_STRICT_KEX_S "kex-strict-s-v00@openssh.com" +#define SSH_STRICT_KEX_C "kex-strict-c-v00@openssh.com" + /* service types */ #define SSH_SERVICE_USERAUTH "ssh-userauth" #define SSH_SERVICE_USERAUTH_LEN 12 diff --git a/svr-session.c b/svr-session.c index 769f0731..a538e2c5 100644 --- a/svr-session.c +++ b/svr-session.c @@ -370,6 +370,9 @@ static void svr_algos_initialise(void) { algo->usable = 0; } #endif + if (strcmp(algo->name, SSH_STRICT_KEX_C) == 0) { + algo->usable = 0; + } } }