Geschrieben von: Christoph Wille
Kategorie: Sicherheit
This printed page brought to you by AlphaSierraPapa
In sehr vielen - um nicht zu sagen fast allen - Webanwendungen werden Benutzerdaten verwaltet, vom Webforum bis hin zum Webshop. Diese Benutzerdaten umfassen auch die Logininformationen der User, welche neben dem Usernamen auch das Passwort enthalten - und das als Plain Text. Eine Sicherheitslücke par excellence.
Warum ist das eine Sicherheitslücke, wenn man den Usernamen und das Passwort als Plain Text speichert? Nun, stellen Sie sich vor, ein Cracker verschafft sich durch etwaige Betriebssystem- bzw. Serversoftwarefehler Zugang zum System, und kann die Benutzerdatenbank auslesen. Da er den Benutzernamen und das Passwort jedes beliebigen Users kennt, kann er jetzt als "echter" User einloggen und mit dessen Berechtigungen machen was er möchte - von der Bestellung im Webshop hin zu Rufmord im Forum. Und Sie sind der Betreiber...
Wie kann man dieses Sicherheitsrisiko eliminieren? Nun, warum weit schweifen wenn es eine seit Jahrzehnten bekannte und bewährte Methode zur sicheren Speicherung von Passwörtern gibt: unter UNIX werden Passwörter von Benutzern als sogenannter "gesalteter Hash" gespeichert.
Ein Hash ist ein numerischer Wert fixer Länge der Daten beliebiger Länge eindeutig identifiziert. Ein Beispiel für einen Hashalgorithmus ist SHA1, der bereits Thema eines ASP Artikels war. Der geneigte Leser könnte nun einwenden, daß das Speichern des Hashes anstatt des Passworts ausreichen würde - warum aber stimmt das nicht?
Der Grund hierfür ist, daß gegen gehashte Passwörter - ein gutes Beispiel sind die MD5-gehashten Passwörter von NT4 - üblicherweise eine sogenannte Dictionary Attacke gefahren wird. Dabei handelt es sich um einen Brute Force Angriff: alle Wörter in einem Wörterbuch wurden MD5 gehasht, und diese werden nun mit der Passwortdatenbank verglichen. Und raten Sie mal, wie schnell man damit einige Passwörter gefunden hat.
Der gesaltete Hash hat den Sinn, genau solche Attacken ins Leere laufen zu lassen, indem man jedem Passwort vor dem Hashen einen zufälligen Wert, den sogenannten Salt, anhängt - und erst dann den Hash über das Passwort und den Salt berechnet. Zwar muß man zum Vergleich des Passwortes den Salt neben gesalteten Hash mitspeichern, aber der einzige Angriffsvektor bleibt jetzt das Wörterbuch für jedes einzelne gespeicherte Passwort neu mit dem Salt zu codieren - und das dauert dann schon sehr lange.
Wie bereits erwähnt, muß man jetzt anstatt Benutzername und Passwort drei Felder speichern: Benutzername, Salt und das mit dem Salt gehashte Passwort. Ebenfalls bereits erwähnt habe ich, daß wenn diese Daten einem Cracker in die Hände fallen, er mit Standardangriffen ein Problem bekommt, und sehr wahrscheinlich sich ein leichteres Opfer suchen wird.
Ein Punkt muß aber beachtet werden: eine "Passwort-Erinnerungsemail" kann man jetzt nicht mehr schicken - alles was man tun kann, ist dem User ein vollständig neues Passwort zu generieren und zuzusenden. Da auch in diesem Bereich viele Fehler passieren, beginnen wir mit dem .NET Code für das Generieren eines wirklich zufälligen Passwortes.
Die gesamte Klasse enstand im Zuge eines (C# ASP.NET) Community Projekts zusammen mit einem weiteren AspHeute-Autor, nämlich Alexander Zeitler. Auch dort stellte sich die Frage, wie man gute Passwörter generiert, und wie man diese korrekt in einer Datenbank ablegt.
Dafür haben wir die Klasse Password erstellt, die folgende Signatur hat:
namespace DotNetGermanUtils { public class Password { public Password(string strPassword, int nSalt) public static string CreateRandomPassword(int PasswordLength) public static int CreateRandomSalt() public string ComputeSaltedHash() } }
Die Methode zur Generierung eines neuen Passwortes ist statisch, und man kann bestimmen, wie lang das generierte Passwort sein soll:
public static string CreateRandomPassword(int PasswordLength) { String _allowedChars = "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNOPQRSTUVWXYZ23456789"; Byte[] randomBytes = new Byte[PasswordLength]; RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider(); rng.GetBytes(randomBytes); char[] chars = new char[PasswordLength]; int allowedCharCount = _allowedChars.Length; for(int i = 0;i<PasswordLength;i++) { chars[i] = _allowedChars[(int)randomBytes[i] % allowedCharCount]; } return new string(chars); }
Das Prinzip ist ähnlich zur ASP Lösung im Artikel Generieren eines sicheren Paßwortes, allerdings ist hier etwas spezielles eingebaut: wir verwenden kryptographisch sichere Zufallszahlen, um aus dem "Array" _allowedChars Buchstaben für das Passwort auszuwählen. Die Klasse RNGCryptoServiceProvider wurde auch schon im Artikel Unknackbare Verschlüsselung mit Onetime Pads besprochen.
Damit hat man ein wirklich zufälliges Passwort generiert, das man nun dem Benutzer als Initialpasswort setzen kann - nur braucht man dafür einen Salt.
Im Prinzip handelt es sich hier nur um eine Hilfsfunktion, die ebenfalls auf den RNGCryptoServiceProvider zurückgreift:
public static int CreateRandomSalt() { Byte[] _saltBytes = new Byte[4]; RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider(); rng.GetBytes(_saltBytes); return ((((int)_saltBytes[0]) << 24) + (((int)_saltBytes[1]) << 16) + (((int)_saltBytes[2]) << 8) + ((int)_saltBytes[3])); }
Erzeugt wird ein 4 Byte langer Salt (ein Integer, aus Einfachheitsgründen für die Speicherung in Datenbanktabellen). Dieser Salt sowie das generierte Passwort dienen als Grundlage, um den Salted Hash zu erzeugen.
Das Berechnen des Salted Hash ist eine Instanzmethode, die auf zwei Membervariablen zugreift, die im Konstruktor gesetzt werden:
public class Password { private string _password; private int _salt; public Password(string strPassword, int nSalt) { _password = strPassword; _salt = nSalt; }
Damit liefert die Methode ComputeSaltedHash nur noch den Hash selbst zurück, und nimmt keine weiteren Parameter an. Gehashed wird übrigens mit dem bekannten SHA1 Algorithmus:
public string ComputeSaltedHash() { // Create Byte array of password string ASCIIEncoding encoder = new ASCIIEncoding(); Byte[] _secretBytes = encoder.GetBytes(_password); // Create a new salt Byte[] _saltBytes = new Byte[4]; _saltBytes[0] = (byte)(_salt >> 24); _saltBytes[1] = (byte)(_salt >> 16); _saltBytes[2] = (byte)(_salt >> 8); _saltBytes[3] = (byte)(_salt); // append the two arrays Byte[] toHash = new Byte[_secretBytes.Length + _saltBytes.Length]; Array.Copy(_secretBytes, 0, toHash, 0, _secretBytes.Length); Array.Copy(_saltBytes, 0, toHash, _secretBytes.Length, _saltBytes.Length); SHA1 sha1 = SHA1.Create(); Byte[] computedHash = sha1.ComputeHash(toHash); return encoder.GetString(computedHash); }
Damit haben wir die gesamten Funktionen beisammen, und können die Klasse zum Einsatz bringen.
Ich habe ein kleines Beispiel zusammengestellt, das das Erstellen eines neuen Passwortes, eines neuen Salts, und dann das Generieren des Salted Hash zeigt:
using System; using DotNetGermanUtils; namespace HashPassword { class TestApplication { [STAThread] static void Main(string[] args) { // Generate a new random password string string myPassword = Password.CreateRandomPassword(8); // Debug output Console.WriteLine(myPassword); // Generate a new random salt int mySalt = Password.CreateRandomSalt(); // Initialize the Password class with the password and salt Password pwd = new Password(myPassword, mySalt); // Compute the salted hash // NOTE: you store the salt and the salted hash in the datbase string strHashedPassword = pwd.ComputeSaltedHash(); // Debug output Console.WriteLine(strHashedPassword); } } }
Folgender Punkt ist besonders wichtig: Generieren Sie für jeden Benutzer unbedingt einen neuen Salt. Sollten beide User zufällig das gleiche Passwort verwenden, so ist der Salted Hash dennoch verschieden für beide Benutzerkonten!
Der gezeigte Sourcecode zeigt das Erstellen eines neuen Passworts und eines neuen Salts, wie sieht es aus wenn der User einloggen möchte? Nun, nichts leichter als das:
// retrieve salted hash and salt from user database, based on username ... Password pwd = new Password(txtPassword.Text, nSaltFromDatabase); if (pwd.ComputeSaltedHash() == strStoredSaltedHash) { // user is authenticated successfully } else { ...
Im Prinzip nicht anders als bei bisherigen Benutzername/Passwort Implementierungen, aber die Daten sind bedeutend sicherer auch im Falle daß die (serverseitigen) Passwortdaten unbefugten Dritten in die Hände fallen.
Die Klasse, die im heutigen Artikel vorgestellt wurde, kann man in seine eigenen .NET Projekte einbinden - entweder direkt in C# Projekten oder als Assembly in anderen Programmiersprachen. Ausreden für unsichere Passwortspeicherung gibt es ab sofort nicht mehr!
This printed page brought to you by AlphaSierraPapa
Klicken Sie hier, um den Download zu starten.
http://www.aspheute.com/code/20040105.zip
Aber bitte mit Rijndael
http:/www.aspheute.com/artikel/20010528.htm
CAPICOM One
http:/www.aspheute.com/artikel/20020115.htm
Generieren eines sicheren Paßwortes
http:/www.aspheute.com/artikel/20000531.htm
Passwörter mit SHA1 absichern
http:/www.aspheute.com/artikel/20010330.htm
PGP-Verschlüsselung bei Dateien
http:/www.aspheute.com/artikel/20000920.htm
Unknackbare Verschlüsselung mit Onetime Pads
http:/www.aspheute.com/artikel/20010924.htm
Ver- und entschlüsseln von Texten mit PGP
http:/www.aspheute.com/artikel/20000921.htm
©2000-2006 AspHeute.com
Alle Rechte vorbehalten. Der Inhalt dieser Seiten ist urheberrechtlich geschützt.
Eine Übernahme von Texten (auch nur auszugsweise) oder Graphiken bedarf unserer schriftlichen Zustimmung.