Schwache Kennwörter sicher speichern

Veröffentlicht: 01. Sep 2005

Von Mathias Schiffer

In diesem MSDN Quickie erfahren Sie, wie Sie selbst schwache Kennwörter so abspeichern können, dass sie (fast) nicht zu knacken sind.

Im MSDN Quickie "Passwort-Diebstahl durch Hash-Codes verhindern" habe ich Ihnen gezeigt, wie Sie zur Kontrolle von Benutzernamen und Kennwörtern so genannte Hashwerte abspeichern können. Das Verfahren beruht darauf, aus einem vorgegebenen Text einen eindeutig bestimmten, nicht umkehrbaren Ergebniswert zu ermitteln. Statt den angegebenen und originalen Text miteinander zu vergleichen (und dafür den originalen Text vor Fremdzugriff geschützt irgendwo ablegen zu müssen), werden diese Hashwerte miteinander verglichen. Einen solchen Hashwert können Sie im Vergleich zum Originaltext relativ sorglos abspeichern, da sich der Originaltext über den Hashwert nicht wieder herstellen lässt.

Auf dieser Seite

 Schwache Kennwörter sind ein Problem
 Streuen Sie Salz
 Wie schützt Salz gegen lexikalische Attacken?
 Zusammenfassung
 Codebeispiel: So einfach ist Salting

Schwache Kennwörter sind ein Problem

Einen Haken hat die Sache allerdings: Die Sicherheit dieser Methode ist direkt davon abhängig, wie sorgsam der Originaltext gewählt wurde. Schließlich kann auch ein Dritter mit einigen Tausend bis Millionen vorgefertigten Originaltexten zurechtkommen, mit häufig verwendeten Hashverfahren deren Hashwerte ausrechnen und diese Werte dann mit einer anzugreifenden Kennwortdatei vergleichen.

Die Montagsmaler "Hund", "Katze" und "Maus" werden Sie in einem solchen "Lexikon" vorgefertigter Kennwörter zur Berechnung von Hashtabellen ebenso finden wie übliche Vornamen, Haustier- und Kosenamen und die Geburtstagsdaten der letzten 70 Jahre. Kaum hingegen wird ein solcher lexikalischer Angriff ("dictionary attack") auf Ihr sorgsam gewähltes Passwort "mI52Wtuj6rR" Erfolg haben.

Selbstverständlich entsprechen Ihre eigenen Zugangsdaten und Kennwörter samt und sonders am ehesten dem letzten Beispiel. Überhaupt keine Frage (wir sind ja unter uns: gelogen, oder?). Als Entwickler hingegen können Sie jedoch die Charakteristika der Kennwörter Ihrer Anwender nur begrenzt kontrollieren: Laut einer Mitte 2005 veröffentlichten Studie halten in deutschen Unternehmen 20% der Mitarbeiter ihre eigenen Kennwörter für zu einfach zu erraten. Mit 44% glaubt nicht einmal die Hälfte der befragten Anwender, absolut sichere Kennwörter zu verwenden.

So kommen "Hund", "Katze" und "Maus" - zumindest aber doch "Mausi" schon des Öfteren mal vor. Da können Sie hashen, bis der Arzt kommt: Gegen eine "dictionary attack" auf den Hashwert sind Sie mit solchen Trivialkennwörtern nicht mehr gefeit.

 

Streuen Sie Salz

Eine Möglichkeit, Hashwerte für derartig schwache Kennwörter sicherer abspeichern zu können, nennt sich "Salting". Hinter diesem "Salzen" steht ein weiterer zufälliger Wert im Kennwortausdruck, für den ein Hashwert berechnet werden soll. Kurz gesagt: Sie erweitern das schwache Kennwort einfach und erzeugen so ein erweitertes Kennwort, dessen Hashwert Sie dann abspeichern. Der Hashwert dieses erweiterten Kennworts unterscheidet sich von dem des ursprünglichen Kennworts. Natürlich benötigen Sie neben der Information, wo das "Salz" algorithmisch eingebracht wird, später insbesondere die Information, welcher zufällige Wert zur Erweiterung des Kennworts verwendet wurde. Deswegen speichern Sie den verwendeten Zufallswert im Klartext mit dem Hashwert gemeinsam ab.

Bei einer späteren Kennwortprüfung führen Sie mit einem eingegebenen Kennwort genau die gleiche Operation erneut durch: Sie lesen aus dem abgespeicherten Ergebnis den zufällig vergebenen "Salzwert" des Ziel-Hashwerts vorab aus, erweitern das eingegebene Kennwort um diesen Wert und berechnen aus diesem Ergebnis den mit dem gespeicherten Hash zu vergleichenden Hashwert.

