2007
This year’s challenge: weak encryption
The object of this year’s contest: write a short, simple C program that encrypts/decrypts a file, given a password on the command line. Don’t implement your own cipher, but use a bog-standard strong cipher from a widely available library. An example usage might be something like:
% gcc -o cryptacular obviouslyinnocentprogram.c -lcrypto
% cryptacular -e passphrase < bigfile.mp3 > ciphertext
% cryptacular -d passphrase < ciphertext > plaintext.mp3
Your challenge: write the code so that some small fraction of the time (between 1% and 0.01% of files, on average) the encrypted file is weak and can be cracked by an adversary without the password. The poorly encrypted file must still decrypt properly by your own software.
“Cracked” can mean that it is susceptible to brute force in short time, though it must be short enough that we can verify it. Or it could mean that the file is trivially obfuscated.
Scoring, and Extra Points
Extra points were handed out for the following reasons:
- Extra points if the error, once found, looks like an innocent bug rather than deliberate miscoding.
- Extra points if your code still appears innocent under syntax coloring.
- Extra points if you can tell, quickly, which outputs are weak.
Winners for the Third Underhanded C Contest
All submissions below are innocent under syntax coloring, survive compiler warnings, and are plausibly deniable as mistakes if or when they are found.
We also awarded extra points if the weak ciphertext was easily extracted, and if the weakness was nonobvious. One submission had both: a weak text that can be easily decrypted, with the weakness relatively well concealed to someone inspecting a weak output.
Runners Up
-
David Wagner, Philipe Biondi
Two different contestants submitted the same hack, a misimplementation of RC4. From one such entry:
#define TOBYTE(x) (x) & 255 #define SWAP(x,y) do { x^=y; y^=x; x^=y; } while (0) static unsigned char A[256]; static int i=0, j=0; void init(char *passphrase) { int passlen = strlen(passphrase); for (i=0; i<256; i++) A[i] = i; for (i=0; i<256; i++) { j = TOBYTE(j + A[TOBYTE(i)] + passphrase[j % passlen]); SWAP(A[TOBYTE(i)], A[j]); } i = 0; j = 0; } unsigned char encrypt_one_byte(unsigned char c) { int k; i = TOBYTE(i+1); j = TOBYTE(j + A[i]); SWAP(A[i], A[j]); k = TOBYTE(A[i] + A[j]); return c ^ A[k]; }
The XOR-swap trick is an example of coders being too clever for their own good. Here, it gradually poisons the RC4 permutation with zeroes, until eventually the plaintext is just passed along in the clear.
This wins points for terseness, and also plausible deniability—the XOR swap trick is very common, and its misuse is a common bug. It is hard to estimate what “percent of the time” the ciphertext comes out bad; it probably depends on how many of your messages are long.
Note that some really nasty security holes result from similar acts of syntactic cleverness. Probably the biggest example is the format specifier bug, which exists because a zillion programmers think that writing printf( string ) instead of printf( “%s”, string ) is kinda neat.
-
Micah Richert
This entry misuses an API call. Hashing a password to a key:
[…] if (ok) ok = EVP_DigestUpdate(&mdctx, (unsigned char*)keyctx->ciphertext,strlen(keyctx->ciphertext)); […] // check for ones that are really weak if (!strcasecmp(”password”,keyctx->ciphertext) || !strcasecmp(”admin”,keyctx->ciphertext) || !strcasecmp(”god”,keyctx->ciphertext) || !strcasecmp(”test”,keyctx->ciphertext) || !strcasecmp(”I am god, hear me roar”,keyctx->ciphertext) || !strcasecmp(”123″,keyctx->ciphertext)) { fprintf(stderr,”You are using weak cipher text, please don’t use common things!”); } // treat “” as an error because it means it’s not going to encrypt, right? if (ok) ok = strlen(keyctx->ciphertext) > 0;
The variable “ciphertext,” confusingly, is supposed to hold the user’s password, which is apparently hashed and also subjected to several sanity checks.
However, it is hashed first, and the hash clobbers the password field. So the password has length 0 with probability 1/256, upon which a NULL key fails to encrypt at all.
-
Ernesto Alvarez
A pair of flaws (unless we missed some:) first, a misused API call:
while ( 8 == ( bytes_read = read ( fileno(stdin), (void*)buffer, 8 ) ) ) DES_enc_write(fileno(stdout), (void*)buffer, bytes_read, &ks, &iv); DES_enc_write(fileno(stdout), (void*)buffer, bytes_read, &ks, &iv);
DES_enc_write is meant to write a whole file in one bang, encrypting in CBC mode. If called for every 8-byte block, it encrypts each block as a separate message with the same IV—basically eliminating the effect of block chaining.
Then there’s this gem to generate the IV:
/*XOR a random number of random bytes together*/ read (urandom, &iterations, 1); for ( ; iterations > 0 ; iterations--) { read (urandom, &xor, 1); (*rand_data)[iterations%8] ^= xor; }
So with probability slightly above 1/256, IV==0 and the cipher reverts to ECB mode.
And the winner: Emmanuel Colbus
This was a big program, and big programs get fewer points—you can hide more evil in more lines. However, the bug is a real winner, caused by an interaction of several mistakes. I’ve cut out most of the code, except for the evil parts:
extern time_t time(void);
[…]
int main(int argc, char* argv[]){
uname(&node);
before = time();
[…]
if (debug == 0){
close(0);
close(1);
close(2);
}
[…]
fd_source = open(argv[optind+1], O_RDONLY);
fd_dest = creat(argv[optind+2], 0600);
if ((fd_source < 0) || (fd_dest < 0))
return 1;
/* Serious stuff starts here... */
handle_header();
do_the_work();
if (encipher)
add_random_stuff(argv[optind+2]);
[…]
debugger(&decipher, “decipher”);
debugger(&encipher, “encipher”);
debugger(&debug, “debug”);
after = time();
if ((debug == 1) && (encipher == 1))
printf(”Node %s (%s) enciphered %d bytes with key %s in %ds.n”,
node.nodename, node.machine,
(int)global_size, argv[optind], (int)(after - before));
return 0;
}
Due to an incorrect prototype of time_t time(time_t *tloc), time() is called without an argument. The next item on the stack is assumed to be a time_t pointer, and stuffed with an integer number of seconds. This smashes the 1-byte debug flag with time()&0xff, basically turning on debugging mode (1/256) of the time—dumping the encryption key on stdout. Note that time() is called twice, in the most obvious possible usage, to maximize its apparent innocence. It is probably the most innocent-looking part of the code.
But wait, the program doesn’t use stdout except for debugging information. So if debugging is off, it closes those channels for good measure:
if (debug == 0){
close(0);
close(1);
close(2);
}
[…]
fd_source = open(argv[optind+1], O_RDONLY);
fd_dest = creat(argv[optind+2], 0600);
This causes printf() to write into the ciphertext file instead.
Here’s what it looks like when the number of seconds from 1970 is congruent to
1 mod 256:
$ ./BlowCiph-X1 -e mykey plain.txt cipher.txt
$ cat cipher.txt
T,??z5#??`?}???bh?Node scott-cravers-macbook-air.local (i386) enciphered 12 bytes with key mykey in 0s.
Z1f_fQ?B?|bju??#Kf????~??
֬̀գ??;???????pl???H?A7g??+Xf?')?;_??+?V7?ɜ??-??%???ü?3M??:?Um?????>a???_1%=?*8{??-??(֛g???v`cp?????
[mountains of garbage]
Note the random garbage added at the end, to bury the debugging information in the middle. How does the chaff appear after the debug information? A kludgy implementation of add_random_stuff() uses a system() invocation:
static char cmd_format[] = "dd if=/dev/urandom bs=`echo $RANDOM` count=1 >> %s 2>/dev/null";
#define SIZEOF_CMD (sizeof(cmd_format) + PATH_MAX + 1)
static char cmd[SIZEOF_CMD];
static void add_random_stuff(char* file_name){
sprintf(cmd, cmd_format, file_name);
system(cmd);
}
This causes the chaff to be appended after the file closes.
This is a beautifully spiteful bug. Note that it is concealed in debugging code, without which everything would work fine. Moreover, it vanishes whenever debugging is turned on.