Home

 

 

Android DRM System


Ein Beitrag von Evgeny ZinovyevEvgeny Zinovyev

Wer seine sorgsam erstellte App im Android Market verkauft, möchte sicherstellen, dass sie nicht unter der Hand weitergegeben wird. Bisher war es mit einem gerooteten Android-Gerät kein Problem, eine gekaufte App weiterzugeben. Um dies zu verhindern und auch um auf der SD-Karte installierte Apps zu schützen, bietet Google einen DRM-Mechanismus für Apps aus dem Market an.

Mit der Verbreitung des Android Market ist das Problem des effizienten Softwareschutzes wieder akkut geworden. Hauptziel ist und war, die unkontrollierte Verbreitung lizenzgeschützter Anwendungen zu verhindern. Das ursprüngliche Copy-Protection-Konzept von Google, nämlich die Installation der geschützten App in einen geschützen Bereich des Gerätespeichers, ist aus zwei Gründen nicht mehr hinreichend. Erstens gibt es viele gerootete Geräte, von denen sich die geschützen Apps herunterkopieren lassen. Anschließend lassen sich diese Apps auf allen Androidgeräten installieren, da sie selbst keinen Kopierschutz enthalten. Zweitens ist mit diesem Verfahren eine Installation kopiergeschützter Apps auf der SD-Karte nicht möglich. Moderne Android-Apps belegen aber bei der Installation viel Platz. Wenn man mehrere umfangreiche Apps im Gerätespeicher installiert, verbrauchen sie mitunter einen beträchtlichen Teil des internen Speichers. Mit der Zeit war klar, dass der Android Market einen neuen Schutzmechanismus braucht, der zumindest diese Einschränkung aufhebt.

Deswegen hat Google einen neuen Lizenzdienst in den Android Market eingebaut. Das neue Digital Rights Management (DRM) stellt einen lizenzbasierten Service dar. Für Android-Entwickler wird die License Verification Library (LVL) zur Verfügung gestellt. LVL ist eine Android Bibliothek, bestehend aus Klassen und Interfaces, die der Entwickler selbst anpassen kann. Die Bibliothek bildet zusammen mit der Market-App eine Art Schnittstelle zwischen Anwendung und Market License Server. Damit kann die Anwendung zu jeder Zeit ihre Identität, und das Recht auf diesem Gerät ausgeführt zu werden, nachprüfen.

Der Lizenzservice ist nur für die Entwickler relevant, die ihre Apps über den Android Market verkaufen wollen. Die Voraussetzungen sind, dass die Market-App auf dem Android-Gerät installiert ist und die Anwendung für Android API-Version 1.5 oder höher entwickelt wurde. Um den Schutzmechanismus in seiner Anwendung zu verwenden, muss der Entwickler die LVL-Bibliothek in sein Android-Projekt einbinden und die Implementierung anpassen. Wann und wie die Lizenzüberprüfung stattfindet, wird dem Entwickler überlassen.

Zunächst besprechen wir aber die Funktionalität und den Aufbau des Lizenzservices im Android Market. Um nicht zu trocken zu bleiben, tun wir dies gleich mit Unterstützung eines kleinen Implementierungsbeispiels, welches Googles LVL-Bibliothek verwendet. Es befindet sich auf der CD zum Heft. Der Einfachheit halber wurde der Quellcode der LVL-Bibliothek in das Projekt eingebunden, so dass kein paralleles Library-Projekt importiert werden muss.

Nach dem Implementierungsbeispiel werfen wir einen Blick auf die Schwachstellen der LVL und überlegen uns, welche Sicherheitsmaßnahmen helfen könnten, die Anwendung besser zu schützen.

Aufbau

Der Android Market Licensing Service ist ein netzbasierter Service, der sich aus drei Komponenten zusammensetzt. Zwei dieser Komponenten laufen auf dem Android-Gerät: die LVL, die in der Anwendung läuft und die Market-App. Sie sind über Androids Inter Process Communication (IPC) Mechanismus miteinander verbunden und sind für die Serveranfrage und die Bearbeitung der Servernachrichten verantwortlich. Die dritte Komponente ist der Market License Server des Android Markets, der die Anfrage empfängt, bearbeitet und als Lizenzstatus an die Market App des Gerätes zurückschickt. Die Kommunikation mit dem Market License Server erfolgt per Netzwerk (Abbildung 1).

Android DRM System Abbildung 1

Jede Serverantwort ist durch ein RSA-Schlüsselpaar signiert. Das Schlüsselpaar wird vom Market Lizenzservice automatisch für das jeweilige Konto im Android Market erzeugt, über das die App veröffentlich wurde.

Den öffentlichen Schlüssel erhält der Entwickler der App über sein Publisherkonto im Android Market. Der Schlüssel wird im Quellcode der App hinterlegt.

Der Market License Server und die meist auf dem Android-Gerät vorinstallierte Market App sind zwei statische Komponenten, deren Funktionalität nicht geändert werden kann. Deswegen ist die eigene Anwendung zusammen mit der LVL-Bibliothek die einzige Stelle, wo der Entwickler seinen Lizenzquellcode hinterlegt. Wir schauen uns nun den Aufbau der LVL-Bibliothek und das Verfahren einer eigenen Lizenzprüfung genauer an.

Die Bibliothek hat zwei Interfaces, die man implementieren muss, um eine eigene Lizenzprüfung zu realisieren. Setzt man eine Lizenzabfrage ab, schickt der Market License Server eine Nachricht als Antwort. Nachdem die Servernachricht durch den LicenseValidator verifiziert wurde, gelangt sie in Policy-Implementierung, in der der Lizenzstatus abgefragt wird. LVL bringt zwei fertige Implementierungen des Interfaces mit: ServerManagedPolicy, die die Serverantwort für eine bestimmte Zeit aufbewahrt und für Nachprüfungen des Lizenzstatus verwendet, und StrictPolicy, die bei jeder Lizenzüberprüfung mit dem Market Server kommuniziert. Man kann aber auch individuelle Lösungen überlegen und z.B. beide Vorgehen kombinieren.

Man sollte beachten, dass das Policy-Interface der wichtigste Teil für den Entwickler ist, von dessen Implementierung nicht nur die reine Funktionalität des Lizenzverfahrens abhängt, sondern auch die Sicherheit der gesamter Anwendung.

Nachdem die Lizenz für diese Anwendung vom LicenseValidator überprüft worden ist, wird entsprechend dem Ergebnis eine der beiden Methoden des LicenseCheckerCallback-Interfaces aufgerufen. Dieses Interface muss man in der Anwendungsklasse implementieren. Der Entwickler soll dabei die gewünschte Aktion der Lizenzüberprüfung, wie zum Beispiel Statusmeldungen, in den Methoden programmieren.

Lizenzüberprüfungsablauf

Zuerst stellt die Anwendung die Policy-Implementierung und den öffentlichen Schlüssel der LVL bereit, indem man ein Exemplar der selbst implementierten LicenseChecker-Klasse erzeugt. Danach wird mit der checkAccess-Methode dieser Klasse die Lizenzanfrage initiiert. Der einzige Parameter ist die implementierte Callback-Klasse, welche später durch die Serverantwort ausgeführt wird. Wenn man die StrictPolicy-Klasse verwendet, wird die Anfrage direkt über IPC an den Android Market geleitet. Wenn man sich für die ServerManagedPolicy-Klasse entscheidet, wird zuerst nach der temporär gespeicherten Serverantwort gesucht. Die Market App fügt zu der Anfrage Gerätenummer und Name des Google-Benutzerkontos, mit dem das Gerät synchronisiert ist, hinzu. Der Server überprüft, ob diese Anwendung von diesem Benutzer gekauft wurde. Unabhängig vom Ergebnis schickt der Market-Server die Antwort an die Market-App des Android-Geräts zurück. Wenn die Serverantwort eintrifft, verifiziert die LVL die signierten Daten mit dem öffentlichen Schlüssel. Wenn die Verifizierung erfolgreich war, wird eine der Callback-Methoden aufgerufen (Abbildung 2)[1]. Hauptsächlich hier wird die Entscheidung getroffen, wie die Anwendung auf den konkreten Lizenzstatus reagieren soll.

