1. Code
  2. PHP

Hashfunktionen Kenntnisse und Schutz von Passwörtern

Scroll to top

German (Deutsch) translation by Władysław Łucyszyn (you can also view the original English article)

Von Zeit zu Zeit werden Server und Datenbanken gestohlen oder kompromittiert. Es ist wichtig zu verstehen, dass einige wichtige Benutzerdaten, wie z.B. Kennwörter, nicht wiederhergestellt werden können. Heute lernen wir die Grundlagen des Hashings und die Voraussetzungen zum Schutz von Passwörtern in Ihren Webanwendungen kennen.

Wiederveröffentlichtes Tutorial

Alle paar Wochen besuchen wir einige der Lieblingsbeiträge unserer Leser aus der gesamten Geschichte der Website. Dieses Tutorial wurde erstmals im Januar 2011 veröffentlicht.


1. Disclaimer

Kryptologie ist ein ausreichend kompliziertes Theme, und ich bin kein Experte. In diesem Bereich, an vielen Universitäten und Sicherheitsbehörden, wird ständig geforscht.

In diesem Artikel werde ich versuchen, über die Dinge einfach zu erzählen und Ihnen gleichzeitig eine einigermaßen sichere Methode zum Speichern von Passwörtern in einer Webanwendung vorzustellen.


2. Was macht "Hashing"?

Hashing konvertiert ein Datenelement (entweder klein oder groß) in ein relativ kurzes Datenelement wie eine Zeichenfolge oder ein Integer.

Hier kann ein Einweg-Hash-Funktion uns helfen. "Einweg" bedeutet, dass es sehr schwierig (oder praktisch unmöglich) ist, es umzukehren.

Ein häufiges Beispiel für eine Hash-Funktion ist md5(), das in vielen verschiedenen Sprachen und Systemen sehr beliebt ist.

1
$data = "Hello World";
2
$hash = md5($data);
3
echo $hash; // b10a8db164e0754105b7a99be72e3fe5

Mit md5() ist das Ergebnis immer eine 32 Zeichen lange Zeichenfolge. Es hat nur hexadezimale Zeichen. Technisch kann es auch als 128-Bit-Integer (16 Byte) dargestellt werden. Sie können md5() viel längere Zeichenfolgen und Daten, und Sie werden immer noch mit einem Hash dieser Länge enden. Diese Tatsache allein könnte Ihnen einen Hinweis geben, warum dies als "Einweg" -Funktion angesehen wird.


3. Verwendung einer Hash-Funktion zum Speichern von Passwörtern

Der übliche Vorgang bei einer Benutzerregistrierung:

  • Der Benutzer füllt das Registrierungsformular einschließlich des Passwortfelds aus.
  • Das Web-Skript speichert alle Informationen in einer Datenbank.
  • Das Kennwort wird vor dem Speichern über eine Hash-Funktion ausgeführt.
  • Die Originalversion des Passworts wurde nirgendwo gespeichert, daher wird es technisch verworfen.

Und der Anmeldevorgang:

  • Der Benutzer gibt den Benutzernamen (oder die E-Mail-Adresse) und das Passwort ein.
  • Das Skript führt das Kennwort über dieselbe Hashing-Funktion aus.
  • Das Skript findet den Benutzerdatensatz aus der Datenbank und liest das gespeicherte Hash-Passwort.
  • Diese beiden Werte werden verglichen und der Zugriff wird gewährt, wenn sie übereinstimmen.

Sobald wir uns für eine anständige Methode zum Hashing des Passworts entschieden haben, werden wir diesen Prozess später in diesem Artikel implementieren.

Beachten Sie, dass das ursprüngliche Passwort nirgendwo gespeichert wurde. Wenn die Datenbank gestohlen wird, können die Benutzeranmeldungen nicht gefährdet werden, oder? Nun, die Antwort lautet "es kommt darauf an". Schauen wir uns einige mögliche Probleme an.


4. Problem Nr. 1: Hash-Kollision

Eine Hash-Kollision tritt auf, wenn zwei verschiedene Dateneingaben denselben resultierenden Hash erzeugen. Die Wahrscheinlichkeit, dass dies geschieht, hängt von der Funktion,die Sie verwenden.

Wie kann das ausgenutzt werden?

Als Beispiel habe ich einige ältere Skripte gesehen, die crc32() zum Hashing von Passwörtern verwendeten. Diese Funktion generiert als Ergebnis eine 32-Bit-Ganzzahl. Es gibt nur 2^32 (d. H. 4.294.967.296) mögliche Ergebnisse.

Lassen Sie uns ein Passwort hashen:

1
echo crc32('supersecretpassword');
2
// outputs: 323322056

