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.
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.
sudo apt install libssl-dev, kompilieren mit gcc lab.c -lcrypto.gcc lab.c -lcrypt (auf Linux/HTW-Pool verfügbar; unter macOS unterstützt crypt() bcrypt evtl. nicht — dann Linux/Pool nutzen).Hash als Einbahnstraße, Lawineneffekt, warum naive Hashes versagen
Login-Ablauf mit Hash — niemals Klartext in der Datenbank
Dictionary-/Rainbow-Tables — und wie ein Salt sie aushebelt
SHA-256 & bcrypt mit Bibliotheken statt selbstgebautem Hash
Ziel: einen bewusst „schlechten" Hash bauen und sehen, warum er versagt — Kollisionen und kein Lawineneffekt.
bad_hash(str), das einfach die ASCII-Werte aller Zeichen aufaddiert."abc", "bac", "cab" und "hallo" aus."abc", "bac" und "cab" haben denselben Hash 294 — eine Kollision."abc" ist und jemand "bac" eingibt — was passiert beim Login? (Antwort: Er kommt rein, obwohl er dein Passwort gar nicht kennt — gleicher Hash = Zugang.)
#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
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.
mini_hash(str) mit der Formel oben."Hi", "HALLO", "HALLI" und "BALLO"."HALLO" und "HALLI" unterscheiden sich nur im letzten Buchstaben, geben aber verschiedene Hashes — die Reihenfolge zählt nun.#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
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.
mini_hash aus Aufgabe 2."geheim123" in einer Variablen.scanf ein, hashe sie und vergleiche mit dem gespeicherten Hash."Login OK" oder "Falsches Passwort" aus.#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
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.
mini_hash("sommer2024") — so, als hätte ein Angreifer ihn aus einer geklauten Datenbank.#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
Ziel: zeigen, wie ein per-Nutzer-Salt den Dictionary-/Rainbow-Angriff aushebelt — zwei Nutzer mit demselben Passwort bekommen unterschiedliche Hashes.
hash_mit_salt(passwort, salt): Passwort und Salt mit strcat zusammenfügen, dann mini_hash darauf anwenden."sommer2024", aber verschiedene Salts.RAND_bytes), nicht mit rand(). Hier nehmen wir der Einfachheit halber feste Beispiel-Salts.
#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.
Ziel: einen echten kryptografischen Hash benutzen (nicht selbst bauen!) und den Lawineneffekt mit eigenen Augen sehen.
SHA256(), um den Hash zweier fast identischer Strings zu berechnen."Hallo Welt" und "Hallo welt" — ein einziger Buchstabe Unterschied, völlig anderer Hash.gcc lab.c -lcrypto (OpenSSL nötig — siehe Setup oben).
%02x gibt man jedes Byte als zwei Hex-Ziffern aus — so entstehen die typischen 64-stelligen Hash-Strings.
#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.
Ziel: einen Passwort-Hash mit bcrypt erzeugen und prüfen — inklusive Cost-Faktor (absichtlich langsam, damit Angriffe teuer werden).
crypt() (libcrypt) mit einem Setting der Form "$2b$12$...", um ein Passwort zu hashen.strcmp.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).
#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
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.
| Aufgabe | Thema | Passwort |
|---|---|---|
| Aufgabe 1 | Der naive Hash & Kollisionen | kollision |
| Aufgabe 2 | Mini-Hash mit Multiplikation | minihash |
| Aufgabe 3 | Passwort-Login simulieren | login |
| Aufgabe 4 | Dictionary-Angriff | dictionary |
| Aufgabe 5 | Salt hinzufügen | salt |
| Aufgabe 6 | SHA-256 mit OpenSSL | sha256 |
| Aufgabe 7 | bcrypt 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.
© 2026 HTW Berlin · Prof. Dr. Alexandra Mikityuk