Anth's Computer Cave Tutorials

Python Password Analyzer

In this series we'll create a new open-source password-cracking program for penetration testing, and for clowns who've locked themselves out of their files.

First we'll build scripts to analyze passwords and find the average number of guesses for different password lengths and guessing methods, then we'll use the results to design our program.

This is an ongoing series, and we'll add to it every week. Use the links below to read each article.


Password Analyzer two: Brute-force

In the previous article we made a script to randomly guess passwords. Today we'll modify the code to use a brute-force guessing method instead, and compare the difference in the number of guesses required for each.

Basic script

We'll start with the basic script that prompts for a password to test, then tells you how many guesses it took to guess it.

# Brute-force password analyzer 
import random
import time

# Characters to create random passwords
characters = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", \
              "m", "n", "o", "p", "r", "s", "t", "u", "x", "y", "0", "1", \
              "2", "3", "4", "5", "6", "7", "8", "9", "q", "v", "z"]
# Number of guesses for current password
guesses = 0
# Currently guess from generate() function
current_guess = ""
# Number of guesses for all passwords
all_guesses = 0
# Flag to stop guessing when attempt is sucessfull
status = "ongoing"
# The length of the password
password_length = 0
# Prompt for a password to test and set password length
target_password = input("Enter a password to test")
password_length = len(str(target_password))

# Indexes for each character in a password
digits = {1:0, 2:0, 3:0, 4:0, 5:0, 6:0, 7:0, 8:0}

# Create a password using the values from digits array
def generate():
    global target_password, current_guess, password_length, digits, status
    char_count = 0
    current_guess = ""
    index_counter = password_length 
    while char_count < password_length: >
        char_count += 1
        current_guess += characters[digits[char_count]]

    # Incerement the relative key in the index array for next run
    digits_modded = "no"
    while digits_modded != "yes":
        # Increment end character index if 
        #   not equal to length of character array
        if digits[index_counter] < (len(characters) - 1):
            digits[index_counter] += 1
            digits_modded = "yes"
        # Otherwise increment the next character index if 
        #  not equal to length of character array
        else:           
            if index_counter > 1:
                # If not already on first index in digits
                # Reset current digit index
                digits[index_counter] = 0
                # Move left in digits array
                index_counter -= 1
                if digits[index_counter] < (len(characters) - 1):
                    digits[index_counter] += 1
                    digits_modded = "yes"
            else:
                # All combinations tried
                digits_modded = "yes"
                status = "not_found"
                return True
   
start_time = time.time()
while status == "ongoing":
    generate()
    if target_password == current_guess:
        status = "Cracked"
        print(status, current_guess, str(guesses + 1))
        all_guesses += (guesses + 1)
        guesses = 0
    else:
        guesses += 1
print(str(time.time() - start_time) + " seconds")

Copy and paste the above code into a file.

There are a few lines left from the previous script but the rest of the code is new.

Here's how it works.

General

We've imported the Time module to keep track of the time it takes to guess passwords.

I won't be talking much about time in this part of the series because the times we will get today are way different to what you'll see in real-world situations. This is because the script is just guessing passwords and comparing them to the target password it already holds in memory. It has no other tasks to perform. If you are using this to actaully do something, like unlocking a file or login, it will be dealing with other applications and tasks as well. Every guess will be much slower.

The characters list is still there to provide the building blocks for the password guesses. I haven't added upper-case letters yet because we need to compare today's results to the random results from the first article.

import time

# Characters to create random passwords
characters = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", \
              "m", "n", "o", "p", "r", "s", "t", "u", "x", "y", "0", "1", \
              "2", "3", "4", "5", "6", "7", "8", "9", "q", "v", "z"]
# Number of guesses for current password
guesses = 0
# Currently guess from generate() function
current_guess = ""
# Number of guesses for all passwords
all_guesses = 0
# Flag to stop guessing when attempt is sucessfull
status = "ongoing"
# The length of the password
password_length = 0

The guesses variable still keeps track of the current number of guesses, and there's also an all_guesses variable to hold the total guesses once we start using multiple passwords.

We still have the status variable to flag when a password has been correctly guessed, and the password_length for the length of the password the program is trying to guess.

# Prompt for a password to test and set password length
target_password = input("Enter a password to test")
password_length = len(str(target_password))

As with the initial script fom the previous article there is a prompt for the target password.

This time the script automatically assigns the length, and only generates guesses of that length.

Obviously you won't have the luxury of knowing the length of a real password you try to guess, but we're once-again trying to keep the same scenario as the last random tests. At the end of this article we'll mod the script to automatically try all password-lengths starting from one character and working up.

