28. September 2023
StartMagazinSicheres Löschen von Passwörtern aus dem Hauptspeicher

Sicheres Löschen von Passwörtern aus dem Hauptspeicher

Dank permanenter Internet-Anbindung lassen sich Smartphones auch als Medium für Zahlungen im Internet benutzen. Doch vertrauliche Spuren wie Passwörter und Kreditkarteninformationen bleiben meist im DRAM-Speicher zurück. Mit Android 2.2 und dem NDK lässt sich das Problem lösen.

README

Von Haus aus bietet Android keine Möglichkeit, Daten explizit aus dem Speicher zu löschen. Dieser Artikel zeigt, wie man es mit Hilfe des Native Development Kits trotzdem kann und so eine potentielle Memory-Dump Attacke verhindert.

Wenn Sie auf Ihrem Handy ein Passwort in ein Eingabefeld eintippen, legt Android im Hauptspeicher des Prozesses einen Puffer an, der das Passwort in Klartext zwischenspeichert. Dieser Text bleibt – im Klartext wohlbemerkt – so lange im Hauptspeicher liegen, bis der Speicher anderweitig verwendet und dabei überschrieben wird. Das kann geschehen, wenn eine Activity beendet und eine neue an deren Stelle geladen wird, oder wenn der Puffer freigegeben, vom Garbage Collector (GC) entsorgt und durch ein neues Objekt belegt wird. Ein einfacher Memory-Dump [1] des Hauptspeichers kann aber jeder Zeit geheime Informationen enthüllen. Kommt Ihnen kurz nach einer Online-Überweisung das Handy abhanden, dann haben Sie neben dem Verlust des Handys potentiell ein weit grösseres Problem!

In Systemen, die in C programmiert sind, hat man als Entwickler etwas mehr Kontrolle über den Speicher als in einer Umgebung wie Java, die den Speicher automatisch verwaltet. Strings sind in Java unveränderbar. Einen einmal erzeugten String kann man somit nicht mehr explizit aus dem Speicher löschen. Sensitive Daten sollten Sie in Java daher nie in einem String-Objekt speichern [2].

Aus diesem Grund gibt die Klasse JPasswordField der Java Standardbibliothek das Passwort nicht als String, sondern als char[]-Array zurück. Die Dokumentation empfiehlt, den zurückgegebenen char[]-Array nach Verwendung mit Nullen zu überschreiben. Aber eigentlich wird damit nur das Gewissen des Programmierers beruhigt, denn das Passwort bleibt im Passwortfeld in einem Modell vom Typ javax.swing.text.PlainDocument (im Klartext) weiterhin gespeichert.

Das Problem

Wenn also der Programmierer dem Hinweis folgt und das von der Methode getPassword zurückgegebene char[]-Array mit Nullen überschreibt, so löscht er dadurch nur die Daten einer Kopie. Im Textmodell des Passwort-Feldes bleibt das Passwort stehen, bis es der Garbage Collector rezikliert.

Aber auch wenn die Daten direkt in einem char[]-Array abgespeichert sind, hilft das Überschreiben mit Nullen nicht zwingend weiter: Beim Kompaktieren des Speichers kopiert der GC Daten. Es ist also möglich, dass er ein Array, bevor es mit Nullen überschrieben wird, an eine andere Speicherstelle kopiert; der Programmierer überschreibt dann nur noch die Kopie mit Nullen, das Original bleibt im Speicher stehen, bis der freigewordene Speicherplatz anderweitig belegt wird.

Auch das Betriebssystem kann Seiten bei der virtuellen Speicherverwaltung verschieben. Es ist gut möglich, dass ein Objekt, das fix über eine logische Adresse referenziert wird, physikalisch an unterschiedlichen Speicherorten liegt. Es könnte also passieren, dass das Überschreiben eines Feldes physikalisch auf einer neuen Seite vorgenommen wird und das Original unverändert auf einer anderen Seite liegt.

Dieser Artikel zeigt einen Ansatz, wie der Entwickler diese Probleme in Android lösen kann. Er basiert auf folgenden Konzepten:

  • Das Textfeld, in welchem das Passwort eingegeben wird, verwendet für die Speicherung der Daten ein eigenes Textmodell vom Typ char[].
  • Dieses char[]-Array wird in C verwaltet und über Java Native Interface angesprochen. Nach Gebrauch wird das Feld (in C) mit Nullen überschrieben.
  • Das Paging wird für dieses Array mit einem Systemaufruf temporär deaktiviert.

Mit dem Android Native Development Kit (NDK) lassen sich Teile von Android-Anwendungen in C oder C++ programmieren. Das NDK unterstützt die Generierung von Bibliotheken, welche dann in APKs gepackt und auf Android-Geräten installiert werden können. Die für die Kompilierung nötigen System-Header-Dateien und Bibliotheken sind Bestandteil des NDK.

Das NDK eignet sich um aus Java Programmen direkt auf C/C++ Bibliotheken (wie OpenSSL, OpenGL, etc.) zuzugreifen oder um (wie in unserem Beispiel) System-Ressourcen und Hardware direkt zu kontrollieren. Der Einsatz des NDK ist jedoch nicht primär als Performance-Booster gedacht. Der Zuschlag, den man für die reibungslose Integration über das Java Native Interface (JNI) bezahlen muss, macht die Performance-Gewinne, welche durch die Kompilation erreicht wurden, zunichte.

Passwort View

Passwörter werden in Android typischerweise über eine EditText-View eingegeben, auf welcher mit android:inputType="textPassword" der entsprechende Input-Typ gesetzt ist. Dieses Attribut bewirkt, dass auf dem Bildschirm anstelle des eingegebenen Textes Punkte angezeigt werden und die automatische Textvervollständigung deaktiviert ist. Das Passwort selber verwaltet Android in einem internen Modell vom Typ Editable (im Klartext), auf welches mit der Methode getText() zugegriffen werden kann.

Die Klasse EditText stellt einen vollständigen Editor zur Verfügung, entsprechend aufwändig wäre es, eine eigene Klasse mit derselben Funktionalität zu entwickeln. Es ist jedoch möglich in dieser Klasse eine Factory zu registrieren, die verwendet wird, um das Textmodell zu erzeugen. Wir verwenden diese Funktionalität um unsere eigene Implementierung für das Textmodell zu verwenden.

Per Default wird in der Klasse EditText ein Textmodell vom Typ SpannableStringBuilder verwendet. Leider sind nicht alle von dieser Klasse implementierten Interfaces exportiert, d.h. sie sind im Android-Quellcode mit der Annotation @hide markiert und werden daher von der Entwicklungsumgebung ignoriert. Wir mussten daher unsere eigene Klasse PasswordStringBuilder von der Klasse SpannableStringBuilder ableiten. Wir überschreiben alle Methoden, in welchen der Passwort-Text geändert wird, und führen diese Änderungen in einem C-Array nach, auf den wir via JNI zugreifen. Die Klasse EditText erwartet jedoch, dass der im String-Builder verwaltete Text auch wieder ausgelesen werden kann. Wir übergeben daher der Basisklasse Ersatzzeichen, die dann die GUI ausliest und anzeigt.

@Override
public SpannableStringBuilder replace(int start, int end, CharSequence tb, int tbstart, int tbend) {
        if (baseClassCalling) return super.replace(start, end, tb, tbstart, tbend);
        // manipulation of the buffer in C via JNI and replacing the text at start .. end with
        // the char sequence tb[tbstart .. tbend]
        // prepare replacement text
        StringBuilder b = new StringBuilder(tbend);
        for(int i = 0; i < tbend; i++) b.append('*');
        baseClassCalling = true;
        super.replace(start, end, b, tbstart, tbend);
        baseClassCalling = false;
        return this;
}

Listing 1: Methode replace über welche die Android Komponente das Passwort editiert.

