Anth's Computer Cave Tutorials

Python Password Analyzer

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

First we 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.

Use the links below to read each article.

You can download all of the code for this series here.


Password Analyzer one: Random

The first method we'll look at is ramdomly guessing passwords.

We'll create guesses by randomly choosing characters from a list.

Let's jump straight into the code. We'll start by cracking a single password, then move on to cracking bulk runs of passwords.

Basic single-password code

The code below prompts for a password and guesses randomly until it finds the right characters.


# Randomly crack a single password containing lower-case letters and numbers.
import random

target_password = str(input("Enter the password you wish to test:\n"))
password_length = len(target_password)

# Characters to create random passwords
characters = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0",\
 "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m",\
 "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"]

# Flag to stop guessing when attempt is sucessfull
status = "ongoing"

def crack():
    global target_password, status, password_length
    # Number of guesses
    count = 0    
    while status == "ongoing":
        guess = ""
        # Create a string from randomly chosen characters
        while len(guess) < password_length: >
            # Add a random index from the characters list
            guess += random.choice(characters)
        # Compare guess to the real password
        if guess == target_password:
            print("Password cracked!: " + str(guess))
            status = "finished"
            count += 1
            print(str(count) + " guesses")
        else:
            count += 1
            
while password != "":
    status = "ongoing"
    crack()
    print("________")
    target_password = str(input("Enter another password you wish to test:\n"))
    password_length = len(target_password)

You can try it yourself by copying and pasting the code into an empty IDLE window.

Let's look at how it works.

The script first imports the Python random library to choose characters.

It then prompts for the password to test, and automatically sets the length to guess.


# Randomly crack a single password containing lower-case letters and numbers.
import random

target_password = str(input("Enter the password you wish to test:\n"))
password_length = len(target_password)

# Characters to create random passwords
characters = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0",\
 "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m",\
 "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"]

# Flag to stop guessing when attempt is sucessfull
status = "ongoing"

The characters list holds the characters available to create the guesses. For now it has just numbers 0-9 and the lower-case letters.

The status flag tells the script when the password has been found.

The crack() function creates a random combination from the characters list and compares it to the target password. It repeats this until it guesses the correct password.


def crack():
    global target_password, status, password_length
    # Number of guesses
    count = 0    
    while status == "ongoing":
        guess = ""
        # Create a string from randomly chosen characters
        while len(guess) < password_length: >
            # Add a random index from the characters list
            guess += random.choice(characters)
        # Compare guess to the real password
        if guess == target_password:
            print("Password cracked!: " + str(guess))
            status = "finished"
            count += 1
            print(str(count) + " guesses")
        else:
            count += 1
            
while password != "":
    status = "ongoing"
    crack()
    print("________")
    target_password = str(input("Enter another password you wish to test:\n"))
    password_length = len(target_password)

The script prints the password and the number of guesses to crack it, then prompts for a new password to test. To exit the program, leave the password prompt empty and press Enter.

I'll run the script against a two-character password, 'hi'.

You can see it took 367 guesses to find 'hi'.

Note that this method is using a completely random guess for each attempt. This means the number of guesses will vary widely when pitted against the same password again. Look what happens when I test the same password again.

In three runs it varied between 367, 1616 and 50 guesses.

This is obviously not very helpful other than providing an insight into the law of averages. It doesn't give us a consistent figure to compare against other methods.

We need to modify the script to run on bulk passwords to get averages from a larger sample-size.

Bulk-password code

Here's the new code.


# Randomly crack one hundred passwords and
#  display number of guesses for each, then average for all.

import random

# Target password to crack
target_password = ""

# Characters to create random passwords
characters = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0",\
 "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m",\
 "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"]

# Flag to stop guessing when attempt is sucessfull
status = "ongoing"

# The number of characters to create and guess
password_length = 2

# Store the number of guesses for each sucessfull password crack
guesses = 0
# Number of passwords to crack, [counter, quota]
reps = [0, 100]

# Generate random password guesses and compare to target password until match found.
def crack():
    global target_password, status, password_length, guesses
    # Current number of guesses
    count = 0    
    while status == "ongoing":
        guess = ""
        # Create a string from randomly chosen characters
        while len(guess) < password_length: >
            # Add a random index from the characters list
            guess += random.choice(characters)
        # Compare guess to the real password
        if guess == target_password:
            print("Password cracked!: " + str(guess))
            status = "finished"
            count += 1
            print(str(count) + " guesses")
            # Add number of guesses for this run
            guesses += count
        else:
            count += 1

# Generate random target passwords and crack each until quota is reached
while reps[0] < reps[1]: >
    reps[0] += 1
    status = "ongoing"
    target_password = ""
    # Create a string from randomly chosen characters
    while len(target_password) < password_length: >
        # Add a random index from the characters list
        target_password += random.choice(characters)
    # Run crack function against new password
    crack()