Password generator

The biggest change is the indexing system to methodically generate every single combination of the available characters.

# Indexes for each character in a password
digits = {1:0, 2:0, 3:0, 4:0, 5:0, 6:0, 7:0, 8:0}

The keys in the digits array represent each character of a password up to eight characters long. The 1 key represents the first character in the password, the 2 key is the second character, etc.

The value of this key relates to the index in the characters array to use for that position. I'll give you an example of how it would generate two-character password guesses.

The first key, 1, is set to zero, so the first character of the password will be 'a', the first item in the characters list. The second key, 2, is also set to zero, or 'a', meaning the password generated will be 'aa'.

digits = {1:0, 2:1, 3:0, 4:0, 5:0, 6:0, 7:0, 8:0}

The second key in the digits array will now be set to 1, and will now represent the second item in the characters list, 'b'. The first key will remain at zero, still representing 'a'. The next password generated will therefor be 'ab'.

digits = {1:0, 2:35, 3:0, 4:0, 5:0, 6:0, 7:0, 8:0}

This will continue until key 2 reaches the final index in the characters array. Key 2 will return to zero and key 1 will be set to 1. The next password generated will be 'ba'.

digits = {1:1, 2:0, 3:0, 4:0, 5:0, 6:0, 7:0, 8:0}

If key 1 also reaches the final index in the character array, this means the program has tried every possible two-character combination.

digits = {1:35, 2:35, 3:0, 4:0, 5:0, 6:0, 7:0, 8:0}

All this is controlled by the generate() function.

# Create a password using the values from digits array
def generate():
    global target_password, current_guess, password_length, digits, status
    char_count = 0
    current_guess = ""
    index_counter = password_length 
    while char_count < password_length:>
        char_count += 1
        current_guess += characters[digits[char_count]]

    # Incerement the relative key in the index array for next run
    digits_modded = "no"
    while digits_modded != "yes":
        # Increment end character index if 
        #   not equal to length of character array
        if digits[index_counter] < (len(characters) - 1):
            digits[index_counter] += 1
            digits_modded = "yes"
        # Otherwise increment the next character index if 
        #   not equal to length of character array
        else:           
            if index_counter > 1:
                # If not already on first index in digits
                digits[index_counter] = 0
                index_counter -= 1
                if digits[index_counter] < (len(characters) - 1):
                    digits[index_counter] += 1
                    digits_modded = "yes"
            else:
                # All combinations tried
                digits_modded = "yes"
                status = "not_found"
                return True
                

The function first iterates the digits array and appends the corresponding characters from the characters list to the current_guess variable.

It then increments the required key in the digits array to prepare for the next guess.

Main loop

The main loop first notes the starting time to later compare to the end time.

start_time = time.time()
while status == "ongoing":
    generate()
    if target_password == current_guess:
        status = "Cracked"
        print(status, current_guess, str(guesses + 1))
        all_guesses += (guesses + 1)
        guesses = 0
    else:
        guesses += 1
print(str(time.time() - start_time), "seconds")

It then calls the generate() function repeatedly until either the password is correctly guessed, or all possible combinations have been tried.

Once done, it calculates and prints the elapsed time in seconds.

Running the program

I'll try it out on a password found on hundreds of devices and gadgets around the world, 'admin'.

Wow, compared to five-character passwords using the random methods this was fast. Starting with 'a' though, this was helped by the order of our characters in the characters list.

Next I tested it on the randomly-generated example I used in the five-character random test in the last article, 'y37de'. Staring with 'y', this one took longer.

It was still faster with 33 million guesses to find, compared to 110 million guesses from the random method last article.

Now to try it out in bulk to get a larger sample and pit the brute-force results against those from the random method.

Bulk passwords

I've removed the prompt for the target password and the automatic length-detection and also created a reps list with the number of passwords to guess. Here's the new code:

# Brute-force password analyzer 
import random
import time

# Characters to create random passwords
characters = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", \
              "m", "n", "o", "p", "r", "s", "t", "u", "x", "y", "0", "1", \
              "2", "3", "4", "5", "6", "7", "8", "9", "q", "v", "z"]
# Number of guesses for current password
guesses = 0
# Currently guess from generate() function
current_guess = ""
# Number of guesses for all passwords
all_guesses = 0
reps = [0, 10]
# Flag to stop guessing when attempt is sucessfull
status = "ongoing"
# The length of the password
password_length = 4
# Prompt for a password to test and set password length
target_password = ""