In Listing 1 ist die Funktion replace abgebildet. Diese manipuliert das char[]-Array via JNI und ruft dann die Methode der Basisklasse mit dem Text **** auf. In diesem Supercall können weitere Methoden der eigenen Klasse aufgerufen werden. Damit die Klasse solche virtuelle rekursive Aufrufe erkennt und direkt zurück an die Basisklasse delegiert, setzt man ein entsprechendes Flag. Wenn der Benutzer sein Passwort eingibt, dann sieht er auf dem Bildschirm nicht mehr den eingegebenen Text sondern nur noch die Ersatzzeichen (*), wie in Abbildung 1 zu sehen.

Abbildung 1: Eingabe eines Passwortes über unsere EditText Komponente.
Abbildung 1: Eingabe eines Passwortes über unsere EditText Komponente.

Native Funktionen

Auf das im C-Code verwaltete Passwort greifen wir via Java Native Interface (JNI) mit folgenden Funktionen zu:

  • long createPasswordField(int size)
  • char getCharacterAt(long handle, int pos)
  • void setCharacterAt(long handle, int pos, char ch)
  • long closeAndHashPasswordField(long handle, double ms)

Die Methode createPasswordField legt im C-Teil ein Feld auf dem Heap an. Mit den Methoden setCharacterAt und getCharacterAt lässt sich das Passwort manipulieren. Die Methode closeAndHashPasswordField berechnet aus dem Passwort einen Hashwert und löscht das Passwort.

C-Array aufbauen

Die Funktion createPasswordField (siehe Listing 1) ist für die Konstruktion des C-Arrays verantwortlich, dessen Speicheradresse (ein Pointer) an die aufrufende Methode zurückgegeben wird. Diesen Pointer behandelt dann die Java Methode wie ein Handle und übergibt ihn bei allen anderen Methoden als Parameter.

Wie wir bereits erwähnt haben, muss verhindert werden, dass Android via Memory Paging zusätzliche Kopien des C-Arrays erzeugt. Dazu blockieren wir den für den C-Array im Speicher reservierten Bereich mit der Funktion mlock(). Es handelt sich dabei um einen Linux Systemaufruf aus der Bionic-Library, die eine Android-Komponente direkt benutzen kann. Falls der Rückgabewert der Funktion mlock() einen Fehler anzeigt, wird eine Java-Exception geworfen.

Da der C-Array mit malloc() konstruiert worden ist und somit das Betriebssystem den entsprechenden Heap verwaltet [3], nicht die Dalvik VM, kann der GC auf diesem C-Array kein Unheil anrichten, insbesondere keine zusätzlichen Kopien erzeugen.

JNIEXPORT jlong JNICALL Java_ch_fhnw_imvs_pw_PasswordStringBuilder_createPasswordField(JNIEnv *env, jobject thiz, jint size)
{
        /* Allocate so much char as size says */
        jchar *arr = malloc((size+1) * sizeof (jchar));
        if (arr == NULL) {
                LOGE("Null pointer to array");
        } else {
                int i;
                for (i=0; i<= size; i++)
                        arr[i] = 0;
                len = size;
                /* Lock virtual paging for the block containing the char array */
                if (mlock((void *) arr, (size_t) (size+1)*sizeof(jchar)) == -1) {
                        jclass runtimeExceptionCls = (*env)->FindClass(env, "java/lang/RuntimeException");
                        (*env)->ThrowNew(env, runtimeExceptionCls, "mlock() call error");
                }
        }
        return arr;
}

Listing 2: Funktion createPasswordField.

Passwort sicher löschen

Die Funktion closeAndHashPasswordField() (siehe Listing 3) übernimmt die Aufgabe, das im char[]-Feld abgelegte Passwort wieder sicher zu löschen. Da der GC unser Feld nicht umkopiert hat und da wir auch Paging verhindert haben, genügt es, die für das Array reservierten 16-Bit breiten Speicherelemente des C-Arrays zu überschreiben.

