← Startseite

Lab 5: Sicherheit I — Hashing & Passwörter

Fortgeschrittene Algorithmen und Programmierung • HTW Berlin
Dauer: ~ 180 Minuten (inkl. Bonus) • deckt Vorlesung 8 ab (Hashing & Passwörter)

🧠 Worum geht es?

In Vorlesung 8 ging es um Hashing & Passwörter — warum man Passwörter niemals im Klartext speichert, was ein Hash ist (ein Fingerabdruck, eine Einbahnstraße, mit Lawineneffekt), wie der Login-Ablauf mit Hashing funktioniert, was ein Salt macht und welche Algorithmen (bcrypt/Argon2) man für Passwörter nimmt.

Heute baut ihr das alles selbst: vom naiven Hash über einen Mini-Hash, eine Login-Simulation, einen Dictionary-Angriff, Salting — bis hin zu echtem SHA-256 (OpenSSL) und bcrypt. Am Ende habt ihr die komplette Kette von „so macht man es falsch" bis „so macht man es richtig" mit eigenen Händen durchgespielt.

🌐 Programmiersprache

Aufgaben 1–5 sind reines C — sie laufen überall, ohne jedes Setup. Aufgaben 6–7 nutzen C-Krypto-Bibliotheken (OpenSSL bzw. libcrypt). Wer in einer anderen Sprache arbeitet, nutzt deren Krypto-Bibliothek (z. B. hashlib in Python).

Musterlösungen liegen nur in C vor. Die Logik ist 1:1 übertragbar — es ändert sich nur die Syntax. Passwörter sind ganz unten frei zugänglich.

⚙️ Setup für Aufgabe 6 & 7:

🎯 Lernziele

🔑 Hashing verstehen

Hash als Einbahnstraße, Lawineneffekt, warum naive Hashes versagen

🔐 Passwörter sicher speichern

Login-Ablauf mit Hash — niemals Klartext in der Datenbank

🧂 Salt & Angriffe

Dictionary-/Rainbow-Tables — und wie ein Salt sie aushebelt

🛡️ Echte Krypto

SHA-256 & bcrypt mit Bibliotheken statt selbstgebautem Hash

1 Der naive Hash & seine Kollisionen ⏱️ 15 Min VL 8: Hashing

Ziel: einen bewusst „schlechten" Hash bauen und sehen, warum er versagt — Kollisionen und kein Lawineneffekt.

Aufgabe:

  1. Implementiere bad_hash(str), das einfach die ASCII-Werte aller Zeichen aufaddiert.
  2. Gib den Hash für "abc", "bac", "cab" und "hallo" aus.
  3. Beobachte: "abc", "bac" und "cab" haben denselben Hash 294 — eine Kollision.
🤔 Denkfrage: Warum ist das fatal, wenn man so einen Hash für Passwörter nutzt? Überlege: Wenn dein Passwort "abc" ist und jemand "bac" eingibt — was passiert beim Login? (Antwort: Er kommt rein, obwohl er dein Passwort gar nicht kennt — gleicher Hash = Zugang.)
🔒 Musterlösung in C
Falsches Passwort!
#include <stdio.h>

unsigned int bad_hash(char str[]) {
    unsigned int hash = 0;
    for (int i = 0; str[i] != '\0'; i++) {
        hash += str[i];          // ASCII-Wert addieren
    }
    return hash;
}

int main(void) {
    char woerter[4][10] = {"abc", "bac", "cab", "hallo"};
    for (int i = 0; i < 4; i++) {
        printf("bad_hash(\"%s\") = %u\n", woerter[i], bad_hash(woerter[i]));
    }
    return 0;
}

Erwartete Ausgabe:

bad_hash("abc") = 294
bad_hash("bac") = 294
bad_hash("cab") = 294
bad_hash("hallo") = 532

2 Mini-Hash mit Multiplikation ⏱️ 20 Min VL 8: Hashing

Ziel: einen besseren (aber immer noch rein pädagogischen, nicht kryptosicheren) Hash bauen — mit der Formel state = (state*31 + ASCII) % 1000. Jetzt zählt die Reihenfolge der Zeichen, und der Lawineneffekt ist am Anfang stärker.

Aufgabe:

  1. Implementiere mini_hash(str) mit der Formel oben.
  2. Hashe "Hi", "HALLO", "HALLI" und "BALLO".
  3. Beobachte: "HALLO" und "HALLI" unterscheiden sich nur im letzten Buchstaben, geben aber verschiedene Hashes — die Reihenfolge zählt nun.
