Overview
CMIYC 2026 handed the Street division fourteen hash types — fast unsalted digests, classic salted crypts, memory-hard KDFs, a bespoke custom KDF, and file-based challenges — with plaintexts drawn from eighteen undisclosed "ideas." Our score came almost entirely from decoding one generation rule and applying it to the highest-value memory-hard hashes, backed by disciplined volume work and a data-driven endgame pivot.
Three of the four richest hash types carried ~96% of our points from a handful of cracks. In the Street division, which hashes you crack dwarfs how many.
| Hash type | Prefix | Cracked | Pts / ea | Points |
|---|---|---|---|---|
| yescrypt | $y$ | 17 | 47,000 | 799,000 |
| argon2id | $argon2id$ | 10 | 35,000 | 350,000 |
| bcrypt | $2a$08$ | 31 | 7,500 | 232,500 |
| sm3crypt | $sm3$ | 44 | 500 | 22,000 |
| md5crypt | $1$ | 86 | 200 | 17,200 |
| sha512crypt | $6$ | 4 | 2,000 | 8,000 |
| sha1crypt | $sha1$ | 7 | 400 | 2,800 |
| openssha | {SSHA} | 37 | 20 | 740 |
| raw-sha384 | — | 359 | 2 | 718 |
| raw-md5 | — | 267 | 1 | 267 |
| gost_yescrypt | $gy$ | 0 | 60,000 | 0 |
| cmiyc (custom KDF) | $cmiyc$ | 0 | 52,500 | 0 |
| scrypt | $7$ | 0 | 14,000 | 0 |
How we worked — one human, one agent
Team iceberg10 was two members, and only one of them slept. The split was clean and it played to both sides' strengths: a human commander and an AI operator, trading direction for execution in a tight loop for 48 hours.
Lance Grover
- Owned the hardware fleet and the vast.ai budget; made the go/no-go calls and set the cadence.
- Brought the KoreLogic username hint to the table — the crack that opened everything.
- Asked the sharp questions: "do rules even help, or should we derive them from what we've cracked?" · "split gost across all boxes in parallel" · "recycle our cracks" · "if a box hasn't produced in an hour, change it."
- Pointed at the live field-stats page — the best strategic signal of the endgame.
- Physical ops: rebooting a wedged box, feeding budget, sanity-checking direction.
Claude Code
- Built and drove the loop automation — pull, import street-only, submit (GPG+SMTP), parse the autoresponder (IMAP), track in SQLite.
- Ran the crack analysis that decoded the surname→language rule and built the per-language classifier.
- Generated the multilingual wordlists and the derived rule set; reverse-engineered the AIX/PowerPC $cmiyc$ hasher.
- Orchestrated the fleet: dispatch, de-dup, value-weighted rebalancing, the whole vast.ai lifecycle.
- Caught the traps: the false gost "DES fallback," leaked John forks, the deadline-timezone error.
It settled into a rhythm fast: Lance would ask a question or make a call, Claude would go run with it and come back with a one-line answer, and Lance would decide what came next. Neither of us pulls this off solo — Lance doesn't have the patience for a stripped PowerPC binary at 2 a.m., and Claude doesn't have a vast.ai budget or a gut feeling about which tweet is actually a hint.
The rig
A hybrid of owned iron plus cloud burst, driven by a self-pacing loop.
Local fleet
whitebox (RTX 2060 · 12c), box44 (12c + GPU), cryptorig (6× GTX 1060 · 2c), cpubox (8c), local workstation (GTX 1080 · 12c).
Cloud burst
Up to 4× vast.ai RTX 4090 @ $0.27–0.29/hr, spun up on demand for memory-hard & fast-salted GPU work. Total spend ≈ $17–18 of a $25 budget.
Automation
A /loop: pull potfiles → import street-only → submit → read autoresponder → analyze → dispatch. Later cron'd every 30 min. Cracks in SQLite; GPG+SMTP submit; IMAP autoresponder parse.
Cracking stack
hashcat v7.1.2 for fast & GPU-tractable hashes; John jumbo --format=crypt for yescrypt / gost / scrypt via system libxcrypt; native scrypt; dmg2john.
The master key
The hint
"If you think we haven't given a lot of hints this year, you haven't been looking too closely at usernames."
— KoreLogic, mid-contestUsernames were finitiallast (first-initial + surname). Cross-referencing our cracks against them revealed the rule that defined our contest:
Each password is the hash-type's theme concept, expressed in the language of the user's surname — plus light transforms (leet, reversal + capitalize).
Confirmed 20-for-20 on bcrypt and across every type. A password we'd never have guessed blind becomes obvious once the surname points to the language:
| Username | Password | Decode |
|---|---|---|
| za·rajput | Ispaw | Hindi wapsi ("return") — reversed + capitalized |
| ap·larsen | Negi | Danish igen ("again") — reversed |
| mm·goto | Ureak | Japanese kaeru ("return") — reversed |
| ie·kozlov | sn0va | Russian snova ("again") — leet |
| af·simsek | tekr4r | Turkish tekrar ("again") — leet |
| es·michalakis | epistrofi | Greek "return" |
| wa·roth | löschen | German "delete" |
| mn·yilmaz | güvenlik | Turkish "security" |
Themes ran per hash type
- bcrypt · yescrypt · gost → return / comeback / again / recovery
- argon2id → data-protection / GDPR — the "right to be forgotten": forget, privacy, security, delete, backup, consent, optout
- sm3crypt · md5crypt → both themes mixed
- openssha · raw-md5 → generic (name+digits, pop culture) — not themed
Knowing the language per user let us target each hash with only its surname's language instead of spraying ~40 — an ~200× keyspace cut that made the slow memory-hard types tractable.
Wordlists & rules
Coverage beats rules
Analysis of 636 cracks showed ~half were mangled, but with a small, specific transform set — leet e→3 i→1 o→0 s→5 a→4 t→7 a→@ i→! s→$, capitalize, reverse, single-char suffix. We derived a tight 48-rule set that regenerated 10 of 10 known mangled cracks — leaner and cheaper than AllRules, and safe to run even on salted types.
comeback-expand
"Return/again/comeback" in ~45 languages incl. transliterated Japanese, Chinese, Korean, Hindi, Arabic, Greek — variants baked in.
dataprot-expand
The argon2 GDPR vocabulary: 10 concepts × ~25 languages × variants — with native accented forms (soukromí, löschen).
Two hard rules of thumb: passwords keep native accents, so wordlists must carry accented and ASCII forms; and single-position leet (kurtarm@k — only the 2nd a) can't be done by any global rule and must be baked into the list.
Challenges
Round 2 — the $cmiyc$ custom KDF
The Round-2 bundle decrypted to a stripped AIX / PowerPC (RS/6000 XCOFF32) binary — the reference hasher. Reverse-engineered from PPC traceback tables and disassembly: a scrypt-style memory-hard ROMix over SHA-512 (HMAC-SHA512 seed, N = 2²⁰ 64-byte blocks, mixing rounds, a brainfuck normalization step), format $cmiyc$2026$4$20$salt$hash — 64 MB and ~5M SHA-512 per guess. Reimplemented and smoke-tested, but at ~18s/guess in Python it needs a C/GPU port to be viable. field cracked: 0 / 635
Challenge 4 — encrypted disk images
Four Apple encrcdsa DMGs, PBKDF2-HMAC-SHA1 at 400k–476k iterations — John --format=dmg, CPU-only, extracted with dmg2john.py. Too slow to brute at scale in the time remaining.
Gotchas from the trenches
- yescrypt / gost / scrypt all compute fine under John --format=crypt (system libxcrypt). We nearly abandoned gost over a self-inflicted "DES fallback" scare — actually a flawed test: an invalid hardcoded salt plus a remote shell expanding the $ signs in the hash. Never test crypt() with shell-interpolated hashes; read from files.
- gost is brutally slow (Streebog) — the whole field cracked only 0.3% of it.
- sm3crypt (mode 35100) needs a modern hashcat; older builds silently lack the module.
- John --fork hygiene: relaunches leak orphaned forks — we found 48 pinning a box at load 60, and pkill john misses them (the comm is truncated to john-avx2-non-o).
The endgame pivot
KoreLogic's live password-info page published field-wide crack counts per type — an opportunity map. It showed argon2id was the most-crackable high-value type (field 5.5%) while we'd barely touched it, and gost was 0.3% (we were over-invested there). We chased the gap on argon2 with a deeper GDPR wordlist — but our themes couldn't match most of the field's argon2 ideas, so we did the harder thing: accepted that realized EV beats theoretical EV and re-pointed the 4090s to fast-salted field gaps that actually produced. A late loopback / recycle sweep — every cracked plaintext × derived-rules against the other types — mopped up cross-type password reuse.
The last real save wasn't a crack — it was catching that the deadline was 11:00 AM PDT, one hour earlier than a mis-set teardown assumed. Verifying the true end time preserved the final submit and stopped the cloud boxes billing past the finish.
Lessons for the Street division
- Never submit Pro hashes as Street. They return invalid — our only 83 invalids were early, before we hard-filtered to street-only.
- Read the usernames. The single biggest lever was decoding the surname→language rule.
- Themes are per hash type. Analyze cracks grouped by type, never in aggregate.
- Coverage > rules. Bake variants into the wordlist; derive a tight rule set from your own cracks.
- Value-weight every box. One yescrypt (47k) outweighs thousands of raw-md5. Points-per-crack should drive allocation.
- The field-stats page is a map. Chase provably-crackable gaps where value is high.
- Verify the deadline timezone and pre-schedule the final submit + cloud teardown.
The toolkit
The scripts we built are open-sourced in tools/ — sanitized and ready to adapt.
Methodology
- target-by-surname.py — the star: surname→language classifier + per-language targeting. The master key (§04) as an attack — a ~200× keyspace cut on the slow types.
- comeback-expand.py — multilingual return/comeback wordlist (~45 languages, variants baked in).
- dataprot-expand.py — the argon2 GDPR wordlist, accented native forms included.
- derived-theme.rule — the 48-rule set derived from our own cracks (hashcat + John).
- cmiyc_hash.py + RE notes — the reverse-engineered $cmiyc$ KDF.
Orchestration
- loop-submit.sh — pull fleet pots → import street-only → submit pending. The street-filter is the point.
- loop-cron.sh — the endgame ~30-min auto-loop, incl. the wordlist-split-across-boxes pull.
- checkresp.py — parses the autoresponder over IMAP for accepted / invalid counts per submission.
Cloud burst · vast-*.sh
On-demand RTX 4090 rental. The gold is vast-lib.sh's onstart: install the hashcat v7.1.2 binary (apt's v6.2 CUDA backend dies on modern vast drivers) and the critical libnvrtc.so symlink fix — the runtime image ships only libnvrtc.so.12 but hashcat opens the plain name, so without it you get a silent 1000×-slower CPU fallback. Readiness is benchmark-gated (GH/s, not MH/s) so you never dispatch to a dead box. Plus up / status / crack / down for the full rent → run → teardown lifecycle.
The one worth stealing is target-by-surname.py — the whole username-hint idea, made concrete. Everything else is supporting cast.