Kolumne: C# im Fokus

Zeichenketten – aber sicher!
Kommentare

Für die effiziente Verarbeitung von Zeichenketten stellt das .NET Framework seit der ersten Version die String-Klasse bereit. Werden jedoch sensible Daten, wie zum Beispiel Passwörter, verarbeitet, sollte zur Speicherung nicht die allgemeine String-Klasse zum Einsatz kommen.

Windows Developer

Der Artikel „Zeichenketten – aber sicher!“ von Marc André Zhou ist erstmalig erschienen im Windows Developer 7.2012

Verwendung der SecureString-Klasse

In nahezu jedem Anwendungssystem werden Zeichenketten verarbeitet. In den meisten Fällen handelt es sich dabei um nicht besonders schutzwürdige Daten. Teilweise werden aber sensible Daten geladen und verarbeitet. Wird zum Beispiel ein Anwender aufgefordert, sein Passwort einzugeben, muss das eingegebene Passwort vor unbefugten Zugriffen geschützt werden. Der Einsatz der String-Klasse ist an dieser Stelle aus mehreren Gründen nicht empfehlenswert:

  • String-Instanzen können durch die Garbage Collection verschoben werden, dadurch entstehen u. U. mehrere Kopien einer Zeichenkette.
  • Zeichenketten sind nicht verschlüsselt. Potenzielle Angreifer können auf Speicherbereiche zugreifen und über diesen Weg Zeichenketten aus dem Speicher auslesen. Wird zudem der Prozess aus Ressourcengründen ausgelagert, wird die Zeichenkette unverschlüsselt auf der Festplatte abgelegt. Auch hierauf können potenzielle Angreifer zugreifen.
  • Zeichenketten sind nicht änderbar. Bei jeder Änderung wird eine neue Zeichenkette erstellt. So liegen mehrere gleichartige Zeichenketten im Speicher und die Angriffsfläche wird erhöht.
  • Da Zeichenketten nicht änderbar sind, können sie auch nicht gelöscht bzw. mit Nullwerten überschrieben werden.

Aus den oben genannten Punkten gehen die wesentlichen Probleme hervor. Normale Zeichenketten liegen mehr oder wenig ungeschützt im Speicher. Gerade bei der Verwaltung von Passwörtern kann dies ein erhebliches Sicherheitsrisiko darstellen. Die Nutzung der String-Klasse ist daher für solche Fälle nicht möglich. Bis zur .NET-Version 2.0 konnte das Problem durch die Nutzung eines verschlüsselten char-Arrays gelöst werden. Gegenüber einer String-basierten Zeichenkette kann ein char-Array sehr einfach mit Nullwerten überschrieben werden. Im Gegensatz zu einem String wird bei diesem Vorgang keine neue Zeichenkette, bzw. char-Array, erstellt.

Speicherinhalte auslesen

Wie einfach das Auslesen der Speicherinhalte eines jeden aktiven Prozesses ist, zeigt ausschnittsweise Listing 1. Über die kernel32-Methode ReadProcessMemory ist der Zugriff auf beliebige Speicherbereiche möglich. In diesem Beispiel wird der Speicherinhalt des einfachen Konsolenprogramms aus Listing 2 ausgelesen und in eine Textdatei geschrieben. Von Interesse ist der Inhalt der Variable pwd. Liest man den Speicherinhalt von dem aktiven Testprogramm aus, findet sich diese Zeichenkette, wie es auch Abbildung 1 zeigt, ungeschützt und lesbar im Speicher. An diesem Beispiel wird deutlich, wie einfach Inhalte ausgespäht werden können. Daher müssen sensible Daten, bevor sie in den Speicher abgelegt werden, geschützt werden. Für Zeichenketten müssen dafür keine eigenen Verschlüsselungsmethoden implementiert werden. Die Klasse SecureString bietet bereits die notwendigen Verschlüsselungsmechanismen.

Listing 1

while (lpMem < sysInfo.lpMaximumApplicationAddress.ToInt64())
{                        
    result = VirtualQueryEx(process[0].Handle, (IntPtr)lpMem, out mbi, 
                                (uint)sizeof(MEMORY_BASIC_INFORMATION));
    if (result == (int)sizeof(MEMORY_BASIC_INFORMATION))
    {                                
        if (mbi.RegionSize.ToInt64() > 0)
        {
            byte* cbBuffer = (byte*)HeapAlloc(GetProcessHeap(), 
                                        0, mbi.RegionSize.ToInt32());
            ReadProcessMemory(process[0].Handle, mbi.BaseAddress, cbBuffer, 
                                  (uint)mbi.RegionSize.ToInt64(), ref dwTotalRead);
            if (cbBuffer != null)
            {
                for (int index = 0; index < HeapSize(ph, 0, cbBuffer); index++)
                    content.AppendFormat("{0:X2}", (char)cbBuffer[index]);
                HeapFree(GetProcessHeap(), 0, cbBuffer);
            }
            else
                break;
        }
    }
    lpMem = mbi.BaseAddress.ToInt64() + mbi.RegionSize.ToInt64();
}  
Listing 2

static void Main(string[] args)
{
    string pwd = "Das ist das Passwort in plain Text!";
    Console.WriteLine(pwd);
    Console.ReadLine();
}  

Abb. 1: Ausgelesener Speicherinhalt
Abb. 1: Ausgelesener Speicherinhalt
Die SecureString-Klasse