Nehmen wir nun die Rolle einer Person an, die eine Datenbank gestohlen hat und den Hashwert hat. Möglicherweise können wir 323322056 nicht in 'supersecretpassword' konvertieren. Wir können mit einem einfachen Skript ein anderes Passwort ermitteln, das in denselben Hashwert konvertiert wird:

1
set_time_limit(0);
2
$i = 0;
3
while (true) {
4
5
	if (crc32(base64_encode($i)) == 323322056) {
6
		echo base64_encode($i);
7
		exit;
8
	}
9
10
	$i++;
11
}

Das brauch Zeit, sollte aber schließlich eine Zeichenfolge zurückgeben. Wir können diese zurückgegebene Zeichenfolge anstelle von 'supersecretpassword' verwenden und können uns so erfolgreich bei dem Konto dieser Person anmelden.

Nachdem ich dieses Skript für einige Momente auf meinem Computer ausgeführt hatte, habe ich 'MTIxMjY5MTAwNg=='. Lassen Sie es uns testen:

1
echo crc32('supersecretpassword');
2
// outputs: 323322056

3
4
echo crc32('MTIxMjY5MTAwNg==');
5
// outputs: 323322056

Wie kann das verhindert werden?

Heute kann ein leistungsstarker Heim-PC verwendet werden, um eine Hash-Funktion fast eine Milliarde Mal pro Sekunde auszuführen. Wir brauchen also eine Hash-Funktion, die einen sehr großen Bereich hat.

Zum Beispiel könnte md5() geeignet sein, da es 128-Bit-Hashes generiert. Dies entspricht 340.282.366.920.938.463.463.374.607.431.768.211.456 möglichen Ergebnissen. Es ist unmöglich, so viele Iterationen zu durchlaufen, um Kollisionen zu finden. Einige Leute haben jedoch immer noch Möglichkeiten gefunden, dies zu tun (siehe hier).

Sha1

Sha1() ist eine bessere Alternative und generiert einen noch längeren 160-Bit-Hashwert.


5. Problem Nr. 2: Rainbow-Tabellen

Selbst wenn wir das Kollisionsproblem beheben, sind wir noch nicht sicher.

Eine Rainbow-Tabelle wird erstellt, indem die Hashwerte häufig verwendeter Wörter und ihre Kombinationen berechnet werden.

Diese Tabellen können bis zu Millionen oder sogar Milliarden von Zeilen enthalten.

Sie können ein Wörterbuch durchgehen und für jedes Wort Hashwerte generieren. Sie können auch Wörter miteinander kombinieren und auch für diese Hashes generieren. Das ist nicht alles; Sie können sogar Ziffern vor/nach/zwischen Wörtern hinzufügen und diese auch in der Tabelle speichern.

Wenn man bedenkt, wie billig Speicher heutzutage ist, können gigantische Rainbowtabellen hergestellt und verwendet werden.

Wie kann das ausgenutzt werden?

Stellen wir uns vor, eine große Datenbank wird gestohlen, zusammen mit 10 Millionen Passwort-Hashes. Es ist ziemlich einfach, die Regenbogentabelle nach jedem von ihnen zu durchsuchen. Sicherlich werden nicht alle gefunden, aber dennoch... einige von ihnen werden es finden!

Wie kann das verhindert werden?

Wir können versuchen, ein "salt" hinzuzufügen. Hier ist ein Beispiel:

1
$password = "easypassword";
2
3
// this may be found in a rainbow table

4
// because the password contains 2 common words

5
echo sha1($password); // 6c94d3b42518febd4ad747801d50a8972022f956

6
7
// use bunch of random characters, and it can be longer than this

8
$salt = "f#@V)Hu^%Hgfds";
9
10
// this will NOT be found in any pre-built rainbow table

11
echo sha1($salt . $password); // cd56a16759623378628c0d9336af69b74d9d71a5

Grundsätzlich verketten wir die "Salt" -String mit den Passwörtern, bevor wir sie hashen. Die resultierende Zeichenfolge befindet sich offensichtlich nicht auf einem vorgefertigten Regenbogentisch. Aber wir sind noch nicht sicher!


6. Problem Nr. 3: Rainbow-Tabellen (wieder)

Denken Sie daran, dass eine Rainbowtabelle von Grund auf neu erstellt wird, nachdem die Datenbank gestohlen wurde.

Wie kann dies ausgenutzt werden?

Selbst wenn "salt" verwendet wurde, wurde dieses möglicherweise zusammen mit der Datenbank gestohlen. Alles, was sie tun müssen, ist, einen neuen Rainbowtabellenvon Grund auf neu zu erstellen, aber diesmal verketten sie "salt" mit jedem Wort, das sie in den Tisch legen.

