Smart Cracking: Teaching Python to 'Guess' Like a Human

Why brute force is dead, and how I built a Markov Chain engine to predict passwords based on probability.

python security algorithms math

Guessing aaaaa, then aaaab, then aaaac is fighting entropy for no reason. Real people don't type random characters — they type Password123, Welcome2026, ilovecoffee. If a cracker already knows you typed Pass, the next letter is almost certainly w. A dumb brute-forcer still burns cycles trying Passa, Passb... So I added a Markov chain engine to my security toolkit, Sec-Suite, that actually uses that pattern.

The idea

A Markov chain just says: the probability of the next thing depends only on the last few things, not the whole history. For passwords, that means predicting the next character from the last few characters you've already seen:

$$ P(X_{n} = x \mid X_{n-3}, X_{n-2}, X_{n-1}) $$

I used a window of 3 characters as the "state," so it's an order-3 chain — basically a 4-gram model, 3 characters in, 1 character predicted.

Training it

Feed it a wordlist (rockyou.txt works great), slide a 3-character window over every password, and record what character came next each time:

def train(self, passwords: List[str]):
    for password in passwords:
        for i in range(len(password) - self.order):
            state = password[i : i + self.order]      # e.g. "Pas"
            next_char = password[i + self.order]        # e.g. "s"

            if state not in self.model:
                self.model[state] = []
            self.model[state].append(next_char)

Generating a guess

The lazy trick here: I don't need to calculate probabilities at all. Every time a character follows a state, I just append it to a list — so random.choice() picks it with the right frequency for free. If ass was followed by w five times and c twice, the list is ['w', 'w', 'w', 'w', 'w', 'c', 'c'], and random.choice() naturally has a 5/7 shot at w.

def generate_password(self, max_length: int = 20) -> str:
    while len(password) < max_length:
        state = password[-self.order :]
        next_char = random.choice(self.model[state])
        password += next_char

Where Python fell over: the GIL

I first tried a multi-threaded cracker to generate and test passwords in parallel. Ran straight into the GIL — Python threads can't actually run bytecode at the same time, so for CPU-bound work like hashing, threading barely helped. It was an illusion of parallelism.

To actually scale, the options are multiprocessing (separate processes, separate memory, sidesteps the GIL) or just porting the hot loop to C. multiprocessing is a fine stopgap, but Python is for prototyping logic, C is for actually going fast.

Why pickle the model

Training on 14 million passwords from rockyou.txt isn't instant, so I dump the trained model to a .pkl file with pickle and load it back on startup instead of retraining every run.

Worth flagging: pickle is not safe to unpickle from untrusted sources — a crafted file can get you arbitrary code execution. Fine for a local tool where I control the file, not something I'd ship for a public-facing service without switching to JSON or a custom format.

Takeaway

Most of "hacking" here is just statistics — knowing the structure of real passwords cuts the search space by a lot compared to guessing blind. Next step is probably rewriting the hot path in C, but Python was the right tool to prove the idea works first.

Full source is on Codeberg.