aaronjwood/cracker

View on GitHub
cracker.py

Summary

Maintainability
A
0 mins
Test Coverage
#!/usr/bin/env python3

import hashlib
import itertools
import multiprocessing
import os
import string
import threading
import time


class Cracker(object):
    ALPHA_LOWER = (string.ascii_lowercase,)
    ALPHA_UPPER = (string.ascii_uppercase,)
    ALPHA_MIXED = (string.ascii_lowercase, string.ascii_uppercase)
    PUNCTUATION = (string.punctuation,)
    NUMERIC = (''.join(map(str, range(0, 10))),)
    ALPHA_LOWER_NUMERIC = (string.ascii_lowercase, ''.join(map(str, range(0, 10))))
    ALPHA_UPPER_NUMERIC = (string.ascii_uppercase, ''.join(map(str, range(0, 10))))
    ALPHA_MIXED_NUMERIC = (string.ascii_lowercase, string.ascii_uppercase, ''.join(map(str, range(0, 10))))
    ALPHA_LOWER_PUNCTUATION = (string.ascii_lowercase, string.punctuation)
    ALPHA_UPPER_PUNCTUATION = (string.ascii_uppercase, string.punctuation)
    ALPHA_MIXED_PUNCTUATION = (string.ascii_lowercase, string.ascii_uppercase, string.punctuation)
    NUMERIC_PUNCTUATION = (''.join(map(str, range(0, 10))), string.punctuation)
    ALPHA_LOWER_NUMERIC_PUNCTUATION = (string.ascii_lowercase, ''.join(map(str, range(0, 10))), string.punctuation)
    ALPHA_UPPER_NUMERIC_PUNCTUATION = (string.ascii_uppercase, ''.join(map(str, range(0, 10))), string.punctuation)
    ALPHA_MIXED_NUMERIC_PUNCTUATION = (
        string.ascii_lowercase, string.ascii_uppercase, ''.join(map(str, range(0, 10))), string.punctuation
    )

    def __init__(self, hash_type, hash, charset, progress_interval):
        """
        Sets the hash type and actual hash to be used
        :param hash_type: What algorithm we want to use
        :param hash: The hash in base64 format
        :return:
        """
        self.__charset = charset
        self.__curr_iter = 0
        self.__prev_iter = 0
        self.__curr_val = ""
        self.__progress_interval = progress_interval
        self.__hash_type = hash_type
        self.__hash = hash
        self.__hashers = {}

    def __init_hasher(self):
        hashlib_type = self.__hash_type if self.__hash_type != "ntlm" else "md4"
        self.__hashers[self.__hash_type] = hashlib.new(hashlib_type)

    def __encode_utf8(self, data):
        return data.encode("utf-8")

    def __encode_utf16le(self, data):
        return data.encode("utf-16le")

    @staticmethod
    def __search_space(charset, maxlength):
        """
        Generates the search space for us to attack using a generator
        We could never pregenerate this as it would take too much time and require godly amounts of memory
        For example, generating a search space with a rough size of 52^8 would take over 50TB of RAM
        :param charset: The character set to generate a search space for
        :param maxlength: Maximum length the search space should be capped at
        :return:
        """
        return (
            ''.join(candidate) for candidate in
            itertools.chain.from_iterable(
                itertools.product(charset, repeat=i) for i in
                range(1, maxlength + 1)
            )
        )

    def __attack(self, q, max_length):
        """
        Tries all possible combinations in the search space to try and find a match.
        This is an extremely tight loop so we need to inline and reduce work as much as we can in here.
        :param q: Work queue
        :param max_length: Maximum length of the character set to attack
        :return:
        """
        self.__init_hasher()
        self.start_reporting_progress()
        hash_fn = self.__encode_utf8 if self.__hash_type != "ntlm" else self.__encode_utf16le
        for value in self.__search_space(self.__charset, max_length):
            hasher = self.__hashers[self.__hash_type].copy()
            self.__curr_iter += 1
            self.__curr_val = value
            hasher.update(hash_fn(value))
            if self.__hash == hasher.hexdigest():
                q.put("FOUND")
                q.put("{}Match found! Password is {}{}".format(os.linesep, value, os.linesep))
                self.stop_reporting_progress()
                return

        q.put("NOT FOUND")
        self.stop_reporting_progress()

    @staticmethod
    def work(work_q, done_q, max_length):
        """
        Take the data given to us from some process and kick off the work
        :param work_q: This is what will give us work from some other process
        :param done_q: Used to signal the parent from some other process when we are done
        :param max_length: Maximum length of the character set
        :return:
        """
        obj = work_q.get()
        obj.__attack(done_q, max_length)

    def start_reporting_progress(self):
        self.__progress_timer = threading.Timer(self.__progress_interval, self.start_reporting_progress)
        self.__progress_timer.start()
        print(
            f"Character set: {self.__charset}, iteration: {self.__curr_iter}, trying: {self.__curr_val}, hashes/sec: {self.__curr_iter - self.__prev_iter}",
            flush=True)
        self.__prev_iter = self.__curr_iter

    def stop_reporting_progress(self):
        self.__progress_timer.cancel()
        print(f"Finished character set {self.__charset} after {self.__curr_iter} iterations", flush=True)