Da die Anforderung nach im Speicher verschlüsselten Zeichenketten keine Seltenheit ist, wurde mit dem .NET Framework 2.0 die Klasse SecureString eingeführt [1]. Wie aus der Klassenbezeichnung bereits hervorgeht, handelt es sich hierbei um eine "sichere" Zeichenkette. Die Zeichenkette innerhalb einer SecureString-Instanz ist sicher, da diese automatisch mithilfe des Data Protection API (kurz: DPAPI) verschlüsselt im Speicher abgelegt wird. Die Daten werden erst wieder entschlüsselt, wenn das besitzende Programm auf die Daten zugreift. Somit verbleiben die Daten die meiste Zeit verschlüsselt im Speicher. Potenzielle Angreifer haben daher erheblich weniger Zeit die Daten auszuspähen. Auch wenn das Programm aus Ressourcengründen auf eine Festplatte ausgelagert wird, bleibt die Zeichenkette verschlüsselt. Die Garbage Collection verhält sich gegenüber einer SecureString-Instanz anders als gegenüber einer normalen String-Instanz. Übliche String-Instanzen können von der Garbage Collection im Speicher verschoben werden. Dieser Vorgang kann zu Kopien führen, und die Zeichenkette befindet sich öfter im Speicher. Eine SecureString-Instanz dagegen wird nicht im Speicher verschoben. Ebenfalls implementiert die SecureString-Klasse die Schnittstelle IDisposable. Sobald ein SecureString-Objekt per Dispose-Aufruf vernichtet wird, wird der belegte Speicherplatz (mehrmals) mit Nullen überschrieben. Um auch während der Lebensdauer den enthaltenen Inhalt gegenüber äußeren Manipulationen zu schützen, kann mithilfe der Methode ReadOnly ein SecureString-Objekt als schreibgeschützt markiert werden. Ist dies einmal geschehen, kann der Inhalt nicht mehr geändert werden.

SecureString im Einsatz

Die Verwendung der SecureString-Klasse ist nicht sonderlich aufwändig. Die Klasse besitzt zwei Konstruktoren. Ein Konstruktor erwartet keinerlei Parameter und legt somit lediglich eine neue leere SecureString-Instanz an. Dem zweiten Konstruktor kann ein Zeiger auf ein char-Array übergeben werden, das den Inhalt der Zeichenkette enthält. Das übergebene char-Array kann nach Objektanlage zerstört werden, bzw. mit Nullen überschrieben werden, da es nicht von der SecureString-Instanz referenziert wird. Listing 3 zeigt dazu ein entsprechendes Beispiel. Zunächst wird das char-Array secContent mit dem zu verschlüsselnden Inhalt initialisiert. Danach wird eine neue SecureString-Instanz erstellt und das zuvor erstellte char-Array übergeben. Wie aus Listing 3 hervorgeht, muss dieses in einem fixed-Block geschehen. Dies bedeutet auch, dass die Methode als unsafe [2] gekennzeichnet werden muss. Nachdem die Instanz erstellt wurde, wird der Inhalt durch den Aufruf MakeReadOnly als schreibgeschützt markiert. Das garantiert, dass der Inhalt nicht mehr verändert werden kann. Um den Inhalt wieder auslesen zu können, wird zunächst ein Zeiger auf den Speicherbereich benötigt. Er kann über den Aufruf der statischen Methode SecureStringToBSTR der Klasse Marshal abgerufen werden. Anschließend kann die Zeichenkette mit der ebenfalls statischen Methode PtrToStringBSTR ermittelt werden. Die Methode erwartet als einzigen Parameter den zuvor ermittelten Zeiger auf den Speicherbereich. Wird die abgerufene Zeichenkette nicht mehr benötigt, muss sie wieder aus dem Speicher entfernt werden. Innerhalb des Beispiels geschieht dies im finally-Block durch den Aufruf der Methode ZeroFreeBSTR und die Übergabe des Zeigers auf den Speicherbereich. Die Zeitdauer zwischen Abrufen und Zerstören des Wertes sollte möglichst schnell geschehen, damit die Zeichenkette so kurz wie möglich im Speicher abgelegt wird. Wie das Beispiel demonstriert, ist die Verwendung der SecureString-Klasse nicht sonderlich umständlich oder kompliziert. Der Nutzen ist jedoch erheblich, da die gesamte Verschlüsselung automatisch und für den Entwickler völlig transparent geschieht.

Listing 3

private unsafe static void UseSecString()
{
    SecureString secString = null;
    char[] secContent = new char[6];
    secContent[0] = 'S'; secContent[1] = 'e';
    secContent[2] = 'c'; secContent[3] = 'u';
    secContent[4] = 'r'; secContent[5] = 'e';
    fixed(char* buffer = secContent)
    {
        secString = new SecureString(buffer, secContent.Length);
    }
    secString.MakeReadOnly();
    // Read secure string
    IntPtr secStringRead = Marshal.SecureStringToBSTR(secString);
    try
    {
        Console.WriteLine(Marshal.PtrToStringBSTR(secStringRead));
    }
    finally
    {
        Marshal.ZeroFreeBSTR(secStringRead);
    }
    secString.Dispose();
}  
Zusammenfassung

Für normale und unkritische Zeichenketten ist die Verwendung der String-Klasse die erste Wahl. Werden jedoch sensible Informationen wie Passwörter und PIN/TAN verarbeitet, bietet sich für die sichere Ablage im Speicher die Verwendung der SecureString-Klasse an. Da sie intern die genannte DPAPI verwendet, kann die Klasse allerdings nicht auf den Systemen Windows 98, Windows ME und Windows 2000 verwendet werden. Allerdings dürften diese Systeme heutzutage auch nur noch sehr vereinzelt im Einsatz sein.

Marc André Zhou ist als freiberuflicher .NET-Technologieberater tätig. Seine Schwerpunkte sind die .NET-Entwicklung, SharePoint, Softwarearchitekturen und Frameworks. Sie erreichen ihn unter zhou@dev-sky.net.
Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -