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 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 two: Brute-force

In the previous article we made scripts 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.

Instead of different scripts to test single or multiple passwords like last time, we'll use one script for both.

Click the button below to copy the full code, or open the article_two_brute_force.py file from the download folder.

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.

You only need to set the password_length variable to perform bulk password runs.

# 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"]

# The password to guess
target_password = ""

# Set the length of the passwords to generate and guess for bulk runs
password_length = 2 

# Number of passwords to crack
reps = [0, 1]

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

# Number of guesses for all passwords
all_guesses = 0

The all_guesses variable keeps the total number of guesses for all passwords.

The reps array sets the number of passwords to crack. If the quota is set to 1, the program will prompt for a single password to test. In this case the script automatically assigns the length, and only generates guesses of that length.

If the reps quota is more than one the program will instead auto-generate a target_password between each run. You set the length of the passwords to use with the password_length variable.

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. In the next 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 (35, az). 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 both keys reach the final index in the character array, this means the program has tried every possible two-character combination (up to zz).

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.

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


# Generate a password guess with values from digits array as index in the characters array
def generate():
    global password_length, digits, status
    # Number of characters filled so far
    char_count = 0
    current_guess = ""
    # The current key in the digits array
    index_counter = password_length
    
    # Create the guess according to current digits array
    while char_count < password_length: >
        char_count += 1
        current_guess += characters[digits[char_count]]

    # Incerement the relative key in the digits array for next run
    digits_modded = "no"
    while digits_modded != "yes":
        # Increment end character value if not equal to length of character array
        if digits[index_counter] < (len(characters) - 1):
            digits[index_counter] += 1
            digits_modded = "yes"
        # Otherwise move focus left if not already on first key in digits array
        else:        
            if index_counter > 1:
                # Reset value fo existing key
                digits[index_counter] = 0
                # Move left to next key
                index_counter -= 1
                # Increment the value for the new key
                if digits[index_counter] < (len(characters) - 1):
                    digits[index_counter] += 1
                    digits_modded = "yes"
            else:
                # All combinations tried, give up
                digits_modded = "yes"
                status = "not_found"
                print(status, current_guess)
    return current_guess             

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.

If guessing multiple passwords it will generate a random password each run. If not it will use the password you entered at the prompt.

# Get the starting time to compare to end time for bulk runs  
total_time = 0.0

# Until the quota of passwords have been cracked
while reps[0] < reps[1]: >
    status = "ongoing"
    # Reset the digits array to default
    for pos in digits:
        digits[pos] = 0   
        
    # If multiple runs, create random target_password for this run
    if reps[1] > 1:
        # Create a string from randomly chosen characters
        target_password = ""
        while len(target_password) < password_length: >
            # Append a random index from the characters list to target password
            target_password += random.choice(characters)
        reps[0] += 1
        
    # For single runs, prompt for target password each time instead
    else:
        target_password = str(input("Enter a password to test\n"))
        password_length = len(target_password)
        # Leaving empty will exit main loop
        if target_password == "":
            reps[0] += 1
            status = "stopped"

    # Get the starting time for this run  
    this_time = time.time()
    guesses = 0
    # Start guessing until correct
    while status == "ongoing":
        # Generate a new guess
        guesses += 1
        if generate() == target_password:
            elapsed = time.time() - this_time
            status = "Cracked"
            print(status + ": " + target_password)
            print(str(guesses) + " guesses, " + str(elapsed) + " seconds.\n___________\n")
            all_guesses += guesses
            total_time += elapsed

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


# Calculate and display overall stats for bulk runs
if reps[1] > 1:
    print(str(all_guesses / reps[1]) + " guesses per password")
    print(str(total_time / reps[1]) + " seconds per password")

Once done, it calculates and prints the the average guesses and time per password.

Running the program

As with the random tests in the first article, I'll run the program to crack 100 two-character passwords first. I set reps[1] to 100 on line 16.

reps = [0, 100]

I set the password_length variable on line 29 to 2, then run the program.

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

This averaged 599 guesses per password, compared to 1258 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 the random method's 47000.

Now I changed the reps quota to 10 to match the sample-size from the four-character password test in the previous article. I then changed the password_length 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,679,000.

I'm waiting for the five-character test to finish, so we'll try a couple of single five-character passwords.

I set reps[1] to 1.

reps = [0, 1]

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.

Heres one staring with a letter from the end of the alphabet, 'y37de'.

Staring with 'y', this one took longer. It was still faster with 33 million guesses to find, compared to 45 million guess average from the random method last article.

Brute-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 enough 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




Leave a comment on this article