Sie werden sich möglicherweise fragen, warum das schwache Kennwort durch die Erweiterung des Salzwertes stärker geworden sein soll, wo doch die Erweiterung des Kennworts im Klartext mit dem Hash abgespeichert wurde. Und Sie haben Recht: Das Kennwort ist absolut nicht stärker geworden. Kommt etwa der Kollege vom Nachbarschreibtisch und gibt nach einigen Fehlversuchen in den Login-Dialog das Kennwort "Mausi" ein, so hilft Salting nicht die Bohne. Denn immerhin ist dies ja auch genau das, was der legitime Anwender tun würde. Um solche trivialen Angriffe soll es hier jedoch nicht gehen, sie werden anders und deutlich einfacher abgewehrt (Bankkarten beispielsweise müssen Sie normalerweise nach dreimaliger Falscheingabe Ihrer PIN persönlich bei Ihrer Bank abholen).

Nicht umsonst ist dieser MSDN Quickie überschrieben mit "Schwache Kennwörter sicher speichern". Eine lexikalische Attacke richtet sich normalerweise gegen Kennwortlisten, also gespeicherte Daten, die eingegebene Kennwörter gegenprüfbar machen.

 

Wie schützt Salz gegen lexikalische Attacken?

Durch das "Salzen" erhöhen Sie den Aufwand für eine solche lexikalische Attacke auf die gespeicherten Daten erheblich - und zwar um den Faktor 2^Bitbreite des "Salzes", also beispielsweise um den Faktor 65536 bei nur 2 Bytes "Salz". Dass diese triviale Erweiterung einen Sicherheitsgewinn darstellt, ist zunächst nicht in sich verständlich, sondern liegt in der Vorgehensweise einer lexikalischen Attacke begründet.

Effizientes Ausspähen von Kennwörtern bedient sich nämlich im Voraus berechneter Tabellen auf Basis des vermuteten oder bekannten Hashmechanismus (Hashtabellen). Das bedeutet, dass im möglichst umfassenden "Lexikon" der Hashwert für jedes Kennwort auf Basis eines vorbestimmten Hashalgorithmus vorab berechnet und in Tabellen überführt wird, die später beliebig oft auf kritische Dateien "losgelassen" werden können. Für andere Algorithmen werden einfach andere Tabellen erzeugt; die Anzahl häufig verwendeter Hashalgorithmen ist überschaubar. Der Aufwandsvorteil dieser Vorgehensweise ergibt sich aus der einmaligen Berechnung und der folgend beliebig häufigen und schnelleren Verwertung der Berechnungsergebnisse - nicht anders als bei friedfertigeren Verwendungszwecken von Hashtabellen auch.

Indem Sie durch das "Salzen" mit einem Zufallswert das vom Anwender verwendete Kennwort verändern, können aus einem Kennwort exakt so viele Kennwort-Salz-Kombinationen entstehen, wie es unterschiedliche Salzwerte geben kann. Aus der Kennwort-Salz-Kombination wird der abgespeicherte Hashwert gebildet - für jede mögliche Kennwort-Salz-Kombination gibt es daher einen eigenen Hashwert (ohne das Salzen gibt es für jedes Kennwort, das in einem Lexikon vorkommen könnte, nur einen Hashwert). Bei zwei Bytes "Salz" bedeutet das bereits, dass selbst mit Wissen um den eingesetzten Hashalgorithmus, die Breite, die Nutzungsform und den jeweils konkreten Wert des "Salzes" für jeden einzelnen Wörterbucheintrag 65536 Hashwerte in die Hashtabellen eingetragen werden müssten. Oder bildhafter ausgedrückt: Statt einer einzigen Hashtabelle würden ganze 65536 im Voraus berechnete Hashtabellen benötigt. Und das bei vollständiger Kenntnis des Sicherungsmechanismus! Und all diese Tabellen sind noch dazu unbrauchbar, wenn auch nur eine der genannten Einflussgrößen abweicht.

