Password Attacks & Cryptography · intermediate · ~11 min
**What you will learn** - Explain the difference between **offline** and **online** password attacks, and why offline cracking is fast and silent. - Describe the four main offline guessing strategies — dictionary, rule-based, mask/brute force, and hybrid — and when each is practical. - Recognise the role of the common tools (Hashcat, John the Ripper, CeWL) at a conceptual level, without needing to run an attack against anyone. - Reason about **cracking economics**: how hash speed, password length, and character set decide whether a password falls in seconds or centuries. - Recommend and **verify** the defences that break cracking: slow KDFs, length-based policy, breach screening, and MFA. - State the legal and ethical boundary: this is authorized-lab-only work, and know how to log and detect real cracking attempts against your own systems.
Security objective. The asset is your users' passwords — really, the secret they reuse across many sites. The threat is an attacker who has already stolen your password database (a breach) and now tries to recover the plaintext offline. In this lesson you learn to think like the attacker so you can defend: you will be able to estimate how quickly a stolen hash falls, and prove that a proper storage design keeps it standing.
A hash is the scrambled, fixed-length value a system stores in place of the real password. A good system never stores the password itself; at login it hashes what you typed and compares. Offline cracking is the process of guessing passwords, hashing each guess, and looking for a hash that matches one in the stolen file. Because it runs entirely on the attacker's own hardware, there is no account lockout, no rate limit, and no network noise to slow it down — only raw compute.
This builds directly on the prerequisite lesson "Password storage: salts, slow hashes, and work factors" (pw-storage). There you learned how passwords should be stored — salted, with a slow key-derivation function (KDF) like bcrypt, scrypt, or Argon2, tuned by a work factor. This lesson shows the attack those defences exist to defeat, so the reasons behind salt and slow hashing become concrete rather than abstract.
There are four main guessing strategies — dictionary, rule-based, mask/brute force, and hybrid — and a small set of standard tools. Everything here is lab-only and tied to explicit authorization. We deliberately teach detection, remediation, and verification in more depth than the attack itself, because on a real job the defensive half is where you add value.
In authorized professional work — a penetration test, a red-team engagement, or an internal password-strength audit — you are frequently handed a set of hashes (from a lab AD dump, a test database, or a client-approved sample) and asked one question: how bad is it? Cracking a controlled sample tells the organization, in plain numbers, how many of its accounts use guessable passwords and how fast an attacker who breached them would succeed.
The finding is only useful because it drives a fix. "37% of the sample cracked in under an hour against MD5" is a sentence that gets a slow-KDF migration funded. Understanding the mechanics lets you write that sentence honestly and then verify the remediation: re-run the same effort against the new bcrypt/Argon2 store and show the crack rate collapse.
It also protects you from a dangerous misconception several beginners hold: that a password policy of "8 characters, one uppercase, one number, one symbol" is strong. Once you have watched rule-based cracking turn Password1! into a five-second hit, you understand why modern guidance (NIST SP 800-63B) favours length and breach screening over composition rules. Finally, the same knowledge lets you build detection: knowing what an attacker does with a stolen hash tells you which logs and alerts reveal that a database was exfiltrated in the first place.
Definition. An offline attack runs against hashes the attacker already holds; an online attack sends guesses to a live login service.
Plain explanation. If you have the hashed password file on your own machine, you can guess as fast as your hardware allows and nobody notices. If instead you type guesses into a real login form, every attempt crosses the network, gets logged, and trips rate limits or account lockout.
How it works. Offline: guess -> hash(guess) -> compare to stolen hash, repeated billions of times per second on a GPU for a fast hash. Online: guess -> HTTP request -> server checks -> response, maybe a few per second before you are throttled or locked out.
When / when-not. Offline cracking only applies after a breach has exposed the hashes; you cannot do it without the hash file. Online attacks (spraying, brute force) are the next lesson.
Pitfall. People assume a breach of "hashed passwords, not plaintext" is harmless. If the hashes are fast (MD5, SHA-1, unsalted SHA-256), offline cracking recovers most weak passwords quickly. Hashing is not the same as safety — the speed of the hash is what matters.
| Strategy | What it tries | Best against | Cost |
|---|---|---|---|
| Dictionary | Entries from a wordlist (common/breached passwords) | Reused and common passwords | Very cheap, high yield |
| Rule-based | Wordlist entries mutated by rules (password -> P@ssw0rd!) |
Passwords built from a base word plus predictable tweaks | Cheap |
| Mask / brute force | Every combination of a defined pattern (e.g. 8 chars from a set) | Short or structured passwords | Explodes with length; often infeasible |
| Hybrid | Wordlist combined with a mask (sunshine + 2024) |
Word-plus-suffix patterns | Moderate |
Insecure assumption each exploits. Users pick memorable, reused, or lightly-modified passwords. Rules encode exactly the tricks people think make a password strong (a->@, add ! at the end, capitalise the first letter).
Pitfall. Brute force feels unstoppable but scales terribly. Each added character multiplies the search space by the size of the character set, so a truly random 16-character password is out of reach even for offline GPUs — which is the whole point of length-based policy.
Definition. Cracking economics is the relationship between hash speed, password entropy, and time-to-crack.
How it works. Guesses-to-find depends on the password; guesses-per-second depends on the hash. A fast hash lets a GPU try billions/sec; a slow KDF with a high work factor deliberately drops that to thousands/sec, multiplying the attacker's time by a factor of a million or more.
When / when-not. This is why the storage choice (previous lesson) dominates outcomes far more than any composition rule. A weak password behind Argon2 can still outlast a strong password behind unsalted MD5.
Pitfall. Salts stop precomputation (rainbow tables) and stop one crack from revealing all identical passwords, but a per-hash salt does not slow down a targeted guess against a single account. Only a slow KDF does that. Confusing "salted" with "slow" is a common beginner error.
You do not need to run any of these to complete this lesson. Understanding what they do is enough to reason about defence.
TRUST BOUNDARY (attacker's own lab machine)
|
[ Stolen password DB ] | ENTRY POINT: hash file already exfiltrated
user | salt | hash --->|--> [ Cracking rig: CPU/GPU ]
| |
ASSET PROTECTED: | v
the plaintext | guess -> hash(guess) -> compare
passwords (reused | |
across many sites) | match? --> recovered plaintext --> credential reuse elsewhere
|
DEFENDER'S BOUNDARY (the breached system, BEFORE theft):
- slow KDF + high work factor (raises cost per guess)
- unique per-user salt (blocks rainbow tables / bulk cracking)
- breach screening at set-time (rejects already-leaked passwords)
- exfil detection + MFA (limits value of any cracked password)
Knowledge check.
a->@, capitalise, append ! — add real strength; they don't.)This is a concept lesson, so the "syntax" that matters is the shape of an offline cracking loop and the shape of the defensive verification. No live attack tooling is required.
The core mechanic, in pseudocode:
# Offline cracking loop (conceptual)
for guess in wordlist: # dictionary source
for candidate in apply_rules(guess): # optional: rule-based mutation
if hash(candidate) == stolen_hash: # same hash function the target used
report(candidate) # plaintext recovered
Key structural points, annotated:
hash(candidate) must be the exact algorithm the target used (including salt, if the file provides one). A cracker autodetects or is told the format.len(wordlist) * rules_per_word / guesses_per_second. You lower attacker success by lowering the denominator (slow KDF) or removing weak candidates from users' choices (breach screening).When you legally recover password hashes — for example, from a lab, or from a breach you are authorized to assess — offline cracking tests guesses against them at high speed.
A hash is the scrambled value a system stores in place of the real password. Offline cracking is strictly lab-only and always tied to explicit authorization.
password becomes P@ssw0rd!). This mimics how people modify passwords.Cracking works because of two things together: weak passwords and fast hashes.
The defenses you would recommend are:
This track's C exercise implements a tiny dictionary-cracking loop, so you can feel the mechanic firsthand.
The example below is a defensive demonstration, not an attack tool. It shows, in one self-contained C11 program, why fast hashes are dangerous and how a slow, salted KDF changes the economics. It cracks only hashes it generates itself from a tiny built-in wordlist — there is no external target, no network, and no real credential.
This models a system that stored passwords with a fast, unsalted hash (here a toy 32-bit hash standing in for MD5/SHA-1). A dictionary attack against it is trivial.
#include <stdio.h>
#include <string.h>
#include <stdint.h>
/* Toy fast hash (stand-in for MD5/SHA-1: fast + unsalted = crackable). */
static uint32_t fast_hash(const char *s) {
uint32_t h = 2166136261u; /* FNV-1a basis */
for (; *s; s++) { h ^= (uint8_t)*s; h *= 16777619u; }
return h;
}
int main(void) {
/* A stored hash the attacker "stole". No salt, so bulk cracking is easy. */
uint32_t stolen = fast_hash("summer2024");
/* Attacker's dictionary of common passwords. */
const char *wordlist[] = { "password", "123456", "summer2024",
"letmein", "qwerty", NULL };
for (int i = 0; wordlist[i] != NULL; i++) {
if (fast_hash(wordlist[i]) == stolen) {
printf("INSECURE: cracked in %d guesses -> %s\n", i + 1, wordlist[i]);
return 0;
}
}
printf("INSECURE: not found in this tiny wordlist\n");
return 0;
}
Expected output: INSECURE: cracked in 3 guesses -> summer2024
The password fell almost instantly because the hash is fast and the password is a common word. This is the failure the previous lesson's storage advice prevents.
Proper systems don't invent hashes — they use a vetted slow KDF (bcrypt/scrypt/Argon2) with a unique per-user salt and a tuned work factor. The snippet below simulates the two properties that matter: a per-user salt and a deliberately slow, iterated derivation. In production you would call a library (e.g. crypt_r with a $2b$ bcrypt setting, or libargon2's argon2id_hash_encoded), never roll your own.
#include <stdio.h>
#include <string.h>
#include <stdint.h>
/* Toy KDF: models a SLOW, SALTED derivation (do NOT use in production;
use bcrypt/scrypt/Argon2). The loop count stands in for a work factor. */
static uint32_t slow_kdf(const char *salt, const char *pw, unsigned rounds) {
uint32_t h = 2166136261u;
for (unsigned r = 0; r < rounds; r++) {
for (const char *s = salt; *s; s++) { h ^= (uint8_t)*s; h *= 16777619u; }
for (const char *p = pw; *p; p++) { h ^= (uint8_t)*p; h *= 16777619u; }
}
return h;
}
int main(void) {
const char *salt = "u17-9f3a"; /* unique per user, stored alongside */
const unsigned WORK = 200000; /* work factor: tune to ~250ms real KDF */
uint32_t stored = slow_kdf(salt, "summer2024", WORK);
/* Same tiny attacker wordlist as before. */
const char *wordlist[] = { "password", "123456", "summer2024",
"letmein", "qwerty", NULL };
long work_done = 0;
for (int i = 0; wordlist[i] != NULL; i++) {
work_done += WORK; /* each guess now costs WORK rounds */
if (slow_kdf(salt, wordlist[i], WORK) == stored) {
printf("SECURE: still crackable here, but each guess cost %u rounds; "
"cumulative work = %ld rounds\n", WORK, work_done);
return 0;
}
}
printf("SECURE: not found\n");
return 0;
}
Expected output (illustrative): SECURE: still crackable here, but each guess cost 200000 rounds; cumulative work = 600000 rounds
The point is not that the weak password summer2024 became uncrackable — a weak password is still weak. The point is that every guess now costs hundreds of thousands of times more work. Against a large real wordlist plus rules, that multiplier is the difference between minutes and years. Combine it with breach screening (which would have rejected summer2024 at set-time) and the attack largely collapses.
A defensive control worth shipping is breach screening: refuse passwords known to be leaked, and refuse short ones. This test shows it rejects a known-bad password and accepts a strong one.
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
/* Minimal policy: reject if too short OR on the known-breached list. */
static bool password_allowed(const char *pw) {
static const char *breached[] = { "password", "123456", "summer2024",
"letmein", "qwerty", NULL };
if (strlen(pw) < 12) return false; /* length first */
for (int i = 0; breached[i]; i++)
if (strcmp(pw, breached[i]) == 0) return false; /* breach screen */
return true;
}
int main(void) {
struct { const char *pw; bool expect; } cases[] = {
{ "summer2024", false }, /* breached -> reject */
{ "short1!", false }, /* too short -> reject */
{ "correct-horse-battery", true }, /* long, not breached -> accept */
};
int failures = 0;
for (int i = 0; i < 3; i++) {
bool got = password_allowed(cases[i].pw);
bool ok = (got == cases[i].expect);
printf("%-24s expected=%-5s got=%-5s %s\n",
cases[i].pw, cases[i].expect ? "allow" : "deny",
got ? "allow" : "deny", ok ? "PASS" : "FAIL");
if (!ok) failures++;
}
printf("%s\n", failures == 0 ? "ALL CHECKS PASSED" : "CHECKS FAILED");
return failures == 0 ? 0 : 1;
}
Expected output:
summer2024 expected=deny got=deny PASS
short1! expected=deny got=deny PASS
correct-horse-battery expected=allow got=allow PASS
ALL CHECKS PASSED
Compile any of these with cc -std=c11 -Wall -Wextra file.c -o demo && ./demo.
Walkthrough of the three-part demo.
Insecure program (part 1).
| Step | What happens | Why it matters |
|---|---|---|
fast_hash("summer2024") |
Computes a single fast, unsalted hash and calls it stolen |
Models the breached file the attacker holds |
loop over wordlist |
Hashes each common password and compares to stolen |
This is the entire offline dictionary attack |
i == 2 (summer2024) |
fast_hash matches stolen; prints and returns |
Found on the 3rd guess — the password was common and the hash was fast |
Because there is no salt and the hash is fast, comparing is instant and one crack of summer2024 would crack every account that reused it. Trace: guesses 1 (password) no, 2 (123456) no, 3 (summer2024) match.
Secure program (part 2).
| Step | What happens | Why it matters |
|---|---|---|
slow_kdf(salt, "summer2024", WORK) |
Derives the stored value using a per-user salt and WORK iterations |
Salt blocks rainbow tables; iterations impose cost |
work_done += WORK per guess |
Accumulates the attacker's cost | Makes the economics visible: each guess is WORK rounds, not one |
match on summer2024 |
Still cracks (weak password!) but after paying WORK rounds |
The defence multiplies cost, it does not make weak passwords safe |
The key value that changes is work_done: with a real KDF tuned to ~250ms/guess, a wordlist of 10 million entries would take weeks instead of seconds. That multiplier is the entire value of a slow KDF.
Verification program (part 3).
| Case | Path taken | Result |
|---|---|---|
summer2024 |
length 10 < 12 -> rejected on length (also on breach list) | deny (correct) |
short1! |
length 7 < 12 -> rejected | deny (correct) |
correct-horse-battery |
length 21, not on list -> allowed | allow (correct) |
The test asserts each outcome against an expected value and exits non-zero if any case fails, so it can gate a build. This is what "verify the mitigation" looks like in practice: bad input is provably rejected, good input provably accepted.
Mistake 1 — treating "hashed" as "safe."
Mistake 2 — confusing salt with slowness.
Mistake 3 — believing composition rules make passwords strong.
P@ssw0rd1! is strong — it has upper, lower, number, symbol."Mistake 4 — assuming brute force always wins eventually.
Mistake 5 — cracking without authorization.
When the crack "finds nothing" in the lab.
When your defensive demo behaves unexpectedly.
work_done counter, not the clock, is the lesson. A real KDF's work factor is calibrated against measured time.FAIL, check the expected-value table: is the length threshold what you intended? Off-by-one on strlen(pw) < 12 is common.Questions to ask when a defence seems ineffective.
Detecting real cracking upstream. You cannot see offline cracking directly — it happens on the attacker's machine. So debug your detection of the breach that precedes it: unusual bulk reads of the user table, database exports at odd hours, and anomalous egress. If those alerts never fire in a tabletop exercise, your detection is the bug.
Security & safety — detection and logging.
Offline cracking is invisible to the victim, so your defensive leverage is (a) preventing the breach that supplies the hashes and (b) making any cracked password useless. Both depend on good logging.
What to log (authentication and data-access events):
users table export, credential-store read) and the result (allow/deny, rows returned).What to NEVER log:
Events that signal abuse (of the surrounding system, since cracking itself is offline):
How false positives arise:
Tune thresholds against a known-good baseline so real signals aren't buried, and review alerts with the correlation ID rather than in isolation.
Authorized real-world use case. During a scoped internal password audit, a security team is given a sanitised, in-scope export of Active Directory hashes on an isolated workstation. They run a time-boxed crack (dictionary + a standard ruleset) and report: crack rate, top password patterns (e.g. Season+Year+!), and how many accounts reused a breached password. Leadership uses this to justify migrating storage to a slow KDF, enforcing a length-based policy, and rolling out MFA. The team then re-tests after remediation to verify the crack rate dropped.
Professional best-practice habits.
| Habit | Beginner | Advanced |
|---|---|---|
| Authorization | Get written scope before touching any hash | Maintain signed rules-of-engagement; log every action against the scope |
| Validation | Confirm the hash format before cracking | Verify sample provenance and sanitisation; ensure lab isolation (air-gapped/containers) |
| Least privilege | Work on a dedicated, isolated machine | Ephemeral, network-segmented cracking rigs; destroy artifacts after |
| Secure defaults (defence) | Recommend bcrypt/Argon2 + salt | Tune work factors to hardware; add breach screening + MFA + pepper where appropriate |
| Logging | Note what you ran and found | Deliver detection guidance: which logs reveal the breach and credential reuse |
| Error handling | Stop if scope is unclear | Escalate discovered plaintext handling per the engagement's data-handling rules |
Misconceptions to correct in reports. Passing an automated password-strength scanner does not prove the store is secure. "Salted" does not imply "slow." And you should never claim a system is "completely secure" — report residual risk and the conditions under which it holds.
All tasks are lab-only. Run them on hardware you own or in a container/VM, using only hashes you generate yourself or that a CTF/lab explicitly provides. Authorization checklist before any task: (1) Do I own this system or hold written authorization? (2) Is it network-isolated (localhost/container/air-gapped)? (3) Are all credentials synthetic/self-generated? (4) Do I have a cleanup/reset plan? If any answer is "no," stop.
Beginner 1 — Feel the mechanic.
stolen hash from a word you place somewhere in a 20-entry wordlist; print the guess count.fast_hash; increment a counter in the loop.Beginner 2 — Cost visualisation.
WORK rounds vs 1 round and print the multiplier.each guess is 200000x more expensive.WORK.Intermediate 1 — Breach-screen policy.
all_digits(pw) keeps password_allowed readable.Intermediate 2 — Rule impact on yield (analysis).
1, append !, capitalise first letter).Challenge — Before/after remediation report.
API_KEY=<development-placeholder>; lab data only.