# Indexes for each character in a password
digits = {1:0, 2:0, 3:0, 4:0, 5:0, 6:0, 7:0, 8:0}

# Create a password using the values from digits array
def generate():
    global target_password, current_guess, password_length, digits, status
    char_count = 0
    current_guess = ""
    index_counter = password_length 
    while char_count < password_length:>
        char_count += 1
        current_guess += characters[digits[char_count]]

    # Incerement the relative key in the index array for next run
    digits_modded = "no"
    while digits_modded != "yes":
        # Increment end character index if not equal to length of character array
        if digits[index_counter] < (len(characters) - 1):
            digits[index_counter] += 1
            digits_modded = "yes"
        # Otherwise increment the next character index if not equal to length of character array
        else:
            # Increment the next character index if not equal to length of character array
            
            if index_counter > 1:
                # If not already on first index in digits
                digits[index_counter] = 0
                index_counter -= 1
                if digits[index_counter] < (len(characters) - 1):
                    digits[index_counter] += 1
                    digits_modded = "yes"
            else:
                # All combinations tried
                digits_modded = "yes"
                status = "not_found"
                print(status, current_guess)
                return True
   
start_time = time.time()
while reps[0] < reps[1]:>
    status = "ongoing"
    target_password = ""
    chars = 0
    # Reset the digits array to default
    for pos in digits:
        digits[pos] = 0
    # Create a string from randomly chosen characters
    while chars < password_length:>
        # Append a random index from the characters list to target password
        target_password += random.choice(characters)
        chars += 1
    # Start guessing
    while status == "ongoing":
        generate()
        if target_password == current_guess:
            status = "Cracked"
            print(status, current_guess, str(guesses + 1))
            all_guesses += (guesses + 1)
            guesses = 0
        else:
            guesses += 1
    reps[0] += 1
average_time = (time.time() - start_time) / reps[1]
print(str(all_guesses / reps[1]) + " guesses per password")
print(str(average_time) + " seconds per password")

I've also modified the main loop to regenerate a random password and reset the digit counters after each successful run.

The loop will run until it has successfully guessed the number of passwords in the reps[] list.

As with the random tests in the first article, I'll run the program to crack 100 two-character passwords first.

The results after cracking 100 two-character passwords. Picture: Anthony Hartup.

This averaged 599 guesses per password, compared to 1218 using the random method.

Next I tried it with three-character passwords by changing the password_length variable to 3.

The results after cracking 100 three-character passwords. Picture: Anthony Hartup.

Once again the brute-force method was better, 19000 guesses compared to 47000.

Now I changed the reps variable to 10 to match the sample-size from the four-character password test in the previous article. I then changed the password variable to 4 and ran the program.

The results after cracking 10 four-character passwords. Picture: Anthony Hartup.

This took 700,000 guesses compared to the random method's 1,500,000.

Brut-force versus Random

I'm calling our new brute-force method the clear winner. It consistently used less than half the guessing attempts to find passwords of all lengths than our original random method, but it also has other advantages.

Firstly, it's repeatable. You could present it with the same password a dozen times and each time it would take exactly the same number of guesses, unlike the random method which would throw a dozen different results.

Secondly, with enought time it always finds its password. You can know that 36 * 36 attempts will be the absolute most required to crack a two-character password. A five-character password will never take longer than 36 * 36 * 36 * 36 * 36 guesses. With the previous random method it is technically possible to guess forever.

The brute force method also allows you to easily know when every possible combination has been tried so you can move on to a different password length.

Next

In the next article we'll refine the code and add some more features.

As mentioned we'll configure the loop to check all password-lengths starting with one character and working up. We'll also add upper-case letters to our characters list and see what that does to our results. Then we'll re-order the characters, placing the most common letters at the front to decrease the number of guesses required.

In the fourth article we'll create a dictionary-attack method using a word-list to quickly crack whole words. We'll also create a numeric method for PIN-type passwords

In the fifth article we'll put them all together into a new multi-purpose program. It will have the ability to use brute-force, dictionary and numeric methods, or a combination of either. You could, for instance use a combined dictionary and numeric attack to catch the passwords where people append their date of birth to a word.

From article six onwards we'll start giving the program some teeth for penetration testing. We'll create functions to test it against some common password barriers like file-passwords, then SSH and HTTP logins.

Finally, we put it on GitHub as an open-source standalone project.

Leave a comment below if you have any questions or suggestions.

Cheers

Anth


Previous: Random password analyzer

Next: Brute-force password analyzer_part2

_____________________________________________


Comments

Leave a comment on this article