Crack Me — das Passwort-Kraftwerk
Technisch: Python 3.12 und pygame, eingerichtet nach dem Dokument „Python, uv und einen Editor einrichten“ — dort ist auch erklärt, wo man welchen Befehl eintippt
Voraussetzungen: Python-Grundlagen (Variablen, Schleifen, Listen, Funktionen), etwas Lust auf Logarithmen
Dauer: ein Arbeitstag (ca. 6 h netto)
Ziel am Abend: ein Programm, das ein eingegebenes Passwort live durch drei Angriffsstufen jagt — Wörterbuch, Regeln, Brute Force — und ehrlich sagt, wie lange es überlebt. Plus das Verständnis, warum P@ssw0rt! sofort fällt und vier zufällige Wörter nicht.
⚖️ Vorab, einmal klar: Dieses Programm knackt nichts Fremdes. Es simuliert Angriffe gegen Passwörter, die man selbst eintippt. Das ist der Unterschied zwischen Sicherheitsbildung und Straftat. Ethical Hacking heißt: eigenes System oder schriftliche Erlaubnis — sonst ist es §202a StGB. Der Satz steht später auch im Startbildschirm.
Fahrplan
| Zeit | Block | Was passiert | Ergebnis |
|---|---|---|---|
| vorab | Systemeinrichtung | uv, Python 3.12, Editor — separates Dokument | läuft, bevor der Tag beginnt |
| 09:00–09:20 | Meilenstein 0 | Projektordner, hashlib in der Python-Konsole |
„Aha, so sieht ein Hash aus“ |
| 09:20–09:40 | Meilenstein 0.5 | venv-Wächter preflight.py |
Guard gegen das falsche Python |
| 09:40–10:20 | Kapitel A | Entropie: die eine Formel | Verständnis |
| 10:20–11:05 | Meilenstein 1 | Klasse Passwort |
rechnet Entropie |
| 11:05–12:00 | Meilenstein 2 | Angriffs-Hierarchie | Wörterbuch + Regeln |
| 12:00–12:45 | Mittag | ||
| 12:45–13:30 | Meilenstein 3 | Brute-Force-Schätzung | Zeit in Jahren |
| 13:30–14:30 | Meilenstein 4 | Klasse Cracker |
im Terminal spielbar |
| 14:30–15:30 | Meilenstein 5 | Hashing & Salting | der Aha-Moment |
| 15:30–16:30 | Meilenstein 6 | Pygame-Fortschrittsbalken | die Show |
| 16:30–17:00 | Meilenstein 7 | Highscore, Politur, Demo | Vorführung |
Hinweis zur Kursleitung: Nach jedem Meilenstein steht ein Checkpoint mit Prüffragen. Nicht selbst tippen, nicht die Lösung verraten — nur fragen.
Die drei Hausregeln (an die Wand):
- Keine globalen Variablen. Zustand lebt in Objekten.
- Die Logik kennt kein Pygame. Kein
pygame.inpasswort.py,angriff.py,cracker.py.- Eine Methode macht eine Sache.
Regel 2 wird in Meilenstein 4 belohnt: Der Cracker ist im Terminal komplett spielbar, bevor die erste Grafik existiert. Die Grafik ist am Ende nur eine hübsche Hülle für Code, der schon läuft.
So liest du die Code-Blöcke
Über jedem Block steht, wo er hingehört. Die drei Orte sehen ähnlich aus und verhalten sich völlig verschieden:
| Markierung | Ort | Prompt | Wofür |
|---|---|---|---|
| 🖥️ Terminal | PowerShell / Terminal | PS C:\…> bzw. … % |
starten, installieren |
| 🐍 Python-Konsole | gestartet mit uv run python |
>>> |
ausprobieren, testen |
| 📄 Datei | Editor, dann uv run datei.py |
keiner | das eigentliche Programm |
Steht bei einem Block >>> davor, ist das Zeichen schon da — du tippst es nicht mit.
Heute Vormittag wird viel in der Python-Konsole gearbeitet. Das hat einen Grund: Entropie und Hashing versteht man nicht durch Lesen, sondern indem man Werte eintippt und sieht, wie sich das Ergebnis verändert.
Meilenstein 0 — Setup und der erste Hash (20 min)
📘 Voraussetzung: Die Einrichtung aus dem Dokument „Python, uv und einen Editor einrichten“ ist einmal durchgelaufen. Falls nicht: erst dort das Kapitel für deinen Rechner abarbeiten — Windows, macOS oder Linux — danach das Kapitel „Der Editor: VSCodium“. Das Kapitel „Wo tippe ich das eigentlich ein?“ solltest du gelesen haben — dieser Kurs arbeitet den ganzen Vormittag in der Python-Konsole.
Warum Python 3.12 und nicht die neueste Version? Weil es für Python 3.14 und neuer kein fertiges pygame-Paket gibt (gebraucht ab Meilenstein 6). Die Installation versucht dann, pygame selbst zu übersetzen, und scheitert mit einer langen Fehlermeldung. Deshalb überall --python 3.12.
🖥️ Terminal
mkdir crackme
cd crackme
uv venv --python 3.12
uv pip install pygame # erst für Meilenstein 6 nötig, jetzt schon erledigen
Kontrolle:
🖥️ Terminal
uv run python -c "import pygame; print(pygame.version.ver)"
Erscheint eine Versionsnummer, ist alles bereit.
Jetzt in die Python-Konsole — der Ort zum Ausprobieren. Sie startet aus dem Terminal:
🖥️ Terminal
uv run python
Der Prompt wechselt auf >>>. Ab hier gilt Python, und jede Zeile antwortet sofort, ganz ohne print() und ohne dass du etwas speichern musst. Mittippen:
🐍 Python-Konsole
import hashlib
hashlib.sha256("geheim".encode()).hexdigest()
# '2c26b46b...' — 64 Hex-Zeichen, immer gleich lang
hashlib.sha256("geheiM".encode()).hexdigest()
# völlig anderer Wert — ein Bit Eingabe kippt den halben Hash
Drei Fragen zum Selberprobieren:
- Gib zweimal
"passwort"ein. Kommt derselbe Hash? (Ja — das ist der Sinn.) - Kannst du aus dem Hash das Passwort zurückrechnen? (Nein — Einbahnstraße.)
- Wie lang ist der Hash für ein 3-Zeichen-Passwort? Und für ein 30-Zeichen-Passwort?
💡 Ein Hash ist eine mathematische Einbahnstraße: leicht vorwärts, praktisch unmöglich rückwärts. Genau deshalb speichert kein anständiger Dienst dein Passwort im Klartext, sondern nur seinen Hash. Beim Login wird neu gehasht und verglichen. Was ein Angreifer bei einem Datenleck erbeutet, sind Hashes — und der ganze Rest dieses Kurses dreht sich um die Frage: Wie schnell kommt er von dort zum Klartext zurück?
Meilenstein 0.5 — Der Wächter: läuft das im richtigen Python? (20 min)
Ziel: Ein kleines Modul preflight.py, das vor dem eigentlichen Programm prüft, ob ein virtuelles Environment aktiv ist — und sonst eine klare Warnung mit Handlungsanweisung ausgibt.
Warum das die erste echte Aufgabe ist: Der häufigste Anfängerfehler ist kein Logikfehler, sondern ModuleNotFoundError: No module named 'pygame' — obwohl man es doch „installiert hat“. Ursache ist fast immer, dass pip in ein anderes Python installiert hat als das, mit dem das Skript läuft. Ein venv trennt das sauber. Ein Wächter, der das prüft, erspart dir heute Nachmittag eine Viertelstunde Ratlosigkeit — und nebenbei lernst du, wie ein Programm etwas über sich selbst herausfindet.
Hier bauen wir Funktionen, keine Klasse — bewusst. Die Prüfungen halten keinen Zustand fest, sie beantworten je eine Ja/Nein-Frage. Genau dafür sind Funktionen da. (Die Faustregel „Substantiv → Klasse, Verb → Methode“ gilt weiter: Hier gibt es nur Verben, kein Ding, das sich etwas merkt.)
Datei preflight.py:
📄 Datei
import os
import sys
from pathlib import Path
def in_venv() -> bool:
"""True, wenn der laufende Interpreter ein venv ist.
Der Trick: In einem venv zeigt sys.prefix auf die Umgebung, während
sys.base_prefix weiter auf das System-Python zeigt. Ohne venv sind
beide identisch. Das ist verlässlicher als die Umgebungsvariable
VIRTUAL_ENV — die fehlt, wenn man das venv-Python direkt aufruft,
obwohl dann alles korrekt läuft.
"""
# TODO: sys.prefix mit sys.base_prefix vergleichen
...
def finde_lokales_venv() -> Path | None:
"""Sucht im aktuellen Ordner nach einem vorhandenen, aber inaktiven venv.
Ein echtes venv erkennt man NICHT am Ordnernamen, sondern an der Datei
pyvenv.cfg darin. Prüfe die üblichen Namen: .venv, venv, env.
"""
# TODO: für jeden Kandidatennamen schauen, ob (ordner / "pyvenv.cfg")
# existiert. Ersten Treffer zurückgeben, sonst None.
...
def aktivierungsbefehl(venv: Path) -> str:
"""Der passende activate-Aufruf für das aktuelle Betriebssystem.
os.name ist 'nt' auf Windows, sonst 'posix' (macOS/Linux).
"""
# TODO: Windows -> f"{venv}\\Scripts\\activate"
# sonst -> f"source {venv}/bin/activate"
...
def ensure_venv(streng: bool = True) -> bool:
"""Der Wächter. Prüft die Umgebung und meldet das Ergebnis.
streng=True -> bei fehlendem venv das Programm mit sys.exit(1) beenden.
streng=False -> nur warnen, weiterlaufen lassen, False zurückgeben.
"""
# TODO:
# 1) in_venv()? -> kurze Erfolgsmeldung, True zurückgeben
# 2) sonst: Warnung ausgeben + sys.executable zeigen
# (welches Python läuft hier gerade?)
# 3) finde_lokales_venv():
# gefunden -> aktivierungsbefehl() ausgeben ("nur aktivieren!")
# keins da -> Anlege-Befehle ausgeben (uv venv --python 3.12)
# 4) streng -> sys.exit(1), sonst False zurückgeben
...
if __name__ == "__main__":
# Selbsttest: nur berichten, nie abbrechen.
print(f"Interpreter : {sys.executable}")
print(f"sys.prefix : {sys.prefix}")
print(f"base_prefix : {sys.base_prefix}")
ensure_venv(streng=False)
So testest du beide Zustände (das ist der Beweis, nicht das Raten):
🖥️ Terminal — Fall 1: mit Umgebung, muss „venv aktiv“ melden
uv run python preflight.py
🖥️ Terminal — Fall 2: ohne Umgebung, muss warnen und die Anlege-Befehle zeigen
# Windows (PowerShell)
$sys = uv python find --system
& $sys preflight.py
# Mac
"$(uv python find --system)" preflight.py
💡 Warum dieser Umweg? In älteren Anleitungen steht an dieser Stelle
deactivate. Das funktioniert hier nicht, weiluv rundie Umgebung gar nicht erst „aktiviert“ — es startet das Python aus.venvdirekt. Es gibt also nichts zu deaktivieren.uv python find --systemliefert stattdessen den Pfad zu einem Python außerhalb deiner Projektumgebung, und genau damit rufst du dein Skript auf. Findet der Befehl nichts (möglich, wenn auf dem Rechner nur uv-Pythons liegen), reicht für heute der erste Test.Nebenbei ist das ein hübscher Beleg für die Prüffrage unten:
uv runsetztVIRTUAL_ENV, der direkte Aufruf des venv-Pythons aber nicht — und trotzdem läuft er im venv. Wer sich auf die Umgebungsvariable verlässt, liegt in genau diesem Fall falsch.sys.prefix != sys.base_prefixliegt immer richtig.
Wenn beide Läufe das Richtige sagen, hängst du eine Zeile ganz oben in spielen.py (Meilenstein 4) und später in gui.py:
📄 Datei spielen.py
from preflight import ensure_venv
ensure_venv() # bricht ab, wenn kein venv aktiv ist
Für Schnelle: Erweitere ensure_venv um paket: str = None. Ist ein venv aktiv, prüfe mit importlib.util.find_spec(paket), ob z. B. "pygame" importierbar ist — und gib sonst den passenden uv pip install-Befehl aus. Damit fängst du die „ins falsche Python installiert“-Falle direkt ab.
✅ Checkpoint 0.5 — Prüffragen: - Warum ist
sys.prefix != sys.base_prefixbesser als ein Blick aufos.environ["VIRTUAL_ENV"]? - Wieso suchst du nachpyvenv.cfgund nicht einfach nach einem Ordner namens.venv? -ensure_venvgibt Warnungen mitfile=sys.stderraus, Erfolg mit normalempygamedrin. Es prüft nur die Umgebung.)Hinweis Kursleitung: Eine vollständige Referenz-Implementierung liegt separat bereit, falls es klemmt — erst nach eigenem Versuch zeigen.
Kapitel A — Entropie: die einzige Formel, die du heute brauchst (40 min)
Kein Frontalunterricht. Herleiten.
Frage 1: Ein Passwort besteht aus einer einzigen Ziffer (0–9). Wie viele Möglichkeiten muss ein Angreifer durchprobieren? → 10.
Frage 2: Zwei Ziffern? → 100 = 10². Drei? → 1000 = 10³.
Muster: Bei L Stellen aus einem Vorrat von N Zeichen gibt es N^L Möglichkeiten.
Diese Zahl wächst brutal schnell — zu schnell, um sie bequem hinzuschreiben. Deshalb misst man sie im Logarithmus, und das nennt man Entropie, gemessen in Bit:
H = L · log₂(N)
Was ein Bit bedeutet: Jedes zusätzliche Bit Entropie verdoppelt den Aufwand für den Angreifer. 40 Bit sind nicht doppelt so schwer wie 20 Bit — sie sind 2²⁰-mal, also millionenfach schwerer.
Der Zeichenvorrat N:
| Enthält | trägt zu N bei |
|---|---|
| Kleinbuchstaben | 26 |
| Großbuchstaben | 26 |
| Ziffern | 10 |
| Sonderzeichen | ~33 |
Jetzt der Moment, der alles trägt — rechne beides an der Tafel aus:
P@ssw0rt! → 9 Zeichen, alle vier Klassen, N = 95 → H = 9 · log₂(95) ≈ 59 Bit. Sieht stark aus.
pferd-batterie-heftklammer-kaktus → 33 Zeichen, N = 27 → H ≈ 33 · 4.75 ≈ 157 Bit. Auch stark.
Und hier kommt der Haken, der den ganzen Tag zusammenhält:
Diese Formel überschätzt die Sicherheit von Menschen-Passwörtern dramatisch. Sie gilt nur, wenn jedes Zeichen zufällig und unabhängig gewählt wurde.
P@ssw0rt!ist aber nicht zufällig — es ist ein Wörterbuchwort mit vorhersehbaren Ersetzungen. Die echte Entropie ist winzig. Genau diese Lücke zwischen „sieht sicher aus“ und „ist sicher“ wird dein dreistufiger Cracker gleich schonungslos aufdecken.
Mini-Übung — in der Python-Konsole (uv run python, Prompt >>>):
🐍 Python-Konsole
import math
def entropie_naiv(N, L):
return L * math.log2(N)
entropie_naiv(95, 9) # ~59
entropie_naiv(27, 33) # ~157
entropie_naiv(10, 4) # PIN-Code — wie viele Bit? Fühlt sich das sicher an?
Meilenstein 1 — Die Klasse Passwort (45 min)
Ziel: Ein Objekt, das über sich selbst Auskunft gibt: Länge, Zeichenvorrat, naive Entropie. Ganz ohne Angriff, ganz ohne Grafik.
Datei passwort.py:
📄 Datei
import math
SONDERZEICHEN = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~ "
class Passwort:
"""Ein zu prüfendes Passwort. Kennt seine eigenen Kennzahlen."""
def __init__(self, klartext: str):
self.klartext = klartext
def laenge(self) -> int:
# TODO
...
def zeichenraum(self) -> int:
"""Summe der genutzten Zeichenklassen (26/26/10/len(SONDERZEICHEN))."""
# TODO: any(c.islower() ...) etc., aufaddieren
...
def entropie(self) -> float:
"""Naive Entropie in Bit: L * log2(N). 0.0 bei leerem Passwort."""
# TODO: Achtung — log2(0) kracht. Sonderfall abfangen.
...
def staerke_text(self) -> str:
"""Grobe Einordnung: <28 schwach, <36 mäßig, <60 gut, sonst stark."""
# TODO
...
def __repr__(self) -> str:
return f""
Testen — nicht raten, ausprobieren!
Deine Klasse steht in passwort.py. Zum Ausprobieren startest du die Python-Konsole im selben Ordner und importierst sie:
🖥️ Terminal
uv run python
🐍 Python-Konsole
from passwort import Passwort
print(Passwort("P@ssw0rt!").entropie()) # ~59
print(Passwort("hallo").zeichenraum()) # 26
print(Passwort("Hallo123!").zeichenraum()) # 26+26+10+33 = 95
print(Passwort("")) # muss ohne Absturz durchlaufen
Für Schnelle: Schreib zeichenraum() datengetrieben — eine Liste [(pruef_funktion, groesse), ...] und eine Schleife statt vier if. Frage: Warum ist das besser, wenn morgen Emojis dazukommen sollen?
✅ Checkpoint 1 — Prüffragen: - Warum liefert
entropie()für""eine 0 und keinen Absturz? Wo fängst du das ab? -"aaaa"und"xkqz"bekommen dieselbe Entropie. Ist das gerecht? (Nein — und genau das repariert Meilenstein 2.) - Steht in dieser Datei irgendwoimport pygame? (Darf nicht.)
Meilenstein 2 — Die Angriffs-Hierarchie (55 min)
Ziel: Zeigen, dass die naive Entropie lügt. Zwei Angriffe, die dieselbe Schnittstelle teilen — hier lernt man Vererbung mit Nutzen, nicht als Selbstzweck.
Datei angriff.py:
📄 Datei
from passwort import Passwort
class Angriff:
"""Basisklasse. Jeder Angriff sagt: geknackt oder nicht — und nach wie vielen Versuchen."""
name = "Angriff"
def knackt(self, pw: Passwort) -> int | None:
"""Gibt die Zahl der Versuche zurück, wenn erfolgreich — sonst None."""
raise NotImplementedError
class WoerterbuchAngriff(Angriff):
name = "Wörterbuch"
def __init__(self, woerter: list[str]):
# Set statt Liste: Nachschlagen in O(1). (Erinnerung aus dem OOP-Kurs.)
self.woerter = set(w.lower() for w in woerter)
def knackt(self, pw: Passwort) -> int | None:
# TODO: steht pw.klartext.lower() in self.woerter?
# Wenn ja: Position im Wörterbuch als "Versuche" zurückgeben,
# sonst None.
...
LEET = {"a": "@", "e": "3", "i": "1", "o": "0", "s": "$", "t": "7", "l": "1"}
class RegelAngriff(Angriff):
"""Der gefährliche. Nimmt jedes Wörterbuchwort und wendet die Tricks an,
die echte Menschen benutzen: Groß am Anfang, Zahl/Zeichen hinten, Leetspeak."""
name = "Regeln (Leetspeak & Co.)"
def __init__(self, woerter: list[str]):
self.woerter = [w.lower() for w in woerter]
def varianten(self, wort: str) -> list[str]:
"""Erzeugt die üblichen Verhunzungen eines Grundworts."""
# TODO: mindestens:
# - wort
# - wort.capitalize()
# - wort + "1", wort + "123", wort + "!"
# - Leetspeak: jedes Zeichen per LEET ersetzen
# - Kombinationen davon (capitalize + leet + "!")
...
def knackt(self, pw: Passwort) -> int | None:
# TODO: für jedes Grundwort alle varianten() durchgehen,
# Versuche mitzählen, bei Treffer die Zahl zurückgeben
...
Der Beweis (Terminal):
🐍 Python-Konsole
woerter = ["passwort", "sommer", "drache", "hallo"] # später echte Wortliste
wb = WoerterbuchAngriff(woerter)
regel = RegelAngriff(woerter)
print(wb.knackt(Passwort("passwort"))) # kleine Zahl → sofort weg
print(wb.knackt(Passwort("P@ssw0rt!"))) # None — Wörterbuch allein reicht nicht
print(regel.knackt(Passwort("P@ssw0rt!"))) # eine Zahl! Die Regeln kriegen es doch.
Das ist der Kern des ganzen Spiels: P@ssw0rt! hatte laut Formel 59 Bit — und fällt nach ein paar hundert Versuchen. Die naive Entropie war eine Illusion, weil das Passwort nicht zufällig war, sondern ein Wort plus berechenbare Regeln.
🔥 Hier zahlt sich die Vererbung aus: Zwei völlig verschiedene Angriffe, dieselbe Methode
knackt(). Der Cracker im nächsten Meilenstein muss keinen der beiden kennen — er ruft nur.knackt()und.nameauf. Neue Angriffsart hinzufügen = neue Klasse, null Änderung am Rest.✅ Checkpoint 2 — Prüffragen: - Warum ist
self.woerterbeim Wörterbuchangriff einset, beim Regelangriff einelist? -varianten("drache")— wie viele Strings kommen da raus? Multipliziere: Groß/klein × Leet × Anhänge. - Was passiert mit der „Zahl der Versuche“, wenn das Wörterbuch 100.000 Wörter hat statt 4?
Meilenstein 3 — Brute Force und die Zeit (45 min)
Ziel: Wenn Wörterbuch und Regeln versagen, bleibt rohe Gewalt: alles durchprobieren. Das dauert — und wie lange ist die eigentliche Botschaft.
Kein echtes Durchprobieren (das liefe bis nach Feierabend und länger). Stattdessen gerechnet:
Möglichkeiten = N ^ L
Zeit (Sekunden) = (Möglichkeiten / 2) / Raten_pro_Sekunde
Warum / 2? Im Schnitt findet man ein zufälliges Passwort nach der Hälfte aller Versuche.
Ergänze angriff.py:
📄 Datei
class BruteForce(Angriff):
name = "Brute Force"
def __init__(self, raten_pro_sekunde: float):
self.rate = raten_pro_sekunde
def sekunden(self, pw: Passwort) -> float:
# TODO: (pw.zeichenraum() ** pw.laenge()) / 2 / self.rate
...
def knackt(self, pw: Passwort) -> int | None:
# Brute Force "knackt" letztlich immer — die Frage ist nur: in welcher Zeit.
# Für die Spiellogik geben wir hier None zurück und werten sekunden() aus.
return None
Und ein Helfer, der Sekunden menschlich macht (eigene kleine Datei zeit.py oder unten in angriff.py):
📄 Datei
def zeit_lesbar(sekunden: float) -> str:
"""Aus einer nackten Sekundenzahl wird 'geschätzt 3 Jahre' o.ä."""
einheiten = [
(1, "Sekunden"), (60, "Minuten"), (3600, "Stunden"),
(86400, "Tage"), (31_536_000, "Jahre"),
]
# TODO: passende Einheit finden; wenn > Alter des Universums (~4.35e17 s),
# gib "länger als das Universum existiert" zurück — der beste Lacher des Tages
...
Testen:
🐍 Python-Konsole — Konsole nach jeder Dateiänderung neu starten, sonst gilt noch die alte Fassung
schnell = BruteForce(1e10) # GPU, schneller (ungesalzener) Hash
print(zeit_lesbar(schnell.sekunden(Passwort("abc")))) # Sekundenbruchteile
print(zeit_lesbar(schnell.sekunden(Passwort("Tr0ub4dor&3")))) # …?
print(zeit_lesbar(schnell.sekunden(Passwort("pferd-batterie-heftklammer-kaktus"))))
🔬 Physik/Mathe-Bezug: Jedes zusätzliche Zeichen multipliziert die Zeit mit
N— exponentielles Wachstum, dieselbe Kurve wie Zinseszins oder Kettenreaktion. Deshalb schlägt Länge fast immer Komplexität: Ein Zeichen mehr bringt mehr als ein Sonderzeichen an unvorhersehbarer Stelle.
Meilenstein 4 — Die Klasse Cracker (60 min)
Der wichtigste Meilenstein. Alles kommt zusammen, und am Ende ist das Spiel im Terminal fertig — ohne eine Zeile Pygame.
Datei cracker.py:
📄 Datei
from dataclasses import dataclass
from passwort import Passwort
from angriff import WoerterbuchAngriff, RegelAngriff, BruteForce, zeit_lesbar
@dataclass
class Urteil:
"""Das Ergebnis eines kompletten Angriffslaufs."""
gefallen_durch: str | None # Name des erfolgreichen Angriffs, oder None
versuche: int # bis zum Treffer (0 bei Brute Force)
brute_force_zeit: str # lesbare Restschätzung, falls es so weit kam
entropie: float
class Cracker:
"""Orchestriert die drei Stufen in fester Reihenfolge:
schnell und billig zuerst, teuer zuletzt — genau wie ein echter Angreifer."""
def __init__(self, woerter: list[str], rate: float = 1e10):
self.stufen = [
WoerterbuchAngriff(woerter),
RegelAngriff(woerter),
]
self.brute = BruteForce(rate)
def pruefe(self, pw: Passwort) -> Urteil:
# TODO:
# 1) jede Stufe der Reihe nach: knackt() != None? -> Urteil, fertig
# 2) überlebt alles? -> Brute-Force-Zeit schätzen, ins Urteil packen
...
Aufgabe: das Terminal-Spiel (spielen.py, ca. 25 Zeilen). Ganz oben steht der Wächter aus Meilenstein 0.5 — so scheitert das Programm mit einer klaren Meldung statt mit einem kryptischen Importfehler:
📄 Datei spielen.py
from preflight import ensure_venv
ensure_venv() # kein venv aktiv -> saubere Warnung + Abbruch
Und so sieht eine Runde aus:
=== CRACK ME ===
Bau ein Passwort, das alle drei Stufen überlebt.
Passwort: sommer2024
[Wörterbuch] durchsucht ...
[Regeln] TREFFER nach 1.284 Versuchen — gefallen.
Entropie (naiv): 33 Bit. Echte: viel weniger.
Nächster Versuch?
Passwort: pferd-batterie-heftklammer-kaktus
[Wörterbuch] überlebt.
[Regeln] überlebt.
[Brute Force] geschätzt: länger als das Universum existiert.
>>> GEWONNEN. Punktzahl: 157
Die echte Wortliste: nimm die häufigsten deutschen Wörter (z. B. eine frei verfügbare Häufigkeitsliste, 10.000 Zeilen reichen). Einlesen in drei Zeilen, .strip() nicht vergessen.
✅ Checkpoint 3 — Prüffragen: - Warum läuft der Wörterbuchangriff vor dem Brute Force und nicht danach? - Steht in
passwort.py,angriff.py,cracker.pyirgendwopygame? (Nein → Regel 2 gehalten.) - Das Spiel ist jetzt komplett spielbar. Was fehlt noch außer Optik?Motivations-Moment: Der Kern steht. Der Rest ist Show — und ein zweiter, tieferer Aha-Moment.
Meilenstein 5 — Hashing und Salting: warum dasselbe Passwort mal sicher, mal verloren ist (60 min)
Die entscheidende Erkenntnis des Tages: Wie schnell ein Passwort fällt, hängt nicht nur vom Passwort ab — sondern davon, wie der Dienst es gespeichert hat. Dasselbe Sommer2024! ist bei dem einen Anbieter in Sekunden weg und beim anderen in Jahrhunderten sicher.
Teil 1 — Der Rainbow-Table-Trick
🐍 Python-Konsole
import hashlib
def sha(s): return hashlib.sha256(s.encode()).hexdigest()
# Zwei Nutzer, gleiches Passwort:
print(sha("sommer")) # identischer Hash!
print(sha("sommer")) # identischer Hash!
Das Problem: Gleiches Passwort → gleicher Hash. Ein Angreifer rechnet die Hashes der Top-Million-Passwörter ein einziges Mal vor (eine „Rainbow Table“) und schlägt danach jedes geleakte Passwort nach. Kein Rechnen mehr, nur Nachschlagen.
Teil 2 — Salt macht Hashes einzigartig
🐍 Python-Konsole
import os
def sha_salted(passwort, salt):
return hashlib.sha256((salt + passwort).encode()).hexdigest()
salt1 = os.urandom(8).hex()
salt2 = os.urandom(8).hex()
print(sha_salted("sommer", salt1)) # ...
print(sha_salted("sommer", salt2)) # völlig anders!
Der Salt ist ein zufälliger Zusatz, der pro Nutzer gespeichert wird. Gleiche Passwörter ergeben jetzt verschiedene Hashes — die vorberechnete Tabelle ist wertlos, der Angreifer muss jeden Account einzeln angreifen.
Teil 3 — Schnelle vs. langsame Hashes
Das ist der Grund, warum die rate in Meilenstein 3 so entscheidend war:
| Hash-Verfahren | Ratenversuche/Sekunde (GPU) | gedacht für |
|---|---|---|
| SHA-256 (nackt) | ~10.000.000.000 | Dateiprüfsummen — nicht für Passwörter |
| bcrypt / Argon2 | ~10.000–100.000 | genau für Passwörter, absichtlich langsam |
Eine Idee, ein Effekt: bcrypt macht dasselbe Passwort millionenfach teurer zu knacken, indem es das Hashing absichtlich verlangsamt. Für den einen Login des ehrlichen Nutzers: unmerklich. Für die Milliarden Rateversuche des Angreifers: tödlich.
Aufgabe: Baue in den Cracker einen Umschalter für die Rate (1e10 für „nackter Hash“, 1e5 für „bcrypt“) und lass dasselbe mittelmäßige Passwort durchlaufen. Der Unterschied in zeit_lesbar() ist die ganze Lektion.
✅ Checkpoint 4 — Prüffragen: - Warum wird der Salt nicht geheim gehalten, sondern offen gespeichert? Wogegen schützt er dann? - Zwei Nutzer, Passwort
123456. Mit Salt — sieht man im Leak noch, dass es dasselbe ist? - Wenn du morgen der Admin bist: schnellen oder langsamen Hash? Warum?
Meilenstein 6 — Die Show: Pygame-Fortschrittsbalken (60 min)
Jetzt die Optik — und keine Zeile Spiellogik mehr, nur Darstellung von Ergebnissen, die längst berechnet sind.
Das Herzstück jedes Programms mit Fenster ist die Game Loop: 60-mal pro Sekunde Ereignisse lesen → Zustand aktualisieren → zeichnen.
Datei gui.py:
📄 Datei
import pygame
from passwort import Passwort
from cracker import Cracker
BREITE, HOEHE = 800, 500
HG = (12, 16, 28)
class App:
def __init__(self, woerter):
pygame.init()
self.cracker = Cracker(woerter)
self.fenster = pygame.display.set_mode((BREITE, HOEHE))
self.schrift = pygame.font.SysFont("Consolas", 22) # EINMAL, nicht in der Schleife!
self.uhr = pygame.time.Clock()
self.eingabe = ""
self.urteil = None
self.laeuft = True
def ereignisse(self):
for e in pygame.event.get():
if e.type == pygame.QUIT:
self.laeuft = False
elif e.type == pygame.KEYDOWN:
if e.key == pygame.K_RETURN:
# TODO: self.urteil = self.cracker.pruefe(Passwort(self.eingabe))
...
elif e.key == pygame.K_BACKSPACE:
self.eingabe = self.eingabe[:-1]
else:
self.eingabe += e.unicode
def zeichnen(self):
self.fenster.fill(HG)
# TODO: Eingabefeld, Entropie-Balken (Breite ∝ Entropie),
# drei Stufen-Lämpchen (grün überlebt / rot gefallen),
# Brute-Force-Zeit als Text
pygame.display.flip()
def start(self):
while self.laeuft:
self.ereignisse()
self.zeichnen()
self.uhr.tick(60)
pygame.quit()
Der Balken macht Entropie sichtbar: Balkenbreite proportional zur Entropie, Farbe von rot (schwach) über gelb nach grün (stark). Tippt man Zeichen für Zeichen, wächst er live — man sieht, wie jedes zusätzliche Zeichen mehr bringt als jedes Sonderzeichen.
⚠️ SysFont gehört in __init__, nie in zeichnen() — sonst lädst du 60-mal pro Sekunde eine Schriftart. Merksatz: Was sich nicht ändert, gehört in den Konstruktor.
✅ Checkpoint 5: Grafisch spielbar. Der Balken wächst live mit. Zeit für eine Pause.
Meilenstein 7 — Highscore und Politur (Rest des Tages)
Freie Auswahl:
- Punktzahl = überlebte Stufen × Faktor + Entropie. Bestenliste in
highscore.json(json.dump/json.load). - Live-Tipp unter dem Feld: „Ein Wörterbuchwort erkannt — häng nicht nur eine 1 an.“ Reagiert auf das Urteil.
- Zwei-Spieler-Duell: Wer baut in 60 Sekunden das stärkste merkbare Passwort? (Regel: muss auswendig eintippbar sein.)
- Der Ehrlichkeits-Schalter: Zeige naive Entropie neben echter — wenn ein Angriff greift, war die echte Entropie eben nur
log₂(Versuche). Die Lücke zwischen beiden Zahlen ist die Kernbotschaft, jetzt als Grafik. - Eigenes Wörterbuch laden: Was passiert, wenn der eigene Nachname in der Liste steht?
Anhang A — Fehlermeldungen lesen
Immer von unten nach oben. Letzte Zeile = was, vorletzte = wo.
| Meldung | Bedeutet meistens |
|---|---|
ValueError: math domain error |
log2(0) oder log2(negativ) — Sonderfall vergessen |
AttributeError: 'Passwort' object has no attribute ... |
Tippfehler oder self. nie gesetzt |
TypeError: unhashable type: 'list' |
Liste in ein set gesteckt (nur Tupel/Strings gehen) |
KeyError |
Zugriff auf einen Dict-Schlüssel, den es nicht gibt |
OverflowError / hängt ewig |
N ** L bei riesigem L — nur rechnen, nie ausiterieren |
Die zweite Sorte Fehler kommt nicht aus deinem Code, sondern daraus, dass etwas am falschen Ort steht:
| Meldung | Bedeutet |
|---|---|
ModuleNotFoundError: No module named 'pygame' |
mit python … statt uv run … gestartet |
SyntaxError bei einem uv- oder cd-Befehl |
Terminal-Befehl in der Python-Konsole → exit() |
… nicht als Name eines Cmdlets erkannt / zsh: number expected |
Python-Code im Terminal → uv run python |
ModuleNotFoundError: No module named 'passwort' |
Python-Konsole läuft in einem anderen Ordner als die Datei |
| Änderung wirkt nicht, alter Fehler bleibt | Konsole neu starten — ein Import wird nicht aufgefrischt |
Ausführlich stehen die im Kapitel „Erste Hilfe“ der Systemeinrichtung.
print(self.urteil) verrät fast alles — wenn du __repr__ geschrieben hast.
Anhang B — Spickzettel
import hashlib, math, os
hashlib.sha256(s.encode()).hexdigest() # Hash
os.urandom(8).hex() # zufälliger Salt
math.log2(N) # Logarithmus zur Basis 2
# Entropie
H = L * math.log2(N) # Bit
moeglichkeiten = N ** L # so viele Kombinationen
# Vererbung mit Nutzen
class Angriff:
def knackt(self, pw): raise NotImplementedError
class Woerterbuch(Angriff):
def knackt(self, pw): ... # eigene Umsetzung
# Set = schnelles Nachschlagen
bekannt = {"passwort", "123456"}
"passwort" in bekannt # O(1)
# f-String mit Format
print(f"{H:.1f} Bit, {sekunden:,.0f} s")
⚠️ Rechne N ** L immer nur aus — iteriere es nie durch. 95 ** 12 ist eine harmlose Zahl; alle 95 ** 12 Passwörter zu erzeugen sprengt jede Festplatte.
Anhang C — Wenn es hakt: die Hilfeleiter
Nicht die Lösung geben. In dieser Reihenfolge:
- „Lies die Fehlermeldung laut. Welche Zeile?“
- „Was erwartest du hier? Setz ein
print()davor.“ - „Erklär mir die Methode Satz für Satz.“ (funktioniert oft schon bei Satz zwei)
- „Welche der drei Hausregeln ist hier verletzt?“
- Erst jetzt ein konkreter Zeilenhinweis — nie die fertige Zeile.
Abschluss (15 min)
Vorführen und in drei Sätzen erklären: Was macht Passwort, was macht Angriff, was macht Cracker? Wer das beantworten kann, hat an einem Tag den Kern der IT-Sicherheit verstanden:
Sicherheit ist keine Eigenschaft eines Passworts, sondern ein Wettrennen. Auf der einen Seite Länge und echte Zufälligkeit, auf der anderen Wörterbücher, Regeln, Rechenleistung und die Frage, wie der Dienst gehasht hat. Man gewinnt das Rennen nicht mit
!, sondern mit Bits.
Und die Frage, die hängen bleibt:
„Dein Cracker knackt P@ssw0rt! in einer Sekunde und scheitert an vier zufälligen Wörtern für immer. Beides sah gleich sicher aus. Willst du wissen, welche anderen Dinge in der IT nur aussehen, als wären sie sicher?“
⚖️ Zum Schluss, wie am Anfang: Dieses Werkzeug lief ausschließlich gegen selbst eingegebene Passwörter. Wer dieselben Techniken gegen fremde Konten oder Systeme richtet, hackt nicht ethisch, sondern begeht eine Straftat. Ethical Hacking heißt Auftrag, definierter Umfang und schriftliche Erlaubnis — kein
sudoohne Unterschrift.Die Hinweise auf §202a StGB in diesem Kurs ordnen die Rechtslage grob ein und ersetzen keine Rechtsberatung. Wer beruflich Sicherheitstests durchführt, klärt Umfang und Erlaubnis vorher schriftlich.