Erzeugen und Testen von Dalvik-Bytecode für Android



Ähnliche Dokumente
Binäre Bäume. 1. Allgemeines. 2. Funktionsweise. 2.1 Eintragen

Objektorientierte Programmierung für Anfänger am Beispiel PHP

2. ERSTELLEN VON APPS MIT DEM ADT PLUGIN VON ECLIPSE

Stellen Sie bitte den Cursor in die Spalte B2 und rufen die Funktion Sverweis auf. Es öffnet sich folgendes Dialogfenster

Einführung in die Java- Programmierung

Einführung in die Programmierung

Handbuch Fischertechnik-Einzelteiltabelle V3.7.3

Datensicherung. Beschreibung der Datensicherung

DOKUMENTATION VOGELZUCHT 2015 PLUS

Handbuch B4000+ Preset Manager

CADEMIA: Einrichtung Ihres Computers unter Windows

5 DATEN Variablen. Variablen können beliebige Werte zugewiesen und im Gegensatz zu

CADEMIA: Einrichtung Ihres Computers unter Linux mit Oracle-Java

Professionelle Seminare im Bereich MS-Office

Virtueller Seminarordner Anleitung für die Dozentinnen und Dozenten

SEP 114. Design by Contract

Programmierkurs Java

Der Aufruf von DM_in_Euro 1.40 sollte die Ausgabe 1.40 DM = Euro ergeben.

5.2 Neue Projekte erstellen

Lineargleichungssysteme: Additions-/ Subtraktionsverfahren

Es sollte die MS-DOS Eingabeaufforderung starten. Geben Sie nun den Befehl javac ein.

Objektorientierte Programmierung

Diplomarbeit. Konzeption und Implementierung einer automatisierten Testumgebung. Thomas Wehrspann. 10. Dezember 2008

Objektorientierte Programmierung. Kapitel 12: Interfaces

Verschlüsseln Sie Ihre Dateien lückenlos Verwenden Sie TrueCrypt, um Ihre Daten zu schützen.

Diese Ansicht erhalten Sie nach der erfolgreichen Anmeldung bei Wordpress.

Java Virtual Machine (JVM) Bytecode

Zeichen bei Zahlen entschlüsseln

Handbuch ECDL 2003 Professional Modul 2: Tabellenkalkulation Vorlagen benutzen und ändern

Step by Step Webserver unter Windows Server von Christian Bartl

Erweiterung der Aufgabe. Die Notenberechnung soll nicht nur für einen Schüler, sondern für bis zu 35 Schüler gehen:

Installation der SAS Foundation Software auf Windows

4. Jeder Knoten hat höchstens zwei Kinder, ein linkes und ein rechtes.

Innere Klassen in Java

Einführung in die objektorientierte Programmierung mit Java. Klausur am 19. Oktober 2005

Java Kurs für Anfänger Einheit 5 Methoden

Microsoft PowerPoint 2013 Folien gemeinsam nutzen

Jede Zahl muss dabei einzeln umgerechnet werden. Beginnen wir also ganz am Anfang mit der Zahl,192.

Dokumentation von Ük Modul 302

Programmieren in Java

Installation SQL- Server 2012 Single Node

Übung: Verwendung von Java-Threads

4 Aufzählungen und Listen erstellen

Anleitung über den Umgang mit Schildern

Technische Dokumentation SilentStatistikTool

Kompilieren und Linken

Eine Einführung in die Installation und Nutzung von cygwin

Internet Explorer Version 6

Kapitel 3 Frames Seite 1

Erstellen einer PostScript-Datei unter Windows XP

Die Beschreibung bezieht sich auf die Version Dreamweaver 4.0. In der Version MX ist die Sitedefinition leicht geändert worden.

Um ein solches Dokument zu erzeugen, muss eine Serienbriefvorlage in Word erstellt werden, das auf die von BüroWARE erstellte Datei zugreift.

Lizenzierung von System Center 2012

DriveLock 6. DriveLock und das Windows Sicherheitsproblem mit LNK Dateien. CenterTools Software GmbH

4D Server v12 64-bit Version BETA VERSION

In 15 einfachen Schritten zum mobilen PC mit Paragon Drive Copy 10 und Microsoft Windows Virtual PC

.htaccess HOWTO. zum Schutz von Dateien und Verzeichnissen mittels Passwortabfrage

Session Beans & Servlet Integration. Ralf Gitzel ralf_gitzel@hotmail.de

Tutorium Rechnerorganisation

GeoPilot (Android) die App

Software Engineering Klassendiagramme Assoziationen

Vorkurs C++ Programmierung

Guide DynDNS und Portforwarding

3 Objektorientierte Konzepte in Java

Prinzipien Objektorientierter Programmierung

Grundlagen von Python

Merchant Center und Adwords Produkterweiterung mit Filter

Artikel Schnittstelle über CSV

Tutorial: Entlohnungsberechnung erstellen mit LibreOffice Calc 3.5

Web-Kürzel. Krishna Tateneni Yves Arrouye Deutsche Übersetzung: Stefan Winter

Einführung in Eclipse und Java

Leitfaden zur ersten Nutzung der R FOM Portable-Version für Windows (Version 1.0)

1 topologisches Sortieren

Partitionieren in Vista und Windows 7/8

Abamsoft Finos im Zusammenspiel mit shop to date von DATA BECKER

Suche schlecht beschriftete Bilder mit Eigenen Abfragen

etermin Einbindung in Outlook

etutor Benutzerhandbuch XQuery Benutzerhandbuch Georg Nitsche

Einführung in die Java- Programmierung

Verarbeitung der Eingangsmeldungen in einem Callcenter

Datenübernahme von HKO 5.9 zur. Advolux Kanzleisoftware

Qt-Projekte mit Visual Studio 2005

Gliederung Grundlagen Schlüsselworte try-catch Fehlerobjekte Fehlerklassen Schlüsselwort finally Schlüsselwort throws selbst erstellte Exceptions

Der vorliegende Konverter unterstützt Sie bei der Konvertierung der Datensätze zu IBAN und BIC.