JNIEXPORT jlong JNICALL Java_ch_fhnw_imvs_pw_PasswordStringBuilder_closeAndHashPasswordField(JNIEnv *env, jobject thiz, jlong handle, jdouble ms)
{
        int i;
        long hash = 0;
        double interval = actualTimeInMs() + ms;
        arr = (jchar *) (jint) handle;
        if (ms == 0){
                int n = 100;
                do {
                        for (i = 0; i < len; i++)
                                if (n%2 == 0)
                                        arr[i] = 0;
                                else
                                        arr[i] = 1;
                } while (n--);
        } else {
                int n = 0;
                do {
                        for (i = 0; i < len; i++)
                                if ((n++)%2 == 0)
                                        arr[i] = 0;
                                else
                                        arr[i] = 1;
                } while (!(actualTimeInMs() <= interval));
        }
        LOGI("Password after wipe = %s", arr);
        /* Unlock virtual paging */
        munlock((void *) arr, (size_t) len);
        /* Free heap space occupied by the char array */
        free(arr);
        /* In a productive environment this function should return
         * the hash value of the password. The application then
         * looks up the user's entry in the password shadow file
         * and compares the calculated password's hash value with
         * the stored value.
         */
        return hash;
}

Listing 3: Funktion closeAndHashPasswordField.

Die eigentliche Löscharbeit erledigen die zwei do-while-Kontrollstrukturen. Dazu überschreiben sie den ganzen Puffer-Bereich, der das Passwort beinhaltet, alternierend mit 0x00 bzw. 0x01. Die Operation wird (im Beispiel) 100-mal wiederholt (falls der Parameter ms=0 ist) oder wiederholt, bis eine bestimmte Zeit in Millisekunden verstrichen ist (falls der Parameter ms>0 ist).

Die Art und Weise dieses Löschens des DRAM-Speichers entspricht dem Resultat vieler empirischer Studien [4]. Die Dauer des alternierenden Überschreibens des Pufferbereiches soll zwischen einer Sekunde und einer Minute liegen. Eine Faustregel besagt, dass das Überschreiben umso länger dauern soll, je länger die geheime Information im DRAM-Speicher lag. Für unsere Applikation stellen 10 Sekunden die obere Grenze dar. Eine einführende und gute Übersicht, wie man Information in verschiedenen Medien (DRAM, Harddisk, Flash-Speicher, usw.) sicher löscht, ist in [5] zu finden.

Am Ende wird noch der memory paging Mechanismus reaktiviert, der bei der Konstruktion des C-Arrays gesperrt wurde. Da wir für die Verwaltung des Betriebssystem-Heaps selber verantwortlich sind, müssen wir den in der Funktion createPasswordFile() reservierten Speicherplatz auch wieder freigeben.

Zusammenspiel

Wie lassen sich nun all die besprochenen Komponenten in einer hypothetischen Android ElectronicPay-Applikation zusammenfügen? Die Idee ist, dass unser angepasstes Passwortfeld in einer eigenen Android-Activity bereitgestellt wird. Diese liest das Passwort ein, löscht nach Abschluss der Eingabe alle Spuren des Passwortes gründlich weg, beendet sich mit dem Aufruf der finish-Methode und gibt als Resultat den Hashwert des Passworts oder eine Fehlermeldung zurück.

Die Haupt-Activity vergleicht dieses Resultat mit dem erwarteten Wert, der in einer eigenen SQLite-Datenbank gespeichert sein kann und bewilligt oder sperrt die Bezahlung in Abhängigkeit des Ergebnisses.

Fazit

Wir haben gezeigt, dass eine so banale Angelegenheit, wie das sichere Löschen aller Spuren nach der Eingabe eines Passwortes, dem Entwickler knifflige Probleme bereiten kann. Die hier vorgestellte Lösung ist in Android realisiert, aber die Konzepte lassen sich auch für andere Plattformen mit verwaltetem Speicher anwenden.

Marcel Hilzinger
Marcel Hilzinger
Ich bin Marcel und Gründer von Android User. Unsere Webseite existiert nun bereits seit dem Jahr 2011. Hier findest du eine Vielzahl von Artikeln rund um das Thema Android.

Kommentieren Sie den Artikel

Bitte geben Sie Ihren Kommentar ein!
Bitte geben Sie hier Ihren Namen ein

EMPFEHLUNGEN DER REDAKTION

MAGAZIN

APPS & SPIELE