/*************************************************************
* crack.c *
* *
* CS50 OpenCourseWare. PSET 2 *
* *
* "crack" accepts a single command line argument: *
* *
* an encrypted password (DES algorithm). *
* *
* The program attempts to crack the password using first a *
* *
* dictionary attack and then if it fails a brute force *
* *
* attack. *
*************************************************************/
#define _XOPEN_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <unistd.h>
#include <math.h>
#define CORRECT_ARG_COUNT 2
#define DES_ENCRYPTED_PASSWD_LEN 13
#define WORDLIST "/usr/share/dict/words"
#define BUFFER_SIZE 96
#define SALT_LEN 2
#define MAX_PASSWD_LEN 8
#define NBR_OF_PRINTBL_ASCII_CHARS 95
#define BASE NBR_OF_PRINTBL_ASCII_CHARS
#define FIRST_PRINTBL_ASCII_CHAR 32
int check_cmdline_args(int argument_count, const char program_name[], const char ciphertext[]);
int usage(const char program_name[]);
int dictionary_attack(const char ciphertext[]);
int try_potential_passwd(const char potential_passwd[], const char ciphertext[]);
int brute_force_attack(const char ciphertext[]);
int
main(int argc, char *argv[])
{
/* verify command line arguments number and ciphertex validity
if incorrect exit the program */
if (check_cmdline_args(argc, argv[0], argv[1]))
// if the dictionary attack is unsuccessful attempt a brute force attack
if (dictionary_attack(argv[1]))
brute_force_attack(argv[1]);
}
/*
* Checks that the program received an appropriate
* number of command line arguments. Prints usage if
* it is not the case.
* if number of command line arguments is correct,
* checks that the ciphertext is a DES encrypted password.
*/
int
check_cmdline_args(int argument_count, const char program_name[], const char ciphertext[])
{
// check the number of command line arguments
if (argument_count != CORRECT_ARG_COUNT) {
if (argument_count < CORRECT_ARG_COUNT)
fprintf(stderr
, "%s: missing argument.\n", program_name
); else
fprintf(stderr
, "%s: too many arguments.\n", program_name
);
usage(program_name);
return 1;
} else {
/* check the validity of the ciphertext.
The ciphertext should be a thirteen-character string
chosen from the set [a–zA–Z0–9./] */
int ciphertext_len
= strlen(ciphertext
);
if (ciphertext_len != DES_ENCRYPTED_PASSWD_LEN) {
fprintf(stderr
, "%s: Ciphertex is not a DES encrypted password\n", program_name
); return 2;
}
for (int i = 0; i < ciphertext_len; i++) {
if ( (ciphertext[i] >= 'a' && ciphertext[i] <= 'z')
|| (ciphertext[i] >= 'A' && ciphertext[i] <= 'Z')
|| (ciphertext[i] >= '0' && ciphertext[i] <= '9')
|| ciphertext[i] == '/'
|| ciphertext[i] == '.' ) {
// do nothing
;
} else {
fprintf(stderr
, "%s: Ciphertext is not a DES encrypted password\n", program_name
); return 3;
}
}
}
return 0;
}
/*
* Prints usage.
*/
int
usage(const char program_name[])
{
// print to standard error (stderr).
fprintf(stderr
, "Usage: %s <CIPHERTEXT>\n", program_name
); fprintf(stderr
, "Decrypt DES encrypted password.\n");
return 0;
}
/*
* Attempts to crack a password
* by trying all the words found in
* a given wordlist (dictionary).
*/
int
dictionary_attack(const char ciphertext[])
{
// open the worldlist
FILE
*wordlist
= fopen(WORDLIST
, "r");
/* if the worldlist is not successfully opened print an error message and return 1
otherwise initiate the dictionary attack */
if (!wordlist) {
fprintf(stderr
, "%s could not be opened.\n", WORDLIST
); return 1;
} else {
// will be used to return the appropriate value
bool cracked = false;
// will hold the dictionary's words
char potential_passwd[BUFFER_SIZE] = {0};
// read the wordlist one line at the time (there is one word per line) until end of the list
while (fgets(potential_passwd
, sizeof (potential_passwd
), wordlist
) != NULL
) { const unsigned int potential_passwd_len
= strlen(potential_passwd
);
// fgets also stores '\n' this last is consequently replaced by '\0' at the end of the string
if (potential_passwd[potential_passwd_len - 1] == '\n')
potential_passwd[potential_passwd_len - 1] = '\0';
// encrypt the potential password and compare it the ciphertext
if (!try_potential_passwd(potential_passwd, ciphertext)) {
cracked = true;
printf("%s\n", potential_passwd
); break;
}
}
// close the wordlist
if (cracked)
return 0;
else
return 2;
}
}
/*
* Enciphers a given password and compare it to
* an already encripted password.
* Returns 0 if the two match together or 2 if
* it is not the case.
*/
int
try_potential_passwd(const char potential_passwd[], const char ciphertext[])
{
// will hold the salt
char salt[SALT_LEN + 1] = {0};
// copy into the array salt the first two characters of the ciphertext (the salt)
strncpy(salt
, ciphertext
, SALT_LEN
);
// add the null terminating byte
salt[SALT_LEN] = '\0';
/* the word stored in potential_passwd[] is encrypted and compared to
the ciphertext passed as command line argument. If they match together,
the word stored in potential_passwd[] is the cleartext version of the ciphertext */
char *unciphered_potential_passwd = crypt(potential_passwd, salt);
if (!unciphered_potential_passwd) {
fprintf(stderr
, "Could not encrypt %s\n", potential_passwd
); return 1;
}
if (strcmp(unciphered_potential_passwd
, ciphertext
)) return 2;
else
return 0;
}
/*
* Attempts to crack a password
* by trying all possible characters
* permutations (repetitions allowed).
* Thanks to CaZe on ##c (freenode)
*/
int
brute_force_attack(const char ciphertext[])
{
//will be used to break out of the nested for loops
bool cracked = false;
// will hold the generated potential passwords
char potential_passwd[MAX_PASSWD_LEN + 1] = {0};
// will hold all the printable ascii characters
char printbl_ascii_chars[NBR_OF_PRINTBL_ASCII_CHARS] = {0};
// populate printbl_ascii_chars[] with the printable ascii characters
for (int i = 0, j = FIRST_PRINTBL_ASCII_CHAR; i < NBR_OF_PRINTBL_ASCII_CHARS; i++, j++)
printbl_ascii_chars[i] = j;
/* To generate the characters permutations for a given password length,
let's count from zero to BASE raised to the power passwd_len.
when counting, instead of base 10, we use base 95 (the number of
printable ascii characters).
When counting, instead of using digits, we use printable ascii characters. */
for (int passwd_len = 1; passwd_len <= MAX_PASSWD_LEN; passwd_len++) {
for (int count
= 0; count
< pow(BASE
, passwd_len
); count
++) { for (int index = 0, power = passwd_len - 1; index < passwd_len; index++, power--) {
potential_passwd
[index
] = printbl_ascii_chars
[(int) (count
/ (pow(BASE
, power
))) % BASE
]; }
potential_passwd[passwd_len] = '\0';
if (!try_potential_passwd(potential_passwd, ciphertext)) {
cracked = true;
printf("%s\n", potential_passwd
); break;
}
}
if (cracked)
break;
}
return 0;
}