💡 Hinweis: Die 31 ist der „Mixer" (eine Primzahl — sie sorgt dafür, dass die Reihenfolge der Zeichen zählt). Das % 1000 ist der „Eimer" — eine feste Ausgabegröße, genau wie SHA-256 immer einen 256-Bit-State hat. Echte Hashes nutzen dasselbe Prinzip, nur mit viel größeren Zahlen.
⚠️ Achtung: Das ist nur fürs Verständnis. Niemals selbstgebaute Hashes für echte Passwörter verwenden — dafür gibt es bcrypt & Argon2 (siehe Aufgabe 7).
🔒 Musterlösung in C
Falsches Passwort!
#include <stdio.h>

unsigned int mini_hash(char str[]) {
    unsigned int state = 0;
    for (int i = 0; str[i] != '\0'; i++) {
        state = (state * 31 + str[i]) % 1000;
    }
    return state;
}

int main(void) {
    char woerter[4][10] = {"Hi", "HALLO", "HALLI", "BALLO"};
    for (int i = 0; i < 4; i++) {
        printf("mini_hash(\"%s\") = %u\n", woerter[i], mini_hash(woerter[i]));
    }
    return 0;
}

Erwartete Ausgabe:

mini_hash("Hi") = 337
mini_hash("HALLO") = 398
mini_hash("HALLI") = 392
mini_hash("BALLO") = 272

3 Passwort-Login simulieren ⏱️ 20 Min VL 8: Passwörter

Ziel: den Login-Ablauf nachbauen — bei der „Registrierung" wird nur der Hash gespeichert. Beim Login wird die Eingabe gehasht und mit dem gespeicherten Hash verglichen. Das Klartext-Passwort wird nie gespeichert.

Aufgabe:

  1. Übernimm mini_hash aus Aufgabe 2.
  2. Speichere bei der „Registrierung" den Hash von "geheim123" in einer Variablen.
  3. Lies eine Eingabe mit scanf ein, hashe sie und vergleiche mit dem gespeicherten Hash.
  4. Gib "Login OK" oder "Falsches Passwort" aus.
🤔 Denkfrage: Was passiert, wenn jemand ein anderes Wort eingibt, das zufällig denselben Mini-Hash hat? Genau — er kommt rein. Das ist die Schwäche kleiner Hashes (hier nur 1000 mögliche Werte). Echte Hashes haben 256 Bit — so viele mögliche Werte, dass eine zufällige Kollision praktisch unmöglich ist.
🔒 Musterlösung in C
Falsches Passwort!
#include <stdio.h>

unsigned int mini_hash(char str[]) {
    unsigned int state = 0;
    for (int i = 0; str[i] != '\0'; i++) {
        state = (state * 31 + str[i]) % 1000;
    }
    return state;
}

int main(void) {
    // Registrierung: NUR der Hash wird gespeichert, nie das Klartext-Passwort
    unsigned int gespeicherter_hash = mini_hash("geheim123");

    char eingabe[50];
    printf("Passwort: ");
    scanf("%49s", eingabe);

    if (mini_hash(eingabe) == gespeicherter_hash) {
        printf("Login OK\n");
    } else {
        printf("Falsches Passwort\n");
    }
    return 0;
}

Erwartete Ausgabe (Eingabe geheim123):

Passwort: geheim123
Login OK

4 Dictionary-Angriff: einen Hash knacken ⏱️ 20 Min VL 8: Angriffe

Ziel: zeigen, wie ein Angreifer einen gestohlenen Hash knackt, indem er ein Wörterbuch häufiger Passwörter durchprobiert — das Prinzip hinter Dictionary-Angriffen und Rainbow-Tables.

Aufgabe:

  1. Nimm einen „gestohlenen" Hash an: mini_hash("sommer2024") — so, als hätte ein Angreifer ihn aus einer geklauten Datenbank.
  2. Lege eine Wortliste häufiger Passwörter an und probiere jedes durch.
  3. Wenn ein Hash passt: das geknackte Passwort ausgeben.
🤔 Denkfrage: Warum funktioniert dieser Angriff überhaupt? Weil dasselbe Passwort immer denselben Hash ergibt — ohne Salt. Ein Angreifer kann die Hashes häufiger Passwörter einmal vorberechnen (Rainbow-Table) und dann blitzschnell nachschlagen. Genau das verhindert ein Salt (Aufgabe 5).
🔒 Musterlösung in C
Falsches Passwort!
#include <stdio.h>

unsigned int mini_hash(char str[]) {
    unsigned int state = 0;
    for (int i = 0; str[i] != '\0'; i++) {
        state = (state * 31 + str[i]) % 1000;
    }
    return state;
}

