From f9553005766263d8f6c0f9274f23b1d93fd0bb75 Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Sun, 21 Jun 2020 16:34:09 +0100 Subject: [PATCH 01/11] Docs: use less personalised example Windows prompts. The previous prompts were part of transcripts pasted directly from a particular historical cmd session, but that's no reason to keep them lying around confusingly, especially since we keep regenerating some of those transcripts outside that historical context. Replace them all with nice simple C:\> which shouldn't confuse anyone with extraneous detail. --- doc/plink.but | 16 ++++++++-------- doc/pscp.but | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/doc/plink.but b/doc/plink.but index 25139bec..72449293 100644 --- a/doc/plink.but +++ b/doc/plink.but @@ -39,7 +39,7 @@ Once you've got a console window to type into, you can just type version of Plink you're using, and gives you a brief summary of how to use Plink: -\c Z:\sysosd>plink +\c C:\>plink \c Plink: command-line connection utility \c Release 0.73 \c Usage: plink [options] [user@]host [command] @@ -100,7 +100,7 @@ Once this works, you are ready to use Plink. To make a simple interactive connection to a remote server, just type \c{plink} and then the host name: -\c Z:\sysosd>plink login.example.com +\c C:\>plink login.example.com \c \c Debian GNU/Linux 2.2 flunky.example.com \c flunky login: @@ -117,7 +117,7 @@ In order to connect with a different protocol, you can give the command line options \c{-ssh}, \c{-telnet}, \c{-rlogin} or \c{-raw}. To make an SSH connection, for example: -\c Z:\sysosd>plink -ssh login.example.com +\c C:\>plink -ssh login.example.com \c login as: If you have already set up a PuTTY saved session, then instead of @@ -125,7 +125,7 @@ supplying a host name, you can give the saved session name. This allows you to use public-key authentication, specify a user name, and use most of the other features of PuTTY: -\c Z:\sysosd>plink my-ssh-session +\c C:\>plink my-ssh-session \c Sent username "fred" \c Authenticating with public key "fred@winbox" \c Last login: Thu Dec 6 19:25:33 2001 from :0.0 @@ -196,18 +196,18 @@ Once you have done all this, you should be able to run a remote command on the SSH server machine and have it execute automatically with no prompting: -\c Z:\sysosd>plink login.example.com -l fred echo hello, world +\c C:\>plink login.example.com -l fred echo hello, world \c hello, world \c -\c Z:\sysosd> +\c C:\> Or, if you have set up a saved session with all the connection details: -\c Z:\sysosd>plink mysession echo hello, world +\c C:\>plink mysession echo hello, world \c hello, world \c -\c Z:\sysosd> +\c C:\> Then you can set up other programs to run this Plink command and talk to it as if it were a process on the server machine. diff --git a/doc/pscp.but b/doc/pscp.but index c8c5b94d..e6b8eed3 100644 --- a/doc/pscp.but +++ b/doc/pscp.but @@ -37,7 +37,7 @@ Once you've got a console window to type into, you can just type version of PSCP you're using, and gives you a brief summary of how to use PSCP: -\c Z:\owendadmin>pscp +\c C:\>pscp \c PuTTY Secure Copy client \c Release 0.73 \c Usage: pscp [options] [user@]host:source target From 4510a622ea16390d18014e4523fbc9cbe492578c Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Mon, 14 Oct 2019 19:58:59 +0100 Subject: [PATCH 02/11] Tighten up a comparison in ssh2_userauth_add_sigblob. If a malicious SSH agent were to send an RSA signature blob _longer_ than the key modulus while BUG_SSH2_RSA_PADDING was enabled, then it could DoS the client, because the put_padding call would keep allocating memory in 'strbuf *substr' until address space ran out. --- ssh2userauth.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ssh2userauth.c b/ssh2userauth.c index dace56f1..4d0d0392 100644 --- a/ssh2userauth.c +++ b/ssh2userauth.c @@ -1788,7 +1788,7 @@ static void ssh2_userauth_add_sigblob( /* debug("modulus length is %d\n", len); */ /* debug("signature length is %d\n", siglen); */ - if (mod_mp.len != sig_mp.len) { + if (mod_mp.len > sig_mp.len) { strbuf *substr = strbuf_new(); put_data(substr, sigblob.ptr, sig_prefix_len); put_uint32(substr, mod_mp.len); From 45287b627d1b1c4db4c7bd485a5adcc63b8d608a Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Sun, 9 Feb 2020 08:24:28 +0000 Subject: [PATCH 03/11] Rework parsing of agent key list. Now, in both SSH-1 and SSH-2, we go through the whole response from the SSH agent, parse out the public blob and comment of every key, and stash them in a data structure to iterate through later. Previously, we were iterating through the agent response _in situ_, while it was still stored in the s->agent_response memory buffer in the form the agent sent it, and had the ongoing s->asrc BinarySource pointing at it. This led to a remotely triggerable stale-pointer bug: as soon as we send a _second_ agent request trying to authenticate with one of the keys, it causes s->agent_response to be freed. In normal usage this doesn't happen, because if a server sends PK_OK (or an RSA1 challenge) then it's going to accept our response, so we never go back to iterating over the rest of the agent's key list. But if a server sends PK_OK or an RSA1 challenge and _then_ rejects authentication after we go to the effort of responding, we'll go back to iterating over the agent's key list and cause a crash. So now, we extract everything we need from the key-list agent response, and by the time we're making further agent requests, we don't need it any more. --- ssh1login.c | 272 +++++++++++++++++++++++++++++-------------------- ssh2userauth.c | 171 +++++++++++++++++-------------- 2 files changed, 260 insertions(+), 183 deletions(-) diff --git a/ssh1login.c b/ssh1login.c index 803e590e..64782868 100644 --- a/ssh1login.c +++ b/ssh1login.c @@ -12,6 +12,12 @@ #include "sshppl.h" #include "sshcr.h" +typedef struct agent_key { + RSAKey key; + strbuf *comment; + ptrlen blob; /* only used during initial parsing of agent response */ +} agent_key; + struct ssh1_login_state { int crState; @@ -47,11 +53,11 @@ struct ssh1_login_state { void *agent_response_to_free; ptrlen agent_response; BinarySource asrc[1]; /* response from SSH agent */ - int keyi, nkeys; + size_t agent_keys_len; + agent_key *agent_keys; + size_t agent_key_index, agent_key_limit; bool authed; RSAKey key; - mp_int *challenge; - strbuf *agent_comment; int dlgret; Filename *keyfile; RSAKey servkey, hostkey; @@ -99,7 +105,6 @@ PacketProtocolLayer *ssh1_login_new( s->savedhost = dupstr(host); s->savedport = port; s->successor_layer = successor_layer; - s->agent_comment = strbuf_new(); return &s->ppl; } @@ -118,9 +123,15 @@ static void ssh1_login_free(PacketProtocolLayer *ppl) if (s->publickey_blob) strbuf_free(s->publickey_blob); sfree(s->publickey_comment); - strbuf_free(s->agent_comment); if (s->cur_prompt) free_prompts(s->cur_prompt); + if (s->agent_keys) { + for (size_t i = 0; i < s->agent_keys_len; i++) { + freersakey(&s->agent_keys[i].key); + strbuf_free(s->agent_keys[i].comment); + } + sfree(s->agent_keys); + } sfree(s->agent_response_to_free); if (s->auth_agent_query) agent_cancel_query(s->auth_agent_query); @@ -504,122 +515,165 @@ static void ssh1_login_process_queue(PacketProtocolLayer *ppl) get_uint32(s->asrc); /* skip length field */ if (get_byte(s->asrc) == SSH1_AGENT_RSA_IDENTITIES_ANSWER) { - s->nkeys = toint(get_uint32(s->asrc)); - if (s->nkeys < 0) { - ppl_logevent("Pageant reported negative key count %d", - s->nkeys); - s->nkeys = 0; - } - ppl_logevent("Pageant has %d SSH-1 keys", s->nkeys); - for (s->keyi = 0; s->keyi < s->nkeys; s->keyi++) { - size_t start, end; - start = s->asrc->pos; - get_rsa_ssh1_pub(s->asrc, &s->key, - RSA_SSH1_EXPONENT_FIRST); - end = s->asrc->pos; - strbuf_clear(s->agent_comment); - put_datapl(s->agent_comment, get_string(s->asrc)); - if (get_err(s->asrc)) { - ppl_logevent("Pageant key list packet was truncated"); - break; - } - if (s->publickey_blob) { - ptrlen keystr = make_ptrlen( - (const char *)s->asrc->data + start, end - start); + size_t nkeys = get_uint32(s->asrc); + size_t origpos = s->asrc->pos; - if (keystr.len == s->publickey_blob->len && - !memcmp(keystr.ptr, s->publickey_blob->s, - s->publickey_blob->len)) { - ppl_logevent("Pageant key #%d matches " - "configured key file", s->keyi); - s->tried_publickey = true; - } else - /* Skip non-configured key */ - continue; + /* + * Check that the agent response is well formed. + */ + for (size_t i = 0; i < nkeys; i++) { + get_rsa_ssh1_pub(s->asrc, NULL, RSA_SSH1_EXPONENT_FIRST); + get_string(s->asrc); /* comment */ + if (get_err(s->asrc)) { + ppl_logevent("Pageant's response was truncated"); + goto parsed_agent_query; } - ppl_logevent("Trying Pageant key #%d", s->keyi); - pkt = ssh_bpp_new_pktout(s->ppl.bpp, SSH1_CMSG_AUTH_RSA); - put_mp_ssh1(pkt, s->key.modulus); - pq_push(s->ppl.out_pq, pkt); - crMaybeWaitUntilV((pktin = ssh1_login_pop(s)) - != NULL); - if (pktin->type != SSH1_SMSG_AUTH_RSA_CHALLENGE) { - ppl_logevent("Key refused"); - continue; + } + + /* + * Copy the list of public-key blobs out of the Pageant + * response. + */ + BinarySource_REWIND_TO(s->asrc, origpos); + s->agent_keys_len = nkeys; + s->agent_keys = snewn(s->agent_keys_len, agent_key); + for (size_t i = 0; i < nkeys; i++) { + memset(&s->agent_keys[i].key, 0, + sizeof(s->agent_keys[i].key)); + + const char *blobstart = get_ptr(s->asrc); + get_rsa_ssh1_pub(s->asrc, &s->agent_keys[i].key, + RSA_SSH1_EXPONENT_FIRST); + const char *blobend = get_ptr(s->asrc); + + s->agent_keys[i].comment = strbuf_new(); + put_datapl(s->agent_keys[i].comment, get_string(s->asrc)); + + s->agent_keys[i].blob = make_ptrlen( + blobstart, blobend - blobstart); + } + + ppl_logevent("Pageant has %"SIZEu" SSH-1 keys", nkeys); + + if (s->publickey_blob) { + /* + * If we've been given a specific public key blob, + * filter the list of keys to try from the agent + * down to only that one, or none if it's not + * there. + */ + ptrlen our_blob = ptrlen_from_strbuf(s->publickey_blob); + size_t i; + + for (i = 0; i < nkeys; i++) { + if (ptrlen_eq_ptrlen(our_blob, s->agent_keys[i].blob)) + break; } - ppl_logevent("Received RSA challenge"); - s->challenge = get_mp_ssh1(pktin); + + if (i < nkeys) { + ppl_logevent("Pageant key #%"SIZEu" matches " + "configured key file", i); + s->agent_key_index = i; + s->agent_key_limit = i+1; + } else { + ppl_logevent("Configured key file not in Pageant"); + s->agent_key_index = 0; + s->agent_key_limit = 0; + } + } else { + /* + * Otherwise, try them all. + */ + s->agent_key_index = 0; + s->agent_key_limit = nkeys; + } + } else { + ppl_logevent("Failed to get reply from Pageant"); + } + parsed_agent_query:; + + for (; s->agent_key_index < s->agent_key_limit; + s->agent_key_index++) { + ppl_logevent("Trying Pageant key #%"SIZEu, s->agent_key_index); + pkt = ssh_bpp_new_pktout(s->ppl.bpp, SSH1_CMSG_AUTH_RSA); + put_mp_ssh1(pkt, + s->agent_keys[s->agent_key_index].key.modulus); + pq_push(s->ppl.out_pq, pkt); + crMaybeWaitUntilV((pktin = ssh1_login_pop(s)) + != NULL); + if (pktin->type != SSH1_SMSG_AUTH_RSA_CHALLENGE) { + ppl_logevent("Key refused"); + continue; + } + ppl_logevent("Received RSA challenge"); + + { + mp_int *challenge = get_mp_ssh1(pktin); if (get_err(pktin)) { - mp_free(s->challenge); + mp_free(challenge); ssh_proto_error(s->ppl.ssh, "Server's RSA challenge " "was badly formatted"); return; } - { - strbuf *agentreq; - const char *ret; + strbuf *agentreq = strbuf_new_for_agent_query(); + put_byte(agentreq, SSH1_AGENTC_RSA_CHALLENGE); - agentreq = strbuf_new_for_agent_query(); - put_byte(agentreq, SSH1_AGENTC_RSA_CHALLENGE); - put_uint32(agentreq, mp_get_nbits(s->key.modulus)); - put_mp_ssh1(agentreq, s->key.exponent); - put_mp_ssh1(agentreq, s->key.modulus); - put_mp_ssh1(agentreq, s->challenge); - put_data(agentreq, s->session_id, 16); - put_uint32(agentreq, 1); /* response format */ - ssh1_login_agent_query(s, agentreq); - strbuf_free(agentreq); - crMaybeWaitUntilV(!s->auth_agent_query); + rsa_ssh1_public_blob( + BinarySink_UPCAST(agentreq), + &s->agent_keys[s->agent_key_index].key, + RSA_SSH1_EXPONENT_FIRST); - ret = s->agent_response.ptr; - if (ret) { - if (s->agent_response.len >= 5+16 && - ret[4] == SSH1_AGENT_RSA_RESPONSE) { - ppl_logevent("Sending Pageant's response"); - pkt = ssh_bpp_new_pktout( - s->ppl.bpp, SSH1_CMSG_AUTH_RSA_RESPONSE); - put_data(pkt, ret + 5, 16); - pq_push(s->ppl.out_pq, pkt); - crMaybeWaitUntilV( - (pktin = ssh1_login_pop(s)) - != NULL); - if (pktin->type == SSH1_SMSG_SUCCESS) { - ppl_logevent("Pageant's response " - "accepted"); - if (flags & FLAG_VERBOSE) { - ptrlen comment = ptrlen_from_strbuf( - s->agent_comment); - ppl_printf("Authenticated using RSA " - "key \"%.*s\" from " - "agent\r\n", - PTRLEN_PRINTF(comment)); - } - s->authed = true; - } else - ppl_logevent("Pageant's response not " - "accepted"); - } else { - ppl_logevent("Pageant failed to answer " - "challenge"); - sfree((char *)ret); - } - } else { - ppl_logevent("No reply received from Pageant"); - } - } - mp_free(s->key.exponent); - mp_free(s->key.modulus); - mp_free(s->challenge); - if (s->authed) - break; + put_mp_ssh1(agentreq, challenge); + mp_free(challenge); + + put_data(agentreq, s->session_id, 16); + put_uint32(agentreq, 1); /* response format */ + ssh1_login_agent_query(s, agentreq); + strbuf_free(agentreq); + crMaybeWaitUntilV(!s->auth_agent_query); } - sfree(s->agent_response_to_free); - s->agent_response_to_free = NULL; - if (s->publickey_blob && !s->tried_publickey) - ppl_logevent("Configured key file not in Pageant"); - } else { - ppl_logevent("Failed to get reply from Pageant"); + + { + const unsigned char *ret = s->agent_response.ptr; + if (ret) { + if (s->agent_response.len >= 5+16 && + ret[4] == SSH1_AGENT_RSA_RESPONSE) { + ppl_logevent("Sending Pageant's response"); + pkt = ssh_bpp_new_pktout( + s->ppl.bpp, SSH1_CMSG_AUTH_RSA_RESPONSE); + put_data(pkt, ret + 5, 16); + pq_push(s->ppl.out_pq, pkt); + crMaybeWaitUntilV( + (pktin = ssh1_login_pop(s)) + != NULL); + if (pktin->type == SSH1_SMSG_SUCCESS) { + ppl_logevent("Pageant's response " + "accepted"); + if (flags & FLAG_VERBOSE) { + ptrlen comment = ptrlen_from_strbuf( + s->agent_keys[s->agent_key_index]. + comment); + ppl_printf("Authenticated using RSA " + "key \"%.*s\" from " + "agent\r\n", + PTRLEN_PRINTF(comment)); + } + s->authed = true; + } else + ppl_logevent("Pageant's response not " + "accepted"); + } else { + ppl_logevent("Pageant failed to answer " + "challenge"); + sfree((char *)ret); + } + } else { + ppl_logevent("No reply received from Pageant"); + } + } + if (s->authed) + break; } if (s->authed) break; diff --git a/ssh2userauth.c b/ssh2userauth.c index 4d0d0392..2e7ffb0c 100644 --- a/ssh2userauth.c +++ b/ssh2userauth.c @@ -18,6 +18,11 @@ #define BANNER_LIMIT 131072 +typedef struct agent_key { + strbuf *blob, *comment; + ptrlen algorithm; +} agent_key; + struct ssh2_userauth_state { int crState; @@ -69,9 +74,9 @@ struct ssh2_userauth_state { void *agent_response_to_free; ptrlen agent_response; BinarySource asrc[1]; /* for reading SSH agent response */ - size_t pkblob_pos_in_agent; - int keyi, nkeys; - ptrlen pk, alg, comment; + size_t agent_keys_len; + agent_key *agent_keys; + size_t agent_key_index, agent_key_limit; int len; PktOut *pktout; bool want_user_input; @@ -173,6 +178,13 @@ static void ssh2_userauth_free(PacketProtocolLayer *ppl) if (s->successor_layer) ssh_ppl_free(s->successor_layer); + if (s->agent_keys) { + for (size_t i = 0; i < s->agent_keys_len; i++) { + strbuf_free(s->agent_keys[i].blob); + strbuf_free(s->agent_keys[i].comment); + } + sfree(s->agent_keys); + } sfree(s->agent_response_to_free); if (s->auth_agent_query) agent_cancel_query(s->auth_agent_query); @@ -300,8 +312,6 @@ static void ssh2_userauth_process_queue(PacketProtocolLayer *ppl) * Find out about any keys Pageant has (but if there's a public * key configured, filter out all others). */ - s->nkeys = 0; - s->pkblob_pos_in_agent = 0; if (s->tryagent && agent_exists()) { ppl_logevent("Pageant is running. Requesting keys."); @@ -317,48 +327,75 @@ static void ssh2_userauth_process_queue(PacketProtocolLayer *ppl) get_uint32(s->asrc); /* skip length field */ if (get_byte(s->asrc) == SSH2_AGENT_IDENTITIES_ANSWER) { - int keyi; - - s->nkeys = toint(get_uint32(s->asrc)); + size_t nkeys = get_uint32(s->asrc); + size_t origpos = s->asrc->pos; /* - * Vet the Pageant response to ensure that the key count - * and blob lengths make sense. + * Check that the agent response is well formed. */ - if (s->nkeys < 0) { - ppl_logevent("Pageant response contained a negative" - " key count %d", s->nkeys); - s->nkeys = 0; - goto done_agent_query; - } else { - ppl_logevent("Pageant has %d SSH-2 keys", s->nkeys); + for (size_t i = 0; i < nkeys; i++) { + get_string(s->asrc); /* blob */ + get_string(s->asrc); /* comment */ + if (get_err(s->asrc)) { + ppl_logevent("Pageant's response was truncated"); + goto done_agent_query; + } + } - /* See if configured key is in agent. */ - for (keyi = 0; keyi < s->nkeys; keyi++) { - size_t pos = s->asrc->pos; - ptrlen blob = get_string(s->asrc); - get_string(s->asrc); /* skip comment */ - if (get_err(s->asrc)) { - ppl_logevent("Pageant response was truncated"); - s->nkeys = 0; - goto done_agent_query; - } + /* + * Copy the list of public-key blobs out of the Pageant + * response. + */ + BinarySource_REWIND_TO(s->asrc, origpos); + s->agent_keys_len = nkeys; + s->agent_keys = snewn(s->agent_keys_len, agent_key); + for (size_t i = 0; i < nkeys; i++) { + s->agent_keys[i].blob = strbuf_new(); + put_datapl(s->agent_keys[i].blob, get_string(s->asrc)); + s->agent_keys[i].comment = strbuf_new(); + put_datapl(s->agent_keys[i].comment, get_string(s->asrc)); - if (s->publickey_blob && - blob.len == s->publickey_blob->len && - !memcmp(blob.ptr, s->publickey_blob->s, - s->publickey_blob->len)) { - ppl_logevent("Pageant key #%d matches " - "configured key file", keyi); - s->keyi = keyi; - s->pkblob_pos_in_agent = pos; + /* Also, extract the algorithm string from the start + * of the public-key blob. */ + BinarySource src[1]; + BinarySource_BARE_INIT_PL(src, ptrlen_from_strbuf( + s->agent_keys[i].blob)); + s->agent_keys[i].algorithm = get_string(src); + } + + ppl_logevent("Pageant has %"SIZEu" SSH-2 keys", nkeys); + + if (s->publickey_blob) { + /* + * If we've been given a specific public key blob, + * filter the list of keys to try from the agent down + * to only that one, or none if it's not there. + */ + ptrlen our_blob = ptrlen_from_strbuf(s->publickey_blob); + size_t i; + + for (i = 0; i < nkeys; i++) { + if (ptrlen_eq_ptrlen(our_blob, ptrlen_from_strbuf( + s->agent_keys[i].blob))) break; - } } - if (s->publickey_blob && !s->pkblob_pos_in_agent) { + + if (i < nkeys) { + ppl_logevent("Pageant key #%"SIZEu" matches " + "configured key file", i); + s->agent_key_index = i; + s->agent_key_limit = i+1; + } else { ppl_logevent("Configured key file not in Pageant"); - s->nkeys = 0; + s->agent_key_index = 0; + s->agent_key_limit = 0; } + } else { + /* + * Otherwise, try them all. + */ + s->agent_key_index = 0; + s->agent_key_limit = nkeys; } } else { ppl_logevent("Failed to get reply from Pageant"); @@ -457,17 +494,7 @@ static void ssh2_userauth_process_queue(PacketProtocolLayer *ppl) s->tried_pubkey_config = false; s->kbd_inter_refused = false; - - /* Reset agent request state. */ s->done_agent = false; - if (s->agent_response.ptr) { - if (s->pkblob_pos_in_agent) { - s->asrc->pos = s->pkblob_pos_in_agent; - } else { - s->asrc->pos = 9; /* skip length + type + key count */ - s->keyi = 0; - } - } while (1) { /* @@ -688,7 +715,8 @@ static void ssh2_userauth_process_queue(PacketProtocolLayer *ppl) } else #endif /* NO_GSSAPI */ - if (s->can_pubkey && !s->done_agent && s->nkeys) { + if (s->can_pubkey && !s->done_agent && + s->agent_key_index < s->agent_key_limit) { /* * Attempt public-key authentication using a key from Pageant. @@ -696,16 +724,7 @@ static void ssh2_userauth_process_queue(PacketProtocolLayer *ppl) s->ppl.bpp->pls->actx = SSH2_PKTCTX_PUBLICKEY; - ppl_logevent("Trying Pageant key #%d", s->keyi); - - /* Unpack key from agent response */ - s->pk = get_string(s->asrc); - s->comment = get_string(s->asrc); - { - BinarySource src[1]; - BinarySource_BARE_INIT_PL(src, s->pk); - s->alg = get_string(src); - } + ppl_logevent("Trying Pageant key #%"SIZEu, s->agent_key_index); /* See if server will accept it */ s->pktout = ssh_bpp_new_pktout( @@ -715,8 +734,10 @@ static void ssh2_userauth_process_queue(PacketProtocolLayer *ppl) put_stringz(s->pktout, "publickey"); /* method */ put_bool(s->pktout, false); /* no signature included */ - put_stringpl(s->pktout, s->alg); - put_stringpl(s->pktout, s->pk); + put_stringpl(s->pktout, + s->agent_keys[s->agent_key_index].algorithm); + put_stringpl(s->pktout, ptrlen_from_strbuf( + s->agent_keys[s->agent_key_index].blob)); pq_push(s->ppl.out_pq, s->pktout); s->type = AUTH_TYPE_PUBLICKEY_OFFER_QUIET; @@ -729,11 +750,13 @@ static void ssh2_userauth_process_queue(PacketProtocolLayer *ppl) } else { strbuf *agentreq, *sigdata; + ptrlen comment = ptrlen_from_strbuf( + s->agent_keys[s->agent_key_index].comment); if (flags & FLAG_VERBOSE) ppl_printf("Authenticating with public key " "\"%.*s\" from agent\r\n", - PTRLEN_PRINTF(s->comment)); + PTRLEN_PRINTF(comment)); /* * Server is willing to accept the key. @@ -746,13 +769,16 @@ static void ssh2_userauth_process_queue(PacketProtocolLayer *ppl) put_stringz(s->pktout, "publickey"); /* method */ put_bool(s->pktout, true); /* signature included */ - put_stringpl(s->pktout, s->alg); - put_stringpl(s->pktout, s->pk); + put_stringpl(s->pktout, + s->agent_keys[s->agent_key_index].algorithm); + put_stringpl(s->pktout, ptrlen_from_strbuf( + s->agent_keys[s->agent_key_index].blob)); /* Ask agent for signature. */ agentreq = strbuf_new_for_agent_query(); put_byte(agentreq, SSH2_AGENTC_SIGN_REQUEST); - put_stringpl(agentreq, s->pk); + put_stringpl(agentreq, ptrlen_from_strbuf( + s->agent_keys[s->agent_key_index].blob)); /* Now the data to be signed... */ sigdata = strbuf_new(); ssh2_userauth_add_session_id(s, sigdata); @@ -774,8 +800,11 @@ static void ssh2_userauth_process_queue(PacketProtocolLayer *ppl) if (get_byte(src) == SSH2_AGENT_SIGN_RESPONSE && (sigblob = get_string(src), !get_err(src))) { ppl_logevent("Sending Pageant's response"); - ssh2_userauth_add_sigblob(s, s->pktout, - s->pk, sigblob); + ssh2_userauth_add_sigblob( + s, s->pktout, + ptrlen_from_strbuf( + s->agent_keys[s->agent_key_index].blob), + sigblob); pq_push(s->ppl.out_pq, s->pktout); s->type = AUTH_TYPE_PUBLICKEY; } else { @@ -796,14 +825,8 @@ static void ssh2_userauth_process_queue(PacketProtocolLayer *ppl) } /* Do we have any keys left to try? */ - if (s->pkblob_pos_in_agent) { + if (++s->agent_key_index >= s->agent_key_limit) s->done_agent = true; - s->tried_pubkey_config = true; - } else { - s->keyi++; - if (s->keyi >= s->nkeys) - s->done_agent = true; - } } else if (s->can_pubkey && s->publickey_blob && s->privatekey_available && !s->tried_pubkey_config) { From 555aabebde10eb6df5487f6c0fb2dfea836a5589 Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Wed, 5 Feb 2020 21:14:26 +0000 Subject: [PATCH 04/11] Uppity: option to always send PK_OK / RSA1 challenge. This allows me to deliberately provoke the conditions for the stale-pointer bug in the agent key list parsing. --- ssh1login-server.c | 25 ++++++++++++++++++++++--- ssh2userauth-server.c | 30 ++++++++++++++++++------------ sshserver.h | 2 ++ unix/uxserver.c | 2 ++ 4 files changed, 44 insertions(+), 15 deletions(-) diff --git a/ssh1login-server.c b/ssh1login-server.c index 3316a6c3..cf71f5c5 100644 --- a/ssh1login-server.c +++ b/ssh1login-server.c @@ -292,18 +292,34 @@ static void ssh1_login_server_process_queue(PacketProtocolLayer *ppl) mp_int *modulus = get_mp_ssh1(pktin); s->authkey = auth_publickey_ssh1( s->authpolicy, s->username, modulus); + + if (!s->authkey && + s->ssc->stunt_pretend_to_accept_any_pubkey) { + mp_int *zero = mp_from_integer(0); + mp_int *fake_challenge = mp_random_in_range(zero, modulus); + + pktout = ssh_bpp_new_pktout( + s->ppl.bpp, SSH1_SMSG_AUTH_RSA_CHALLENGE); + put_mp_ssh1(pktout, fake_challenge); + pq_push(s->ppl.out_pq, pktout); + + mp_free(zero); + mp_free(fake_challenge); + } + mp_free(modulus); } - if (!s->authkey) + if (!s->authkey && + !s->ssc->stunt_pretend_to_accept_any_pubkey) continue; - if (s->authkey->bytes < 32) { + if (s->authkey && s->authkey->bytes < 32) { ppl_logevent("Auth key far too small"); continue; } - { + if (s->authkey) { unsigned char *rsabuf = snewn(s->authkey->bytes, unsigned char); @@ -343,6 +359,9 @@ static void ssh1_login_server_process_queue(PacketProtocolLayer *ppl) return; } + if (!s->authkey) + continue; + { ptrlen response = get_data(pktin, 16); ptrlen expected = make_ptrlen( diff --git a/ssh2userauth-server.c b/ssh2userauth-server.c index 7e67e557..3ec46ed0 100644 --- a/ssh2userauth-server.c +++ b/ssh2userauth-server.c @@ -203,7 +203,7 @@ static void ssh2_userauth_server_process_queue(PacketProtocolLayer *ppl) goto failure; } } else if (ptrlen_eq_string(s->method, "publickey")) { - bool has_signature, success; + bool has_signature, success, send_pk_ok, key_really_ok; ptrlen algorithm, blob, signature; const ssh_keyalg *keyalg; ssh_key *key; @@ -217,7 +217,23 @@ static void ssh2_userauth_server_process_queue(PacketProtocolLayer *ppl) algorithm = get_string(pktin); blob = get_string(pktin); - if (!auth_publickey(s->authpolicy, s->username, blob)) + key_really_ok = auth_publickey(s->authpolicy, s->username, blob); + send_pk_ok = key_really_ok || + s->ssc->stunt_pretend_to_accept_any_pubkey; + + if (!has_signature) { + if (!send_pk_ok) + goto failure; + + pktout = ssh_bpp_new_pktout( + s->ppl.bpp, SSH2_MSG_USERAUTH_PK_OK); + put_stringpl(pktout, algorithm); + put_stringpl(pktout, blob); + pq_push(s->ppl.out_pq, pktout); + continue; /* skip USERAUTH_{SUCCESS,FAILURE} epilogue */ + } + + if (!key_really_ok) goto failure; keyalg = find_pubkey_alg_len(algorithm); @@ -227,16 +243,6 @@ static void ssh2_userauth_server_process_queue(PacketProtocolLayer *ppl) if (!key) goto failure; - if (!has_signature) { - ssh_key_free(key); - pktout = ssh_bpp_new_pktout( - s->ppl.bpp, SSH2_MSG_USERAUTH_PK_OK); - put_stringpl(pktout, algorithm); - put_stringpl(pktout, blob); - pq_push(s->ppl.out_pq, pktout); - continue; /* skip USERAUTH_{SUCCESS,FAILURE} epilogue */ - } - sigdata = strbuf_new(); ssh2_userauth_server_add_session_id(s, sigdata); put_byte(sigdata, SSH2_MSG_USERAUTH_REQUEST); diff --git a/sshserver.h b/sshserver.h index 129d6fec..349f7010 100644 --- a/sshserver.h +++ b/sshserver.h @@ -16,6 +16,8 @@ struct SshServerConfig { unsigned long ssh1_cipher_mask; bool ssh1_allow_compression; + + bool stunt_pretend_to_accept_any_pubkey; }; Plug *ssh_server_plug( diff --git a/unix/uxserver.c b/unix/uxserver.c index b063e50e..ae29b980 100644 --- a/unix/uxserver.c +++ b/unix/uxserver.c @@ -782,6 +782,8 @@ int main(int argc, char **argv) filename_free(logfile); conf_set_int(conf, CONF_logtype, LGTYP_SSHRAW); conf_set_int(conf, CONF_logxfovr, LGXF_OVR); + } else if (!strcmp(arg, "--pretend-to-accept-any-pubkey")) { + ssc.stunt_pretend_to_accept_any_pubkey = true; } else { fprintf(stderr, "%s: unrecognised option '%s'\n", appname, arg); exit(1); From 08f1e2a5066ea95559945af339a60ca14560d764 Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Thu, 11 Jun 2020 15:57:18 +0100 Subject: [PATCH 05/11] Add an option to disable the dynamic host key policy. This mitigates CVE-2020-14002: if you're in the habit of clicking OK to unknown host keys (the TOFU policy - trust on first use), then an active attacker looking to exploit that policy to substitute their own host key in your first connection to a server can use the host key algorithm order in your KEXINIT to (not wholly reliably) detect whether you have a key already stored for this host, and if so, abort their attack to avoid giving themself away. However, for users who _don't_ use the TOFU policy and instead check new host keys out of band, the dynamic policy is more useful. So it's provided as a configurable option. --- config.c | 4 ++++ doc/config.but | 21 +++++++++++++++++++++ putty.h | 1 + settings.c | 2 ++ ssh2transport.c | 10 ++++++---- windows/winhelp.h | 1 + 6 files changed, 35 insertions(+), 4 deletions(-) diff --git a/config.c b/config.c index db83adb4..16808bcb 100644 --- a/config.c +++ b/config.c @@ -2486,6 +2486,10 @@ void setup_config_box(struct controlbox *b, bool midsession, HELPCTX(ssh_hklist), hklist_handler, P(NULL)); c->listbox.height = 5; + + ctrl_checkbox(s, "Prefer algorithms for which a host key is known", + 'p', HELPCTX(ssh_hk_known), conf_checkbox_handler, + I(CONF_ssh_prefer_known_hostkeys)); } /* diff --git a/doc/config.but b/doc/config.but index ee179668..8cc96d0a 100644 --- a/doc/config.but +++ b/doc/config.but @@ -2544,6 +2544,27 @@ If the first key type PuTTY finds is below the \q{warn below here} line, you will see a warning box when you make the connection, similar to that for cipher selection (see \k{config-ssh-encryption}). +\S{config-ssh-prefer-known-hostkeys} Preferring known host keys + +By default, PuTTY will adjust the preference order for host key +algorithms so that any host keys it already knows are moved to the top +of the list. + +This prevents you from having to check and confirm a new host key for +a server you already had one for (e.g. because the server has +generated an alternative key of a type higher in PuTTY's preference +order, or because you changed the preference order itself). + +However, on the other hand, it can leak information to a listener in +the network about \e{whether} you already know a host key for this +server. + +For this reason, this policy is configurable. By turning this checkbox +off, you can reset PuTTY to always use the exact order of host key +algorithms configured in the preference list described in +\k{config-ssh-hostkey-order}, so that a listener will find out nothing +about what keys you had stored. + \S{config-ssh-kex-manual-hostkeys} \ii{Manually configuring host keys} In some situations, if PuTTY's automated host key management is not diff --git a/putty.h b/putty.h index 33835582..12e9dd5e 100644 --- a/putty.h +++ b/putty.h @@ -1256,6 +1256,7 @@ NORETURN void cleanup_exit(int); X(BOOL, NONE, compression) \ X(INT, INT, ssh_kexlist) \ X(INT, INT, ssh_hklist) \ + X(BOOL, NONE, ssh_prefer_known_hostkeys) \ X(INT, NONE, ssh_rekey_time) /* in minutes */ \ X(STR, NONE, ssh_rekey_data) /* string encoding e.g. "100K", "2M", "1G" */ \ X(BOOL, NONE, tryagent) \ diff --git a/settings.c b/settings.c index 7bf7e4d2..11bb5798 100644 --- a/settings.c +++ b/settings.c @@ -598,6 +598,7 @@ void save_open_settings(settings_w *sesskey, Conf *conf) wprefs(sesskey, "Cipher", ciphernames, CIPHER_MAX, conf, CONF_ssh_cipherlist); wprefs(sesskey, "KEX", kexnames, KEX_MAX, conf, CONF_ssh_kexlist); wprefs(sesskey, "HostKey", hknames, HK_MAX, conf, CONF_ssh_hklist); + write_setting_b(sesskey, "PreferKnownHostKeys", conf_get_bool(conf, CONF_ssh_prefer_known_hostkeys)); write_setting_i(sesskey, "RekeyTime", conf_get_int(conf, CONF_ssh_rekey_time)); #ifndef NO_GSSAPI write_setting_i(sesskey, "GssapiRekey", conf_get_int(conf, CONF_gssapirekey)); @@ -994,6 +995,7 @@ void load_open_settings(settings_r *sesskey, Conf *conf) } gprefs(sesskey, "HostKey", "ed25519,ecdsa,rsa,dsa,WARN", hknames, HK_MAX, conf, CONF_ssh_hklist); + gppb(sesskey, "PreferKnownHostKeys", true, conf, CONF_ssh_prefer_known_hostkeys); gppi(sesskey, "RekeyTime", 60, conf, CONF_ssh_rekey_time); #ifndef NO_GSSAPI gppi(sesskey, "GssapiRekey", GSS_DEF_REKEY_MINS, conf, CONF_gssapirekey); diff --git a/ssh2transport.c b/ssh2transport.c index 88f1f6d4..7ca1d3d8 100644 --- a/ssh2transport.c +++ b/ssh2transport.c @@ -571,9 +571,10 @@ static void ssh2_write_kexinit_lists( } } else if (first_time) { /* - * In the first key exchange, we list all the algorithms - * we're prepared to cope with, but prefer those algorithms - * for which we have a host key for this host. + * In the first key exchange, we list all the algorithms we're + * prepared to cope with, but (if configured to) we prefer + * those algorithms for which we have a host key for this + * host. * * If the host key algorithm is below the warning * threshold, we warn even if we did already have a key @@ -589,7 +590,8 @@ static void ssh2_write_kexinit_lists( for (j = 0; j < lenof(ssh2_hostkey_algs); j++) { if (ssh2_hostkey_algs[j].id != preferred_hk[i]) continue; - if (have_ssh_host_key(hk_host, hk_port, + if (conf_get_bool(conf, CONF_ssh_prefer_known_hostkeys) && + have_ssh_host_key(hk_host, hk_port, ssh2_hostkey_algs[j].alg->cache_id)) { alg = ssh2_kexinit_addalg(kexlists[KEXLIST_HOSTKEY], ssh2_hostkey_algs[j].alg->ssh_id); diff --git a/windows/winhelp.h b/windows/winhelp.h index 823a2429..452f8f49 100644 --- a/windows/winhelp.h +++ b/windows/winhelp.h @@ -102,6 +102,7 @@ #define WINHELP_CTX_ssh_share "config-ssh-sharing" #define WINHELP_CTX_ssh_kexlist "config-ssh-kex-order" #define WINHELP_CTX_ssh_hklist "config-ssh-hostkey-order" +#define WINHELP_CTX_ssh_hk_known "config-ssh-prefer-known-hostkeys" #define WINHELP_CTX_ssh_gssapi_kex_delegation "config-ssh-kex-gssapi-delegation" #define WINHELP_CTX_ssh_kex_repeat "config-ssh-kex-rekey" #define WINHELP_CTX_ssh_kex_manual_hostkeys "config-ssh-kex-manual-hostkeys" From 44adc8be1bdd0ea78c157fe8a1f4af96faf09fea Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Tue, 16 Jun 2020 17:43:36 +0100 Subject: [PATCH 06/11] Fix assorted minor memory leaks. All found by Coverity. --- config.c | 4 +++- import.c | 4 +++- pageant.c | 3 +++ sesschan.c | 4 ++-- sshdss.c | 4 +++- unix/uxagentsock.c | 1 + unix/uxsftp.c | 1 + 7 files changed, 16 insertions(+), 5 deletions(-) diff --git a/config.c b/config.c index 16808bcb..4a7426cf 100644 --- a/config.c +++ b/config.c @@ -1429,7 +1429,7 @@ static void clipboard_selector_handler(union control *ctrl, dlgparam *dlg, #endif ) { #ifdef NAMED_CLIPBOARDS - const char *sval = dlg_editbox_get(ctrl, dlg); + char *sval = dlg_editbox_get(ctrl, dlg); int i; for (i = 0; i < lenof(options); i++) @@ -1444,6 +1444,8 @@ static void clipboard_selector_handler(union control *ctrl, dlgparam *dlg, sval++; conf_set_str(conf, strsetting, sval); } + + sfree(sval); #else int index = dlg_listbox_index(ctrl, dlg); if (index >= 0) { diff --git a/import.c b/import.c index effc65be..e6549f1b 100644 --- a/import.c +++ b/import.c @@ -538,8 +538,10 @@ static ssh2_userkey *openssh_pem_read( strbuf *blob = strbuf_new_nm(); int privptr = 0, publen; - if (!key) + if (!key) { + strbuf_free(blob); return NULL; + } if (key->encrypted) { unsigned char keybuf[32]; diff --git a/pageant.c b/pageant.c index 6ade4ccb..d758e02d 100644 --- a/pageant.c +++ b/pageant.c @@ -1290,6 +1290,9 @@ int pageant_add_keyfile(Filename *filename, const char *passphrase, if (resplen < 5 || response[4] != SSH_AGENT_SUCCESS) { *retstr = dupstr("The already running Pageant " "refused to add the key."); + sfree(skey->comment); + ssh_key_free(skey->key); + sfree(skey); sfree(response); return PAGEANT_ACTION_FAILURE; } diff --git a/sesschan.c b/sesschan.c index f9ea1418..a2bb5dfe 100644 --- a/sesschan.c +++ b/sesschan.c @@ -640,10 +640,10 @@ static void sesschan_notify_remote_exit(Seat *seat) sshfwd_send_exit_signal( sess->c, signame, false, ptrlen_from_asciz(sigmsg)); - sfree(sigmsg); - got_signal = true; } + + sfree(sigmsg); } else { int signum = pty_backend_exit_signum(sess->backend); diff --git a/sshdss.c b/sshdss.c index 2bf26d9a..90dc075a 100644 --- a/sshdss.c +++ b/sshdss.c @@ -72,8 +72,10 @@ static char *dss_cache_str(ssh_key *key) struct dss_key *dss = container_of(key, struct dss_key, sshk); strbuf *sb = strbuf_new(); - if (!dss->p) + if (!dss->p) { + strbuf_free(sb); return NULL; + } append_hex_to_strbuf(sb, dss->p); append_hex_to_strbuf(sb, dss->q); diff --git a/unix/uxagentsock.c b/unix/uxagentsock.c index 3fbbb84f..ba87cd1a 100644 --- a/unix/uxagentsock.c +++ b/unix/uxagentsock.c @@ -29,6 +29,7 @@ Socket *platform_make_agent_socket( if ((errw = make_dir_and_check_ours(socketdir)) != NULL) { *error = dupprintf("%s: %s\n", socketdir, errw); sfree(errw); + sfree(socketdir); return NULL; } diff --git a/unix/uxsftp.c b/unix/uxsftp.c index 3195f902..0f1a98b5 100644 --- a/unix/uxsftp.c +++ b/unix/uxsftp.c @@ -490,6 +490,7 @@ static int ssh_sftp_do_select(bool include_stdin, bool no_fds_ok) if (i < 1 && !no_fds_ok && !toplevel_callback_pending()) { pollwrap_free(pw); + sfree(fdlist); return -1; /* doom */ } From 4ea56076a8cb13946de8180a4d185ed928872387 Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Tue, 16 Jun 2020 17:43:55 +0100 Subject: [PATCH 07/11] Add missing cast in RTF paste data construction. udata[uindex] is a wchar_t, so if we pass it to sprintf("%d") we should cast it to int (because who knows what primitive integer type that might have corresponded to otherwise). I had done this in the first of the two sprintfs that use it, but missed the second one a few lines further on. Spotted by Coverity. --- windows/window.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows/window.c b/windows/window.c index f32c9665..ca8757df 100644 --- a/windows/window.c +++ b/windows/window.c @@ -5255,7 +5255,7 @@ static void wintw_clip_write( (int)udata[uindex]); alen = 1; strcpy(after, "}"); } else { - blen = sprintf(before, "\\u%d", udata[uindex]); + blen = sprintf(before, "\\u%d", (int)udata[uindex]); alen = 0; after[0] = '\0'; } } From caf4802b0a64622092f673bac11964229f262273 Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Tue, 16 Jun 2020 17:47:20 +0100 Subject: [PATCH 08/11] setup_utmp: add error check in case getpwuid fails. Spotted by Coverity: if there is no password file entry associated with our numeric uid, we'll press on anyway and dereference NULL. --- unix/uxpty.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/unix/uxpty.c b/unix/uxpty.c index 721074fc..d7b8e177 100644 --- a/unix/uxpty.c +++ b/unix/uxpty.c @@ -206,6 +206,8 @@ static void setup_utmp(char *ttyname, char *location) struct timeval tv; pw = getpwuid(getuid()); + if (!pw) + return; /* can't stamp utmp if we don't have a username */ memset(&utmp_entry, 0, sizeof(utmp_entry)); utmp_entry.ut_type = USER_PROCESS; utmp_entry.ut_pid = getpid(); From 41053e9dcd153744b61bd95b822aa0af81a14d6e Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Tue, 16 Jun 2020 17:48:18 +0100 Subject: [PATCH 09/11] GTK: fix control flow in do_cmdline(). In commit 4ecc3f3c09 I did a knee-jerk fix of a macro of the form #define SECOND_PASS_ONLY { body; } on the grounds that it was syntax-unsafe, so I wrapped it in the standard do while(0): #define SECOND_PASS_ONLY do { body; } while (0) But in this case, that was a bogus transformation, because the body executed 'continue' with the intention of affecting the containing loop (outside the macro). Moreover, ten lines above the macro definition was a comment specifically explaining why it _couldn't_ be wrapped in do while (0) ! Since then I've come up with an alternative break-and-continue-proof wrapper for macros that are supposed to expand to something that's syntactically a C statement. So I've used that instead, and while I'm at it, fixed the neighbouring EXPECTS_ARG as well. Spotted by Coverity, and well spotted indeed! How embarrassing. --- unix/gtkmain.c | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/unix/gtkmain.c b/unix/gtkmain.c index e7a0eff3..ec8f7da4 100644 --- a/unix/gtkmain.c +++ b/unix/gtkmain.c @@ -315,22 +315,25 @@ bool do_cmdline(int argc, char **argv, bool do_everything, Conf *conf) char *val; /* - * Macros to make argument handling easier. Note that because - * they need to call `continue', they cannot be contained in - * the usual do {...} while (0) wrapper to make them - * syntactically single statements; hence it is not legal to - * use one of these macros as an unbraced statement between - * `if' and `else'. + * Macros to make argument handling easier. + * + * Note that because they need to call `continue', they cannot be + * contained in the usual do {...} while (0) wrapper to make them + * syntactically single statements. I use the alternative if (1) + * {...} else ((void)0). */ -#define EXPECTS_ARG { \ - if (--argc <= 0) { \ - err = true; \ - fprintf(stderr, "%s: %s expects an argument\n", appname, p); \ - continue; \ - } else \ - val = *++argv; \ -} -#define SECOND_PASS_ONLY do { if (!do_everything) continue; } while (0) +#define EXPECTS_ARG if (1) { \ + if (--argc <= 0) { \ + err = true; \ + fprintf(stderr, "%s: %s expects an argument\n", appname, p); \ + continue; \ + } else \ + val = *++argv; \ + } else ((void)0) +#define SECOND_PASS_ONLY if (1) { \ + if (!do_everything) \ + continue; \ + } else ((void)0) while (--argc > 0) { const char *p = *++argv; From 2316b4442691e56a226e355273a5bbeb41e5ab8e Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Tue, 16 Jun 2020 18:02:39 +0100 Subject: [PATCH 10/11] Missing NULL check in swap_screen. Coverity points out that this function is mostly written as if it's intended to allow for term->screen and/or term->alt_screen to be NULL, but makes an unguarded call to find_last_nonempty_line on one of them. I don't immediately remember _why_ I needed to deal with those pointers being null, but it was probably a safety precaution against swap_screen being called during setup or during reconfiguration, in which case it seems sensible to keep it even if it's not needed in the _current_ state of the code. So, added the missing check. --- terminal.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/terminal.c b/terminal.c index 8f65a73e..08556d02 100644 --- a/terminal.c +++ b/terminal.c @@ -2066,7 +2066,9 @@ static void swap_screen(Terminal *term, int which, ttr = term->alt_screen; term->alt_screen = term->screen; term->screen = ttr; - term->alt_sblines = find_last_nonempty_line(term, term->alt_screen) + 1; + term->alt_sblines = ( + term->alt_screen ? + find_last_nonempty_line(term, term->alt_screen) + 1 : 0); t = term->curs.x; if (!reset && !keep_cur_pos) term->curs.x = term->alt_x; From 014d4fb151369f255b3debed7d15a154fd9036f5 Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Sat, 20 Jun 2020 15:02:57 +0100 Subject: [PATCH 11/11] Update version number for 0.74 release. --- Buildscr | 2 +- LATEST.VER | 2 +- doc/plink.but | 2 +- doc/pscp.but | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Buildscr b/Buildscr index a98f5f3f..776fb53c 100644 --- a/Buildscr +++ b/Buildscr @@ -35,7 +35,7 @@ module putty ifeq "$(RELEASE)" "" set Ndate $(!builddate) ifneq "$(Ndate)" "" in . do echo $(Ndate) | perl -pe 's/(....)(..)(..)/$$1-$$2-$$3/' > date ifneq "$(Ndate)" "" read Date date -set Epoch 17161 # update this at every release +set Epoch 17433 # update this at every release ifneq "$(Ndate)" "" in . do echo $(Ndate) | perl -ne 'use Time::Local; /(....)(..)(..)/ and print timegm(0,0,0,$$3,$$2-1,$$1) / 86400 - $(Epoch)' > days ifneq "$(Ndate)" "" read Days days diff --git a/LATEST.VER b/LATEST.VER index 6ab5ccfd..3ea25f59 100644 --- a/LATEST.VER +++ b/LATEST.VER @@ -1 +1 @@ -0.73 +0.74 diff --git a/doc/plink.but b/doc/plink.but index 72449293..ea622151 100644 --- a/doc/plink.but +++ b/doc/plink.but @@ -41,7 +41,7 @@ use Plink: \c C:\>plink \c Plink: command-line connection utility -\c Release 0.73 +\c Release 0.74 \c Usage: plink [options] [user@]host [command] \c ("host" can also be a PuTTY saved session name) \c Options: diff --git a/doc/pscp.but b/doc/pscp.but index e6b8eed3..c8585764 100644 --- a/doc/pscp.but +++ b/doc/pscp.but @@ -39,7 +39,7 @@ use PSCP: \c C:\>pscp \c PuTTY Secure Copy client -\c Release 0.73 +\c Release 0.74 \c Usage: pscp [options] [user@]host:source target \c pscp [options] source [source...] [user@]host:target \c pscp [options] -ls [user@]host:filespec