mirror of
https://git.tartarus.org/simon/putty.git
synced 2025-01-09 17:38:00 +00:00
1d323d5c80
This server is NOT SECURE! If anyone is reading this commit message, DO NOT DEPLOY IT IN A HOSTILE-FACING ENVIRONMENT! Its purpose is to speak the server end of everything PuTTY speaks on the client side, so that I can test that I haven't broken PuTTY when I reorganise its code, even things like RSA key exchange or chained auth methods which it's hard to find a server that speaks at all. (For this reason, it's declared with [UT] in the Recipe file, so that it falls into the same category as programs like testbn, which won't be installed by 'make install'.) Working title is 'Uppity', partly for 'Universal PuTTY Protocol Interaction Test Yoke', but mostly because it looks quite like the word 'PuTTY' with part of it reversed. (Apparently 'test yoke' is a very rarely used term meaning something not altogether unlike 'test harness', which is a bit of a stretch, but it'll do.) It doesn't actually _support_ everything I want yet. At the moment, it's a proof of concept only. But it has most of the machinery present, and the parts it's missing - such as chained auth methods - should be easy enough to add because I've built in the required flexibility, in the form of an AuthPolicy object which can request them if it wants to. However, the current AuthPolicy object is entirely trivial, and will let in any user with the password "weasel". (Another way in which this is not a production-ready server is that it also has no interaction with the OS's authentication system. In particular, it will not only let in any user with the same password, but it won't even change uid - it will open shells and forwardings under whatever user id you started it up as.) Currently, the program can only speak the SSH protocol on its standard I/O channels (using the new FdSocket facility), so if you want it to listen on a network port, you'll have to run it from some kind of separate listening program similar to inetd. For my own tests, I'm not even doing that: I'm just having PuTTY spawn it as a local proxy process, which also conveniently eliminates the risk of anyone hostile connecting to it. The bulk of the actual code reorganisation is already done by previous commits, so this change is _mostly_ just dropping in a new set of server-specific source files alongside the client-specific ones I created recently. The remaining changes in the shared SSH code are numerous, but all minor: - a few extra parameters to BPP and PPL constructors (e.g. 'are you in server mode?'), and pass both sets of SSH-1 protocol flags from the login to the connection layer - in server mode, unconditionally send our version string _before_ waiting for the remote one - a new hook in the SSH-1 BPP to handle enabling compression in server mode, where the message exchange works the other way round - new code in the SSH-2 BPP to do _deferred_ compression the other way round (the non-deferred version is still nicely symmetric) - in the SSH-2 transport layer, some adjustments to do key derivation either way round (swapping round the identifying letters in the various hash preimages, and making sure to list the KEXINITs in the right order) - also in the SSH-2 transport layer, an if statement that controls whether we send SERVICE_REQUEST and wait for SERVICE_ACCEPT, or vice versa - new ConnectionLayer methods for opening outgoing channels for X and agent forwardings - new functions in portfwd.c to establish listening sockets suitable for remote-to-local port forwarding (i.e. not under the direction of a Conf the way it's done on the client side).
904 lines
32 KiB
C
904 lines
32 KiB
C
/*
|
|
* Binary packet protocol for SSH-2.
|
|
*/
|
|
|
|
#include <assert.h>
|
|
|
|
#include "putty.h"
|
|
#include "ssh.h"
|
|
#include "sshbpp.h"
|
|
#include "sshcr.h"
|
|
|
|
struct ssh2_bpp_direction {
|
|
unsigned long sequence;
|
|
ssh2_cipher *cipher;
|
|
ssh2_mac *mac;
|
|
int etm_mode;
|
|
const struct ssh_compression_alg *pending_compression;
|
|
};
|
|
|
|
struct ssh2_bpp_state {
|
|
int crState;
|
|
long len, pad, payload, packetlen, maclen, length, maxlen;
|
|
unsigned char *buf;
|
|
size_t bufsize;
|
|
unsigned char *data;
|
|
unsigned cipherblk;
|
|
PktIn *pktin;
|
|
struct DataTransferStats *stats;
|
|
int cbc_ignore_workaround;
|
|
|
|
struct ssh2_bpp_direction in, out;
|
|
/* comp and decomp logically belong in the per-direction
|
|
* substructure, except that they have different types */
|
|
ssh_decompressor *in_decomp;
|
|
ssh_compressor *out_comp;
|
|
|
|
int is_server;
|
|
int pending_newkeys;
|
|
int pending_compression, seen_userauth_success;
|
|
|
|
BinaryPacketProtocol bpp;
|
|
};
|
|
|
|
static void ssh2_bpp_free(BinaryPacketProtocol *bpp);
|
|
static void ssh2_bpp_handle_input(BinaryPacketProtocol *bpp);
|
|
static void ssh2_bpp_handle_output(BinaryPacketProtocol *bpp);
|
|
static PktOut *ssh2_bpp_new_pktout(int type);
|
|
|
|
static const struct BinaryPacketProtocolVtable ssh2_bpp_vtable = {
|
|
ssh2_bpp_free,
|
|
ssh2_bpp_handle_input,
|
|
ssh2_bpp_handle_output,
|
|
ssh2_bpp_new_pktout,
|
|
ssh2_bpp_queue_disconnect, /* in sshcommon.c */
|
|
};
|
|
|
|
BinaryPacketProtocol *ssh2_bpp_new(
|
|
LogContext *logctx, struct DataTransferStats *stats, int is_server)
|
|
{
|
|
struct ssh2_bpp_state *s = snew(struct ssh2_bpp_state);
|
|
memset(s, 0, sizeof(*s));
|
|
s->bpp.vt = &ssh2_bpp_vtable;
|
|
s->bpp.logctx = logctx;
|
|
s->stats = stats;
|
|
s->is_server = is_server;
|
|
ssh_bpp_common_setup(&s->bpp);
|
|
return &s->bpp;
|
|
}
|
|
|
|
static void ssh2_bpp_free(BinaryPacketProtocol *bpp)
|
|
{
|
|
struct ssh2_bpp_state *s = container_of(bpp, struct ssh2_bpp_state, bpp);
|
|
sfree(s->buf);
|
|
if (s->out.cipher)
|
|
ssh2_cipher_free(s->out.cipher);
|
|
if (s->out.mac)
|
|
ssh2_mac_free(s->out.mac);
|
|
if (s->out_comp)
|
|
ssh_compressor_free(s->out_comp);
|
|
if (s->in.cipher)
|
|
ssh2_cipher_free(s->in.cipher);
|
|
if (s->in.mac)
|
|
ssh2_mac_free(s->in.mac);
|
|
if (s->in_decomp)
|
|
ssh_decompressor_free(s->in_decomp);
|
|
sfree(s->pktin);
|
|
sfree(s);
|
|
}
|
|
|
|
void ssh2_bpp_new_outgoing_crypto(
|
|
BinaryPacketProtocol *bpp,
|
|
const struct ssh2_cipheralg *cipher, const void *ckey, const void *iv,
|
|
const struct ssh2_macalg *mac, int etm_mode, const void *mac_key,
|
|
const struct ssh_compression_alg *compression, int delayed_compression)
|
|
{
|
|
struct ssh2_bpp_state *s;
|
|
assert(bpp->vt == &ssh2_bpp_vtable);
|
|
s = container_of(bpp, struct ssh2_bpp_state, bpp);
|
|
|
|
if (s->out.cipher)
|
|
ssh2_cipher_free(s->out.cipher);
|
|
if (s->out.mac)
|
|
ssh2_mac_free(s->out.mac);
|
|
if (s->out_comp)
|
|
ssh_compressor_free(s->out_comp);
|
|
|
|
if (cipher) {
|
|
s->out.cipher = ssh2_cipher_new(cipher);
|
|
ssh2_cipher_setkey(s->out.cipher, ckey);
|
|
ssh2_cipher_setiv(s->out.cipher, iv);
|
|
|
|
s->cbc_ignore_workaround = (
|
|
(ssh2_cipher_alg(s->out.cipher)->flags & SSH_CIPHER_IS_CBC) &&
|
|
!(s->bpp.remote_bugs & BUG_CHOKES_ON_SSH2_IGNORE));
|
|
|
|
bpp_logevent(("Initialised %.200s outbound encryption",
|
|
ssh2_cipher_alg(s->out.cipher)->text_name));
|
|
} else {
|
|
s->out.cipher = NULL;
|
|
s->cbc_ignore_workaround = FALSE;
|
|
}
|
|
s->out.etm_mode = etm_mode;
|
|
if (mac) {
|
|
s->out.mac = ssh2_mac_new(mac, s->out.cipher);
|
|
mac->setkey(s->out.mac, mac_key);
|
|
|
|
bpp_logevent(("Initialised %.200s outbound MAC algorithm%s%s",
|
|
ssh2_mac_alg(s->out.mac)->text_name,
|
|
etm_mode ? " (in ETM mode)" : "",
|
|
(s->out.cipher &&
|
|
ssh2_cipher_alg(s->out.cipher)->required_mac ?
|
|
" (required by cipher)" : "")));
|
|
} else {
|
|
s->out.mac = NULL;
|
|
}
|
|
|
|
if (delayed_compression && !s->seen_userauth_success) {
|
|
s->out.pending_compression = compression;
|
|
s->out_comp = NULL;
|
|
|
|
bpp_logevent(("Will enable %s compression after user authentication",
|
|
s->out.pending_compression->text_name));
|
|
} else {
|
|
s->out.pending_compression = NULL;
|
|
|
|
/* 'compression' is always non-NULL, because no compression is
|
|
* indicated by ssh_comp_none. But this setup call may return a
|
|
* null out_comp. */
|
|
s->out_comp = ssh_compressor_new(compression);
|
|
|
|
if (s->out_comp)
|
|
bpp_logevent(("Initialised %s compression",
|
|
ssh_compressor_alg(s->out_comp)->text_name));
|
|
}
|
|
}
|
|
|
|
void ssh2_bpp_new_incoming_crypto(
|
|
BinaryPacketProtocol *bpp,
|
|
const struct ssh2_cipheralg *cipher, const void *ckey, const void *iv,
|
|
const struct ssh2_macalg *mac, int etm_mode, const void *mac_key,
|
|
const struct ssh_compression_alg *compression, int delayed_compression)
|
|
{
|
|
struct ssh2_bpp_state *s;
|
|
assert(bpp->vt == &ssh2_bpp_vtable);
|
|
s = container_of(bpp, struct ssh2_bpp_state, bpp);
|
|
|
|
if (s->in.cipher)
|
|
ssh2_cipher_free(s->in.cipher);
|
|
if (s->in.mac)
|
|
ssh2_mac_free(s->in.mac);
|
|
if (s->in_decomp)
|
|
ssh_decompressor_free(s->in_decomp);
|
|
|
|
if (cipher) {
|
|
s->in.cipher = ssh2_cipher_new(cipher);
|
|
ssh2_cipher_setkey(s->in.cipher, ckey);
|
|
ssh2_cipher_setiv(s->in.cipher, iv);
|
|
|
|
bpp_logevent(("Initialised %.200s inbound encryption",
|
|
ssh2_cipher_alg(s->in.cipher)->text_name));
|
|
} else {
|
|
s->in.cipher = NULL;
|
|
}
|
|
s->in.etm_mode = etm_mode;
|
|
if (mac) {
|
|
s->in.mac = ssh2_mac_new(mac, s->in.cipher);
|
|
mac->setkey(s->in.mac, mac_key);
|
|
|
|
bpp_logevent(("Initialised %.200s inbound MAC algorithm%s%s",
|
|
ssh2_mac_alg(s->in.mac)->text_name,
|
|
etm_mode ? " (in ETM mode)" : "",
|
|
(s->in.cipher &&
|
|
ssh2_cipher_alg(s->in.cipher)->required_mac ?
|
|
" (required by cipher)" : "")));
|
|
} else {
|
|
s->in.mac = NULL;
|
|
}
|
|
|
|
if (delayed_compression && !s->seen_userauth_success) {
|
|
s->in.pending_compression = compression;
|
|
s->in_decomp = NULL;
|
|
|
|
bpp_logevent(("Will enable %s decompression after user authentication",
|
|
s->in.pending_compression->text_name));
|
|
} else {
|
|
s->in.pending_compression = NULL;
|
|
|
|
/* 'compression' is always non-NULL, because no compression is
|
|
* indicated by ssh_comp_none. But this setup call may return a
|
|
* null in_decomp. */
|
|
s->in_decomp = ssh_decompressor_new(compression);
|
|
|
|
if (s->in_decomp)
|
|
bpp_logevent(("Initialised %s decompression",
|
|
ssh_decompressor_alg(s->in_decomp)->text_name));
|
|
}
|
|
|
|
/* Clear the pending_newkeys flag, so that handle_input below will
|
|
* start consuming the input data again. */
|
|
s->pending_newkeys = FALSE;
|
|
|
|
/* And schedule a run of handle_input, in case there's already
|
|
* input data in the queue. */
|
|
queue_idempotent_callback(&s->bpp.ic_in_raw);
|
|
}
|
|
|
|
int ssh2_bpp_rekey_inadvisable(BinaryPacketProtocol *bpp)
|
|
{
|
|
struct ssh2_bpp_state *s;
|
|
assert(bpp->vt == &ssh2_bpp_vtable);
|
|
s = container_of(bpp, struct ssh2_bpp_state, bpp);
|
|
|
|
return s->pending_compression;
|
|
}
|
|
|
|
static void ssh2_bpp_enable_pending_compression(struct ssh2_bpp_state *s)
|
|
{
|
|
BinaryPacketProtocol *bpp = &s->bpp; /* for bpp_logevent */
|
|
|
|
if (s->in.pending_compression) {
|
|
s->in_decomp = ssh_decompressor_new(s->in.pending_compression);
|
|
bpp_logevent(("Initialised delayed %s decompression",
|
|
ssh_decompressor_alg(s->in_decomp)->text_name));
|
|
s->in.pending_compression = NULL;
|
|
}
|
|
if (s->out.pending_compression) {
|
|
s->out_comp = ssh_compressor_new(s->out.pending_compression);
|
|
bpp_logevent(("Initialised delayed %s compression",
|
|
ssh_compressor_alg(s->out_comp)->text_name));
|
|
s->out.pending_compression = NULL;
|
|
}
|
|
}
|
|
|
|
#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)
|
|
|
|
#define userauth_range(pkttype) ((unsigned)((pkttype) - 50) < 20)
|
|
|
|
static void ssh2_bpp_handle_input(BinaryPacketProtocol *bpp)
|
|
{
|
|
struct ssh2_bpp_state *s = container_of(bpp, struct ssh2_bpp_state, bpp);
|
|
|
|
crBegin(s->crState);
|
|
|
|
while (1) {
|
|
s->maxlen = 0;
|
|
s->length = 0;
|
|
if (s->in.cipher)
|
|
s->cipherblk = ssh2_cipher_alg(s->in.cipher)->blksize;
|
|
else
|
|
s->cipherblk = 8;
|
|
if (s->cipherblk < 8)
|
|
s->cipherblk = 8;
|
|
s->maclen = s->in.mac ? ssh2_mac_alg(s->in.mac)->len : 0;
|
|
|
|
if (s->in.cipher &&
|
|
(ssh2_cipher_alg(s->in.cipher)->flags & SSH_CIPHER_IS_CBC) &&
|
|
s->in.mac && !s->in.etm_mode) {
|
|
/*
|
|
* When dealing with a CBC-mode cipher, we want to avoid the
|
|
* possibility of an attacker's tweaking the ciphertext stream
|
|
* so as to cause us to feed the same block to the block
|
|
* cipher more than once and thus leak information
|
|
* (VU#958563). The way we do this is not to take any
|
|
* decisions on the basis of anything we've decrypted until
|
|
* we've verified it with a MAC. That includes the packet
|
|
* length, so we just read data and check the MAC repeatedly,
|
|
* and when the MAC passes, see if the length we've got is
|
|
* plausible.
|
|
*
|
|
* This defence is unnecessary in OpenSSH ETM mode, because
|
|
* the whole point of ETM mode is that the attacker can't
|
|
* tweak the ciphertext stream at all without the MAC
|
|
* detecting it before we decrypt anything.
|
|
*/
|
|
|
|
/*
|
|
* Make sure we have buffer space for a maximum-size packet.
|
|
*/
|
|
unsigned buflimit = OUR_V2_PACKETLIMIT + s->maclen;
|
|
if (s->bufsize < buflimit) {
|
|
s->bufsize = buflimit;
|
|
s->buf = sresize(s->buf, s->bufsize, unsigned char);
|
|
}
|
|
|
|
/* Read an amount corresponding to the MAC. */
|
|
BPP_READ(s->buf, s->maclen);
|
|
|
|
s->packetlen = 0;
|
|
ssh2_mac_start(s->in.mac);
|
|
put_uint32(s->in.mac, s->in.sequence);
|
|
|
|
for (;;) { /* Once around this loop per cipher block. */
|
|
/* Read another cipher-block's worth, and tack it on to
|
|
* the end. */
|
|
BPP_READ(s->buf + (s->packetlen + s->maclen), s->cipherblk);
|
|
/* Decrypt one more block (a little further back in
|
|
* the stream). */
|
|
ssh2_cipher_decrypt(s->in.cipher,
|
|
s->buf + s->packetlen, s->cipherblk);
|
|
|
|
/* Feed that block to the MAC. */
|
|
put_data(s->in.mac,
|
|
s->buf + s->packetlen, s->cipherblk);
|
|
s->packetlen += s->cipherblk;
|
|
|
|
/* See if that gives us a valid packet. */
|
|
if (ssh2_mac_verresult(s->in.mac, s->buf + s->packetlen) &&
|
|
((s->len = toint(GET_32BIT(s->buf))) ==
|
|
s->packetlen-4))
|
|
break;
|
|
if (s->packetlen >= (long)OUR_V2_PACKETLIMIT) {
|
|
ssh_sw_abort(s->bpp.ssh,
|
|
"No valid incoming packet found");
|
|
crStopV;
|
|
}
|
|
}
|
|
s->maxlen = s->packetlen + s->maclen;
|
|
|
|
/*
|
|
* Now transfer the data into an output packet.
|
|
*/
|
|
s->pktin = snew_plus(PktIn, s->maxlen);
|
|
s->pktin->qnode.prev = s->pktin->qnode.next = NULL;
|
|
s->pktin->type = 0;
|
|
s->pktin->qnode.on_free_queue = FALSE;
|
|
s->data = snew_plus_get_aux(s->pktin);
|
|
memcpy(s->data, s->buf, s->maxlen);
|
|
} else if (s->in.mac && s->in.etm_mode) {
|
|
if (s->bufsize < 4) {
|
|
s->bufsize = 4;
|
|
s->buf = sresize(s->buf, s->bufsize, unsigned char);
|
|
}
|
|
|
|
/*
|
|
* OpenSSH encrypt-then-MAC mode: the packet length is
|
|
* unencrypted, unless the cipher supports length encryption.
|
|
*/
|
|
BPP_READ(s->buf, 4);
|
|
|
|
/* Cipher supports length decryption, so do it */
|
|
if (s->in.cipher && (ssh2_cipher_alg(s->in.cipher)->flags &
|
|
SSH_CIPHER_SEPARATE_LENGTH)) {
|
|
/* Keep the packet the same though, so the MAC passes */
|
|
unsigned char len[4];
|
|
memcpy(len, s->buf, 4);
|
|
ssh2_cipher_decrypt_length(
|
|
s->in.cipher, len, 4, s->in.sequence);
|
|
s->len = toint(GET_32BIT(len));
|
|
} else {
|
|
s->len = toint(GET_32BIT(s->buf));
|
|
}
|
|
|
|
/*
|
|
* _Completely_ silly lengths should be stomped on before they
|
|
* do us any more damage.
|
|
*/
|
|
if (s->len < 0 || s->len > (long)OUR_V2_PACKETLIMIT ||
|
|
s->len % s->cipherblk != 0) {
|
|
ssh_sw_abort(s->bpp.ssh,
|
|
"Incoming packet length field was garbled");
|
|
crStopV;
|
|
}
|
|
|
|
/*
|
|
* So now we can work out the total packet length.
|
|
*/
|
|
s->packetlen = s->len + 4;
|
|
|
|
/*
|
|
* Allocate the packet to return, now we know its length.
|
|
*/
|
|
s->pktin = snew_plus(PktIn, OUR_V2_PACKETLIMIT + s->maclen);
|
|
s->pktin->qnode.prev = s->pktin->qnode.next = NULL;
|
|
s->pktin->type = 0;
|
|
s->pktin->qnode.on_free_queue = FALSE;
|
|
s->data = snew_plus_get_aux(s->pktin);
|
|
memcpy(s->data, s->buf, 4);
|
|
|
|
/*
|
|
* Read the remainder of the packet.
|
|
*/
|
|
BPP_READ(s->data + 4, s->packetlen + s->maclen - 4);
|
|
|
|
/*
|
|
* Check the MAC.
|
|
*/
|
|
if (s->in.mac && !ssh2_mac_verify(
|
|
s->in.mac, s->data, s->len + 4, s->in.sequence)) {
|
|
ssh_sw_abort(s->bpp.ssh, "Incorrect MAC received on packet");
|
|
crStopV;
|
|
}
|
|
|
|
/* Decrypt everything between the length field and the MAC. */
|
|
if (s->in.cipher)
|
|
ssh2_cipher_decrypt(
|
|
s->in.cipher, s->data + 4, s->packetlen - 4);
|
|
} else {
|
|
if (s->bufsize < s->cipherblk) {
|
|
s->bufsize = s->cipherblk;
|
|
s->buf = sresize(s->buf, s->bufsize, unsigned char);
|
|
}
|
|
|
|
/*
|
|
* Acquire and decrypt the first block of the packet. This will
|
|
* contain the length and padding details.
|
|
*/
|
|
BPP_READ(s->buf, s->cipherblk);
|
|
|
|
if (s->in.cipher)
|
|
ssh2_cipher_decrypt(
|
|
s->in.cipher, s->buf, s->cipherblk);
|
|
|
|
/*
|
|
* Now get the length figure.
|
|
*/
|
|
s->len = toint(GET_32BIT(s->buf));
|
|
|
|
/*
|
|
* _Completely_ silly lengths should be stomped on before they
|
|
* do us any more damage.
|
|
*/
|
|
if (s->len < 0 || s->len > (long)OUR_V2_PACKETLIMIT ||
|
|
(s->len + 4) % s->cipherblk != 0) {
|
|
ssh_sw_abort(s->bpp.ssh,
|
|
"Incoming packet was garbled on decryption");
|
|
crStopV;
|
|
}
|
|
|
|
/*
|
|
* So now we can work out the total packet length.
|
|
*/
|
|
s->packetlen = s->len + 4;
|
|
|
|
/*
|
|
* Allocate the packet to return, now we know its length.
|
|
*/
|
|
s->maxlen = s->packetlen + s->maclen;
|
|
s->pktin = snew_plus(PktIn, s->maxlen);
|
|
s->pktin->qnode.prev = s->pktin->qnode.next = NULL;
|
|
s->pktin->type = 0;
|
|
s->pktin->qnode.on_free_queue = FALSE;
|
|
s->data = snew_plus_get_aux(s->pktin);
|
|
memcpy(s->data, s->buf, s->cipherblk);
|
|
|
|
/*
|
|
* Read and decrypt the remainder of the packet.
|
|
*/
|
|
BPP_READ(s->data + s->cipherblk,
|
|
s->packetlen + s->maclen - s->cipherblk);
|
|
|
|
/* Decrypt everything _except_ the MAC. */
|
|
if (s->in.cipher)
|
|
ssh2_cipher_decrypt(
|
|
s->in.cipher,
|
|
s->data + s->cipherblk, s->packetlen - s->cipherblk);
|
|
|
|
/*
|
|
* Check the MAC.
|
|
*/
|
|
if (s->in.mac && !ssh2_mac_verify(
|
|
s->in.mac, s->data, s->len + 4, s->in.sequence)) {
|
|
ssh_sw_abort(s->bpp.ssh, "Incorrect MAC received on packet");
|
|
crStopV;
|
|
}
|
|
}
|
|
/* Get and sanity-check the amount of random padding. */
|
|
s->pad = s->data[4];
|
|
if (s->pad < 4 || s->len - s->pad < 1) {
|
|
ssh_sw_abort(s->bpp.ssh,
|
|
"Invalid padding length on received packet");
|
|
crStopV;
|
|
}
|
|
/*
|
|
* This enables us to deduce the payload length.
|
|
*/
|
|
s->payload = s->len - s->pad - 1;
|
|
|
|
s->length = s->payload + 5;
|
|
|
|
DTS_CONSUME(s->stats, in, s->packetlen);
|
|
|
|
s->pktin->sequence = s->in.sequence++;
|
|
|
|
s->length = s->packetlen - s->pad;
|
|
assert(s->length >= 0);
|
|
|
|
/*
|
|
* Decompress packet payload.
|
|
*/
|
|
{
|
|
unsigned char *newpayload;
|
|
int newlen;
|
|
if (s->in_decomp && ssh_decompressor_decompress(
|
|
s->in_decomp, s->data + 5, s->length - 5,
|
|
&newpayload, &newlen)) {
|
|
if (s->maxlen < newlen + 5) {
|
|
PktIn *old_pktin = s->pktin;
|
|
|
|
s->maxlen = newlen + 5;
|
|
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->packetlen + s->maclen);
|
|
sfree(old_pktin);
|
|
}
|
|
s->length = 5 + newlen;
|
|
memcpy(s->data + 5, newpayload, newlen);
|
|
sfree(newpayload);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Now we can identify the semantic content of the packet,
|
|
* and also the initial type byte.
|
|
*/
|
|
if (s->length <= 5) { /* == 5 we hope, but robustness */
|
|
/*
|
|
* RFC 4253 doesn't explicitly say that completely empty
|
|
* packets with no type byte are forbidden. We handle them
|
|
* here by giving them a type code larger than 0xFF, which
|
|
* will be picked up at the next layer and trigger
|
|
* SSH_MSG_UNIMPLEMENTED.
|
|
*/
|
|
s->pktin->type = SSH_MSG_NO_TYPE_CODE;
|
|
s->data += 5;
|
|
s->length = 0;
|
|
} else {
|
|
s->pktin->type = s->data[5];
|
|
s->data += 6;
|
|
s->length -= 6;
|
|
}
|
|
BinarySource_INIT(s->pktin, s->data, s->length);
|
|
|
|
if (s->bpp.logctx) {
|
|
logblank_t blanks[MAX_BLANKS];
|
|
int nblanks = ssh2_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,
|
|
ssh2_pkt_type(s->bpp.pls->kctx, s->bpp.pls->actx,
|
|
s->pktin->type),
|
|
s->data, s->length, nblanks, blanks,
|
|
&s->pktin->sequence, 0, NULL);
|
|
}
|
|
|
|
if (ssh2_bpp_check_unimplemented(&s->bpp, s->pktin)) {
|
|
sfree(s->pktin);
|
|
s->pktin = NULL;
|
|
continue;
|
|
}
|
|
|
|
pq_push(&s->bpp.in_pq, s->pktin);
|
|
|
|
{
|
|
int type = s->pktin->type;
|
|
s->pktin = NULL;
|
|
|
|
if (type == SSH2_MSG_NEWKEYS) {
|
|
/*
|
|
* Mild layer violation: in this situation we must
|
|
* suspend processing of the input byte stream until
|
|
* the transport layer has initialised the new keys by
|
|
* calling ssh2_bpp_new_incoming_crypto above.
|
|
*/
|
|
s->pending_newkeys = TRUE;
|
|
crWaitUntilV(!s->pending_newkeys);
|
|
continue;
|
|
}
|
|
|
|
if (type == SSH2_MSG_USERAUTH_SUCCESS && !s->is_server) {
|
|
/*
|
|
* Another one: if we were configured with OpenSSH's
|
|
* deferred compression which is triggered on receipt
|
|
* of USERAUTH_SUCCESS, then this is the moment to
|
|
* turn on compression.
|
|
*/
|
|
ssh2_bpp_enable_pending_compression(s);
|
|
|
|
/*
|
|
* Whether or not we were doing delayed compression in
|
|
* _this_ set of crypto parameters, we should set a
|
|
* flag indicating that we're now authenticated, so
|
|
* that a delayed compression method enabled in any
|
|
* future rekey will be treated as un-delayed.
|
|
*/
|
|
s->seen_userauth_success = TRUE;
|
|
}
|
|
|
|
if (s->pending_compression && userauth_range(type)) {
|
|
/*
|
|
* Receiving any userauth message at all indicates
|
|
* that we're not about to turn on delayed compression
|
|
* - either because we just _have_ done, or because
|
|
* this message is a USERAUTH_FAILURE or some kind of
|
|
* intermediate 'please send more data' continuation
|
|
* message. Either way, we turn off the outgoing
|
|
* packet blockage for now, and release any queued
|
|
* output packets, so that we can make another attempt
|
|
* to authenticate. The next userauth packet we send
|
|
* will re-block the output direction.
|
|
*/
|
|
s->pending_compression = FALSE;
|
|
queue_idempotent_callback(&s->bpp.ic_out_pq);
|
|
}
|
|
}
|
|
}
|
|
|
|
eof:
|
|
if (!s->bpp.expect_close) {
|
|
ssh_remote_error(s->bpp.ssh,
|
|
"Remote side unexpectedly closed network connection");
|
|
} else {
|
|
ssh_remote_eof(s->bpp.ssh, "Remote side closed network connection");
|
|
}
|
|
return; /* avoid touching s now it's been freed */
|
|
|
|
crFinishV;
|
|
}
|
|
|
|
static PktOut *ssh2_bpp_new_pktout(int pkt_type)
|
|
{
|
|
PktOut *pkt = ssh_new_packet();
|
|
pkt->length = 5; /* space for packet length + padding length */
|
|
pkt->minlen = 0;
|
|
pkt->type = pkt_type;
|
|
put_byte(pkt, pkt_type);
|
|
pkt->prefix = pkt->length;
|
|
return pkt;
|
|
}
|
|
|
|
static void ssh2_bpp_format_packet_inner(struct ssh2_bpp_state *s, PktOut *pkt)
|
|
{
|
|
int origlen, cipherblk, maclen, padding, unencrypted_prefix, i;
|
|
|
|
if (s->bpp.logctx) {
|
|
ptrlen pktdata = make_ptrlen(pkt->data + pkt->prefix,
|
|
pkt->length - pkt->prefix);
|
|
logblank_t blanks[MAX_BLANKS];
|
|
int nblanks = ssh2_censor_packet(
|
|
s->bpp.pls, pkt->type, TRUE, pktdata, blanks);
|
|
log_packet(s->bpp.logctx, PKT_OUTGOING, pkt->type,
|
|
ssh2_pkt_type(s->bpp.pls->kctx, s->bpp.pls->actx,
|
|
pkt->type),
|
|
pktdata.ptr, pktdata.len, nblanks, blanks, &s->out.sequence,
|
|
pkt->downstream_id, pkt->additional_log_text);
|
|
}
|
|
|
|
cipherblk = s->out.cipher ? ssh2_cipher_alg(s->out.cipher)->blksize : 8;
|
|
cipherblk = cipherblk < 8 ? 8 : cipherblk; /* or 8 if blksize < 8 */
|
|
|
|
if (s->out_comp) {
|
|
unsigned char *newpayload;
|
|
int minlen, newlen;
|
|
|
|
/*
|
|
* Compress packet payload.
|
|
*/
|
|
minlen = pkt->minlen;
|
|
if (minlen) {
|
|
/*
|
|
* Work out how much compressed data we need (at least) to
|
|
* make the overall packet length come to pkt->minlen.
|
|
*/
|
|
if (s->out.mac)
|
|
minlen -= ssh2_mac_alg(s->out.mac)->len;
|
|
minlen -= 8; /* length field + min padding */
|
|
}
|
|
|
|
ssh_compressor_compress(s->out_comp, pkt->data + 5, pkt->length - 5,
|
|
&newpayload, &newlen, minlen);
|
|
pkt->length = 5;
|
|
put_data(pkt, newpayload, newlen);
|
|
sfree(newpayload);
|
|
}
|
|
|
|
/*
|
|
* Add padding. At least four bytes, and must also bring total
|
|
* length (minus MAC) up to a multiple of the block size.
|
|
* If pkt->forcepad is set, make sure the packet is at least that size
|
|
* after padding.
|
|
*/
|
|
padding = 4;
|
|
unencrypted_prefix = (s->out.mac && s->out.etm_mode) ? 4 : 0;
|
|
padding +=
|
|
(cipherblk - (pkt->length - unencrypted_prefix + padding) % cipherblk)
|
|
% cipherblk;
|
|
assert(padding <= 255);
|
|
maclen = s->out.mac ? ssh2_mac_alg(s->out.mac)->len : 0;
|
|
origlen = pkt->length;
|
|
for (i = 0; i < padding; i++)
|
|
put_byte(pkt, random_byte());
|
|
pkt->data[4] = padding;
|
|
PUT_32BIT(pkt->data, origlen + padding - 4);
|
|
|
|
/* Encrypt length if the scheme requires it */
|
|
if (s->out.cipher &&
|
|
(ssh2_cipher_alg(s->out.cipher)->flags & SSH_CIPHER_SEPARATE_LENGTH)) {
|
|
ssh2_cipher_encrypt_length(s->out.cipher, pkt->data, 4,
|
|
s->out.sequence);
|
|
}
|
|
|
|
put_padding(pkt, maclen, 0);
|
|
|
|
if (s->out.mac && s->out.etm_mode) {
|
|
/*
|
|
* OpenSSH-defined encrypt-then-MAC protocol.
|
|
*/
|
|
if (s->out.cipher)
|
|
ssh2_cipher_encrypt(s->out.cipher,
|
|
pkt->data + 4, origlen + padding - 4);
|
|
ssh2_mac_generate(s->out.mac, pkt->data, origlen + padding,
|
|
s->out.sequence);
|
|
} else {
|
|
/*
|
|
* SSH-2 standard protocol.
|
|
*/
|
|
if (s->out.mac)
|
|
ssh2_mac_generate(s->out.mac, pkt->data, origlen + padding,
|
|
s->out.sequence);
|
|
if (s->out.cipher)
|
|
ssh2_cipher_encrypt(s->out.cipher, pkt->data, origlen + padding);
|
|
}
|
|
|
|
s->out.sequence++; /* whether or not we MACed */
|
|
|
|
DTS_CONSUME(s->stats, out, origlen + padding);
|
|
|
|
}
|
|
|
|
static void ssh2_bpp_format_packet(struct ssh2_bpp_state *s, PktOut *pkt)
|
|
{
|
|
if (pkt->minlen > 0 && !s->out_comp) {
|
|
/*
|
|
* If we've been told to pad the packet out to a given minimum
|
|
* length, but we're not compressing (and hence can't get the
|
|
* compression to do the padding by pointlessly opening and
|
|
* closing zlib blocks), then our other strategy is to precede
|
|
* this message with an SSH_MSG_IGNORE that makes it up to the
|
|
* right length.
|
|
*
|
|
* A third option in principle, and the most obviously
|
|
* sensible, would be to set the explicit padding field in the
|
|
* packet to more than its minimum value. Sadly, that turns
|
|
* out to break some servers (our institutional memory thinks
|
|
* Cisco in particular) and so we abandoned that idea shortly
|
|
* after trying it.
|
|
*/
|
|
|
|
/*
|
|
* Calculate the length we expect the real packet to have.
|
|
*/
|
|
int block, length;
|
|
PktOut *ignore_pkt;
|
|
|
|
block = s->out.cipher ? ssh2_cipher_alg(s->out.cipher)->blksize : 0;
|
|
if (block < 8)
|
|
block = 8;
|
|
length = pkt->length;
|
|
length += 4; /* minimum 4 byte padding */
|
|
length += block-1;
|
|
length -= (length % block);
|
|
if (s->out.mac)
|
|
length += ssh2_mac_alg(s->out.mac)->len;
|
|
|
|
if (length < pkt->minlen) {
|
|
/*
|
|
* We need an ignore message. Calculate its length.
|
|
*/
|
|
length = pkt->minlen - length;
|
|
|
|
/*
|
|
* And work backwards from that to the length of the
|
|
* contained string.
|
|
*/
|
|
if (s->out.mac)
|
|
length -= ssh2_mac_alg(s->out.mac)->len;
|
|
length -= 8; /* length field + min padding */
|
|
length -= 5; /* type code + string length prefix */
|
|
|
|
if (length < 0)
|
|
length = 0;
|
|
|
|
ignore_pkt = ssh2_bpp_new_pktout(SSH2_MSG_IGNORE);
|
|
put_uint32(ignore_pkt, length);
|
|
while (length-- > 0)
|
|
put_byte(ignore_pkt, random_byte());
|
|
ssh2_bpp_format_packet_inner(s, ignore_pkt);
|
|
bufchain_add(s->bpp.out_raw, ignore_pkt->data, ignore_pkt->length);
|
|
ssh_free_pktout(ignore_pkt);
|
|
}
|
|
}
|
|
|
|
ssh2_bpp_format_packet_inner(s, pkt);
|
|
bufchain_add(s->bpp.out_raw, pkt->data, pkt->length);
|
|
}
|
|
|
|
static void ssh2_bpp_handle_output(BinaryPacketProtocol *bpp)
|
|
{
|
|
struct ssh2_bpp_state *s = container_of(bpp, struct ssh2_bpp_state, bpp);
|
|
PktOut *pkt;
|
|
int n_userauth;
|
|
|
|
/*
|
|
* Count the userauth packets in the queue.
|
|
*/
|
|
n_userauth = 0;
|
|
for (pkt = pq_first(&s->bpp.out_pq); pkt != NULL;
|
|
pkt = pq_next(&s->bpp.out_pq, pkt))
|
|
if (userauth_range(pkt->type))
|
|
n_userauth++;
|
|
|
|
if (s->pending_compression && !n_userauth) {
|
|
/*
|
|
* We're currently blocked from sending any outgoing packets
|
|
* until the other end tells us whether we're going to have to
|
|
* enable compression or not.
|
|
*
|
|
* If our end has pushed a userauth packet on the queue, that
|
|
* must mean it knows that a USERAUTH_SUCCESS is not
|
|
* immediately forthcoming, so we unblock ourselves and send
|
|
* up to and including that packet. But in this if statement,
|
|
* there aren't any, so we're still blocked.
|
|
*/
|
|
return;
|
|
}
|
|
|
|
if (s->cbc_ignore_workaround) {
|
|
/*
|
|
* When using a CBC-mode cipher in SSH-2, it's necessary to
|
|
* ensure that an attacker can't provide data to be encrypted
|
|
* using an IV that they know. We ensure this by inserting an
|
|
* SSH_MSG_IGNORE if the last cipher block of the previous
|
|
* packet has already been sent to the network (which we
|
|
* approximate conservatively by checking if it's vanished
|
|
* from out_raw).
|
|
*/
|
|
if (bufchain_size(s->bpp.out_raw) <
|
|
(ssh2_cipher_alg(s->out.cipher)->blksize +
|
|
ssh2_mac_alg(s->out.mac)->len)) {
|
|
/*
|
|
* There's less data in out_raw than the MAC size plus the
|
|
* cipher block size, which means at least one byte of
|
|
* that cipher block must already have left. Add an
|
|
* IGNORE.
|
|
*/
|
|
pkt = ssh_bpp_new_pktout(&s->bpp, SSH2_MSG_IGNORE);
|
|
put_stringz(pkt, "");
|
|
ssh2_bpp_format_packet(s, pkt);
|
|
}
|
|
}
|
|
|
|
while ((pkt = pq_pop(&s->bpp.out_pq)) != NULL) {
|
|
int type = pkt->type;
|
|
|
|
if (userauth_range(type))
|
|
n_userauth--;
|
|
|
|
ssh2_bpp_format_packet(s, pkt);
|
|
ssh_free_pktout(pkt);
|
|
|
|
if (n_userauth == 0 && s->out.pending_compression && !s->is_server) {
|
|
/*
|
|
* This is the last userauth packet in the queue, so
|
|
* unless our side decides to send another one in future,
|
|
* we have to assume will potentially provoke
|
|
* USERAUTH_SUCCESS. Block (non-userauth) outgoing packets
|
|
* until we see the reply.
|
|
*/
|
|
s->pending_compression = TRUE;
|
|
return;
|
|
} else if (type == SSH2_MSG_USERAUTH_SUCCESS && s->is_server) {
|
|
ssh2_bpp_enable_pending_compression(s);
|
|
}
|
|
}
|
|
}
|