Ir al contenido

Crack Me — das Passwort-Kraftwerk

Martin Schmid mit Claude · 2026-08-13

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):

  1. Keine globalen Variablen. Zustand lebt in Objekten.
  2. Die Logik kennt kein Pygame. Kein pygame. in passwort.py, angriff.py, cracker.py.
  3. 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, weil uv run die Umgebung gar nicht erst „aktiviert“ — es startet das Python aus .venv direkt. Es gibt also nichts zu deaktivieren. uv python find --system liefert 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 run setzt VIRTUAL_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_prefix liegt 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_prefix besser als ein Blick auf os.environ["VIRTUAL_ENV"]? - Wieso suchst du nach pyvenv.cfg und nicht einfach nach einem Ordner namens .venv? - ensure_venv gibt Warnungen mit file=sys.stderr aus, Erfolg mit normalem print. Warum trennt man das? - Verletzt dieses Modul Hausregel 2? (Nein — kein pygame drin. 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 irgendwo import 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 .name auf. Neue Angriffsart hinzufügen = neue Klasse, null Änderung am Rest.

Checkpoint 2 — Prüffragen: - Warum ist self.woerter beim Wörterbuchangriff ein set, beim Regelangriff eine list? - 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.py irgendwo pygame? (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:

  1. „Lies die Fehlermeldung laut. Welche Zeile?“
  2. „Was erwartest du hier? Setz ein print() davor.“
  3. „Erklär mir die Methode Satz für Satz.“ (funktioniert oft schon bei Satz zwei)
  4. „Welche der drei Hausregeln ist hier verletzt?“
  5. 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 sudo ohne 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.