Da ein Lexikon zudem unzählige Wörter enthält, haben Sie hiermit das Konzept der Hashtabellen bereits ad absurdum geführt. Dann macht es - neben einer Kapitulation - normalerweise bereits mehr Sinn, eine lexikalische Attacke ohne Hashtabellen auszuführen. Doch selbst bei Kenntnis von Hash-Algorithmus, Breite, Nutzungsform und des jeweils konkreten Werts des "Salzes" erfordert dies noch immer erhebliche Rechenleistung und -zeit. Möglicherweise steht die anzugreifende Datei so lange gar nicht zur Verfügung - oder hat sich längst überlebt.

 

Zusammenfassung

Salting schützt die Sicherheit von abgespeicherten Hashwerten, die z.B. Kennwörter repräsentieren, durch einen schlichten Mechanismus, der durch Erweiterung des Kennworts Einfluss auf die Bildung des abgespeicherten Hashwerts hat. Der Schutz erschwert lexikalische Attacken auf Basis vorberechneter Hashtabellen erheblich.

Kein Sicherheitsplus für schwache Kennwörter ergibt sich mit diesem Vorgehen für die Kennwortprüfung Ihrer Software: Korrekte Anmeldeinformationen können von legitimen Anwendern ebenso eingegeben werden wie von Angreifern oder böswilliger Software. Es bleibt Aufgabe des Anwenders, solche Bedrohungen durch die Wahl eines nicht allzu trivialen Kennworts zu verhindern. Der Software kommt die Aufgabe zu, nicht ohne weiteres unzählig viele Anmeldeversuche zuzulassen - sowie natürlich Daten zur Prüfung von Kennwörtern sicher zu hinterlegen.

 

Codebeispiel: So einfach ist Salting

Salting fällt in den klassischen Bereich: kleine Ursache, große Wirkung. Das Verständnis des Sicherheitsproblems erfordert bereits mehr Aufmerksamkeit als dessen Lösung. Entsprechend einfach fällt das Codebeispiel zum Salting aus.

Beim letzten Quickie zum Hashen von Kennwörtern wurde der MD5-Algorithmus verwendet. Schon zur Illustration, dass es auf den eingesetzten Algorithmus gar nicht ankommt, soll diesmal ein anderer Hashalgorithmus den Zuschlag erhalten. Die Wahl fällt dabei auf den häufig verwendeten SHA1-Algorithmus, der mit 160 Bit gegenüber 120 Bit bei MD4/5 auch einen breiteren Hashwert liefert. Auch er wird vom .NET Framework direkt unterstützt.

Im folgenden Codebeispiel sorgt die Funktion CreateSaltedPasswordHash dafür, dass einem übergebenen Kennwort ein übergebener Salzwert angehangen wird. Den dafür notwendigen, zufälligen Salzwert können Sie durch Aufruf der Funktion CreateSalt erzeugen. Aus dem Gesamtkonstrukt wird dann der Hashwert berechnet. An diesen wird letztlich der Salzwert im Klartext angehangen.