In einer generischen Rainbowtabelle kann beispielsweise "easypassword" vorhanden sein. Aber in dieser neuen Regenbogentabelle haben sie auch "f#@V)Hu^%Hgfdseasypassword". Wenn sie alle 10 Millionen gestohlenen gesalzenen Hashes gegen diese Tabelle laufen lassen, können sie wieder einige Streichhölzer finden.

Wie kann dies verhindert werden?

Wir können stattdessen ein "unique salt" verwenden, das sich für jeden Benutzer ändert.

Ein Kandidat für diese Art von Salt ist der ID-Wert des Benutzers aus der Datenbank:

1
$hash = sha1($user_id . $password);

Dies setzt voraus, dass sich die ID-Nummer eines Benutzers nie ändert, was normalerweise der Fall ist.

Wir können auch eine zufällige Zeichenfolge für jeden Benutzer generieren und diese als eindeutiges Salt verwenden. Aber wir müssten sicherstellen, dass wir das irgendwo im Benutzerdatensatz speichern.

1
// generates a 22 character long random string

2
function unique_salt() {
3
4
	return substr(sha1(mt_rand()),0,22);
5
}
6
7
$unique_salt = unique_salt();
8
9
$hash = sha1($unique_salt . $password);
10
11
// and save the $unique_salt with the user record

12
// ...

Diese Methode schützt uns vor Rainbow Tables, da jetzt jedes einzelne Passwort mit einem anderen Wert gesalzen wurde. Der Angreifer müsste 10 Millionen separate Regenbogentabellen erstellen, was völlig unpraktisch wäre.


7. Problem Nr. 4: Hash-Geschwindigkeit

Die meisten Hashing-Funktionen wurden unter Berücksichtigung der Geschwindigkeit entwickelt, da sie häufig zur Berechnung von Prüfsummenwerten für große Datenmengn und Dateien und zur Überprüfung der Datenintegrität verwendet werden.

Wie kann dies ausgenutzt werden?

Wie bereits erwähnt, kann ein moderner PC mit leistungsstarken GPUs (ja, Grafikkarten) so programmiert werden, dass ungefähr eine Milliarde Hashes pro Sekunde berechnet werden. Auf diese Weise können sie einen Brute-Force-Angriff verwenden, um jedes einzelne mögliche Passwort auszuprobieren.

Sie denken vielleicht, dass das Erfordernis eines mindestens 8 Zeichen langen Passworts es vor einem Brute-Force-Angriff schützen könnte, aber lassen Sie uns feststellen, ob dies tatsächlich der Fall ist:

  • Wenn das Passwort Kleinbuchstaben, Großbuchstaben und Zahlen enthalten kann, sind dies 62 (26 + 26 + 10) mögliche Zeichen.
  • Eine 8 Zeichen lange Zeichenfolge hat 62^8 mögliche Versionen. Das sind etwas mehr als 218 Billionen.
  • Mit einer Rate von 1 Milliarde Hashes pro Sekunde kann dies in etwa 60 Stunden gelöst werden.

Und für 6 Zeichen lange Passwörter, was ebenfalls häufig vorkommt, würde es weniger als 1 Minute dauern.

Sie können auch 9 oder 10 Zeichen lange Passwörter benötigen, aber Sie könnten einige Ihrer Benutzer nerven.

Wie kann dies verhindert werden?

Verwenden Sie eine langsamere Hash-Funktion.

Stellen Sie sich vor, Sie verwenden eine Hash-Funktion, die nur 1 Million Mal pro Sekunde auf derselben Hardware ausgeführt werden kann, anstatt 1 Milliarde Mal pro Sekunde. Der Angreifer würde dann 1000-mal länger brauchen, um einen Hash brutal zu erzwingen. 60 Stunden würden fast 7 Jahre werden!

Eine Möglichkeit, dies zu tun, besteht darin, es selbst zu implementieren:

1
function myhash($password, $unique_salt) {
2
3
	$salt = "f#@V)Hu^%Hgfds";
4
	$hash = sha1($unique_salt . $password);
5
6
	// make it take 1000 times longer

7
	for ($i = 0; $i < 1000; $i++) {
8
		$hash = sha1($hash);
9
	}
10
11
	return $hash;
12
}

Oder Sie können einen Algorithmus verwenden, der einen "Kostenparameter" wie BLOWFISH unterstützt. In PHP kann dies mit der Funktion crypt() erfolgen.

1
function myhash($password, $unique_salt) {
2
3
	// the salt for blowfish should be 22 characters long

4
5
	return crypt($password, '$2a$10$'.$unique_salt);
6
7
}

Der zweite Parameter der Funktion crypt() enthält einige Werte, die durch das Dollarzeichen ($) getrennt sind.

Der erste Wert ist '$2a', was darauf hinweist, dass wir den BLOWFISH-Algorithmus verwenden werden.

