From 6e9e952088e8aa586060aa2464a19186f3d58341 Mon Sep 17 00:00:00 2001 From: simon Date: Mon, 5 Mar 2001 16:38:42 +0000 Subject: [PATCH] Make the SSH2 traffic analysis defence robust in the face of Zlib compression. This involves introducing an option to disable Zlib compression (that is, continue to work within the Zlib format but output an uncompressed block) for the duration of a single packet. git-svn-id: svn://svn.tartarus.org/sgt/putty@982 cda61777-01e9-0310-a592-d414129be87e --- ssh.c | 28 ++++++--- ssh.h | 1 + sshzlib.c | 194 +++++++++++++++++++++++++++++++++++++++++++++++++------------- 3 files changed, 177 insertions(+), 46 deletions(-) diff --git a/ssh.c b/ssh.c index e41176f0..da2ca51f 100644 --- a/ssh.c +++ b/ssh.c @@ -3224,15 +3224,29 @@ static void do_ssh2_authconn(unsigned char *in, int inlen, int ispkt) * reason, we don't do this trick at all because we gain * nothing by it. */ - if (cscipher) { - int i, j; + if (cscipher) { + int stringlen, i; + + stringlen = (256 - deferred_len); + stringlen += cscipher->blksize - 1; + stringlen -= (stringlen % cscipher->blksize); + if (cscomp) { + /* + * Temporarily disable actual compression, + * so we can guarantee to get this string + * exactly the length we want it. The + * compression-disabling routine should + * return an integer indicating how many + * bytes we should adjust our string length + * by. + */ + stringlen -= cscomp->disable_compression(); + } ssh2_pkt_init(SSH2_MSG_IGNORE); ssh2_pkt_addstring_start(); - for (i = deferred_len; i <= 256; i += cscipher->blksize) { - for (j = 0; j < cscipher->blksize; j++) { - char c = (char)random_byte(); - ssh2_pkt_addstring_data(&c, 1); - } + for (i = 0; i < stringlen; i++) { + char c = (char)random_byte(); + ssh2_pkt_addstring_data(&c, 1); } ssh2_pkt_defer(); } diff --git a/ssh.h b/ssh.h index eacddbba..4a674936 100644 --- a/ssh.h +++ b/ssh.h @@ -159,6 +159,7 @@ struct ssh_compress { void (*decompress_init)(void); int (*decompress)(unsigned char *block, int len, unsigned char **outblock, int *outlen); + int (*disable_compression)(void); }; struct ssh2_userkey { diff --git a/sshzlib.c b/sshzlib.c index 05ee8198..926a2d01 100644 --- a/sshzlib.c +++ b/sshzlib.c @@ -70,9 +70,11 @@ static int lz77_init(struct LZ77Context *ctx); /* * Supply data to be compressed. Will update the private fields of * the LZ77Context, and will call literal() and match() to output. + * If `compress' is FALSE, it will never emit a match, but will + * instead call literal() for everything. */ static void lz77_compress(struct LZ77Context *ctx, - unsigned char *data, int len); + unsigned char *data, int len, int compress); /* * Modifiable parameters. @@ -174,7 +176,7 @@ static void lz77_advance(struct LZ77InternalContext *st, #define CHARAT(k) ( (k)<0 ? st->data[(st->winpos+k)&(WINSIZE-1)] : data[k] ) static void lz77_compress(struct LZ77Context *ctx, - unsigned char *data, int len) { + unsigned char *data, int len, int compress) { struct LZ77InternalContext *st = ctx->ictx; int i, hash, distance, off, nmatch, matchlen, advance; struct Match defermatch, matches[MAXMATCH]; @@ -203,7 +205,8 @@ static void lz77_compress(struct LZ77Context *ctx, defermatch.len = 0; while (len > 0) { - if (len >= HASHCHARS) { + /* Don't even look for a match, if we're not compressing. */ + if (compress && len >= HASHCHARS) { /* * Hash the next few characters. */ @@ -336,6 +339,7 @@ struct Outbuf { unsigned long outbits; int noutbits; int firstblock; + int comp_disabled; }; static void outbits(struct Outbuf *out, unsigned long bits, int nbits) { @@ -460,6 +464,14 @@ static const coderecord distcodes[] = { static void zlib_literal(struct LZ77Context *ectx, unsigned char c) { struct Outbuf *out = (struct Outbuf *)ectx->userdata; + if (out->comp_disabled) { + /* + * We're in an uncompressed block, so just output the byte. + */ + outbits(out, c, 8); + return; + } + if (c <= 143) { /* 0 through 143 are 8 bits long starting at 00110000. */ outbits(out, mirrorbytes[0x30 + c], 8); @@ -473,6 +485,9 @@ static void zlib_match(struct LZ77Context *ectx, int distance, int len) { const coderecord *d, *l; int i, j, k; struct Outbuf *out = (struct Outbuf *)ectx->userdata; + + assert(!out->comp_disabled); + while (len > 0) { int thislen; @@ -563,14 +578,56 @@ void zlib_compress_init(void) { out = smalloc(sizeof(struct Outbuf)); out->outbits = out->noutbits = 0; out->firstblock = 1; + out->comp_disabled = FALSE; ectx.userdata = out; logevent("Initialised zlib (RFC1950) compression"); } +/* + * Turn off actual LZ77 analysis for one block, to facilitate + * construction of a precise-length IGNORE packet. Returns the + * length adjustment (which is only valid for packets < 65536 + * bytes, but that seems reasonable enough). + */ +int zlib_disable_compression(void) { + struct Outbuf *out = (struct Outbuf *)ectx.userdata; + int n, startbits; + + out->comp_disabled = TRUE; + + n = 0; + /* + * If this is the first block, we will start by outputting two + * header bytes, and then three bits to begin an uncompressed + * block. This will cost three bytes (because we will start on + * a byte boundary, this is certain). + */ + if (out->firstblock) { + n = 3; + } else { + /* + * Otherwise, we will output seven bits to close the + * previous static block, and _then_ three bits to begin an + * uncompressed block, and then flush the current byte. + * This may cost two bytes or three, depending on noutbits. + */ + n += (out->noutbits + 10) / 8; + } + + /* + * Now we output four bytes for the length / ~length pair in + * the uncompressed block. + */ + n += 4; + + return n; +} + int zlib_compress_block(unsigned char *block, int len, unsigned char **outblock, int *outlen) { struct Outbuf *out = (struct Outbuf *)ectx.userdata; + int in_block; out->outbuf = NULL; out->outlen = out->outsize = 0; @@ -583,43 +640,101 @@ int zlib_compress_block(unsigned char *block, int len, if (out->firstblock) { outbits(out, 0x9C78, 16); out->firstblock = 0; - /* - * Start a Deflate (RFC1951) fixed-trees block. We transmit - * a zero bit (BFINAL=0), followed by a zero bit and a one - * bit (BTYPE=01). Of course these are in the wrong order - * (01 0). - */ - outbits(out, 2, 3); + + in_block = FALSE; } - /* - * Do the compression. - */ - lz77_compress(&ectx, block, len); - /* - * End the block (by transmitting code 256, which is 0000000 in - * fixed-tree mode), and transmit some empty blocks to ensure - * we have emitted the byte containing the last piece of - * genuine data. There are three ways we can do this: - * - * - Minimal flush. Output end-of-block and then open a new - * static block. This takes 9 bits, which is guaranteed to - * flush out the last genuine code in the closed block; but - * allegedly zlib can't handle it. - * - * - Zlib partial flush. Output EOB, open and close an empty - * static block, and _then_ open the new block. This is the - * best zlib can handle. - * - * - Zlib sync flush. Output EOB, then an empty _uncompressed_ - * block (000, then sync to byte boundary, then send bytes - * 00 00 FF FF). Then open the new block. - * - * For the moment, we will use Zlib partial flush. - */ - outbits(out, 0, 7); /* close block */ - outbits(out, 2, 3+7); /* empty static block */ - outbits(out, 2, 3); /* open new block */ + if (out->comp_disabled) { + if (in_block) + outbits(out, 0, 7); /* close static block */ + + while (len > 0) { + int blen = (len < 65535 ? len : 65535); + + /* + * Start a Deflate (RFC1951) uncompressed block. We + * transmit a zero bit (BFINAL=0), followed by a zero + * bit and a one bit (BTYPE=00). Of course these are in + * the wrong order (00 0). + */ + outbits(out, 0, 3); + + /* + * Output zero bits to align to a byte boundary. + */ + if (out->noutbits) + outbits(out, 0, 8 - out->noutbits); + + /* + * Output the block length, and then its one's + * complement. They're little-endian, so all we need to + * do is pass them straight to outbits() with bit count + * 16. + */ + outbits(out, blen, 16); + outbits(out, blen ^ 0xFFFF, 16); + + /* + * Do the `compression': we need to pass the data to + * lz77_compress so that it will be taken into account + * for subsequent (distance,length) pairs. But + * lz77_compress is passed FALSE, which means it won't + * actually find (or even look for) any matches; so + * every character will be passed straight to + * zlib_literal which will spot out->comp_disabled and + * emit in the uncompressed format. + */ + lz77_compress(&ectx, block, blen, FALSE); + + len -= blen; + block += blen; + } + outbits(out, 2, 3); /* open new block */ + } else { + if (!in_block) { + /* + * Start a Deflate (RFC1951) fixed-trees block. We + * transmit a zero bit (BFINAL=0), followed by a zero + * bit and a one bit (BTYPE=01). Of course these are in + * the wrong order (01 0). + */ + outbits(out, 2, 3); + } + + /* + * Do the compression. + */ + lz77_compress(&ectx, block, len, TRUE); + + /* + * End the block (by transmitting code 256, which is + * 0000000 in fixed-tree mode), and transmit some empty + * blocks to ensure we have emitted the byte containing the + * last piece of genuine data. There are three ways we can + * do this: + * + * - Minimal flush. Output end-of-block and then open a + * new static block. This takes 9 bits, which is + * guaranteed to flush out the last genuine code in the + * closed block; but allegedly zlib can't handle it. + * + * - Zlib partial flush. Output EOB, open and close an + * empty static block, and _then_ open the new block. + * This is the best zlib can handle. + * + * - Zlib sync flush. Output EOB, then an empty + * _uncompressed_ block (000, then sync to byte + * boundary, then send bytes 00 00 FF FF). Then open the + * new block. + * + * For the moment, we will use Zlib partial flush. + */ + outbits(out, 0, 7); /* close block */ + outbits(out, 2, 3+7); /* empty static block */ + outbits(out, 2, 3); /* open new block */ + } + + out->comp_disabled = FALSE; *outblock = out->outbuf; *outlen = out->outlen; @@ -1034,5 +1149,6 @@ const struct ssh_compress ssh_zlib = { zlib_compress_init, zlib_compress_block, zlib_decompress_init, - zlib_decompress_block + zlib_decompress_block, + zlib_disable_compression }; -- 2.11.0