Android DRM System Abbildung 2

Wie man sieht, ist die Rolle des Market-Servers durch den Vergleich der Anfrage mit eigenen Datenbanksätzen begrenzt und die Bearbeitung des Lizenzstatus findet erst auf dem Gerät statt. Anhand seiner Informationen erstellt der Server die Lizenzantwort, in der der Lizenzstatus gespeichert ist. Man unterscheidet zwischen „NOT_LICENSED“, „LICENSED“ und mehreren „ERROR_...“ -Lizenzstati[1]. Zusätzlich werden mit dem Lizenzstatus einige Extras geschickt. Zum Beispiel Gültigkeitsperiode des Status. Der Entwickler kann selbst bestimmen, wann, wo und wie oft er den Lizenzstatus abfragt. Man kann zum Beispiel die Serverantwort „NOT_LICENSED“ auf eigene Art behandeln.

Damit bietet die LVL den Entwicklern sowohl den Spielraum, eigene Vorstellungen des Lizensierungsablaufs umzusetzen, als auch die mitgelieferten fertigen Klassen der LVL zu nutzen, um schnelle eigene Lösungen zu implementieren. Bevor man sich für eines der beiden zu implementierenden Interfaces entscheidet, sollte man beachten, dass die mitgelieferte Beispielprogrammierung keinen hinreichenden Schutz bietet. Warum dies so ist, sehen wir später.

Nun setzen wir die Theorie in eine Beispielanwendung um.

Vorbereitung

Bevor wir anfangen, brauchen wir im Android Market ein freigeschaltetes Entwickler-Konto. Auch um nur zu Testen muss das Konto vorhanden sein. Über die Kontoeinstellungen bekommt man den öffentlichen Schlüssel, den wir später in die Anwendung einbauen. Auf der gleichen Webseite im Android Market muss man die Testkonten angeben und den zu emulierenden Lizenzstatus auswählen. Der Gerät bzw. der Emulator muss mit einem der Testkonten synchronisiert werden. Damit die Testkonten von Market Server akzeptiert werden, muss man die Anwendung in den Android Market hochladen. Wenn man das Android-Gerät mit dem Entwickler-Konto synchronisiert, brauch man dies jedoch nicht machen[1].

Wenn man mit dem Emulator arbeiten will, braucht man Google APIs Add-On, API 8 (release 2) oder eine spätere Version. Erst seit dieser Version ist der Android Market Service im Emulator vorhanden. Dabei lädt man die LVL als zusätzliches Market Licensing Packet über den ADV-Manager herunter. Sie befindet sich anschließend zusammen mit der Beispielanwendung von Google unter <android-sdk>/market_licensing/library/. Es ist empfehlenswert, bevor man damit arbeitet, die LVL ins eigene Arbeitsverzeichnis des Android-Projekts zu kopieren. Wie bereits erwähnt wurde, besteht die LVL aus Quellcode und wird jedes Mal mit der Anwendung kompiliert. Um die LVL in einem eigenen Projekt zu verwenden, kann man sie einfach als Library-Projekt in das eigene Android-Projekt einbinden[1].

Beispielprojekt anlegen

Wir legen nun ein neues Android-Projekt[5] mit dem Namen „LVL-Beispiel“ an und binden die LVL in das Projekt ein. Die Beispielanwendung besteht nur aus einer Activity, die den Lizenzstatus in Form einer Textzeile ausgibt. Nennen wir sie MainActivity. Die Activity besitzt zwei Bildschirmelemente, nämlich ein Textfeld für die Darstellung des Lizenzstatus und die Schaltfläche, die die Lizenzüberprüfung initiiert.<

Um den Market Service benutzen zu können, setzen wir in der Manifest-Datei folgende Permission:

<uses-permission android:name="com.android.vending.CHECK_LICENSE"/>

Policy implementieren

Direkt am Anfang fügen wir den aus den Einstellungen des Publisherkontos kopierten öffentlichen Schlüssel in den MainActvity-Quellcode ein. Dieser Schlüssel wird bei der Erzeugung der LicenseChecker-Instanz mitübergeben.

privatestaticfinal String BASE64_PUBLIC_KEY = "...";;

Zuerst muss man das Policy-Interface implementieren oder ein vorgefertigtes Interface verwenden. Wir nehmen die ServerManagedPolicy-Klasse, die die Serverantworten aufbewahrt und diese nutzt, um die Anwendung freizuschalten. Dabei wird der Lizenzstatus in den Speicher geschrieben. Um den Speicher gegen Angriffe zu schützen, beinhaltet die LVL einen Obfuscator, der den AES-Algorithmus benutzt, um die Daten zu kodieren/dekodieren.

Zunächst importieren wir den AESObfuscator in die Activity und fügen ein beliebig langes Array mit zufällig ausgewählten Zahlen hinzu. Dies bietet zusätzlichen Schutz für die Lizenzdaten.

private static final byte[] SALT =
        new byte[] {-25, 65,30, -128,-103 };

Zwei weitere Parameter, die der AESObfuscator braucht, sind der Anwendungs-Identifikator (üblicherweise Paketname) und der Geräte-Identifikator. Als Geräte-Identifikator verwenden wir „ANDROID_ID“.

Wenn man sich für die StrictPolicy-Implementierung entscheidet, wird man den Obfuscator gar nicht brauchen, da der Lizenzstatus jedes Mal vom Lizenzserver abgefragt und nicht im Gerät gespeichert wird.

Callback implementieren

In der Activity implementieren wir den LicenseCheckerCallback als innere Klasse. Das Interface hat drei Methoden, die man in der Implementierung überschreibt. Die Methoden der Klasse werden aus der Police-Implementierung heraus aufgerufen, wenn der neue Lizenzstatus vom Server eintrifft. Die Methoden werden verwendet, um dem Anwender Rückmeldung über den Lizenzstatus zu geben.

Listing 1: Callback implementierung.

private class MyLicenseCheckerCallback implements LicenseCheckerCallback {

    public void allow() {
        if (isFinishing()) {
            return;
        }
        displayResult(getString(R.string.allow));
    }
    public void dontAllow() {
        if (isFinishing()) {
            return;
        }

        displayResult(getString(R.string.dont_allow));
    }

    public void applicationError(ApplicationErrorCode errorCode) {
        if (isFinishing()) {
            return;
        }

        String result = String.format(getString(R.string.application_error), errorCode);
        displayResult(result);
    }
}

Es ist sehr wichtig nicht zu vergessen, dass der LicenseCheckerCallback nicht im UI-Thread aufgerufen wird. Um die Oberfläche zu aktualisieren, muss man sich aber im UI-Thread befinden. Die Threadkommunikation geschieht in Android mit Hilfe von Handlern. Den Handler muss man im UI-Thread initialisieren und dem LicenseCheckerCallback bzw. seinem Thread übergeben. Da die Activity und LicenseCheckerCallback im gleichen Namensraum sind, muss man nicht den Handler explizit übergeben, sondern es reicht, wenn man ihm als Klassenvariable deklariert und in onCreate-Methode der Activity initialisiert. Damit ist er für die Callback-Methoden sichtbar.

Listing 2: Handler.

private Handler mHandler;

@Override

public void onCreate(Bundle savedInstanceState) {
    ...
    mHandler = new Handler();

}

private void displayResult(final String result) {
    mHandler.post(new Runnable() {

        public void run() {

            mStatusText.setText(result);

            setProgressBarIndeterminateVisibility(false);

            mCheckLicenseButton.setEnabled(true);
        }
    });
}

Man muss im weiteren Verlauf nur den LicenseChecker und die LicenseCheckerCallback-Objekte initialisieren und die Lizenzüberprüfung aufrufen. Ersteres machen wir in der onCreate-Methode.

Listing 3: onCreate().

@Override

public void onCreate(final Bundle savedInstanceState) {

    super.onCreate(savedInstanceState);

    …

    String deviceId = Secure.getString(getContentResolver(), Secure.ANDROID_ID);

    mLicenseCheckerCallback = new MyLicenseCheckerCallback();

    mChecker =
        new LicenseChecker(this, new ServerManagedPolicy(this, new AESObfuscator(SALT, getPackageName(), deviceId)), BASE64_PUBLIC_KEY);
}

Zweiteres wird beim Drücken der Schaltfläche in der Methode doCheck passieren.

Listing 3: Aufruf der Lizenzüberprüfung.

public void doCheck(final View v) {

    mCheckLicenseButton.setEnabled(false);

    setProgressBarIndeterminateVisibility(true);

    mStatusText.setText(R.string.checking_license);        

    mChecker.checkAccess(mLicenseCheckerCallback);

}

ProGuard

Eigentlich sind wir mit der Anwendung fertig. Was wir noch machen können, um die Anwendung zu schützen, ist den ProGuard-Obfuscator einschalten. ProGuard ersetzt die im Quellcode vorkommende Namen von Klassen, Variablen und Methoden durch kurze Zeichenfolgen und optimiert und reinigt den Quellcode. Dadurch wird die Quellcode schwer zu lesen. Seit Android 2.2 ist ProGuard standardmäßig in jedes Android-Projekt integriert und vorkonfiguriert. Dabei wird im root-Verzeichnis des Android-Projekts eine Datei proguard.cfg Datei angelegt. Um ProGuard zu aktivieren, muss man in der default.properties-Datei folgende Zeile einfügen.

proguard.config=proguard.cfg

Die proguard.cfg - Datei ist die ProGuard-Konfigurationdatei, die nach eigenen Wünsche angepasst werden kann[2][3].

Sicherheitsmaßnahmen

In der letzten Zeit wurde in Android-Foren und Blogs viel über die Schwachstellen in der Implementierung der LVL geschrieben. Die Entwickler klagen vor allem auf die Einfachheit, mit der man den Schutz umgehen kann. Und dabei muss der Hacker nicht einmal viel Erfahrung in Sachen Programmierung mitbringen.

Android-Anwendungen sind in Java geschrieben. Beim Kompilieren des Javacodes wird ein Android-eigener Bytecode erzeugt, der dann in der DVM (Dalvik Virtual Machine) läuft. Der Bytecode lässt sich aber einfach dekompilieren (reverse engineering). Als Folge können sowohl der Anwendungs-Quellcode als auch LVL-Quellcode, der über die externe Bibliothek mitkompiliert wurde, einfach gelesen und manipuliert werden.

In diesem Abschnitt sind die Methoden beschrieben, die die Sicherheit der Anwendung auf ein höheres Niveau bringen. Generell geht es hier darum, die Schwachstellen der LVL zu beseitigen.

Ein Hacker kann zwischen zwei Angriffsstrategien wählen: er kann die Callback-Methoden ändern oder in der LVL die Serverantwort abfangen und umleiten oder komplett ignorieren. Die Serverantwort nachzumachen scheint nahezu unmöglich, da die Antwort mit dem RSA-Schlüsselpaar signiert ist.

Einfacher ist es an die Serverantwort in der LVL oder der Anwendung zu kommen. Der Hacker kann die Anwendung dekompilieren, den Quellcode ändern (den Schutz entfernen) und die Anwendung wieder zusammenbauen.

Quellcode Obfuscator

Man kann zwar die Dekompilierung nicht verhindern, man kann aber den Quellcode so unleserlich machen, dass der Hacker viel Zeit braucht, die richtige Stelle im Quellcode zu finden, an der die Lizenzüberprüfung implementiert ist.

Diese Aufgabe übernimmt der sogenannte Obfuscator. Das Tool macht den Quellcode schwer zu lesen, indem es die Namen mit Zeichenfolgen ersetzt. Wir haben bereits den ProGuard-Obfuscator in unserer Beispielanwendung benutzt. Er wird seit API-Version 2.2 mitgeliefert und kann einfach im Projekt verwendet werden. Das Tool beseitigt leider die Sicherheitslücken nicht, erschwert aber den Hackern die Arbeit[2][3][4].

Die LVL ändern

Diese Aufgabe ist nicht so einfach umzusetzen. Aber Entwickler, die mehrere Apps über den Android-Market verkaufen wollen, sollten sich diesen Punkt unbedingt anschauen. Entwickler, die die LVL in ihrer Anwendung verwenden, lassen oft die LVL unverändert. Zwar ersetzt der Obfuscator die Namen durch Zeichenfolgen, lässt aber dabei das Programmiermuster unverändert. Diejenigen, die den Aufbau der LVL gut kennen, haben kein Problem, die richtige Stelle im obfuscateten Code zu finden und die Lizenzprüfung zu deaktivieren. Man muss dazu nicht die ganze LVL ändern, sondern es reicht, wenn man die Stellen ändert, an der das Validieren und die Lizenzabfrage stattfindet. Daher ist eine eigene Implementierung des Policy-Interfaces und die Änderung an LienseValidator sehr empfehlenswert[4].

Die Anwendung manipulationssicher machen

Die folgenden beiden Maßnahmen beziehen sich auf Anwendungen, die zusätzlich einen eigenen Anwendungsserver für die Lizenzprüfung verwenden. Da der Anwendungsserver für den Hacker unerreichbar ist, kann man ihn für zusätzliche Quellcode- oder Lizenzstatusüberprüfung ausnutzen.

Zum Beispiel kann man die Integrität des Quellcodes prüfen. Dabei bildet man die Prüfsumme des Quellcodes und lagert sie auf den Server aus. Jede Änderung am Quellcode erzeugt eine andere Prüfsumme, die bei der Lizenzüberprüfung mit der Prüfsumme auf dem Server verglichen wird. Dadurch wird klar, ob der Quellcode verändert wurde oder nicht[4].

Die Lizenzüberprüfung auslagern

Wenn die Anwendung Online-Inhalte vom Anwendungsserver bezieht, kann man einen Schritt weiter gehen und den Lizenzstatus nicht in der Anwendung überprüfen, sondern den Lizenzstatus an den Anwendungsserver weiterleiten.

Die signierte Lizenzserverantwort wird auf dem Anwendungsserver untersucht. Die Validierung auf dem Server stellt sicher, dass es sich um den originalen Lizenzstatus handelt. Wenn nicht, kann der Server z.B. die Kommunikation mit der Anwendung unterbrechen[4].

Fazit

Das DRM-System für Android auf Basis des Market Servers ist einen Schritt in die richtige Richtung, hinterlässt aber zuerst einen zwiespältigen Eindruck. Nachteil dieses Verfahrens ist, dass es zu einfach ist mittels Dekompilierung an den Quellcode der Anwendung und der LVL zu kommen und damit die Lizenzprüfung zu umgehen. Der Vorteil ist, dass der Kauf der Anwendung registriert wird und man den Service nutzen kann, um den Lizenzstatus abzufragen.

Ein weiterer Vorteil ist, dass die signierte Serverantwort sehr schwer zu fälschen ist. Im Idealfall verwendet man zusätzlich eine eigene Serverkomponente für die Lizenzüberprüfung. Diese implementiert eigene Policies und Validierungen und sorgt so für optimalen Schutz der eigenen Anwendung gegen unerlaubtes Kopieren.

Wenn man der LVL vertraut und auf einen eigenen Lizenzserver verzichtet, sollte man einige Zeit investieren, um die Bibliothek gründlich umzubauen. Wünschenswert wäre noch, dass die LVL in das SDK eingebaut würde.