Verhindert, dass eine Methode überschrieben wird. public final int holekontostand() {...} public final class Girokonto extends Konto {...

Task: Nmap Skripte ausführen

Sie werden sehen, dass Sie für uns nur noch den direkten PDF-Export benötigen. Warum?

Inhalt. 1 Einleitung AUTOMATISCHE DATENSICHERUNG AUF EINEN CLOUDSPEICHER

Outlook. sysplus.ch outlook - mail-grundlagen Seite 1/8. Mail-Grundlagen. Posteingang

.procmailrc HOWTO. zur Mailfilterung und Verteilung. Stand:

Workshop: Eigenes Image ohne VMware-Programme erstellen

Dokumentation zum Spielserver der Software Challenge

IRF2000 Application Note Lösung von IP-Adresskonflikten bei zwei identischen Netzwerken

S/W mit PhotoLine. Inhaltsverzeichnis. PhotoLine

Anleitung für Berichte in Word Press, auf der neuen Homepage des DAV Koblenz

Kurzanleitung. Toolbox. T_xls_Import

Die Programmiersprache Java. Dr. Wolfgang Süß Thorsten Schlachter

Lizenzierung von SharePoint Server 2013

Transkript:

Erzeugen und Testen von Dalvik-Bytecode für Android Generating and testing Dalvik bytecode for Android Master-Thesis von Thomas Pilot Januar 2013 SECURE SOFTWARE ENGINEERING GROUP

Erzeugen und Testen von Dalvik-Bytecode für Android Generating and testing Dalvik bytecode for Android Vorgelegte Master-Thesis von Thomas Pilot Prüfer: Eric Bodden, Ph.D. Betreuer: Eric Bodden, Ph.D. Tag der Einreichung:

Erklärung zur Master-Thesis Hiermit versichere ich nach Paragraph 22 Absatz 7 der Allgemeinen Prüfungsbestimmungen der TU Darmstadt vom 18. Juli 2012, die vorliegende Master- Thesis ohne Hilfe Dritter nur mit den angegebenen Quellen und Hilfsmitteln angefertigt zu haben. Alle Stellen, die aus Quellen entnommen wurden, sind als solche kenntlich gemacht. Diese Arbeit hat in gleicher oder ähnlicher Form noch keiner Prüfungsbehörde vorgelegen. Die Übereinstimmung von schriftlicher und elektronischer Fassung wird bestätigt. Darmstadt, den 18. Januar 2013 (Thomas Pilot)

Zusammenfassung Seit seiner Entstehung im Jahr 2008 hat der Marktanteil der Android-Platform auf Smartphones kontinuierlich zugenommen, drei von vier verkauften Geräten laufen unter dieser Platform. Auf diesen Smartphones sammeln sich eine Vielzahl sicherheitssensitiver Daten an, sowohl privater als auch geschäftlicher Natur. Dadurch ist die Platform ein lohnenswertes Ziel von Angreifern und sollte entsprechend überprüft werden. Neben dem zugrundeliegenden Linux-Kernel ist bei einer Sicherheitsanalyse die Android- Laufzeitumgebung von Bedeutung. Diese basiert auf der Dalvik genannten virtuellen Maschine, für die keine Werkzeuge zur umfassenden Analyse und Veränderung des Codes zur Verfügung stehen, anders als etwa für die Java Virtual Machine (JVM). In dieser Arbeit stellen wir einen neuen Ansatz vor, wie Dalvik-Bytecode mithilfe des Java- Analyse-Frameworks Soot erzeugt werden kann. Dadurch können etwa bestehende Android- Programme so verändert werden, dass sie zur Laufzeit überwacht werden. In der Arbeit gehen wir auf die Besonderheiten der registerbasierten virtuellen Maschine sowie die spezifischen Design-Ziele bei der Bytecode-Erzeugung ein. Außerdem beschreiben wir prototypisch, wie dieser Ansatz mithilfe des sogenannten Fuzz-Testings auf Implementierungsfehler getestet werden kann. Diese Methode ist auch für eine spätere Sicherheitsanalyse der Dalvik Virtual Machine selbst nützlich. Abstract Since it s initial release in 2008 the Android platform s market share for smartphones increased continuously: three out of four sold devices run on the Android platform. A multitude of security-sensitive data is accumulated on these smartphones, the data being personal or related to business. The platform therefore is a valuable target for attackers and should be inspected accordingly. Conducting a security analysis, one has to take into account the underlying Linux kernel and the new Android runtime environment. The latter is based on a virtual machine called Dalvik, for which there are no tools for analysis or code transformation, unlike for the Java Virtual Machine. In this work we present a novel approach to generate Dalvik bytecode by using the analysis framework Soot for Java. With the approach existing Android programs can be changed to being monitored at runtime, for instance. We explain the virtual machine s register-based characteristics and the specific design goals for the bytecode generation. In addition we present a prototype method to test our implementation with so called fuzz testing. This method is also useful for a security analysis of the Dalvik Virtual Machine itself.

Inhaltsverzeichnis 1 Einleitung 4 1.1 Motivation............................................ 4 1.2 Beiträge und Struktur dieser Arbeit............................. 4 1.3 Der Bytecode im Kontext................................... 5 1.3.1 Die Android-Platform................................. 5 1.3.2 Das Dex-Format.................................... 6 1.4 Soot und Jimple......................................... 7 1.5 Fuzzing als Testansatz..................................... 7 2 Design 8 2.1 Implementierung........................................ 8 2.1.1 Benötigte Register einer Methode.......................... 8 2.1.2 Exception-Handler................................... 9 2.1.3 Umwandlung von Jimple-Statements....................... 10 2.1.4 Umwandlung von Jimple-Ausdrücken....................... 12 2.1.5 Besonderheiten von Dex-Instruktionen...................... 14 2.1.6 Abschließende Arbeiten................................ 15 2.1.7 Unbenutzte Opcodes................................. 18 2.2 Testansatz............................................. 18 2.2.1 Ablauf des Testens................................... 19 2.2.2 App-Installation und Code-Verifizierung...................... 20 2.2.3 App-Ausführung.................................... 20 3 Evaluation 21 3.1 Auswirkungen der App-Konvertierung........................... 21 3.2 Gefundene Fehler........................................ 23 3.3 Grenzen der Methodik..................................... 24 4 Abschluss 25 4.1 Verwandte Arbeiten....................................... 25 4.2 Offene Aufgaben........................................ 25 4.3 Zusammenfassung....................................... 26 Abkürzungsverzeichnis 27 Literaturverzeichnis 28 3

1 Einleitung 1.1 Motivation Laut dem Marktforschungsunternehmen IDC ist die Android-Platform mittlerweile auf drei von vier neu verkauften Smartphones zu finden [Cor12]. Auf diesen Geräten sammeln sich eine Vielzahl sicherheits-sensitiver Daten an, sowohl privater als auch geschäftlicher Natur. Dadurch ist die Platform ein lohnenswertes Ziel von Angreifern und sollte entsprechend überprüft werden. Android wurde in einer ersten Version im Jahr 2008 veröffentlicht, basiert aber auf dem etablierten Linux-Kernel mit seinem Dateirechte-System, der Prozess-Isolierung und sicherer Interprozess-Kommunikation [And12]. Neu ist hingegen die Dalvik genannte virtuelle Maschine, die neben einiger Bibliotheken die Laufzeitumgebung des Systems darstellt. Die Dalvik Virtual Machine (DVM) ist an die JVM angelehnt, hat aber ein eigenes Bytecode-Format und ist auf die begrenzten Ressourcen einer Smartphone-Umgebung angepasst. So läuft jedes Android- Programm wie bei Java in einer eigenen virtuellen Maschine, die Initialisierung einer solchen geht jedoch schneller vonstatten. Außerdem ist der Dalvik-Bytecode näher an realen Maschinen orientiert und arbeitet nicht auf einem Stack, sondern auf Registern. Bisher existieren kaum Studien, die die Sicherheit der Android-Platform auf Bytecode-Ebene untersuchen. Meist wurden höhere Stufen wie die der Zugriffsrechte (Permissions) bei Apps betrachtet. Andererseits existiert in der DVM-Implementierung von Google ein Verifier, der den Bytecode einer App bei der Installation und vor dem ersten Start analysiert. Ein potenzieller Angreifer auf dieser Ebene müsste deshalb Bytecode erzeugen, den der Verifier als gültig akzeptiert. Neben diesen statischen Analysen sind auch zunehmend solche zur Laufzeit von Bedeutung. Literatur zu dynamischen Analysen der DVM, wie sie in einer Java-Umgebung per Bytecode-Instrumentierung durchaus üblich sind, ist uns nicht bekannt. Es mangelt letztlich an spezifischen Werkzeugen, Dalvik-Bytecode zu generieren und die DVM zu testen. 1.2 Beiträge und Struktur dieser Arbeit Diese Arbeit beinhaltet zwei Beiträge im Bereich DVM und Dalvik-Bytecode: Zum einen stellen wir einen neuen Ansatz vor, Dalvik-Bytecode zu generieren. Dieser benutzt ein Analyse- Framework, das den Bytecode bereits in eine Zwischendarstellung einlesen kann. Somit wird es möglich, eingelesene Apps verändert wieder herauszuschreiben oder neue Programme zu erzeugen. Zum anderen beschreiben wir, wie dieser Ansatz automatisiert auf Implementierungsfehler getestet werden kann. Dies geschieht, indem eingelesene Android-Apps ohne explizite Veränderung wieder generiert werden und auf einem Emulator kontrolliert gestartet werden. Da aus der Zwischendarstellung leicht ungewöhnlicher Dalvik-Code erzeugt werden kann, eignet sich diese Methode auch für eine spätere Sicherheitsanalyse der DVM und des oben genannten Verifiers selbst. Der Rest der Arbeit ist wie folgt aufgebaut: Diese Einleitung führt noch kurz in die Architektur der Android-Platform und des Dalvik-Formates ein, gibt einen Überblick zum Analyse- Framework sowie der Zwischendarstellung und schließt mit einer allgemeinen Beschreibung des Fuzz-Testings. In Kapitel 2 beschreiben wir im Detail das Design der Implementierung und den Testansatz. In Kapitel 3 wird dieser Testansatz mit auf dem Markt vorhandenen Apps benutzt, um Implementierungsfehler zu finden und um die Grenzen dieser Methodik aufzuzeigen. Kapitel 4 schließt die Arbeit ab, indem verwandte Arbeiten und offene Aufgaben aufgeführt werden und eine Zusammenfassung gegeben wird. 4

1.3 Der Bytecode im Kontext Zum besseren Verständnis des Bytecodes selbst lohnt sich ein Blick auf die Umgebung, in der er vorkommt. Dazu gehört die ausführende Platform sowie das Dateiformat, in dem der Bytecode abgelegt ist. 1.3.1 Die Android-Platform Die grundlegende Architektur der Android-Platform ist in Abbildung 1 auf Seite 5 zu sehen. Apps Application Framework Activity Manager Window Manager View-System... Laufzeitumgebung Bibliotheken Dalvik VM Core Libraries libc SSL Webkit... Linux-Kernel Abbildung 1: Architektur der Android-Platform Die Platform kommuniziert durch einen Linux-Kernel mit der darunterliegenden Hardware. Darüber liegen diverse Bibliotheken und die Laufzeitumgebung, welche dann beide über ein Application Framework von den eigentlichen Apps benutzt werden. Die Laufzeitumgebung beinhaltet neben einigen Core Libraries 1 schließlich die DVM. Sie ist in C/C++ sowie Assembler geschrieben und als Open Source unter der Apache-Lizenz verfügbar 2. Die DVM ist register-basiert und damit näher an realer Hardware orientiert als die stackbasierte JVM: Operanden werden nicht auf einen Stack gelegt oder von dort geholt, sondern liegen in Registern. Dadurch müssen tendenziell weniger Instruktionen ausgeführt [SCEG08] und damit analysiert oder instrumentiert werden, andererseits steigt die Größe des Bytecodes durch die explizite Adressierung von Operanden. Letzteres vereinfacht die Analyse, macht die Codegenerierung durch die nötige Registerallokation allerdings komplexer. In der DVM können bis zu 65.536 Register verwendet werden, sie sind 32 Bit breit und haben keinen speziellen Verwendungszweck (englisch general-purpose register). Die DVM führt den Bytecode von Apps aus, die normalerweise als Android Packages (APKs) vorliegen. Diese sind Zip-Archive, in denen sich neben Signatur-Dateien und sonstigen binären Ressourcen die Dateien classes.dex und AndroidManifest.xml befinden erstere enthält die Klassen für die DVM, letztere unter anderem die Definition der zu startenden Activity-Klasse. Ein Programmierer benutzt normalerweise das dx-tool, um aus beliebig vielen Java-Klassen die classes.dex für seine App zu erzeugen. Bei der Konvertierung werden viele Java-Eigenschaften (Organisation in Klassen mit Methoden und Feldern, Zugriffsmodifizierer etc.) übernommen, der Bytecode wird allerdings auf das geänderte, register-basierte Maschinenmodell angepasst. In einer.dex-datei verwenden die Klassen außerdem einen gemeinsamen Pool aus Konstanten, 1 ähnlich dem Java Runtime Environment (JRE) der Java-Platform, aber nicht vollständig kompatibel zur Microoder Standard-Edition 2 https://android.googlesource.com/platform/dalvik.git 5

also z.b. Strings, Methoden- und Feld-Identifier. Dadurch reduziert sich die Größe der.dex-datei im Vergleich zur Verwendung separater Pools. 1.3.2 Das Dex-Format Eine Datei im Dex-Format besteht aus einem Header, Indexstrukturen und ansonsten im Wesentlichen aus einer Liste von Klassendefinitionen. Abbildung 2 auf Seite 6 zeigt den logischen Aufbau einer Klasse im Dex-Format. Für eine vollständige, hier nicht benötigte Definition des Formates sei auf [Pro12] verwiesen. Class Data Item Class Def Item Bytecode Code Item Encoded Method Exception Handler... Method ID Access Flags Encoded Method... Encoded Field... Encoded Field... Class Access ID Flags Super Class Interfaces... Abbildung 2: Logischer Aufbau einer Klasse im Dex-Format Eine Klassendefinition (Class Def Item) besteht aus dem eigentlichen Inhalt einer Klasse (Class Data Item) sowie einigen Meta-Daten wie Klassen-Identifier, Zugriffsmodifizierern, Superklasse, Liste der implementierten Interfaces etc. Ein Class Data Item wiederum enthält eine Liste von statischen und Instanzen-Feldern (Encoded Field) sowie eine Liste von direkten und virtuellen Methoden. Direkte Methoden sind die nicht überschreibbaren (statische und private sowie Konstruktoren), virtuelle alle anderen. Eine Methode selbst besteht aus Informationen zum Code (Code Item), einem Methoden-Identifier und den Zugriffsmodifizierern. Das Code Item enthält schließlich den Bytecode als Array aus Instruktionen sowie Informationen zu Exception-Handlern und andere Informationen. Der Bytecode kann unter anderem Referenzen auf Methoden, Felder und Klassen enthalten. Diese sowie andere Referenzen in einer Dex-Datei könnten auch Ziel eines Angreifers sein, indem er etwa eine Feldreferenz auf ein nicht existentes Feld verweisen lässt. Da sich diese Arbeit jedoch auf den Bytecode selbst beschränkt, werden solche Referenzen in andere Bereiche einer Dex-Datei immer als gültig angenommen. Listing 1 auf Seite 6 zeigt ein Hello World -Programm aus Dex-Instruktionen 3. 1 sget object v0, field@0000 // Ljava / lang / System ;. out : Ljava / i o / PrintStream ; 2 const string v1, string@0001 // " Hello world! " 3 invoke virtual {v0, v1 }, method@0002 // Ljava / i o / PrintStream ;. p r i n t l n : ( Ljava / lang / S t r i n g ; ) V 4 return void Listing 1: Hello World als Dex-Bytecode-Mnemonics 3 Zur besseren Übersicht ist nur der Bytecode der main-methode dargestellt, keine weiteren Methoden- oder Klasseninformationen. 6

Der Bytecode besteht aus vier Instruktionen: Zunächst wird eine Referenz auf das statische Feld System.out in Register v0 geladen, dann der (konstante) String Hello world! nach Register v1. Die dritte Instruktion ruft die Methode java.io.printstream.println(string) auf dem Object in v0 auf, dabei wird der String in v1 als Parameter benutzt. Schließlich wird der Kontrollfluss von der Methode an den Aufrufer zurückgegeben, ohne Rückgabewert. 1.4 Soot und Jimple Der Beitrag dieser Arbeit, Dalvik-Bytecode zu generieren, basiert auf dem Analyse-Framework Soot [VRCG + 99]. Dieses wurde um die Jahrtausendwende veröffentlicht und war ursprünglich dazu gedacht, Java-Bytecode zu analysieren und zu transformieren. Mittlerweile unterstützt es diverse Ein- und Ausgabeformate, Analysen und Zwischendarstellungen. Das Framework ist in Java geschrieben und kann etwa um eigene Code-Transformationen erweitert werden. Soot kann aus Dex-Dateien Code in der Jimple-Zwischendarstellung generieren [BKLTM12]. Jimple- Instruktionen arbeiten in der Regel auf maximal drei typisierten lokalen Variablen (englisch three address code). Ein Hello World -Programm in Jimple zeigt Listing 2 auf Seite 7. 1 public c l a s s HelloWorld extends java. lang. Object { 2 public s t a t i c void main ( java. lang. S t r i n g [ ] ) { 3 j a v a. lang. S t r i n g [] args ; 4 j a v a. i o. PrintStream out ; 5 6 args := @parameter0 : java. lang. S t r i n g [ ] ; 7 out = <j a v a. lang. System : j ava. i o. PrintStream out >; 8 v i r t u a l i n v o k e out.< java. i o. PrintStream : void p r i n t l n ( j a v a. lang. S t r i n g )>(" Hello world! " ) ; 9 return ; 10 } 11 } Listing 2: Hello World in einer Jimple-Zwischendarstellung Zunächst werden die benötigten lokalen Variablen deklariert. Man beachte, dass in Zeile 6 auch der Methoden-Parameter args zunächst in einer lokalen Variable gespeichert wird 4. Dann wird das System.out -Objekt in einer lokalen Variable gespeichert und schließlich auf dieser Variablen die println(string) -Methode aufgerufen der konstante Parameter benötigt keine Variable. Zuletzt wird der Kontrollfluss wieder zurückgegeben. 1.5 Fuzzing als Testansatz Um die Bytecode-Generierung auf Implementierungsfehler zu testen, haben wir einen Fuzz- Testing-Ansatz benutzt. Dazu erhält die zu testende Komponente hier: die Jimple-nach-Dex- Konvertierung mehr oder weniger zufällig veränderte Eingabedaten. Diese ge fuzz ten Daten sollen die Implementierung an ihre Grenzen führen und so Fehler aufdecken. Da unsere Eingabedaten Android-Apps sind, bot sich ein Ansatz an, der die Apps einliest, den Jimple-Code verändert und wieder als Dex-Code rausschreibt. Wenn letzteres gelingt, deutet das auf eine robuste Implementierung hin. Nebenbei kann durch diesen Ansatz auch die DVM selbst getestet werden, da die so generierten Apps weiterhin lauffähig sein sollten. 4 Eine im Beispiel nicht vorhandene this -Referenz würde ebenso zu einer Variablen transformiert. 7

Da die Vorarbeiten zum Fuzzing länger dauerten als gedacht, ist derzeit nur ein solcher verändernder Prototyp realisiert, weitere müssen als offene Aufgabe verbleiben (siehe Abschnitt 4.2). Grundsätzlich hat die Art der Veränderungen großen Einfluss auf die Effektivität des Testansatzes. Sind diese zu wahllos, entsteht Code, der bereits syntaktisch falsch ist und so wahrscheinlich gar nicht zur Ausführung kommt. Gezieltere Veränderungen sind aufwendiger und können auch dazu führen, dass ungewöhnliche Fälle nicht in Betracht gezogen werden. Einen umfassenden Überblick über das sogenannte Mutation Testing bietet [JH11]. 2 Design 2.1 Implementierung Die Implementierung der Jimple-nach-Dex-Konvertierung basiert auf einer Bibliothek des smali/baksmali -Projektes [Sma12]. Smali/baksmali ist ein in Java geschriebener Assembler beziehungsweise Disassembler für das Dex-Format, er beinhaltet deshalb auch eine Bibliothek zum Lesen und Schreiben von Dex-Dateien. Von dieser sogenannten dexlib werden auch die Datenstrukturen für die einzelnen zu speichernden Information genutzt, sodass sich unsere Implementierung nicht mit dem byteweisen Aufbau einer Dex-Datei befassen muss. Soot kommuniziert mit der Implementierung über die Klasse soot.todex.dexprinter. Der DexPrinter wird aufgerufen, nachdem das Framework alle Analysen und Transformationen der übergebenen Klassen 5 durchgeführt hat. Durch dexlib ist es allerdings nötig, dass beim Schreiben von Jimple-Klassen diese einzeln hinzugefügt werden und erst anschließend in eine Datei geschrieben werden können. Dies ergibt sich aus dem Dex-Format, da dort ja mehrere Klassen in einer Datei gespeichert werden, anders als bei den sonstigen von Soot unterstützten Ausgabeformaten. Eine Klasse wird vom DexPrinter mit den entsprechend Abbildung 2 auf Seite 6 benötigten Informationen verarbeitet. Dabei sind nur geringe Anpassungen der von Soot bereitgestellten Informationen nötig. So ist eine Umwandlung der Typbezeichner in das unter [Pro12] beschriebene Format nötig, außerdem entsprechen die Zugriffsmodifizierer von Methoden im Dex-Format nicht ganz denen in Java: Es gibt einen Modifizierer, der Konstruktoren markiert, und synchronisierte, nicht-native Methoden werden mit declared_synchronized markiert, nicht mit synchronized. 2.1.1 Benötigte Register einer Methode Das in Abschnitt 1.3.2 erwähnte Code Item einer Methode beinhaltet ebenfalls einige Dexspezifische Informationen: Die Anzahl der von der Methode benötigten Register sowie die Menge der sogenannten in- und out-register. Um diese drei Angaben zu verstehen, lohnt sich ein Blick auf die Aufrufkonvention der DVM, wie sie in Abbildung 3 auf Seite 9 dargestellt ist. Die Register eines Methoden-Frames lassen sich in drei Kategorien einteilen, wie unter (1.) zu sehen: Register für lokale Variablen (lokal0-1), Register für die Parameter der Methode (in0-2) und Register für die Parameter aufzurufender Methoden (out0-1). Da die Größe eines Frames nach dessen Erstellung nicht mehr verändert wird, müssen auch letztere schon zu Beginn feststehen, auch wenn nicht alle out-register bei jedem Methodenaufruf verwendet werden. Es sind auch nur die in- und local-register für den Aufrufer verfügbar, im Beispiel als v0-4. 5 Die Klassen liegen zu diesem Zeitpunkt als SootClass vor, welche Java-basiert sind. Bei den Methoden wird ein valider JimpleBody angenommen. 8

niedrige Adressen hohe Adressen (1.) Frame des (2.) Aufrufers (out0) (out1) v0 = local0 v1 = local1 v2 = in0 v3 = in1 v4 = in2 invoke(v1,v4) Frame des Aufrufers out0 = v1 out1 = v4 v0 = local0 v1 = local1 v2 = in0 v3 = in1 v4 = in2 Frame des Aufgerufenen v0 = local0 v1 = in0 v2 = in1 Abbildung 3: Register in den Aufruf-Frames von Dalvik Ein Aufruf mit einem der invoke -Opcodes und z.b. zwei Parametern, deren Werte etwa in v1 und v4 liegen, hat zwei Konsequenzen. Zunächst kopiert die DVM die Werte von v1 und v4 in die out-register 0 und 1 des Aufrufers (englisch caller) damit sie vom Aufgerufenen (callee ) dann unabhängig verwendet werden können und im Speicher in jedem Fall hintereinander liegen. Dann wird ein neues Aufruf-Frame für den Aufgerufenen erstellt, in dem es wieder local-, in- und out-register gibt. Die in-register sind dabei die letzten Register (im Beispiel: v1 und v2). Sie liegen an der gleichen Speicherstelle wie die entsprechenden out-register des Aufrufers, womit aus Sicht des Aufgerufenen also in v1 der Wert des v1-registers des Aufrufers liegt, sowie in v2 der des v4-registers des Aufrufers. Die Menge der in-register ist deshalb also die Summe der für die Parameter einer Methode benötigten Register. long- und double-parameter brauchen zwei statt einem Register und bei nicht-statischen Methoden ist das erste in-register noch für die Referenz auf this freizuhalten. Die Menge der out-register ist dagegen das Maximum der für die Parameter eines Methodenaufrufs nötigen Register. Wenn eine Methode keine andere aufruft, ist sie also gleich null. Für die für einen Parameter benötigten Register gelten natürlich die gleichen Regeln wie bei den in-registern. Die Anzahl der benötigten Register schließlich ist von den Instruktionen im Methodenrumpf abhängig, mindestens jedoch gleich der Anzahl der in-register. Der Rückgabewert einer Methode steht dem Aufrufer übrigens nicht über ein Register zur Verfügung, sondern per move-result-opcode, der auf einen speziellen Speicherort in der DVM zugreift. 2.1.2 Exception-Handler Bevor wir nun die Bytecode-Generierung für die einzelnen Instruktionen beschreiben, sei noch auf die Exception-Handler hingewiesen, die in einem Code Item definiert werden. Diese stammen aus den try-catch-blöcken der Methode, wie sie in der linken Spalte in Abbildung 4 auf Seite 10 gezeigt sind. Ein Try-Block wird im Dex-Format als TryItem abgebildet, welches die Adresse seiner ersten Bytecode-Instruktion, die Anzahl der Instruktionen im Block und einen Verweis auf einen EncodedCatchHandler enthält. Dieser beschreibt die zugehörigen Catch-Blöcke als Liste von EncodedTypeAddrPairs, die jeweils aus dem Typ der zu fangenden Exception und der Adresse der ersten Instruktion des Catch-Blockes bestehen. Im Dex-Format ist auch noch ein sogenannter 9

1 try { 2 c = b / a; 3 } 4 catch (ArithmeticException ae) { 5 log(ae); 6 } 7 catch (Exception e) { 8 throw new Error(e); 9 } Adresse zu Instruktion 2 Anzahl Instruktionen im try- Block TryItem EncodedTypeAddrPair Arithmetic Exception Adresse zu Instruktion 5 EncodedCatchHandler EncodedTypeAddrPair Exception Adresse zu Instruktion 8 catchall- Handler (-1) Abbildung 4: Links ein beispielhafter try-catch-block in Java-Syntax, rechts sein Abbild im Dex- Format CatchAll-Handler vorgesehen, der alle Exceptions fängt. Im entsprechenden Feld steht ebenfalls die Adresse der ersten Instruktion. Ein solcher Handler kann bei der Behandlung von finally-blöcken nützlich sein 6. Mangels spezieller CatchAll-Handler-Unterstützung in Soot (er wird als normaler Handler konstruiert, der java.lang.throwable fängt) setzen wir das Feld jedoch konstant auf -1, was kein Handler vorhanden bedeutet. 2.1.3 Umwandlung von Jimple-Statements Die einzelnen Jimple-Instruktionen werden nacheinander abgearbeitet und in eine oder mehrere Dex-Instruktionen umgewandelt. Anschließend müssen noch einige Informationen aktualisiert werden, denn erst danach stehen z.b. die Adressen im Bytecode fest, zu denen gesprungen werden soll (siehe Abschnitt 2.1.6). Wenn eine Jimple-Instruktion umgewandelt werden soll, wird zunächst eine Pseudo-Instruktion an die bisherige Dex-Liste angefügt. Sie belegt später keinen Platz im Bytecode und dient als Ziel für Sprünge oder die in Abbildung 4 auf Seite 10 gezeigten Adressen für das Exception-Handling. Da sie die originale Jimple-Instruktion enthält, muss keine separate Datenstruktur für das Mapping gepflegt werden, welche Jimple-Instruktion aus welcher Dex-Instruktion entstanden ist. Diese Information wird für die Arbeiten benötigt, die nach der Umwandlung durchgeführt werden. Die eigentliche Umwandlung erfolgt dann im soot.todex.stmtvisitor, der die fünfzehn verschiedenen Arten von Jimple-Statements abhandelt. Innerhalb dieser Statements gibt es verschiedene Unterstrukturen wie Ausdrücke, lokale Variablen, Konstanten und sogenannte Immediates (Oberbegriff für lokale Variablen und Konstanten). Ausdrücke werden durch einen soot.todex.exprvisitor verarbeitet (siehe Abschnitt 2.1.4), lokale Variablen erhalten bei ihrer ersten Verwendung ein eigenes Register und Konstanten werden per const-opcode in ein Register geladen. Im Einzelnen umgewandelt werden: AssignStmt Ein AssignStmt besteht aus einer linken und einer rechten Seite, die diverse Typen haben können. Grundsätzlich soll mit diesem Statement die rechte Seite der linken zugewiesen werden. Wenn ein Immediate einer Feld- oder Arrayreferenz 7 zugewiesen werden soll, wird der 6 In neueren javac-versionen wird der Code des finally-blockes mehrmals in den Bytecode hineinkopiert (code inlining), z.b. an das Ende der try- und catch-blöcke. In diesen Blöcken, außerhalb des kopierten Bytecodes, kann eine Exception auftreten. Sie kann potenziell weitergeworfen werden, muss also nicht von der Methode behandelt werden. Durch einen Exception-Handler, der alle Exceptions fängt, kann so sichergestellt werden, dass der kopierte finally-bytecode trotzdem noch vor Verlassen der Methode ausgeführt wird. 7 StaticFieldRef, InstanceFieldRef beziehungsweise ArrayRef 10

entsprechende put-opcode aus Dex verwendet. Soll umgekehrt eine solche Referenz in eine lokale Variable kopiert werden, verwendet die Implementierung den entsprechenden get- Opcode. Stehen auf beiden Seiten lokale Variablen, resultiert das in einem move-befehl, steht rechts eine Konstante und links eine lokale Variable, entsteht eine const-instruktion. Der Fall rechts ein Ausdruck, links eine lokale Variable wird an den ExprVisitor delegiert (siehe Abschnitt 2.1.4). Falls der Ausdruck ein Methodenaufruf war, wird anschließend noch eine moveresult-instruktion angehängt, um die tatsächliche Zuweisung des Methodenaufrufs (genauer: des Rückgabewertes der Methode) an eine lokale Variable abzubilden. BreakpointStmt Ein BreakpointStmt wird nicht umgewandelt, da im Dex-Format keine Breakpoints vorgesehen sind. EnterMonitorStmt / ExitMonitorStmt Diese Statements reservieren ein Lock auf einem Objekt in einer lokalen Variable bzw. geben es wieder frei. Sie werden in Dex mit den entsprechenden monitor-enter- und monitor-exit- Opcodes umgesetzt. GotoStmt Das GotoStmt stellt einen unbedingen Sprung an ein enthaltenes anderes Statement dar. Es wird für das Dex-Format in eine goto-instruktion umgewandelt. Das Ziel wird dabei zunächst als Jimple-Statement gespeichert, die Auflösung zu einer konkreten Adresse im Bytecode erfolgt später (siehe Abschnitt 2.1.6). IdentityStmt Ein IdentityStmt ist eine besondere Form von Zuweisung. Damit werden in Jimple die besonderen Speicherorte für gefangene Exceptions, this und Methoden-Parameter abgebildet 8. Da die letzten beiden in Dex laut Aufrufkonvention schon in Registern vorhanden sind 9, ist in diesen Fällen keine Dex-Instruktion nötig. Wir speichern allerdings die dafür in Jimple verwendeten lokalen Variablen (die linken Seiten solcher Zuweisungen ), damit andere Dex-Instruktionen auf diese Speicherorte mit der richtigen Registernummer zugreifen können. Gefangene Exceptions benötigen dagegen ein neues Register, sie werden per move-exception-opcode dorthin kopiert. IfStmt Ein IfStmt stellt einen bedingten Sprung dar und besteht aus einem Ausdruck und einem anderen Statement. Wertet der Ausdruck zur Laufzeit zu wahr aus, wird das Statement angesprungen. Die Umwandlung in eine Dex-Instruktion übernimmt der ExprVisitor, er generiert die passende if-instruktion. Das Sprungziel wird wie beim GotoStmt zunächst als Jimple-Statement gespeichert und später zu einer Adresse aufgelöst. InvokeStmt Ein InvokeStmt besteht aus einer InvokeExpr. Sie wird mit dem ExprVisitor behandelt und resultiert in einem der invoke-befehle. 8 als CaughtExceptionRef, ThisRef beziehungsweise ParameterRef 9 Siehe die Abbildung 3 auf Seite 9 und die Erklärung dazu. 11

LookupSwitchStmt Dieses Statement stellt das Switch-Case-Konstrukt dar, wobei die Fall-Labels (case labels ) als Schlüssel-Ziel-Paare abgebildet sind. Diese Form eines Switches ist dann sinnvoll, wenn die Labels zahlenmäßig eher weiter voneinander entfernt sind. Im LookupSwitchStmt liegen die Schlüssel als Integer-Konstanten vor, die Ziele sind andere Statements, genauso wie das default-ziel. Der Wert, der für die Auswahl eines Cases benutzt wird, liegt als Immediate vor. Das Statement wird mit dem sparse-switch-opcode implementiert. Dieser enthält das Register für den Immediate und die Adresse für den sogenannten Payload. Der Payload wird ans Ende des Bytecodes angefügt (siehe Abschnitt 2.1.6) und enthält die Fälle als Integer-Statement-Paare. Im default-fall wird nicht zum Payload gesprungen, weshalb nach dem sparse-switch-opcode noch ein unbedingter Sprung zum default-ziel eingefügt wird, per goto. NopStmt Ein NopStmt steht dafür, dass die virtuelle Maschine an dieser Stelle keine Operation durchführen soll. Es wird in einen nop-opcode umgewandelt. RetStmt RetStmt ist ein veraltetes Jimple-Konstrukt und hat auch keine Entsprechung im Dex-Format. Aus diesen beiden Gründen wirft die Implementierung einen Fehler aus, falls sie auf ein RetStmt stößt. ReturnStmt Durch ein ReturnStmt kehrt der Kontrollfluss von einer Methode zu ihrem Aufrufer zurück, mit dem Rückgabewert als Immediate. Es wird mit einem return-opcode umgesetzt. ReturnVoidStmt Ein ReturnVoidStmt beendet ebenfalls eine Methode, allerdings ohne einen Rückgabewert zu übermitteln. Dies wird in Dex durch den return-void-opcode realisiert. TableSwitchStmt Dieses Statement stellt wie ein LookupSwitchStmt (s.o.) ein Switch-Case-Konstrukt dar. Es wird allerding genutzt, wenn die Labels zahlenmäßig nahe beieinander liegen, da sie dann als Index in eine Tabelle verwendet werden können: Das erste Label hat sein Ziel in der ersten Zeile usw. Lücken in dem Intervall verbrauchen also auch Tabellenzeilen. Ein TableSwitchStmt wird mit dem packed-switch-opcode umgesetzt. Ansonsten gilt die Beschreibung für das Lookup- SwitchStmt, nur im Payload werden nun eben nur der erste Schlüssel und alle Ziele gespeichert, statt Schlüssel-Ziel-Paaren. ThrowStmt Das ThrowStmt wirft eine Exception, die zuvor in einem Immediate gespeichert wurde. In Dex entspricht das einer throw-instruktion. 2.1.4 Umwandlung von Jimple-Ausdrücken Wie oben dargelegt, enthalten einige Statements noch Ausdrücke. die mit einem ExprVisitor behandelt werden. Falls der Ausdruck Teil einer Zuweisung ist, bekommt er vom StmtVisitor das 12

Zielregister, bei einem bedingten Sprung aufgrund eines Ausdruckes bekommt er das Sprungziel mitgeteilt. In Jimple gibt es 32 verschiedene Ausdrücke, die wie folgt zusammengefasst und umgewandelt werden können: Berechnende binäre Ausdrücke Diese Ausdrücke 10 verknüpfen zwei numerische oder boolsche Operanden und speichern das Ergebnis der Operation in einem Register ab. Dabei gibt es im Dex-Format für drei Situationen optimierte Opcodes: Wenn der zweite Operand eine Integer-Konstante ist, kann dieser direkt in der Instruktion gespeichert werden, womit nur ein statt zwei Quellregister benötigt wird. Diese Opcodes enden auf /lit16 für 16-Bit-Konstanten und /lit8 für welche mit 8 Bit. Die andere Optimierung wird mit /2addr-Opcodes erreicht: Wenn das Register des ersten Operanden gleichzeitig das Zielregister ist, müssen für diese Instruktion nur zwei statt drei Register angegeben werden, was den Bytecode verkleinert. Die dritte Optimierung betrifft das Xor mit -1, wenn der andere Operand ein int oder long ist dies entspricht der Erzeugung des Einerkomplements, wofür es die speziellen Opcodes not-int bzw. not-long gibt. Vergleichende binäre Ausdrücke Hierbei werden die Inhalte zweier Register miteinander verglichen, im Falle von Ganzzahlen 11 wird bei positivem Vergleich zu einem anderen Statement gesprungen, bei Gleitkommazahlen 12 wird das Ergebnis des Vergleichs in ein drittes Register geschrieben. Letzterer Vergleich passiert mit den cmp-opcodes, der Ganzzahlen-Vergleich mit if-instruktionen. Dort gibt es auch eine Optimierungsmöglichkeit: Ist die zweite Ganzzahl 0, kann mit den if-z-befehlen ein Register eingespart werden. Dies ist etwa für den Vergleich einer Objektreferenz mit null von Bedeutung. Methodenaufrufe Es gibt in Jimple fünf verschiedene Ausdrücke, um Methodenaufrufe darzustellen: DynamicInvokeExpr, SpecialInvokeExpr, VirtualInvokeExpr, InterfaceInvokeExpr und StaticInvokeExpr. DynamicInvoke ist mit dem Java Development Kit (JDK) 7 hinzugekommen und wird von Dex nicht unterstützt, weshalb unsere Implementierung einen Fehler ausgibt, wenn sie einen solchen Ausdruck findet. Die anderen Ausdrücke werden alle ähnlich nach Dex umgewandelt. SpecialInvokes sind Aufrufe von Konstruktoren (auch von Superklassen) und von privaten Methoden. Die Ausdrücke haben eine Referenz auf die Methode, die aufzurufen ist, Informationen zu den Parametern (als Immediate) und eine Referenz auf das Objekt, auf dem die Methode aufgerufen wird (als lokale Variable). Ein Aufruf einer super -Methode erhält den invoke-super-opcode, die Aufrufe von privaten Methoden und sonstigen Konstruktoren den Opcode invoke-direct. VirtualInvokes stellen die Aufrufe von überschreibbaren Methoden dar, enthalten dieselben Informationen wie SpecialInvokes und werden per invoke-virtual abgehandelt. InterfaceInvokes sind Methodenaufrufe auf Interfaces, d.h. ähnlich wie bei den VirtualInvokes muss die virtuelle Maschine den Aufruf noch zu einer konkreten Methode auflösen. Auf Bytecode-Ebene unterscheiden sie sich von diesen nur im Opcode, der hier invoke-interface ist. StaticInvokes schließlicht rufen statische Methoden auf. Neben dem Opcode invoke-static unterscheiden 10 AddExpr, SubExpr, MulExpr, DivExpr, RemExpr, AndExpr, OrExpr, XorExpr, ShlExpr, ShrExpr und UshrExpr 11 EqExpr, GeExpr, GtExpr, LeExpr, LtExpr und NeExpr 12 CmpExpr, CmpgExpr und CmplExpr 13

sie sich lediglich insofern, als sie natürlich keine Referenz auf ein Objekt enthalten, auf dem die Methode aufgerufen werden soll. Unäre Ausdrücke Bei einer LengthExpr wird die Länge eines Arrays in eine lokale Variable geschrieben. In Dex benutzen wir dazu den array-length-opcode, der die Länge eines in einem Register abgelegten Arrays in ein anderes schreibt. Der andere Ausdruck mit nur einem Parameter, NegExpr, negiert den Wert eines Immediate. Die neg-opcodes in Dex machen genau das. Ausdrücke zu Typen Die InstanceOfExpr stellt den Typcheck dar, mit dem eine Referenz überprüft wird. In Dex gibt es dafür den instance-of-opcode, der 1 oder 0 in ein Register schreibt, wenn die Referenz in einem anderen Register eine Instanz des gegebenen Types ist bzw. nicht ist. Die Typumwandlung geschieht mittels CastExpr, für die es im Dex-Format verschiedene Opcodes gibt. Quelle ist in Jimple immer ein Immediate, in Dex also ein Register. Wenn der Typ, zu dem gecastet werden soll, keine Referenz ist, wird ein primitiver Cast generiert. Je nach Quell- und Cast-Typ ist das einer der X-to-Y -Opcodes. Falls ein primitiver Typ mangels passendem Opcode nicht direkt zu casten ist, erstellt unsere Implementierung zwei Cast-Operationen über ein temporäres Register. Dieses ist nötig, damit die Typen der Dalvik-Register nicht verändert werden es könnte ja zwischen beiden Casts ein Fehler entstehen, wodurch der ursprüngliche Cast nur halb ausgeführt werden würde. Eine weitere Besonderheit ergibt sich aus einer gewissen Typ-Toleranz von Dalvik: So muss etwa ein byte nicht in einen int konvertiert werden, es gibt auch keinen entsprechenden Opcode. Stattdessen reicht ein einfacher move-befehl von Quell- zu Zielregister. Ebenfalls keinen echten Cast benötigen Referenzen: Es wird einfach der check-cast-opcode verwendet, evtl. auch über ein temporäres Zwischenregister (wegen der Typen, s.o.). Der Opcode bewirkt zur Laufzeit des Programmes eine ClassCastException, falls die Typen nicht zusammenpassen. Ansonsten ist eine tatächliche Umwandlung nicht nötig, das erfolgreich gecastete Objekt muss höchstens noch in ein anderes Register verschoben werden. Objekterzeugung Um in Jimple eine neue Instanz eines Types zu erzeugen, gibt es die NewExpr. Sie wird in Dex mit dem new-instance-opcode abgebildet. Da damit keine Array-Typen erzeugt werden können, gibt es noch die NewArrayExpr bzw. new-array. Sie benötigen natürlich noch ein Immediate bzw. ein Register, um die gewünschte Größe des Arrays angeben zu können. Für mehrdimensionale Arrays existiert darüber hinaus NewMultiArrayExpr bzw. filled-new-array. Für Dex hängen wir nach letzterer Instruktion noch eine move-result-object-instruktion an, damit das neue Array in ein Register verschoben wird, wo es dann genutzt werden kann. 2.1.5 Besonderheiten von Dex-Instruktionen Das Dex-Format hat noch einige Besonderheiten, die bei einer konkreten Implementierung nicht zu vernachlässigen sind. Die bisherige Erwähnung von Dex-Opcodes ignorierte der Übersichtlichkeit halber Unter-Opcodes für verschiedene Typen. So kann etwa eine normale AddExpr eigentlich zu den Opcodes add-int, add-long, add-float und add-double führen. Andererseits kann man auch nicht einfach blind den Typen nehmen, der in Jimple definiert ist, da es z.b. kein add-byte gibt stattdessen muss man in dem Fall add-int verwenden. Z.B. bei den 14

vergleichenden binären Ausdrücken muss auch beachtet werden, dass in Dex (Object)null == (int)0 gilt, in Jimple aber die Null-Konstante etwas anderes ist als die Integer-Konstante 0. Eine Null-Konstante muss deshalb vor Weiterverwendung in die Integer-Konstante geändert werden. Weiterhin sei hier erwähnt, dass long- und double-werte immer zwei aneinanderhängende Register benötigen. Opcodes mit diesen Typen enden auf -wide. Bei einem Methodenaufruf belegt ein long oder double konsequenterweise auch zwei Parameter. Erwähnenswert ist noch, dass jedem Opcode ein Instruktionsformat zugeordnet ist 13 und dass hierbei das Format 3rc hervorsticht. Es wird für die filled-new-array/range- und die fünf invoke-*/range-opcodes verwendet. Ersterer kommt zum Einsatz, wenn das zu erstellende Array mehr als fünf Dimensionen hat, letztere bei Methodenaufrufen mit mehr als fünf Parametern. Die normalen Opcodes haben nämlich Instruktionsformate, die maximal fünf Register zulassen. Das Format 3rc erlaubt bis zu 256 Register. Dies wird dadurch erreicht, dass man ein Startregister und die Anzahl der folgenden Register angibt, eben eine Range. Die verwendeten Register müssen also fortlaufend sein. 2.1.6 Abschließende Arbeiten Nachdem nun prinzipiell alle Dex-Instruktionen erstellt wurden, sind noch einige Aspekte zu aktualisieren, bevor sie an die dexlib weitergereicht werden können. Zunächst müssen noch die zwischengespeicherten Payloads der Switch-Statements (siehe Abschnitt 2.1.3) angehängt werden. Diese sollen einer Empfehlung nach am Ende eines Methoden-Bytecodes liegen: Die Dex-Spezifikation verbietet, dass die Payloads durch normalen Kontrollfluss erreicht werden. Um nicht umständlich um einen Payload in der Mitte springen zu müssen, empfiehlt sie eine Platzierung am Ende. Zu dieser Zeit stehen nun auch alle Ziele im Dex-Bytecode fest, die von bedingten und unbedingten Sprüngen und Switch-Konstrukten verwendet werden können. Also erhalten alle Instruktionen einen Offset gemäß ihrer Größe, beginnend bei 0. Anschließend werden diese Offsets in den Instruktionen eingetragen, in denen sie als Ziele verwendet werden. Ein weiterer Aspekt sind die verwendeten Register: Bisher wurden die Registernummern bei der Erstellung durch den soot.todex.registerallocator einfach hochgezählt. Erst, wenn alle Instruktionen vorliegen, kann deren Verwendung optimiert werden. Dabei müssen verschiedene Dex-spezifische Bedingungen eingehalten werden: Methodenparameter müssen die höchstnummerierten Register im Bytecode einer Methode sein. In Jimple standen die entsprechenden Statements für die Parameter (siehe Abschnitt 2.1.3) am Anfang des Codes, außerdem war noch nicht abzusehen, wieviele weiteren Register benötigt werden long- und double-werte benötigen zwei Register, die aufeinanderfolgend nummeriert sein müssen Instruktionen im 3rc -Format erlauben bis zu 256 Register, die ebenfalls aufeinanderfolgend nummeriert sein müssen (siehe Abschnitt 2.1.5) die meisten Opcodes können aufgrund ihres Instruktionsformates nicht mit der vollen Registerbandbreite (0-65.535) arbeiten, da das Format nur vier oder acht Bit für ein Register vorsieht 13 http://source.android.com/tech/dalvik/instruction-formats.html 15

Bei einer Registeroptimierung in diesem Kontext sollte auch das übergeordnete Ziel der Implementierung beachtet werden: Optimalerweise sollte eine Dex-Datei, wenn sie von Soot eingelesen wurde und keine sonstigen Transformationen durchläuft, wieder genauso ausgegeben werden, damit Analyseergebnisse nicht durch diese Round Trip -Konvertierung beeinflusst werden. Wie in den vorherigen Abschnitten gesehen, treten bei der Konvertierung von Jimple nach Dex bisher kaum Veränderungen auf 14. Allerdings verletzt der Bytecode durch die naive Konvertierung an vielen Stellen noch die oben genannten Bedingungen. Optimierung kann hier also nicht bedeuten, möglichst schnellen Code zu erzeugen, oder z.b. (trotz virtueller Maschine) die Anzahl der verwendeten Register zu minimieren vielmehr soll korrekter, möglichst originalgetreuer Bytecode entstehen. Deshalb verwendet die Implementierung (in soot.todex.registerassigner) an dieser Stelle keinen der üblichen Algorithmen zur Registerallokation, sondern lehnt sich sehr stark an das dx-tool an. Dieses ist im Android-Software Development Kit (SDK) enthalten und konvertiert.class-dateien in das Dex-Format. Die Registerallokation ist dort in der Klasse com.android.dx.dex.code.outputfinisher zu finden. Registerallokation im Detail Der RegisterAssigner geht nun wie folgt vor, um die Register unter den genannten Bedingungen final zuzuweisen: Zunächst werden die Register für die Parameter hinter die anderen geschoben, sodass sie die mit den höchsten Nummern sind. Am unteren Ende werden anschließend in einem Fixpunktverfahren solange neue Register angefügt, bis keine weiteren benötigt werden. D.h. die Registernummern werden z.b. alle um 2 inkrementiert, weil aktuell zwei zusätzliche Register benötigt werden. Diese haben dann die Nummern 0 und 1. Durch die Inkrementierung könnten alte Register ungültig werden. Deshalb prüft das Verfahren am Anfang der nächsten Iteration wieder, wieviele inkompatible Register es gibt, und ob die bisher reservierten neuen Register für sie ausreichen. In einem zweiten Schritt werden dann alle Instruktionen mit inkompatiblen Register korrigiert. Dies kann auf zwei Weisen geschehen: Entweder wird die Instruktion durch eine passende ersetzt: Zum Beispiel kann eine const/4-instruktion (Registernummer muss kleiner 16 sein) durch eine mit const/16 (Nummer kleiner 256) ersetzt werden. Oder es werden vor und gegebenenfalls nach der Instruktion neue moves hinzugefügt, die die Registerinhalte in niedrigere, kompatible Register verschieben bzw. von dort wieder herausholen. Abbildung 5 auf Seite 16 zeigt ein einfaches Beispiel: 1... 2 invoke-super v19, v20 3... 1... 2 move v0, v19 3 move v1, v20 4 invoke-super v0, v1 5... Abbildung 5: links: invoke-super-instruktion mit inkompatiblen Registern: maximal v15 wäre jeweils erlaubt, da nur je 4 Bit zur Verfügung stehen. rechts: durch Einfügen zweier moves behobener Code 14 Eine mögliche Beeinflussung durch das Einlesen, also die Dex-nach-Jimple-Konvertierung soll hier außen vor bleiben, da wir darauf im Rahmen dieser Arbeit keinen Einfluss haben. 16

Eine Instruktion kann ein Register nicht nur lesen, sondern als sogenanntes Ergebnis-Register auch schreiben. Wenn das Ergebnis-Register inkompatibel ist, muss nach der Instruktion ein weiterer move eingefügt werden, wie Abbildung 6 auf Seite 17 zeigt: 1... 2 iget v16, v19 3... 1... 2 move v0, v19 3 iget v0, v0 4 move v16, v0 5... Abbildung 6: links: iget-instruktion mit inkompatiblen Registern. rechts: durch Einfügen zweier moves behobene Instruktion Im Beispiel sieht man eine iget-instruktion mit inkompatiblen Registern. Es ist jeweils maximal v15 erlaubt, da nur je 4 Bit zur Verfügung stehen. Die iget-instruktion speichert eine Instanzvariable aus dem Objekt in Register v19 in das Register v16, das also ein Ergebnis- Register ist. Durch Einfügen des oberen moves wurde das Objekt-Register korrigiert, mit dem unteren das Ergebnis-Register. Die Registerallokation ist abgeschlossen, nachdem jede Instruktion mit inkompatiblen Registern ersetzt oder durch moves korrigiert wurde. Bedingte und unbedingte Sprünge Auch die Offsets in den Sprüngen können Instruktionsformate verletzen: Bei den if- und goto -Opcodes gibt es welche, die für das Sprungziel nur 8 oder 16 Bit erlauben. Bei den Switches tritt das Problem nicht auf, da dort die vollen 32 Bit möglich sind. Um die Offsets zu korrigieren, wird ebenfalls ein Fixpunkt-Verfahren angewandt: Der Algorithmus initialisiert die Offsets neu und geht dann einmal durch alle Instruktionen und behebt währenddessen fehlerhafte Sprünge. Solange dabei Instruktionen verändert wurden, werden diese beiden Schritte wiederholt. Dies ist nötig, da durch die Korrektur auch neue Instruktionen hinzukommen können, wodurch Offsets wieder ungültig werden können. Einfach zu beheben sind unbedingte Sprünge: Es gibt für sie die Opcodes goto, goto/16 und goto/32. Wie die Namen vermuten lassen, kann eine fehlerhafte goto-instruktion (für 8- Bit-Offsets) durch eine mit goto/16 (16 Bit) ersetzt werden, diese wiederum durch eine mit goto/32 (32 Bit). Komplexer ist die Korrektur bei if-instruktionen, die Offsets bis 16 Bit zulassen und keine alternativen Opcodes haben. Unsere Implementierung bedient sich dabei einer Transformation, die in Abbildung 7 auf Seite 17 dargestellt ist. 1 if (test) goto bar 2 foo: 3... 4 bar: 5... max. 16 Bit 1 if (!test) goto foo 2 goto bar 3 foo: 4... 5 bar: 6... max. 16 Bit max. 32 Bit Abbildung 7: links: Bedingter Sprung mit potenziell zu großem Sprung-Offset. rechts, gleichwertig: Durch Negation der Bedingung gewonnener, unbedingter Sprung mit der Möglichkeit eines größeren Offsets. 17

Der bedingte Sprung von Zeile 1 nach bar in Zeile 4 erlaubt einen maximalen Offset von 16 Bit. Wenn nicht gesprungen wird, geht die Ausführung bei foo in Zeile 2 weiter. Wenn wir die Bedingung für den Sprung negieren, muss dieser nun zum sehr nahen foo zeigen. Das ursprüngliche bar erreichen wir durch einen unbedingten Sprung, der neu hinter die if-instruktion, aber vor foo eingefügt wird. Dieser Sprung wird ja gerade ausgeführt, wenn die Bedingung nicht zutrifft, also genau dann wenn unsere ursprüngliche, nicht negierte es täte. Nach diesen Arbeiten werden die Datenstrukturen unserer Implementierung in die der dexlib umgewandelt und dorthin weitergereicht. Damit kann sie nun den Bytecode und damit den letzten benötigten Teil einer Klassendefinition gemäß Abbildung 2 auf Seite 6 erzeugen. 2.1.7 Unbenutzte Opcodes Einige für Dex-Bytecode definierte Opcodes werden von der Implementierung bewusst nicht benutzt. Unterschiedlichste Gründe führen dazu, dass niemals Instruktionen mit diesen Opcodes generiert werden: rsub-int und rsub-int/lit8 Diese Opcodes führen die sogenannte umgekehrte Substraktion (englisch reverse substraction) auf zwei Ganzzahlen a und b durch: Statt wie im three address code üblich für die Anweisung minus c, a, b den Wert c = a minus b zu berechnen, wird c = b minus a ausgeführt. Eine solche Operation kann auf Jimple-Ebene nicht erkannt werden. const/high16 und const-wide/high16 Diese beiden Opcodes laden eine 16-Bit-Ganzzahl in die höhere Hälfte eines Registers bzw. Register-Paares, die untere Hälfte wird dabei mit Nullen aufgefüllt. Für unsere Implementierung konnte keine sinnvolle Verwendung dafür gefunden werden. fill-array-data Dieser Opcode ermöglicht es, Arrays effizient mit Daten zu initialisieren. Er benutzt dafür einen Payload mit zusätzlichen Daten, ähnlich der beiden Switch-Opcodes. Da Soot bei seiner Konvertierung die Array-Initialisierung in kleinere Operationen aufteilt, gibt es hierfür ebenfalls keine Verwendung. const-string/jumbo Ein Jumbo-Opcode ermöglicht es, bei Referenzen einen besonders großen Index anzugeben (statt 16 Bit ist Platz für bis zu 32 Bit). Da die dexlib diese Referenzen verwaltet, wandelt sie bei Bedarf einen normalen const-string-opcode auch selbst in const-string/jumbo um. 2.2 Testansatz Um nun die in Abschnitt 2.1 beschriebene Implementierung zu testen, werden Android-Apps durch Soot eingelesen, wieder rausgeschrieben und kontrolliert auf einem Android-Emulator installiert und gestartet. Einen Überblick über die verwendeten Komponenten bietet Abbildung 8 auf Seite 19. Das Test-Framework besteht hauptsächlich aus Soot, das die zu testende Implementierung todex und Dexpler zum Einlesen von Dex-Dateien enthält. Daneben enthält es Code, um den 18