int main(void) {
    unsigned int gestohlener_hash = mini_hash("sommer2024");  // aus der geklauten DB

    char woerterbuch[5][12] = {"123456", "passwort", "qwertz", "sommer2024", "hallo"};
    int n = 5;

    for (int i = 0; i < n; i++) {
        if (mini_hash(woerterbuch[i]) == gestohlener_hash) {
            printf("Passwort geknackt: %s\n", woerterbuch[i]);
            return 0;
        }
    }
    printf("Nicht im Woerterbuch gefunden.\n");
    return 0;
}

Erwartete Ausgabe:

Passwort geknackt: sommer2024

5 Salt hinzufügen ⏱️ 20 Min VL 8: Salt

Ziel: zeigen, wie ein per-Nutzer-Salt den Dictionary-/Rainbow-Angriff aushebelt — zwei Nutzer mit demselben Passwort bekommen unterschiedliche Hashes.

Aufgabe:

  1. Schreibe hash_mit_salt(passwort, salt): Passwort und Salt mit strcat zusammenfügen, dann mini_hash darauf anwenden.
  2. Anna und Bob haben dasselbe Passwort "sommer2024", aber verschiedene Salts.
  3. Gib beide Hashes aus und beobachte: trotz gleichem Passwort sind die Hashes verschieden.
💡 Hinweis: Ein Salt muss nicht geheim sein — er wird im Klartext neben dem Hash in der Datenbank gespeichert. Wichtig ist nur: pro Nutzer einzigartig & zufällig. In echten Systemen erzeugt man das Salt kryptografisch zufällig (z. B. mit RAND_bytes), nicht mit rand(). Hier nehmen wir der Einfachheit halber feste Beispiel-Salts.
⚠️ Effekt: Vorberechnete Rainbow-Tables (ohne Salt) sind jetzt nutzlos — der Angreifer müsste sie pro Salt neu berechnen. Bei einem zufälligen Salt pro Nutzer wird das unbezahlbar.
🔒 Musterlösung in C
Falsches Passwort!
#include <stdio.h>
#include <string.h>

unsigned int mini_hash(char str[]) {
    unsigned int state = 0;
    for (int i = 0; str[i] != '\0'; i++) {
        state = (state * 31 + str[i]) % 1000;
    }
    return state;
}

// Passwort + Salt zusammenfuegen, dann hashen
unsigned int hash_mit_salt(char passwort[], char salt[]) {
    char kombiniert[100];
    strcpy(kombiniert, passwort);
    strcat(kombiniert, salt);
    return mini_hash(kombiniert);
}

int main(void) {
    // Anna und Bob: DASSELBE Passwort, unterschiedliche Salts
    printf("Anna: %u\n", hash_mit_salt("sommer2024", "X9q2pZ"));
    printf("Bob:  %u\n", hash_mit_salt("sommer2024", "L8mNqW"));
    return 0;
}

Erwartete Ausgabe (zwei UNTERSCHIEDLICHE Zahlen, z. B.):

Anna: 514
Bob:  366

Wichtig: die genauen Zahlen sind egal — entscheidend ist, dass Anna und Bob trotz gleichem Passwort verschiedene Hashes haben.

6 Echtes SHA-256 mit OpenSSL ⏱️ 25 Min VL 8: echte Krypto

Ziel: einen echten kryptografischen Hash benutzen (nicht selbst bauen!) und den Lawineneffekt mit eigenen Augen sehen.

Aufgabe:

  1. Nutze OpenSSLs SHA256(), um den Hash zweier fast identischer Strings zu berechnen.
  2. Gib die 32 Bytes als Hex aus (jedes Byte als zwei Hex-Ziffern).
  3. Vergleiche "Hallo Welt" und "Hallo welt" — ein einziger Buchstabe Unterschied, völlig anderer Hash.
⚙️ Kompilieren: gcc lab.c -lcrypto (OpenSSL nötig — siehe Setup oben).
💡 Hinweis: SHA-256 liefert immer genau 32 Bytes (= 256 Bit). Mit %02x gibt man jedes Byte als zwei Hex-Ziffern aus — so entstehen die typischen 64-stelligen Hash-Strings.
🤔 Denkfrage: SHA-256 ist hervorragend für Datei-Integrität (Download-Prüfsummen, Git-Commits). Für Passwörter ist es aber zu schnell — moderne GPUs schaffen Milliarden SHA-256/Sekunde, ein Angreifer probiert also extrem schnell durch. Deshalb gibt es absichtlich langsame Passwort-Hashes — siehe Aufgabe 7.
🔒 Musterlösung in C
Falsches Passwort!
#include <stdio.h>
#include <string.h>
#include <openssl/sha.h>

