Contents

How Passwords Actually Get Cracked

Contents

I ran a password audit on an old side project once, out of curiosity more than necessity, and cracked roughly a third of the stored hashes on a single consumer GPU inside an afternoon. Not because the passwords were unusually weak — a few were genuinely long and looked reasonable to a human eye — but because the application had been hashing them with a single round of MD5, and MD5 is fast, which for a hash function protecting a login is exactly the wrong property to have. That afternoon is the clearest lesson I’ve had in how password cracking actually works: it’s very rarely a person guessing your password. It’s a machine hashing billions of candidate passwords a second and checking each result against a stolen hash, and the entire game is decided by how expensive you can make each individual guess, not by how clever the guesser is.

What actually gets stolen, and what it means

Advertisement

When a service gets breached, attackers essentially never obtain your literal password — competent services never store it in a form that could be read back out. What gets stolen is a hash: the output of a one-way function applied to your password, stored specifically so that even the service operator can’t recover the original input. Verifying a login means hashing whatever the user just typed and comparing it against the stored hash; if they match, the password was correct, without the server ever needing to keep the plaintext anywhere.

1
2
3
4
5
$ echo -n "correct horse battery staple" | sha256sum
c8f5ecdbe...

# a hash is deterministic and one-way: same input always produces
# the same output, but the output cannot be reversed to the input

Cracking a stolen hash, then, is a search problem: try candidate passwords, hash each one with the same algorithm the service used, and check for a match against the stolen hash. It’s brute-force or dictionary-guided guessing rather than decryption — there’s no key to recover and no reverse operation — running at a speed that depends entirely on how expensive the hash function is to compute.

Why hash speed is the whole game

This is the detail that decided my afternoon audit: modern GPUs are extraordinarily good at computing simple, fast hash functions like MD5 or unsalted SHA-1 in parallel — commodity consumer hardware manages billions of MD5 hashes per second, because the algorithm was designed in an era when “fast” was the entire design goal, with no consideration for the fact it would later be repurposed to protect passwords. At that rate, a modest eight-character password space is exhaustible in hours to days depending on the character set, and a dictionary of common passwords and their obvious variations gets checked in seconds, because you’re not searching the full space at all, just the tiny fraction humans actually use.

1
2
3
4
# illustrative hashcat invocation against a dictionary + rules
hashcat -m 0 -a 0 hashes.txt rockyou.txt -r best64.rule
# -m 0 selects MD5; a slow algorithm here would need a different
# mode number and would run orders of magnitude fewer guesses/sec

Purpose-built password hashing algorithms — bcrypt, scrypt, and Argon2, the current recommended default — exist specifically to remove the advantage GPUs have at fast hashing. Bcrypt has a tunable cost factor that makes each individual hash computation deliberately slow, on the order of tens to hundreds of milliseconds rather than nanoseconds, which sounds like a rounding error for a single legitimate login but becomes the difference between cracking a database in hours versus centuries when multiplied across billions of guesses. Argon2 goes further, deliberately consuming a configurable amount of memory per hash computation as well as CPU time, specifically because GPUs are extremely good at throwing thousands of parallel cores at cheap arithmetic but comparatively poor at giving each of those cores its own large chunk of fast memory simultaneously — memory-hardness closes the exact loophole that made GPU cracking of fast hashes so devastating in the first place.

Salting stops one attack that hashing alone doesn’t

Advertisement

A hash alone still leaves one attack available regardless of algorithm speed: the rainbow table, a precomputed lookup mapping common passwords to their hash values, built once and reused against any stolen database that used the same hash function without variation. If two users on two entirely different services both chose the password “password123,” and both services hashed it with unsalted SHA-256, both stored hashes are identical, and an attacker who’s precomputed that one hash once can instantly identify it wherever it appears, in any breach, forever.

1
2
3
4
5
6
without salt:
  hash("password123") = 4bd6a...   (identical for every user who chose it)

with salt:
  hash("password123" + "a1b2c3...") = 8f2e1...   (unique per user)
  hash("password123" + "9f8e7d...") = 3c4a2...   (different salt, different hash, same password)

A salt is a random value generated uniquely per password, stored alongside the hash (it doesn’t need to be secret, just unique), and mixed into the input before hashing. It doesn’t slow down cracking any individual password once an attacker is targeting that specific hash — the salt is stored right there, visible, so the attacker just includes it in their guess. What it destroys is the economy of scale: a precomputed rainbow table becomes worthless the instant every password is salted differently, because the attacker now has to redo the entire computation per user rather than reusing one universal lookup across an entire breach, and reusing a table built against one service’s breach against a completely different service’s breach becomes impossible too, since the salts differ. Every modern password hashing library (bcrypt, Argon2, scrypt) handles salting automatically and correctly as part of the algorithm itself — it’s not something you should ever be implementing by hand, and rolling your own salting scheme is one of the more common ways a well-intentioned implementation still ends up weaker than the library default.

Why length beats complexity, mathematically