Der zweite Wert, in diesem Fall '$10', ist der "Kostenparameter". Dies ist der Basis-2-Logarithmus für die Anzahl der Iterationen (10 => 2^10=1024 Iterationen). Diese Zahl kann zwischen 04 und 31 liegen.

Lassen Sie uns ein Beispiel ausführen:

1
function myhash($password, $unique_salt) {
2
	return crypt($password, '$2a$10$'.$unique_salt);
3
4
}
5
function unique_salt() {
6
	return substr(sha1(mt_rand()),0,22);
7
}
8
9
10
$password = "verysecret";
11
12
echo myhash($password, unique_salt());
13
// result: $2a$10$dfda807d832b094184faeu1elwhtR2Xhtuvs3R9J1nfRGBCudCCzC

Der resultierende Hash enthält den Algorithmus ($2a), den Kostenparameter ($10) und das verwendete Salt mit 22 Zeichen. Der Rest ist der berechnete Hash. Lassen Sie uns einen Test durchführen:

1
// assume this was pulled from the database

2
$hash = '$2a$10$dfda807d832b094184faeu1elwhtR2Xhtuvs3R9J1nfRGBCudCCzC';
3
4
// assume this is the password the user entered to log back in

5
$password = "verysecret";
6
7
if (check_password($hash, $password)) {
8
	echo "Access Granted!";
9
} else {
10
	echo "Access Denied!";
11
}
12
13
14
function check_password($hash, $password) {
15
16
	// first 29 characters include algorithm, cost and salt

17
	// let's call it $full_salt

18
	$full_salt = substr($hash, 0, 29);
19
20
	// run the hash function on $password

21
	$new_hash = crypt($password, $full_salt);
22
23
	// returns true or false

24
	return ($hash == $new_hash);
25
}

Wenn wir dies ausführen, sehen wir "Zugriff gewährt!"


8. Sammeln wir alles zusammen

Am Ende schreiben wir eine Utility-Klasse, die auf dem basiert, was wir bisher gelernt haben:

1
class PassHash {
2
3
	// blowfish

4
	private static $algo = '$2a';
5
6
	// cost parameter

7
	private static $cost = '$10';
8
9
10
	// mainly for internal use

11
	public static function unique_salt() {
12
		return substr(sha1(mt_rand()),0,22);
13
	}
14
15
	// this will be used to generate a hash

16
	public static function hash($password) {
17
18
		return crypt($password,
19
					self::$algo .
20
					self::$cost .
21
					'$' . self::unique_salt());
22
23
	}
24
25
26
	// this will be used to compare a password against a hash

27
	public static function check_password($hash, $password) {
28
29
		$full_salt = substr($hash, 0, 29);
30
31
		$new_hash = crypt($password, $full_salt);
32
33
		return ($hash == $new_hash);
34
35
	}
36
37
}

Hier ist die Verwendung bei der Benutzerregistrierung:

1
// include the class

2
require ("PassHash.php");
3
4
// read all form input from $_POST

5
// ...

6
7
// do your regular form validation stuff

8
// ...

9
10
// hash the password

11
$pass_hash = PassHash::hash($_POST['password']);
12
13
// store all user info in the DB, excluding $_POST['password']

14
// store $pass_hash instead

15
// ...

Und hier ist die Verwendung während eines Benutzeranmeldevorgangs:

1
// include the class

2
require ("PassHash.php");
3
4
// read all form input from $_POST

5
// ...

6
7
// fetch the user record based on $_POST['username']  or similar

8
// ...

9
10
// check the password the user tried to login with

11
if (PassHash::check_password($user['pass_hash'], $_POST['password']) {
12
	// grant access

13
	// ...

14
} else {
15
	// deny access

16
	// ...

17
}

9. Ein Hinweis zur Verfügbarkeit von Blowfish

Der Blowfish-Algorithmus ist nicht in allen Systemen implementiert, obwohl er mittlerweile recht beliebt ist. Sie können Ihr System mit folgendem Code überprüfen:

1
if (CRYPT_BLOWFISH == 1) {
2
	echo "Yes";
3
} else {
4
	echo "No";
5
}

Ab PHP 5.3 müssen Sie sich jedoch keine Sorgen mehr machen. PHP wird mit dieser integrierten Implementierung ausgeliefert.


Abschluss

Diese Methode zum Hashing von Kennwörtern sollte für die meisten Webanwendungen solide genug sein. Vergessen Sie jedoch nicht: Sie können auch verlangen, dass Ihre Mitglieder stärkere Kennwörter verwenden, indem Sie Mindestlängen, gemischte Zeichen, Ziffern und Sonderzeichen erzwingen.

Eine Frage an Sie, Leser: Wie haben Sie Ihre Passwörter gehasht? Können Sie Verbesserungen gegenüber dieser Implementierung empfehlen?