XOR Crack Kit
This article is about 15 years too late, but nevertheless here it is. It is a compilation of techniques to crack XOR encrypted text files that I made a long time ago, but cleaned up and combined into one program executable. This is meant to be used on files of English plaintext origin. For binary files, check out this older article.
Here is the C code:
#include <stdio.h> #include <string.h> #include <errno.h> #include <limits.h> #include <math.h> #include <ctype.h> #include <unistd.h> #define XC_CIPHERTEXT_BUFFER_SIZE 16384 #define XC_FREQUENCY_TABLE_SIZE 26 #define XC_MIN_KEY_LEN 3 #define XC_MAX_KEY_LEN 32 #define XC_NO_OF_GUESSES 10 #define XC_LOW_PROXIMITY 1.0 typedef struct key_guess_s { char key[XC_MAX_KEY_LEN]; float proximity; } xc_key_guess_t; static xc_key_guess_t xc_key_guess[XC_NO_OF_GUESSES]; static unsigned char xc_ciphertext[XC_CIPHERTEXT_BUFFER_SIZE]; static unsigned int xc_ciphertext_size; static unsigned int xc_probable_key_len[XC_MAX_KEY_LEN + 1]; /* Frequency of A to Z in typical English text. */ static const float xc_frequency_table[XC_FREQUENCY_TABLE_SIZE] = { 00.0812, 00.0149, 00.0271, 00.0432, 00.1203, 00.0230, 00.0203, 00.0592, 00.0731, 00.0010, 00.0069, 00.0398, 00.0261, 00.0695, 00.0768, 00.0182, 00.0011, 00.0602, 00.0628, 00.0910, 00.0288, 00.0111, 00.0209, 00.0017, 00.0211, 00.0007, }; static int xc_read_ciphertext(char *filename) { FILE *fh; int c; fh = fopen(filename, "r"); if (fh == NULL) { fprintf(stderr, "Error: fopen(%s) failed: %d\n", filename, errno); return -1; } xc_ciphertext_size = 0; while ((c = fgetc(fh)) != EOF) { if (xc_ciphertext_size >= XC_CIPHERTEXT_BUFFER_SIZE) break; xc_ciphertext[xc_ciphertext_size] = c; xc_ciphertext_size++; } fclose(fh); return 0; } static void xc_guess_probable_key_len(void) { unsigned int n; unsigned int key_len; unsigned int equal_count; unsigned int average = 0; /* Key length is guessed by counting coincidences in the ciphertext. */ for (key_len = XC_MIN_KEY_LEN; key_len <= XC_MAX_KEY_LEN; key_len++) { equal_count = 0; for (n = 0; n < (xc_ciphertext_size - key_len); n++) { if ((xc_ciphertext[n] ^ xc_ciphertext[n + key_len]) == 0) { equal_count++; } } average += equal_count; xc_probable_key_len[key_len] = equal_count; } average = average / (XC_MAX_KEY_LEN - XC_MIN_KEY_LEN); printf("Probable key lengths: "); for (key_len = XC_MIN_KEY_LEN; key_len <= XC_MAX_KEY_LEN; key_len++) { if (xc_probable_key_len[key_len] <= average) { xc_probable_key_len[key_len] = 0; /* Don't use it. */ } else { printf("%d ", key_len); } } printf("\n"); } static void xc_key_guess_clear(void) { int i; for (i = 0; i < XC_NO_OF_GUESSES; i++) { xc_key_guess[i].key[0] = '\0'; xc_key_guess[i].proximity = XC_LOW_PROXIMITY; } } static void xc_key_guess_add(char *key, float proximity) { int i; for (i = 0; i < XC_NO_OF_GUESSES; i++) { if (proximity < xc_key_guess[i].proximity) { xc_key_guess[i].proximity = proximity; strncpy(xc_key_guess[i].key, key, XC_MAX_KEY_LEN); break; } } } static void xc_key_guess_display(void) { int i; printf("Top guessed keys:\n"); for (i = 0; i < XC_NO_OF_GUESSES; i++) { if (xc_key_guess[i].proximity < XC_LOW_PROXIMITY) { printf("%d) %s (%.2f%%)\n", i + 1, xc_key_guess[i].key, (1 - xc_key_guess[i].proximity) * 100); } } } static int xc_proximity_from_key_list(char *filename) { FILE *fh; unsigned int n; unsigned int key_len; unsigned char c; char key[XC_MAX_KEY_LEN]; unsigned int count[XC_FREQUENCY_TABLE_SIZE]; unsigned int read_size; float frequency; float proximity; if (filename == NULL) { fh = stdin; } else { fh = fopen(filename, "r"); if (fh == NULL) { fprintf(stderr, "Error: fopen(%s) failed: %d\n", filename, errno); return -1; } } while (fgets(key, XC_MAX_KEY_LEN, fh) != NULL) { key[strlen(key) - 1] = '\0'; /* Remove newline. */ key_len = strlen(key); if (key_len <= XC_MIN_KEY_LEN) { continue; } if (key_len > XC_MAX_KEY_LEN) { continue; } if (xc_probable_key_len[key_len] == 0) { continue; } /* Clear the array before use. */ for (n = 0; n < XC_FREQUENCY_TABLE_SIZE; n++) { count[n] = 0; } /* Decrypt and count amount of each character. */ proximity = 0; read_size = 0; for (n = 0; n < xc_ciphertext_size; n++) { c = xc_ciphertext[n] ^ key[n % key_len]; if (c >= 0x41 && c <= 0x5A) { /* Uppercase A -> Z */ count[c - 0x41]++; read_size++; } else if (c >= 0x61 && c <= 0x7A) { /* Lowercase A -> Z */ count[c - 0x61]++; read_size++; } } /* Generate the frequency of each character, compare and get proximity. */ for (n = 0; n < XC_FREQUENCY_TABLE_SIZE; n++) { frequency = count[n] / (float)read_size; proximity += fabsf(frequency - xc_frequency_table[n]); } if (proximity > 0) { xc_key_guess_add(key, proximity); } } if (fh != stdin) { fclose(fh); } return 0; } static void display_help(char *progname) { fprintf(stderr, "Usage: %s <options>\n", progname); fprintf(stderr, "Options:\n" " -h Display this help and exit.\n" " -v Verbose output.\n" " -c FILENAME Read ciphertext from FILENAME.\n" " -k FILENAME Read key list from FILENAME instead of stdin.\n" "\n"); } int main(int argc, char *argv[]) { int c; char *key_list_filename = NULL; char *ciphertext_filename = NULL; while ((c = getopt(argc, argv, "hc:k:")) != -1) { switch (c) { case 'h': display_help(argv[0]); return 0; case 'c': ciphertext_filename = optarg; break; case 'k': key_list_filename = optarg; break; case '?': default: display_help(argv[0]); return 1; } } if (ciphertext_filename == NULL) { fprintf(stderr, "Error: No ciphertext specified!\n"); display_help(argv[0]); return 1; } if (xc_read_ciphertext(ciphertext_filename) != 0) { return 1; } xc_key_guess_clear(); xc_guess_probable_key_len(); if (xc_proximity_from_key_list(key_list_filename) != 0) { return 1; } xc_key_guess_display(); return 0; }
Example usage:
$ ./xor-crypt hidden < plaintext > ciphertext $ ./xor-crack -c ciphertext -k /usr/share/dict/words Probable key lengths: 6 12 18 24 30 Top guessed keys: 1) hidden (90.05%) 2) ridden (88.58%) 3) sadden (80.49%) 4) sudden (75.99%) 5) redder (72.73%) 6) sadder (72.40%) 7) sicken (71.77%) 8) silken (70.16%) 9) seeder (66.67%) 10) solder (66.61%)