mirror of
https://git.tartarus.org/simon/putty.git
synced 2025-03-13 10:33:51 -05:00

I've tried to separate out as many individually coherent changes from this work as I could into their own commits, but here's where I run out and have to commit the rest of this major refactoring as a big-bang change. Most of ssh.c is now no longer in ssh.c: all five of the main coroutines that handle layers of the SSH-1 and SSH-2 protocols now each have their own source file to live in, and a lot of the supporting functions have moved into the appropriate one of those too. The new abstraction is a vtable called 'PacketProtocolLayer', which has an input and output packet queue. Each layer's main coroutine is invoked from the method ssh_ppl_process_queue(), which is usually (though not exclusively) triggered automatically when things are pushed on the input queue. In SSH-2, the base layer is the transport protocol, and it contains a pair of subsidiary queues by which it passes some of its packets to the higher SSH-2 layers - first userauth and then connection, which are peers at the same level, with the former abdicating in favour of the latter at the appropriate moment. SSH-1 is simpler: the whole login phase of the protocol (crypto setup and authentication) is all in one module, and since SSH-1 has no repeat key exchange, that setup layer abdicates in favour of the connection phase when it's done. ssh.c itself is now about a tenth of its old size (which all by itself is cause for celebration!). Its main job is to set up all the layers, hook them up to each other and to the BPP, and to funnel data back and forth between that collection of modules and external things such as the network and the terminal. Once it's set up a collection of packet protocol layers, it communicates with them partly by calling methods of the base layer (and if that's ssh2transport then it will delegate some functionality to the corresponding methods of its higher layer), and partly by talking directly to the connection layer no matter where it is in the stack by means of the separate ConnectionLayer vtable which I introduced in commit 8001dd4cb, and to which I've now added quite a few extra methods replacing services that used to be internal function calls within ssh.c. (One effect of this is that the SSH-1 and SSH-2 channel storage is now no longer shared - there are distinct struct types ssh1_channel and ssh2_channel. That means a bit more code duplication, but on the plus side, a lot fewer confusing conditionals in the middle of half-shared functions, and less risk of a piece of SSH-1 escaping into SSH-2 or vice versa, which I remember has happened at least once in the past.) The bulk of this commit introduces the five new source files, their common header sshppl.h and some shared supporting routines in sshcommon.c, and rewrites nearly all of ssh.c itself. But it also includes a couple of other changes that I couldn't separate easily enough: Firstly, there's a new handling for socket EOF, in which ssh.c sets an 'input_eof' flag in the BPP, and that responds by checking a flag that tells it whether to report the EOF as an error or not. (This is the main reason for those new BPP_READ / BPP_WAITFOR macros - they can check the EOF flag every time the coroutine is resumed.) Secondly, the error reporting itself is changed around again. I'd expected to put some data fields in the public PacketProtocolLayer structure that it could set to report errors in the same way as the BPPs have been doing, but in the end, I decided propagating all those data fields around was a pain and that even the BPPs shouldn't have been doing it that way. So I've reverted to a system where everything calls back to functions in ssh.c itself to report any connection- ending condition. But there's a new family of those functions, categorising the possible such conditions by semantics, and each one has a different set of detailed effects (e.g. how rudely to close the network connection, what exit status should be passed back to the whole application, whether to send a disconnect message and/or display a GUI error box). I don't expect this to be immediately perfect: of course, the code has been through a big upheaval, new bugs are expected, and I haven't been able to do a full job of testing (e.g. I haven't tested every auth or kex method). But I've checked that it _basically_ works - both SSH protocols, all the different kinds of forwarding channel, more than one auth method, Windows and Linux, connection sharing - and I think it's now at the point where the easiest way to find further bugs is to let it out into the wild and see what users can spot.
354 lines
11 KiB
C
354 lines
11 KiB
C
/*
|
|
* Binary packet protocol for SSH-1.
|
|
*/
|
|
|
|
#include <assert.h>
|
|
|
|
#include "putty.h"
|
|
#include "ssh.h"
|
|
#include "sshbpp.h"
|
|
#include "sshcr.h"
|
|
|
|
struct ssh1_bpp_state {
|
|
int crState;
|
|
long len, pad, biglen, length, maxlen;
|
|
unsigned char *data;
|
|
unsigned long realcrc, gotcrc;
|
|
int chunk;
|
|
PktIn *pktin;
|
|
|
|
ssh1_cipher *cipher;
|
|
|
|
struct crcda_ctx *crcda_ctx;
|
|
|
|
int pending_compression_request;
|
|
ssh_compressor *compctx;
|
|
ssh_decompressor *decompctx;
|
|
|
|
BinaryPacketProtocol bpp;
|
|
};
|
|
|
|
static void ssh1_bpp_free(BinaryPacketProtocol *bpp);
|
|
static void ssh1_bpp_handle_input(BinaryPacketProtocol *bpp);
|
|
static void ssh1_bpp_handle_output(BinaryPacketProtocol *bpp);
|
|
static void ssh1_bpp_queue_disconnect(BinaryPacketProtocol *bpp,
|
|
const char *msg, int category);
|
|
static PktOut *ssh1_bpp_new_pktout(int type);
|
|
|
|
static const struct BinaryPacketProtocolVtable ssh1_bpp_vtable = {
|
|
ssh1_bpp_free,
|
|
ssh1_bpp_handle_input,
|
|
ssh1_bpp_handle_output,
|
|
ssh1_bpp_new_pktout,
|
|
ssh1_bpp_queue_disconnect,
|
|
};
|
|
|
|
BinaryPacketProtocol *ssh1_bpp_new(void)
|
|
{
|
|
struct ssh1_bpp_state *s = snew(struct ssh1_bpp_state);
|
|
memset(s, 0, sizeof(*s));
|
|
s->bpp.vt = &ssh1_bpp_vtable;
|
|
ssh_bpp_common_setup(&s->bpp);
|
|
return &s->bpp;
|
|
}
|
|
|
|
static void ssh1_bpp_free(BinaryPacketProtocol *bpp)
|
|
{
|
|
struct ssh1_bpp_state *s = FROMFIELD(bpp, struct ssh1_bpp_state, bpp);
|
|
if (s->cipher)
|
|
ssh1_cipher_free(s->cipher);
|
|
if (s->compctx)
|
|
ssh_compressor_free(s->compctx);
|
|
if (s->decompctx)
|
|
ssh_decompressor_free(s->decompctx);
|
|
if (s->crcda_ctx)
|
|
crcda_free_context(s->crcda_ctx);
|
|
sfree(s->pktin);
|
|
sfree(s);
|
|
}
|
|
|
|
void ssh1_bpp_new_cipher(BinaryPacketProtocol *bpp,
|
|
const struct ssh1_cipheralg *cipher,
|
|
const void *session_key)
|
|
{
|
|
struct ssh1_bpp_state *s;
|
|
assert(bpp->vt == &ssh1_bpp_vtable);
|
|
s = FROMFIELD(bpp, struct ssh1_bpp_state, bpp);
|
|
|
|
assert(!s->cipher);
|
|
|
|
if (cipher) {
|
|
s->cipher = ssh1_cipher_new(cipher);
|
|
ssh1_cipher_sesskey(s->cipher, session_key);
|
|
|
|
assert(!s->crcda_ctx);
|
|
s->crcda_ctx = crcda_make_context();
|
|
}
|
|
}
|
|
|
|
#define BPP_READ(ptr, len) do \
|
|
{ \
|
|
crMaybeWaitUntilV(s->bpp.input_eof || \
|
|
bufchain_try_fetch_consume( \
|
|
s->bpp.in_raw, ptr, len)); \
|
|
if (s->bpp.input_eof) \
|
|
goto eof; \
|
|
} while (0)
|
|
|
|
static void ssh1_bpp_handle_input(BinaryPacketProtocol *bpp)
|
|
{
|
|
struct ssh1_bpp_state *s = FROMFIELD(bpp, struct ssh1_bpp_state, bpp);
|
|
|
|
crBegin(s->crState);
|
|
|
|
while (1) {
|
|
s->maxlen = 0;
|
|
s->length = 0;
|
|
|
|
{
|
|
unsigned char lenbuf[4];
|
|
BPP_READ(lenbuf, 4);
|
|
s->len = toint(GET_32BIT_MSB_FIRST(lenbuf));
|
|
}
|
|
|
|
if (s->len < 0 || s->len > 262144) { /* SSH1.5-mandated max size */
|
|
ssh_sw_abort(s->bpp.ssh,
|
|
"Extremely large packet length from server suggests"
|
|
" data stream corruption");
|
|
crStopV;
|
|
}
|
|
|
|
s->pad = 8 - (s->len % 8);
|
|
s->biglen = s->len + s->pad;
|
|
s->length = s->len - 5;
|
|
|
|
/*
|
|
* Allocate the packet to return, now we know its length.
|
|
*/
|
|
s->pktin = snew_plus(PktIn, s->biglen);
|
|
s->pktin->qnode.prev = s->pktin->qnode.next = NULL;
|
|
s->pktin->qnode.on_free_queue = FALSE;
|
|
s->pktin->type = 0;
|
|
|
|
s->maxlen = s->biglen;
|
|
s->data = snew_plus_get_aux(s->pktin);
|
|
|
|
BPP_READ(s->data, s->biglen);
|
|
|
|
if (s->cipher && detect_attack(s->crcda_ctx,
|
|
s->data, s->biglen, NULL)) {
|
|
ssh_sw_abort(s->bpp.ssh,
|
|
"Network attack (CRC compensation) detected!");
|
|
crStopV;
|
|
}
|
|
|
|
if (s->cipher)
|
|
ssh1_cipher_decrypt(s->cipher, s->data, s->biglen);
|
|
|
|
s->realcrc = crc32_compute(s->data, s->biglen - 4);
|
|
s->gotcrc = GET_32BIT(s->data + s->biglen - 4);
|
|
if (s->gotcrc != s->realcrc) {
|
|
ssh_sw_abort(s->bpp.ssh, "Incorrect CRC received on packet");
|
|
crStopV;
|
|
}
|
|
|
|
if (s->decompctx) {
|
|
unsigned char *decompblk;
|
|
int decomplen;
|
|
if (!ssh_decompressor_decompress(
|
|
s->decompctx, s->data + s->pad, s->length + 1,
|
|
&decompblk, &decomplen)) {
|
|
ssh_sw_abort(s->bpp.ssh,
|
|
"Zlib decompression encountered invalid data");
|
|
crStopV;
|
|
}
|
|
|
|
if (s->maxlen < s->pad + decomplen) {
|
|
PktIn *old_pktin = s->pktin;
|
|
|
|
s->maxlen = s->pad + decomplen;
|
|
s->pktin = snew_plus(PktIn, s->maxlen);
|
|
*s->pktin = *old_pktin; /* structure copy */
|
|
s->data = snew_plus_get_aux(s->pktin);
|
|
|
|
smemclr(old_pktin, s->biglen);
|
|
sfree(old_pktin);
|
|
}
|
|
|
|
memcpy(s->data + s->pad, decompblk, decomplen);
|
|
sfree(decompblk);
|
|
s->length = decomplen - 1;
|
|
}
|
|
|
|
/*
|
|
* Now we can find the bounds of the semantic content of the
|
|
* packet, and the initial type byte.
|
|
*/
|
|
s->data += s->pad;
|
|
s->pktin->type = *s->data++;
|
|
BinarySource_INIT(s->pktin, s->data, s->length);
|
|
|
|
if (s->bpp.logctx) {
|
|
logblank_t blanks[MAX_BLANKS];
|
|
int nblanks = ssh1_censor_packet(
|
|
s->bpp.pls, s->pktin->type, FALSE,
|
|
make_ptrlen(s->data, s->length), blanks);
|
|
log_packet(s->bpp.logctx, PKT_INCOMING, s->pktin->type,
|
|
ssh1_pkt_type(s->pktin->type),
|
|
get_ptr(s->pktin), get_avail(s->pktin), nblanks, blanks,
|
|
NULL, 0, NULL);
|
|
}
|
|
|
|
pq_push(&s->bpp.in_pq, s->pktin);
|
|
|
|
{
|
|
int type = s->pktin->type;
|
|
s->pktin = NULL;
|
|
|
|
switch (type) {
|
|
case SSH1_SMSG_SUCCESS:
|
|
case SSH1_SMSG_FAILURE:
|
|
if (s->pending_compression_request) {
|
|
/*
|
|
* This is the response to
|
|
* SSH1_CMSG_REQUEST_COMPRESSION.
|
|
*/
|
|
if (type == SSH1_SMSG_SUCCESS) {
|
|
/*
|
|
* If the response was positive, start
|
|
* compression.
|
|
*/
|
|
assert(!s->compctx);
|
|
assert(!s->decompctx);
|
|
|
|
s->compctx = ssh_compressor_new(&ssh_zlib);
|
|
s->decompctx = ssh_decompressor_new(&ssh_zlib);
|
|
}
|
|
|
|
/*
|
|
* Either way, cancel the pending flag, and
|
|
* schedule a run of our output side in case we
|
|
* had any packets queued up in the meantime.
|
|
*/
|
|
s->pending_compression_request = FALSE;
|
|
queue_idempotent_callback(&s->bpp.ic_out_pq);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
eof:
|
|
if (!s->bpp.expect_close) {
|
|
ssh_remote_error(s->bpp.ssh,
|
|
"Server unexpectedly closed network connection");
|
|
} else {
|
|
ssh_remote_eof(s->bpp.ssh, "Server closed network connection");
|
|
}
|
|
|
|
crFinishV;
|
|
}
|
|
|
|
static PktOut *ssh1_bpp_new_pktout(int pkt_type)
|
|
{
|
|
PktOut *pkt = ssh_new_packet();
|
|
pkt->length = 4 + 8; /* space for length + max padding */
|
|
put_byte(pkt, pkt_type);
|
|
pkt->prefix = pkt->length;
|
|
pkt->type = pkt_type;
|
|
pkt->downstream_id = 0;
|
|
pkt->additional_log_text = NULL;
|
|
return pkt;
|
|
}
|
|
|
|
static void ssh1_bpp_format_packet(struct ssh1_bpp_state *s, PktOut *pkt)
|
|
{
|
|
int pad, biglen, i, pktoffs;
|
|
unsigned long crc;
|
|
int len;
|
|
|
|
if (s->bpp.logctx) {
|
|
ptrlen pktdata = make_ptrlen(pkt->data + pkt->prefix,
|
|
pkt->length - pkt->prefix);
|
|
logblank_t blanks[MAX_BLANKS];
|
|
int nblanks = ssh1_censor_packet(
|
|
s->bpp.pls, pkt->type, TRUE, pktdata, blanks);
|
|
log_packet(s->bpp.logctx, PKT_OUTGOING, pkt->type,
|
|
ssh1_pkt_type(pkt->type),
|
|
pktdata.ptr, pktdata.len, nblanks, blanks,
|
|
NULL, 0, NULL);
|
|
}
|
|
|
|
if (s->compctx) {
|
|
unsigned char *compblk;
|
|
int complen;
|
|
ssh_compressor_compress(s->compctx, pkt->data + 12, pkt->length - 12,
|
|
&compblk, &complen, 0);
|
|
/* Replace the uncompressed packet data with the compressed
|
|
* version. */
|
|
pkt->length = 12;
|
|
put_data(pkt, compblk, complen);
|
|
sfree(compblk);
|
|
}
|
|
|
|
put_uint32(pkt, 0); /* space for CRC */
|
|
len = pkt->length - 4 - 8; /* len(type+data+CRC) */
|
|
pad = 8 - (len % 8);
|
|
pktoffs = 8 - pad;
|
|
biglen = len + pad; /* len(padding+type+data+CRC) */
|
|
|
|
for (i = pktoffs; i < 4+8; i++)
|
|
pkt->data[i] = random_byte();
|
|
crc = crc32_compute(pkt->data + pktoffs + 4,
|
|
biglen - 4); /* all ex len */
|
|
PUT_32BIT(pkt->data + pktoffs + 4 + biglen - 4, crc);
|
|
PUT_32BIT(pkt->data + pktoffs, len);
|
|
|
|
if (s->cipher)
|
|
ssh1_cipher_encrypt(s->cipher, pkt->data + pktoffs + 4, biglen);
|
|
|
|
bufchain_add(s->bpp.out_raw, pkt->data + pktoffs,
|
|
biglen + 4); /* len(length+padding+type+data+CRC) */
|
|
}
|
|
|
|
static void ssh1_bpp_handle_output(BinaryPacketProtocol *bpp)
|
|
{
|
|
struct ssh1_bpp_state *s = FROMFIELD(bpp, struct ssh1_bpp_state, bpp);
|
|
PktOut *pkt;
|
|
|
|
if (s->pending_compression_request) {
|
|
/*
|
|
* Don't send any output packets while we're awaiting a
|
|
* response to SSH1_CMSG_REQUEST_COMPRESSION, because if they
|
|
* cross over in transit with the responding SSH1_CMSG_SUCCESS
|
|
* then the other end could decode them with the wrong
|
|
* compression settings.
|
|
*/
|
|
return;
|
|
}
|
|
|
|
while ((pkt = pq_pop(&s->bpp.out_pq)) != NULL) {
|
|
int type = pkt->type;
|
|
ssh1_bpp_format_packet(s, pkt);
|
|
ssh_free_pktout(pkt);
|
|
|
|
if (type == SSH1_CMSG_REQUEST_COMPRESSION) {
|
|
/*
|
|
* When we see the actual compression request go past, set
|
|
* the pending flag, and stop processing packets this
|
|
* time.
|
|
*/
|
|
s->pending_compression_request = TRUE;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void ssh1_bpp_queue_disconnect(BinaryPacketProtocol *bpp,
|
|
const char *msg, int category)
|
|
{
|
|
PktOut *pkt = ssh_bpp_new_pktout(bpp, SSH1_MSG_DISCONNECT);
|
|
put_stringz(pkt, msg);
|
|
pq_push(&bpp->out_pq, pkt);
|
|
}
|