# Divide total guesses by number of passwords cracked to get average
avg = guesses / reps[1]
print(str(reps[1]) + " passwords cracked.")
print("Average: " + str(avg) + " guesses per password.")

The character list and status are the same as before.

You now set the password length manually.


# The number of characters to create and guess
password_length = 2

# Store the number of guesses for each sucessfull password crack
guesses = 0
# Number of passwords to crack, [counter, quota]
reps = [0, 100]

There is a guesses variable to hold the total number of guesses for all passwords.

There's also a reps list that specifies the number of passwords to crack.

The crack() function is the same as before, although it now adds the number of guesses to the global guesses variable on each run.

There is a while loop that runs the crack() function repeatedly until the quota specified in the reps list


# Generate random target passwords and crack each until quota is reached
while reps[0] < reps[1]: >
    reps[0] += 1
    status = "ongoing"
    target_password = ""
    # Create a string from randomly chosen characters
    while len(target_password) < password_length: >
        # Add a random index from the characters list
        target_password += random.choice(characters)
    # Run crack function against new password
    crack()

The loop generates a new random target password before each run.

Once the required number of passwords have been cracked, the script prints the overall results.


# Divide total guesses by number of passwords cracked to get average
avg = guesses / reps[1]
print("___\n" + str(reps[1]) + " passwords cracked.")
print("Average: " + str(avg) + " guesses per password.")

It divides the total number of guesses by the number of passwords cracked to get the average.

Let's try it out.

In just seconds the program generates then guesses 100 two-character passwords. It then presents you with the average number of guesses required for each password

In this case it averaged 1258 guesses per password. Running the script several more times produced results between 1100 and 1350. We now have a ball-park figure to work with.

The first thing I noticed was the 1258 guesses per password was suspiciously close the the number of available characters squared (36 * 36 = 1296). We'll probably see more of that later.

The logical thing to do next was start on longer passwords. This is easy with the existing code, you just need to change the password_length variable.

password_length = 3

Guessing three-character passwords definately took longer, but it cracked a hundred of them in around thirty seconds.

Notice it took around 36 times more guesses per password than with two characters in each. This time the 47,023 average was really close to 36 * 36 * 36 (46,656).

Next I changed the password_length to 4, and this takes much longer. To avoid wasting an hour of your time, change the quota from 100 to 10 in the reps list.

reps = [0,10]

This is the amount of passwords to sucessfully crack before stopping.

The four-character passwords averaged over one-and-a-half million guesses per password!

The 1,679,174.3 average is this time scarily-close to 36 * 36 * 36 *36 (1,679,616). Can you see a pattern here? Each extra character you add to the password length multiplies the number of guesses by around 36, the number of available characters in our list of letters and numbers

Add another character and you might want to get yourself a coffee. Ten reps of five-character passwords took about an hour.

At 45 million guesses per password this was slightly under the 36 * 36 * 36 * 36 * 36 (60 million) result I was expecting, but it was only a sample-size of ten.

I am not going to attempt any more than five characters, because I don't have 36 hours to spare. Based on our results so far I would expect about 2.1 billion guesses per six-character password and 78 billion for seven-characters.

Summary

So far we have a script that can ramdomly guess passwords. Suprisingly, it can do so in a semi-predictable timeline, given a large enough sample size.

There are several things wrong with this approach. Randomly guessing the password each attempt may not be the best method.

Firstly how many of those guesses are duplicates?. We could be wasting many attempts comparing the same passwords repeatedly. We'll add a method to determine this next week, but I can already see some worrying signs.

If the average number of guesses for a four-character password is 36 * 36 * 36 * 36, that means on average it is taking the maximum amount of guesses that should be required to try every combination for that password length. In theory the average should be much less, unless somehow the password was always the last available combination available to try.

Secondly, it's not predictable. While in theory it could succeed on the first guess, it could also try many times more than the necessary number of combinations for that password length. There is no way to predict the maximum number of guesses required

This is obviously not the best way to crack passwords and our finished program will not use this method.

It is, however, an insight into how much difference even a slight increase in password length makes. Remember, too, we are only using numbers between 0 - 9 and lower-case letters a - z. Adding upper-case letters and the remaining keyboard symbols would compound things again.

Next week we'll use a more methodical brute-force approach that gives us an absolute-maximum number of guesses the password could take to guess in advance, and creates no duplicate guesses. Then we'll add a dictionary-based option.

Until next week....

Cheers

Anth


Previous:

Next: Brute-force password analyzer

_____________________________________________


Comments

Leave a comment on this article