© shaiith - Fotolia.com

Timing-Angriffe und deren Auswirkungen

Bei einem Timing-Angriff ("Timing Attack") handelt es sich um eine Form eines Seitenkanalangriffs auf eine bestimmte Implementierung in Hard- oder Software. Die Möglichkeit eines Timing-Angriffs kann selbst bei einfachsten Entwicklungen zu erheblichen Auswirkungen auf die Sicherheit eines Systems führen.

Was ist ein Timing-Angriff?

Bei einem Timing-Angriff werden statistische Verfahren genutzt, um Unterschiede in der Zeitdauer einer bestimmten Operation zu erfahren. Es wird also beispielsweise gemessen, wie lange ein Webserver braucht, um auf eine Loginanfrage zu reagieren. Sollten sich bei den Messungen bestimmte Muster feststellen lassen, so könnten aus diesen Informationen extrahiert werden, die eigentlich geheim gehalten werden sollten. Klingt recht kompliziert, lässt sich aber anhand eines einfachen Beispiels recht eindrucksvoll demonstrieren.

In meinem Github Repository habe ich unter anderem ein in PHP geschriebenes Loginsystem hinterlegt. Der eigentliche, serverseitige Login wird von der Datei "do-login.php" ausgeführt. Im folgenden Beispiel habe ich den Code zur Demonstration etwas modifiziert und auf das wesentliche reduziert:

<?php

// Username & Password als Variable speichern
$username = $_POST['username'];
$password = $_POST['password'];

// Datenbankabfrage des Usernamens
$query = "SELECT * FROM users WHERE username='" .  $_POST['username'] . "'";
$result = mysql_query($query);

// Wenn der Username existiert, dann ...
if (mysql_num_rows($result) == 1)
{ 

	// Speicher den Datensatz des Users in $row
	$row = mysql_fetch_assoc($result);
 
	// Generiere einen Hash des Userpassworts	
	$hash = password_hash($password);

	// Vergleiche Hash mit gespeichertem Hash
	if ( $hash == $row['password'] )
	{
		echo "Authentication successfully.";
	}
	else
	{
		echo "Authentication failed.";
	}
}
// Es existiert kein User mit dem Usernamen
else
{
	echo "Authentication failed.";
}

?>

Zum Login in eine Anwendung bzw. auf einer Webseite wird ein Code wie oberer nahezu als Standard eingesetzt. Schlecht ist er auf dem ersten Blick nicht:

  • Es ist für den Benutzer nicht ersichtlich, ob ein Username existiert. Er bekommt auch bei einem nicht existenten Usernamen stets die Meldung "Authentication failed" zurück.
  • Der Code ist aufgeräumt, auf das wesentliche reduziert. Der Hash wird erst berechnet, wenn der User auch tatsächlich existiert, was effizient ist.
  • Die SQL Abfrage wurde einfach gehalten, um Fehler durch Komplexität zu vermeiden.

Bei genauerem Hinsehen hat der Code jedoch einige erhebliche Schwachstellen.

Schwachstellen

Wer sich etwas mit PHP auskennt, kann versuchen, die Probleme im Quellcode vor dem Weiterlesen des Textes zu identifizieren.

Schwachstelle 1: SQL-Injection

Die dringendste Schwachstelle hat nichts mit Timing zu tun, es handelt sich hierbei um eine Möglichkeit der SQL-Injection. Der obere Code prüft die Eingaben des Nutzers nicht und sendet diese wie übergeben an den SQL Server. Ein Benutzer könnte als Usernamen beispielsweise

'; DROP users;

verwenden. Das vollständige SQL Statement heißt nun:

SELECT * FROM users WHERE username=''; DROP users;

Bei Ausführung dieses SQL Statements würde die Userdatenbank gelöscht werden. Eine Lösungsmöglichkeit könnte in der Nutzung von Prepared Statements liegen:

$stmt = $dbconnect->prepare("SELECT id FROM sl_user WHERE username=?");
$stmt->bind_param("s", $_POST['username']);
$stmt->execute();

Ein wichtiges Keyword für die Prüfung und Nachbearbeitung von Benutzereingaben sind "Sanitize Filters".

Schwachstelle 2: Timing-Attack

Ziel des oberen Codes ist es, dem Nutzer nicht zu verraten, ob der übergebene Benutzername existiert oder nicht. Der Code jedoch sagt: Wenn ein Benutzer nicht existiert, errechne auch nicht den Hash des Passworts. Genau hier liegt das Problem: Die Zeitdauer des Loginversuchs ist länger, wenn ein Benutzername existiert, da nur dann der Hash berechnet wird. An dieser Stelle wird meist zu Recht die Frage gestellt: Ist dies in der Praxis relevant? In vielen Empfehlungen ist oftmals zu lesen: Hash das Passwort nicht nur einfach, sondern in mehreren Runden, beispielsweise in 16 Runden.

$options = [
	'cost' => 16,
];
$hash = password_hash($password, PASSWORD_BCRYPT, $options);