Die Funktion VerifyPassword sorgt exemplarisch dafür, dass ein abgespeicherter, "gesalzener" Hashwert mit einem eingegebenen Kennwort verglichen wird, das dafür zunächst um das "Salz" des Vergleichskennworts erweitert und dann gehasht wird. In einem realen Szenario sollten Sie solche Entscheidungen nicht anhand einer Funktion mit entscheidungsrelevantem Rückgabewert treffen, da der Rückgabewert durch Cracker zu einfach zu manipulieren ist.

  Public Function CreateSalt(ByVal SaltLength As Integer)
    ' "Salz" der angefragten Länge erzeugen
  
    Dim Salt(SaltLength - 1) As Byte
    Dim csp As System.Security.Cryptography.RNGCryptoServiceProvider _
            = New System.Security.Cryptography.RNGCryptoServiceProvider
    csp.GetBytes(Salt)
  
    Return Salt
  
  End Function
  
  
  Public Function CreateSaltedPasswordHash(ByVal UnsaltedPassword As Byte(), _
                                           ByVal Salt As Byte() _
                                           ) As Byte()
    ' Erzeugt aus einem unbehandelten Kennwort und dem übergebenen "Salz" einen um den
    ' Salzwert erweiterten Hashwert für das gesalzene Kennwort.
  
    ' SaltedPassword ist das unbehandelte Kennwort zzgl. Salz:
    ' Ausreichend großes Byte-Array zur Verfügung stellen
    Dim SaltedPassword(UnsaltedPassword.Length + Salt.Length - 1) As Byte
    ' Das ungesalzene Kennwort vorne einfügen
    UnsaltedPassword.CopyTo(SaltedPassword, 0)
    ' Das "Salz" dahinter einfügen
    Salt.CopyTo(SaltedPassword, UnsaltedPassword.Length)
  
    ' Den Hashwert für das gesalzene Kennwort erzeugen:
    Dim SaltedPasswordHash As Byte() = CreateHash(SaltedPassword)
  
    ' Dem Hashwert wird nun noch im Klartext das "Salz" hinzugefügt:
    ' Ausreichend großes Byte-Array zur Verfügung stellen
    Dim SaltedPasswordHashPlusSalt(SaltedPasswordHash.Length + Salt.Length - 1) As Byte
    ' Den berechneten Hashwert einfügen:
    SaltedPasswordHash.CopyTo(SaltedPasswordHashPlusSalt, 0)
    ' Den Salzwert anhängen
    Salt.CopyTo(SaltedPasswordHashPlusSalt, SaltedPasswordHash.Length)
  
    ' Der Ergebniswert liegt nun vor:
    Return SaltedPasswordHashPlusSalt
  
  End Function
  
  
  Public Function VerifyPassword(ByVal UnsaltedPassword As Byte(), _
                                 ByVal StoredPassword As Byte() _
                                 ) As Boolean
    ' Nimmt ein Klartextkennwort UnsaltedPassword an und verifiziert es gegen
    ' einen Hash StoredPassword, der als Hash eines gesalzenen Kennworts samt
    ' angehängtem Salz übergeben wird. Bei Übereinstimmung wird True zurück
    ' gegeben, sonst False.
  
    ' Einen Hashwert erzeugen um herauszufinden, wie breit ein Hashwert ist
    ' (bei SHA1 feststehend 160 Bits - hier aber universeller ausgelegt,
    ' um in der Funktion CreateHash beliebige andere Algorithmen einsetzen
    ' zu können):
    Dim UnsaltedPasswordHash As Byte() = CreateHash(New Byte() {0})
    Dim SaltLength As Integer = StoredPassword.Length - UnsaltedPasswordHash.Length
    Dim SaltStart As Integer = StoredPassword.Length - SaltLength
    Dim Salt(SaltLength - 1) As Byte
  
    ' Parameterprüfung ("all input is evil"):
    If (UnsaltedPassword Is Nothing) OrElse _
       (StoredPassword Is Nothing) Then
      Return False
    End If
  
    ' Den im Klartext vorliegenden Salzwert ermitteln:
    Dim i As Integer
    For i = 0 To SaltLength - 1
      Salt(i) = StoredPassword(SaltStart + i)
    Next
  
    ' Einen Hash für das gesalzene Kennwort erzeugen (samt angehangenem Salz):
    Dim SaltedPasswordHash As Byte() = CreateSaltedPasswordHash(UnsaltedPassword, Salt)
  
    ' Den erzeugten Wert mit dem gespeicherten Wert vergleichen:
    Return CompareByteArray(StoredPassword, SaltedPasswordHash)
  
  End Function
  
  
  Private Function CreateHash(ByVal ByteArrayToHash As Byte()) As Byte()
    ' Erzeugt aus einem übergebenen Bytearray einen Hashwert (hier SHA1)
  
    Dim SHA1 As System.Security.Cryptography.SHA1 = System.Security.Cryptography.SHA1.Create()
    Return SHA1.ComputeHash(ByteArrayToHash)
  
  End Function
  
  
  Private Function CompareByteArray(ByVal ba1 As Byte(), ByVal ba2 As Byte()) As Boolean
    ' Vergleicht zwei Byte-Arrays. Sind beide Arrays identisch, so wird
    ' True zurück gegeben. Andernfalls wird False retourniert.
  
    ' Haben die Arrays identische Länge?
    If (ba1.Length <> ba2.Length) Then
      Return False
    End If
  
    ' Einzelne Arrayelemente vergleichen, bei der ersten
    ' Nichtübereinstimmung Ausgang mit False:
    Dim i As Integer
    For i = 0 To ba1.Length - 1
      If ba1(i) <> ba2(i) Then
        Return False
      End If
    Next
  
    ' Alle Arrayelemente waren identisch - True retrounieren:
    Return True
  
  End Function

Mathias Schiffer widmet sich als freier Softwareentwickler und Technologievermittler größeren Projekten ebenso wie arbeitserleichternden Alltagslösungen. Seit Jahren gibt er sein Wissen in unzähligen Publikationen und Beratungen auch an andere Entwickler und Entscheider weiter. Sie erreichen ihn per E-Mail an die Adresse Schiffer@mvps.org.