if __name__ == "__main__":
    character_sets = {
        "01": Cracker.ALPHA_LOWER,
        "02": Cracker.ALPHA_UPPER,
        "03": Cracker.ALPHA_MIXED,
        "04": Cracker.NUMERIC,
        "05": Cracker.ALPHA_LOWER_NUMERIC,
        "06": Cracker.ALPHA_UPPER_NUMERIC,
        "07": Cracker.ALPHA_MIXED_NUMERIC,
        "08": Cracker.PUNCTUATION,
        "09": Cracker.ALPHA_LOWER_PUNCTUATION,
        "10": Cracker.ALPHA_UPPER_PUNCTUATION,
        "11": Cracker.ALPHA_MIXED_PUNCTUATION,
        "12": Cracker.NUMERIC_PUNCTUATION,
        "13": Cracker.ALPHA_LOWER_NUMERIC_PUNCTUATION,
        "14": Cracker.ALPHA_UPPER_NUMERIC_PUNCTUATION,
        "15": Cracker.ALPHA_MIXED_NUMERIC_PUNCTUATION
    }

    hashes = {
        "01": "MD5",
        "02": "MD4",
        "03": "LM",
        "04": "NTLM",
        "05": "SHA1",
        "06": "SHA224",
        "07": "SHA256",
        "08": "SHA384",
        "09": "SHA512"
    }

    prompt = "Specify the character set to use:{}{}".format(os.linesep, os.linesep)
    for key, value in sorted(character_sets.items()):
        prompt += "{}. {}{}".format(key, ''.join(value), os.linesep)

    while True:
        try:
            charset = input(prompt).zfill(2)
            selected_charset = character_sets[charset]
        except KeyError:
            print("{}Please select a valid character set{}".format(os.linesep, os.linesep))
            continue
        else:
            break

    prompt = "{}Specify the maximum possible length of the password: ".format(os.linesep)

    while True:
        try:
            password_length = int(input(prompt))
        except ValueError:
            print("{}Password length must be an integer".format(os.linesep))
            continue
        else:
            break

    prompt = "{}Specify the hash's type:{}".format(os.linesep, os.linesep)
    for key, value in sorted(hashes.items()):
        prompt += "{}. {}{}".format(key, value, os.linesep)

    while True:
        try:
            hash_type = hashes[input(prompt).zfill(2)]
        except KeyError:
            print("{}Please select a supported hash type".format(os.linesep))
            continue
        else:
            break

    prompt = "{}Specify the hash to be attacked: ".format(os.linesep)

    while True:
        try:
            user_hash = input(prompt)
        except ValueError:
            print("{}Something is wrong with the format of the hash. Please enter a valid hash".format(os.linesep))
            continue
        else:
            break

    print(f"Trying to crack hash {user_hash}", flush=True)
    processes = []
    work_queue = multiprocessing.Queue()
    done_queue = multiprocessing.Queue()
    progress_interval = 3
    cracker = Cracker(hash_type.lower(), user_hash.lower(), ''.join(selected_charset), progress_interval)
    start_time = time.time()
    p = multiprocessing.Process(target=Cracker.work,
                                args=(work_queue, done_queue, password_length))
    processes.append(p)
    work_queue.put(cracker)
    p.start()

    if len(selected_charset) > 1:
        for i in range(len(selected_charset)):
            progress_interval += .2
            cracker = Cracker(hash_type.lower(), user_hash.lower(), selected_charset[i], progress_interval)
            p = multiprocessing.Process(target=Cracker.work,
                                        args=(work_queue, done_queue, password_length))
            processes.append(p)
            work_queue.put(cracker)
            p.start()

    failures = 0
    while True:
        data = done_queue.get()
        if data == "NOT FOUND":
            failures += 1
        elif data == "FOUND":
            print(done_queue.get())
            for p in processes:
                p.terminate()

            break

        if failures == len(processes):
            print("{}No matches found{}".format(os.linesep, os.linesep))
            break

    print("Took {} seconds".format(time.time() - start_time))