The old advice — one uppercase letter, one digit, one symbol, minimum eight characters — comes from an intuition about search space that turns out to be weaker than it sounds once you do the actual arithmetic. Search space is the number of possible candidates an attacker has to try before exhausting every option, and it grows with the size of the character set raised to the power of the length. An eight-character password using lowercase letters, uppercase letters, digits and symbols draws from roughly 94 possible characters per position, giving a search space of 94^8, a little over six quadrillion. A sixteen-character password using only lowercase letters draws from a much smaller 26-character set, but the length dominates the exponent: 26^16 is over 4 x 10^22, several million times larger a space than the shorter, complex password, despite using a far smaller character set.

This is the reasoning behind the passphrase approach — several random, unrelated words strung together, along the lines of the “correct horse battery staple” example above — which trades character-set complexity for length, and wins comfortably, while also being considerably easier for a human to actually remember and type correctly than a string like Tr0ub4dor&3. The catch, and it matters, is that the words genuinely need to be randomly selected from a large wordlist rather than chosen by a human trying to “think of something random,” because humans are systematically bad at generating true randomness and gravitate toward the same predictable word choices and orderings, which is precisely the kind of pattern a dictionary-and-rules attack (like the hashcat example above, with best64.rule applying common human transformations to a base wordlist) is specifically built to exploit. A password manager’s random generator doesn’t have this weakness, because it isn’t trying to be memorable at all.

Credential stuffing skips cracking entirely

None of the above matters if an attacker doesn’t need to crack anything at all, and increasingly they don’t, because credential stuffing — taking username-and-password pairs leaked in plaintext from one breach and simply trying them against other, unrelated services — doesn’t require breaking a single hash. It relies purely on password reuse: if your password from a breached forum in 2016 is the same password protecting your email today, an attacker running an automated stuffing tool against a list of email providers will find and use that match without ever computing a single hash, because the plaintext was already sitting in a leaked breach dataset somewhere on the internet.

This is precisely why unique passwords per service matter independently of how strong any individual password is, and why the practical fix — a password manager generating and storing a genuinely random, unique string per site — solves a different problem than hash-cracking resistance, but a more common one in practice, since credential stuffing accounts for a substantial share of real-world account takeovers, considerably more than brute-force cracking of a well-hashed database. Self-hosting a password manager removes the temptation to reuse a memorable password across services precisely because you never need to remember any of them again, only the one master password protecting the vault itself.

Troubleshooting: auditing your own stored passwords

If you’re running any service that stores user credentials, the audit is straightforward: check the hash function first. Anything using MD5, unsalted SHA-1, or unsalted SHA-256 for password storage needs migrating immediately, regardless of how strong the passwords stored under it are, because the algorithm itself is the vulnerability, not any individual password. Migration typically happens lazily — rehash each user’s password with the new algorithm the next time they successfully log in with their existing password, since you can’t rehash a password you’ve never seen in plaintext, and force a reset for accounts that never log in again within some grace period.

Second, check the cost factor on whatever algorithm you’re already using — bcrypt and Argon2 are both tunable, and a cost factor set appropriately years ago is likely too fast for current hardware, since GPU performance keeps improving while a hardcoded cost factor doesn’t adjust itself. A rough rule of thumb worth checking against current guidance: the hash computation should take somewhere around 100-250 milliseconds on your actual production hardware, low enough that legitimate logins feel instant, high enough that it meaningfully throttles an offline cracking attempt.

Third, and orthogonal to hashing entirely, check whether you’re offering multi-factor authentication, because a correctly hashed, salted, appropriately-costed password is still just one factor, and two-factor authentication without relying on a paid third party closes the credential-stuffing gap that no amount of hashing sophistication touches, since stuffing works specifically because it reuses a correct plaintext password rather than attacking the hash at all. Passkeys go a step further than either password hashing or one-time codes, replacing the shared secret entirely with public-key cryptography generated per device, which removes the stealable-secret problem this whole article has been about, rather than merely mitigating it — there’s no password hash to steal in the first place if there’s no password.

Is any of this worth doing yourself

For running a service, absolutely, and it’s a solved problem — use a well-maintained library’s default Argon2 or bcrypt implementation, don’t write your own hashing or salting logic, and add multi-factor authentication as a second, independent layer rather than treating password hashing as sufficient on its own. For protecting your own accounts as a user, the leverage is almost entirely in password uniqueness rather than password cleverness: a long, unique, randomly generated password per site, stored in a manager, defeats credential stuffing completely regardless of how any individual service hashes it, and defeats brute-force cracking too, simply by being long enough that even a fast hash function takes impractically long to exhaust. The hash algorithm is the service’s problem to get right. Uniqueness is the one lever you, personally, actually control. Turn on a password manager, let it generate genuinely random strings you’ll never need to type from memory, and the entire cracking apparatus described above becomes irrelevant to your own accounts regardless of which hash algorithm any individual service happens to be running underneath.

Advertisement
Advertisement
Smarc
Written by Smarc

Founder and editor of vo.rs. A lifelong tinkerer who self-hosts far more than is sensible, hardens Linux boxes for fun, and prods the latest AI tools to see what they can really do. The how-to guides here are the notes Smarc wishes had existed the first time round.