void print_sha256(char text[]) {
    unsigned char hash[32];                          // SHA-256 = 32 Bytes
    SHA256((unsigned char *)text, strlen(text), hash);
    for (int i = 0; i < 32; i++) {
        printf("%02x", hash[i]);                     // jedes Byte als 2 Hex-Ziffern
    }
    printf("  <- \"%s\"\n", text);
}

int main(void) {
    print_sha256("Hallo Welt");
    print_sha256("Hallo welt");   // ein Buchstabe anders -> komplett anderer Hash
    return 0;
}

Kompilieren: gcc lab.c -lcrypto

Hinweis: (unsigned char *) ist hier nur eine nötige Typ-Anpassung für die Bibliothek — einfach so übernehmen, das gehört zum Aufruf von SHA256() dazu.

Erwartete Ausgabe: zwei komplett unterschiedliche 64-stellige Hex-Hashes — der Lawineneffekt. Schon ein winziger Unterschied im Input verändert den kompletten Hash.

7 bcrypt mit crypt() ⏱️ 25 Min · BONUS VL 8: echte Krypto

Ziel: einen Passwort-Hash mit bcrypt erzeugen und prüfen — inklusive Cost-Faktor (absichtlich langsam, damit Angriffe teuer werden).

Aufgabe:

  1. Nutze crypt() (libcrypt) mit einem Setting der Form "$2b$12$...", um ein Passwort zu hashen.
  2. Sichere den erzeugten Hash, dann prüfe einen Login per strcmp.
  3. Beobachte: Der Hash enthält Algorithmus, Cost-Faktor und Salt — alles in einem String.
⚙️ Kompilieren: gcc lab.c -lcrypt. Wichtig: crypt() gibt einen Zeiger auf einen statischen Puffer zurück — deshalb den ersten Hash mit strcpy sichern, bevor crypt() erneut aufgerufen wird (sonst wird er überschrieben).
🤔 Denkfrage: Cost 12 = 2¹² Runden ≈ 250 ms pro Hash. Warum ist „langsam" hier gut? Der ehrliche Nutzer merkt 250 ms beim Login nicht. Ein Angreifer aber schafft statt Milliarden Versuchen/Sekunde nur noch ca. 40 Versuche/Sekunde — das macht Brute-Force praktisch unmöglich. Und steigt die Hardware, erhöht man einfach den Cost-Faktor.
🔒 Musterlösung in C
Falsches Passwort!
#include <stdio.h>
#include <string.h>
#include <crypt.h>

int main(void) {
    // Cost-Faktor 12 (2^12 Runden) — absichtlich langsam
    char setting[] = "$2b$12$abcdefghijklmnopqrstuv";

    // Registrierung: Passwort hashen (Hash enthaelt Algorithmus, Cost UND Salt)
    char gespeichert[100];
    strcpy(gespeichert, crypt("sommer2024", setting));
    printf("Gespeicherter Hash: %s\n", gespeichert);

    // Login: Eingabe gegen den gespeicherten Hash pruefen
    char check[100];
    strcpy(check, crypt("sommer2024", gespeichert));
    if (strcmp(check, gespeichert) == 0) {
        printf("Login OK\n");
    } else {
        printf("Falsches Passwort\n");
    }
    return 0;
}

Kompilieren: gcc lab.c -lcrypt

Erwartete Ausgabe:

Gespeicherter Hash: $2b$12$abcdefghijklmnopqrstuv...
Login OK

🔑 Passwörter für die Musterlösungen

Die Passwörter stehen unten — frei zugänglich. Bitte fair spielen: erst selbst implementieren, dann mit der Musterlösung vergleichen. Eine Lösung, die ihr nur abgeschrieben habt, hilft euch in der Klausur nicht.

AufgabeThemaPasswort
Aufgabe 1Der naive Hash & Kollisionenkollision
Aufgabe 2Mini-Hash mit Multiplikationminihash
Aufgabe 3Passwort-Login simulierenlogin
Aufgabe 4Dictionary-Angriffdictionary
Aufgabe 5Salt hinzufügensalt
Aufgabe 6SHA-256 mit OpenSSLsha256
Aufgabe 7bcrypt mit crypt() (Bonus)bcrypt

Hinweis für Python/Java/Rust-Lösungen: die Algorithmen sind 1:1 übertragbar — übersetzt nur die Syntax. Eigene Musterlösungen für jede Sprache gibt es nicht.

← Startseite

© 2026 HTW Berlin · Prof. Dr. Alexandra Mikityuk