Je mehr Runden, desto höher die Zeit der Berechnung - und desto besser funktionieren Verfahren wie die oben beschriebene Timing-Attacke. Der Fakt, dass mehr Runden besser sind, ist meist korrekt, es muss jedoch beachtet werden, dass bereits diese vermeintlich kleinen Änderungen die Wahrscheinlichkeit für ungewollte Seitenkanalangriffe deutlich erhöhen können. Die genannten 16 Runden reichen aus, um bei vielen Systemen nicht nur eine messbare, sondern auch spürbare Verlangsamung zu erzeugen.

Um ein Gefühl für die Rundengeschwindigkeit zu bekommen, habe ich unter https://itsecblog.de/bcrypt/ ein Tool zum Testen bereit gestellt.

Eine mögliche Lösung würde darin bestehen, die Hash-Berechnung unabhängig der Existenz eines Benutzers durchzuführen.

Schwachstelle 3: Timing-Attack

Im Quellcode befindet sich eine weitere Möglichkeit für Timing-Attacken, und zwar in der Zeile:

if ( $hash == $row['password'] )

Um das Problem nachvollziehen zu können, muss verstanden werden, wie ein Vergleich von 2 Strings technisch in PHP funktioniert: Es wird von links nach rechts Zeichen für Zeichen auf Gleichheit getestet. Wird ein Unterschied festgestellt, bricht die Funktion mit dem Rückgabewert "nicht gleich" ab.

Ein Beispiel, insofern der Vergleich eines einzelnen Zeichens eine Sekunde benötigen würde:

$password = abcdef12
$password2 = abcdef12
-> Dauer der Funktion: 8 Sekunden

$password = abcdef12
$password2 = abcd0000
-> Dauer der Funktion: 4 Sekunden

$password = abcdef12
$password2 = ab000000
-> Dauer der Funktion: 2 Sekunden

$password = abcdef12
$password2 = a0000000
-> Dauer der Funktion: 1 Sekunde

In der Theorie wäre es nun möglich, sich Zeichen für Zeichen zum Passwort vorzuarbeiten.

Eine mögliche Lösung würde hier darin bestehen, die Funktion stets gleich lange dauern zu lassen. PHP bietet beim Vergleich eines Password-Hashes beispielsweise die Funktion password_verify. In der PHP-Dokumentation steht hierzu: This function is safe against timing attacks.

Fazit

Timing-Angriffe auszuschließen, ist auch bei sauberster Programmierung nahezu unmöglich. Im oberen Beispiel wurde beispielsweise noch nicht berücksichtigt, dass ein SQL-Server meist minimal schneller antwortet, wenn keine Resultate vorhanden sind. Wichtig ist es jedoch, sich den Risiken von Timing-Attacken bewusst zu sein und deren Auswirkungen, insofern sinnvoll und möglich, zu reduzieren.


Bildnachweise:

  • © shaiith - Fotolia.com

Ein Gedanke zu „Timing-Angriffe und deren Auswirkungen“

  1. Vorab ein großes Lob für den gut geschriebenen und ausführlich erklärenden Beitrag zu dem häufig vernachlässigten Risiko von Timing-Angriffen!

    Dennoch wäre es aus meiner Sicht eine Beschreibung zur Vermeidung der oben genannten Szenarien schön gewesen. Eine – aus meiner Sicht – recht erfolgversprechende Abwehr von Timing-Angriffen ist die Verschleierung mittels usleep(…)- und rand(…)-Befehl. Ersterer weist den Thread an eine bestimmte Anzahl an Millisekunden zu warten, letzterer erzeugt einen – wenn auch nicht qualitativ hochwertige – Zufallszahl. Selten dürften derartige Programmlogiken mehr als 250ms abweichen, so dass eine Verschleierung mit 0-250ms ausreichend sein sollte, um jede Art von Timing-Angriff ins Leere laufen zu lassen.

    Eine weitere Ungenauigkeit liegt in der Verwendung der SQL-Injections speziell bei PHP-Applikationen. Grundsätzlich ist es gut und richtig sich dagegen abzusichern – aber Gott sei Dank bietet PHP zumindest noch eine zusätzliche Hürde, welche gerade die Auswirkungen mehrerer Befehle reduziert – PHP erlaubt in den Standardeinstellungen meines Wissens nach nicht die Ausführung mehrerer Befehle innerhalb eines SQL-Statements. Unabhängig davon lässt sich trotzdem genug Schindluder damit treiben, da auch die Modifizierung eines SELECT-Befehls fatale Folgen haben kann:

    'SELECT * FROM users WHERE username = \'' . $_POST[...] . '\' AND pwd = MD5(\'' + $_POST[...] + '\')

    kann mit wenig Aufwand zu folgendem Statement umgebaut werden:

    'SELECT * FROM users WHERE username = \'owned\' AND pwd = MD5(\'\') OR (username = \'admin\')

    Das oben genannte Statement würde enststehen, wenn im ungesicherten Passwortfeld die folgende Eingabe vorgenommen wird:

    \') OR (username = \'admin\'

    Viele Grüße,
    Stephan Schulze

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert