Timing Attack - Zeitbasierter Seitenangriff
Timing Attacks nutzen Unterschiede in der Ausführungszeit von kryptographischen Operationen oder Vergleichsfunktionen um geheime Informationen abzuleiten. Klassische Angriffe: Passwort-Timing (fruehzeitiger Abbruch bei erstem Unterschied), RSA-Timing (Modularpotenzierung), Cache-Timing (Spectre/Meltdown). Schutz: Constant-Time-Vergleiche (hmac.compare_digest), Blinding, Branch-freie Implementierungen.
Timing Attacks gehören zur Klasse der Side-Channel-Angriffe: Angreifer exploitieren nicht logische Schwächen in einem Algorithmus, sondern physikalische oder zeitliche Eigenschaften seiner Implementierung. Auch ein kryptografisch korrekter Algorithmus kann durch eine unsichere Implementierung angreifbar werden, wenn die Ausführungszeit Geheimnisse verrät.
Das Grundproblem
Unsichere String-Vergleich-Implementierung (konzeptuell)
Funktion: verify_token(user_input, secret)
for i in range(len(user_input)):
if user_input[i] != secret[i]:
return False ← FRÜHER ABBRUCH!
return True
Timing-Leckage
"AAAA..."→ bricht nach Position 1 ab → sehr schnell"XBBB..."→ bricht nach Position 1 ab → gleich schnell"XAAA..."→ bricht nach Position 2 ab → minimal langsamer"XYAA..."→ bricht nach Position 2 ab → gleich schnell
Messung: welches erstes Zeichen ist das richtige? → Das mit der längsten Antwortzeit! Byte für Byte durchprobieren statt alle Kombinationen!
- Ohne Timing: O(2^n) Kombinationen für n-Bit-Schlüssel
- Mit Timing: O(n * 256) Versuche für n-Byte-Schlüssel
- → Exponentiell auf linear reduziert!
Wo kommt Timing-Varianz her?
- Früher Abbruch bei Vergleichen (häufigste Ursache)
- Branch-Prediction: if/else je nach Geheimnis
- Cache-Hits: Daten im CPU-Cache → schneller
- Multiplikation vs. Potenzierung: unterschiedliche Zyklen
- Garbage Collection: GC-Pausen verdecken aber auch!
- Netzwerk-Jitter: macht Remote-Timing schwieriger
Angriffsszenarien
Szenario 1 - Web-Token-Validierung (häufigste Web-Schwachstelle)
Angreifer errät HMAC-Token Byte für Byte durch Timing-Messung.
Methode:
- Tausende Anfragen mit erstem Byte = ‘A’
- Tausende Anfragen mit erstem Byte = ‘B’
- …bis Byte mit längster Antwortzeit gefunden → erstes Byte korrekt!
- Weiter mit zweitem Byte…
Erforderliche Voraussetzungen:
- Antwortzeiten messbar (Latenz < Rauschen)
- Server ohne Rate-Limiting
- Viele Messungen nötig (statistische Analyse)
Remote-Timing: schwieriger (Netzwerk-Jitter), aber möglich! Cloudflare-Forschung: Mikrosekunden-Unterschiede über zehntausende Requests statistisch messbar (t-Test).
Szenario 2 - Benutzername-Enumeration
- Gültiger Username + falsches PW: 200ms (Passwort-Hash berechnet!)
- Ungültiger Username: 5ms (früh abgebrochen, kein Hash)
Angreifer: Username-Enumeration durch Timing-Unterschied - selbst wenn HTTP-Response identisch aussieht.
Beweis: Amazon AWS SDK hatte diesen Bug (CVE-2015-3154)
Szenario 3 - RSA Private Key (Kocher 1996, historisch)
RSA-Entschlüsselung: Modularpotenzierung über private Exponenten. Square-and-Multiply-Algorithmus:
- Bit = 1: Square + Multiply (mehr Operationen!)
- Bit = 0: Square only (weniger Operationen)
Timing-Differenz → Private-Key-Bits extrapolierbar! Betraf frühe Implementierungen ohne Blinding.
Schutz: Montgomery Blinding (Zufalls-Blind-Faktor vor Entschlüsselung)
Szenario 4 - Cache-Timing (Spectre, 2018)
CPU-Cache speichert zuletzt zugegriffene Daten. Angreifer-Prozess misst Zugriffszeit auf eigene Array-Elemente. Wenn Zugriffszeit schnell → Daten noch im Cache → CPU hat bei vorheriger Operation auf diese Adresse zugegriffen!
Spectre trickst CPU-Branch-Prediction aus → spekulativer Zugriff auf Kernel-Memory → Timing-Seitenkanal → Daten lesen!
Betrifft: praktisch alle modernen CPUs (Hardware-Problem!)
Mitigation: Kernel Page Table Isolation (KPTI), Retpoline, Microcode-Updates (Performance-Overhead!)
Szenario 5 - TOTP-Bypass durch Timing
OTP-Validierung ohne Constant-Time:
- Angreifer sendet 000000, misst Antwortzeit
- Wenn “00” stimmt überein → minimal länger
- Iteriert durch alle 6 Stellen sequenziell
Praktisch begrenzt durch: kurze TOTP-Gültigkeit (30s), Netzwerk-Jitter, moderne CT-Implementierungen.
Constant-Time-Implementierungen
Python - FALSCH vs. RICHTIG
# FALSCH: string == string → früher Abbruch möglich!
if user_token == secret_token:
grant_access()
# RICHTIG: hmac.compare_digest - constant time!
import hmac
if hmac.compare_digest(user_token.encode(), secret_token.encode()):
grant_access()
# Gleiche Laufzeit egal ob Tokens übereinstimmen oder nicht!
# AUCH RICHTIG: secrets.compare_digest (Python 3.3+):
import secrets
if secrets.compare_digest(user_token, secret_token):
grant_access()
Node.js - FALSCH vs. RICHTIG
// FALSCH:
if (userToken === secretToken) { ... }
if (userToken == secretToken) { ... }
// RICHTIG: crypto.timingSafeEqual (Node.js built-in):
const crypto = require('crypto');
const a = Buffer.from(userToken);
const b = Buffer.from(secretToken);
// WICHTIG: Längen müssen gleich sein für timingSafeEqual!
if (a.length === b.length && crypto.timingSafeEqual(a, b)) {
grantAccess();
}
// Längenvergleich separat → gibt keine Längeninformation preis,
// da constant-time-equal sowieso 'false' bei unterschiedl. Länge
Java - FALSCH vs. RICHTIG
// FALSCH:
if (userToken.equals(secretToken)) { ... }
// RICHTIG: MessageDigest.isEqual (constant-time):
import java.security.MessageDigest;
if (MessageDigest.isEqual(
userToken.getBytes(),
secretToken.getBytes())) { ... }
Go - FALSCH vs. RICHTIG
// FALSCH:
if userToken == secretToken { ... }
// RICHTIG: subtle.ConstantTimeCompare:
import "crypto/subtle"
if subtle.ConstantTimeCompare([]byte(userToken), []byte(secretToken)) == 1 {
// access granted
}
PHP - FALSCH vs. RICHTIG
// FALSCH: strcmp(), ==, ===
// RICHTIG: hash_equals() (PHP 5.6+)
if (hash_equals($secretToken, $userToken)) { ... }
Timing-Tests im Pentest
1. Benutzername-Enumeration (Timing)
# Tool: ffuf, Burp Intruder mit Timing-Analyse
for user in wordlist:
time curl -s -X POST /login -d "user=$user&pass=WRONG"
# → Mittelwert pro Username + Standardabweichung
# → Ausreißer nach oben → gültiger Username!
2. Token-Vergleich (Remote Timing)
# Tool: timing-attack-framework oder eigenes Skript
for prefix in range(256):
times = []
for _ in range(100): # viele Messungen!
t = measure(POST /verify, token=chr(prefix)+garbage)
times.append(t)
results[prefix] = median(times)
# → Byte mit höchstem Median → wahrscheinlich korrekt
3. Statistische Auswertung
- Einzelne Messung: zu viel Rauschen!
- Median über 50-100 Messungen
- Welch’s t-Test: ist Unterschied statistisch signifikant?
- Tools: timing-attack (Python), bcheck (Burp)
4. Lokale Timing-Tests (für Code-Reviews)
# Python timeit: Nanosekunden-Auflösung
# Finde == vs hmac.compare_digest in Code-Basis:
grep -r "== request\|== token\|== key\|== secret" src/
5. Was zu testen ist
- Login-Endpunkte (Username-Enumeration)
- Token-Validierung (API-Keys, CSRF-Token, OTP)
- Password-Reset-Token-Prüfung
- Berechtigungs-Checks mit String-Vergleich
- Crypto-Bibliotheken (HMAC-Vergleich)
Schutzmaßnahmen
1. Constant-Time-Funktionen überall verwenden
- NUR ct-Vergleiche für Secrets: HMAC, Token, Passwörter, Keys
hmac.compare_digest(Python),timingSafeEqual(Node.js)hash_equals(PHP),MessageDigest.isEqual(Java)- Code-Review Checklist: kein
==bei sicherheitskritischen Vergleichen
2. Username-Enumeration verhindern
- Konstante Antwortzeit bei Login (immer Passwort-Hash berechnen!)
- Selbst bei ungültigem Username einen Dummy-Hash berechnen
- Gleiche HTTP-Response bei falschem User UND falschem Passwort
3. Rate-Limiting
- Macht Timing-Angriffe unpraktisch (zu viele Anfragen nötig)
- Max 5-10 Login-Versuche pro Minute pro IP
4. Netzwerk-Jitter nutzen (schwächere Mitigation)
- Kleine zufällige Verzögerung hinzufügen:
time.sleep(random(0, 5ms)) - Verdeckt Timing-Unterschiede im Millisekunden-Bereich
- Aber: mit genug Messungen oft dennoch messbar!
- Keine echte Lösung, nur Erschwerung
5. Hardware-seitig (Spectre/Meltdown)
- Microcode-Updates installieren
- Kernel mit KPTI, Retpoline
- Browser: timer-Auflösung reduziert (SharedArrayBuffer deaktiviert)
- Performance-Trade-off akzeptieren
6. Kryptobibliotheken
- Nur geprüfte Kryptobibliotheken (OpenSSL, libsodium, Bouncy Castle)
- KEINE eigene Krypto implementieren!
- RSA: Padding-Blinding sicherstellen (OAEP statt PKCS#1 v1.5)
- AES-GCM: Nonce-Wiederverwendung vermeiden (GCM-Nonce-Misuse)