Obfuscator-Techniken, oder wie kann in .NET Programmcode
Transcription
Obfuscator-Techniken, oder wie kann in .NET Programmcode
Obfuscator-Techniken, oder wie kann in .NET Programmcode geschützt werden? Johannes Lipsky, Katja Weidner, Marc Schanne (lipsky|weidner|schanne@fzi.de) 1 Motivation reichen vom Brechen von Kopierschutzmechanismen über das gezielte Aufspüren von Sicherheitslücken bis hin zum Diebstahl geistigen Eigentums. Als Schutzmechanismus vor unerwünschter Spionage kommt zwar auf den ersten Blick auch das Verschlüsseln des Codes in Frage. Jedoch erweist sich diese Möglichkeit als wenig geeignet, da man der CLR zum Ausführen sowohl die Entschlüsselungsmethode als auch den Schlüssel bereitstellen müsste und diese Informationen stehen damit automatisch auch dem Angreifer zur Verfügung. Eine der wichtigsten Entwurfsentscheidungen des Microsoft .NET Frameworks ist die Sprachunabhängigkeit. Quellcode, der in einer .NET Sprache geschrieben wurde, wird vom Compiler in CIL (Common Intermediate Language) Anweisungen übersetzt und in Assemblys zusammengefasst. Eine Assembly stellt die Verpackungseinheit eines .NET Programms dar, sie erscheint nach außen als gewöhnliche Datei (.exe oder .dll), ist jedoch intern völlig anders strukturiert und enthält unter anderem Metadaten, die Typen, Mitglieder und Code-Referenzen zu anderen Assemblys beschreiben. CIL-Anweisungen werden von einer Stack-basierten, virtuellen Maschine, der CLR (Common Language Runtime), des ausführenden Systems zur Laufzeit interpretiert und dabei in maschinenabhängigen Code übersetzt. Eine .NET Assembly enthält also Code, der sich auf einem höheren Abstraktionsniveau befindet, als ein Programm in Maschinencode, das zum Beispiel in C++ geschrieben wurde. Neben den Vorteilen der Sprachunabhängigkeit und der Interoperabilität der .NET Hochsprachen bringen diese Eigenschaften jedoch einen nicht zu unterschätzenden Nachteil mit sich. Die in einer Assembly enthaltenen Daten stellen genug Informationen bereit, um den Quellcode der .NET Sprache fast originalgetreu wieder herzustellen (siehe Abb. 1). Der Schutz geistigen Eigentums, wie etwa Algorithmen, Programmlogik oder Datenstrukturen, ist praktisch nicht vorhanden. Ein Obfuscator überführt eine Assembly in eine Form, die Daten und Programmabläufe verschleiert und das Verständnis des rückübersetzten Codes damit möglichst schwierig macht. Gleichzeitig verändert der Obfuscator jedoch nicht die Funktionalität des Programms. Der Trick eines Obfuscators besteht also darin den Angreifer zu verwirren und gleichzeitig der CLR die selbe Logik zu liefern. Abbildung 2 zeigt den Einsatz eines Obfuscators. Einen hundertprozentigen Schutz gewährt zwar auch ein Obfuscator nicht, macht aber Reverse Engineering zeitaufwendig und damit letztlich unrentabel. In diesem Papier stellen wir zunächst die Möglichkeiten der Deassemblierung und Dekompilierung von .NET Assemblys kurz vor. Danach wird ein detaillierter Einblick in die unterschiedlichen Formen von Obfuscatingtransformationen und deren Arbeitsweise geliefert. Daraus folgern wir einige Bewertungskriterien, die den Leser bei der Wahl eines für seine Zwecke geeigneten Obfuscators unterstützen sollen. Abschließend geben wir eine Übersicht der momentan verfügbaren Obfuscatoren. Ist der Quellcode der Hochsprache erst wieder hergestellt, sind die Möglichkeiten des Angriffs vielfältig und Dekompiler Source Code Compiler Source Code Angriff Assembly CLR P rogrammausführung Abbildung 1: Vorgehen ohne Obfuscator Dekompiler Assembly verschleierter Source Code Angriff geschützte Assembly Obfuscator CLR P rogrammausführung gleiche Funktionalität Abbildung 2: Vorgehen mit Obfuscator 1/15 Abbildung 3: "Hallo Welt" in ILDASM geöffnet Abbildung 4: Die Main-Methode von "Hallo Welt" in ILDASM 2 Deassemblierung von .NET Assemblys Metadaten sowie den CIL Code an. In Verbindung mit dem ebenfalls enthaltenen Programm ILASM.exe ist es sogar möglich, Veränderungen am CIL Code vorzunehmen und die so entstandenen neuen Dateien wieder in eine funktionsfähige Binärdatei zu übersetzen. Dieser Vorgang wird als round tripping bezeichnet. Dieser Abschnitt beschreibt die Möglichkeiten der Deassemblierung von .NET Assemblys exemplarisch anhand der beiden Programme ILDASM und Reflector [11]. Dies soll zeigen, wie wichtig der Schutz eines .NET Programmes ist da sich selbst mit frei verfügbarer Software beachtliche Ergebnisse erzielen lassen. Beide Programme nutzen den Mechanismus der Reflektion, um den Inhalt einer Assembly zu erfassen. Wir demonstrieren ihre Arbeitsweise jeweils mit einem einfachen "Hallo Welt"-Programm und einer komplexeren Funktion aus mscorelib.dll. Als Beispiel dient das Programm "Hallo Welt", das in Beispiel 1 als C# Programmtext dargestellt ist. Das Öffnen des kompilierten "Hallo Welt"-Programms führt zu der in Abbildung 3 gezeigten Ausgabe. In der obersten Ebene der Baumstruktur befindet sich das Manifest der Assembly und der Namensraum "Hallo". Darauf folgt in der nächsten Ebene die einzige Klasse des Namensraums, "HalloWelt". Das erste Element der Klasse enthält Metadaten und zeigt an, dass die Klasse keinen Klassenkonstruktor benötigt. Darauf folgen der Instanzkonstruktor der Klasse und die statische Methode Main. using System; namespace Hallo { public class HalloWelt { static void Main(string[] args) { Console.WriteLine("Hallo Welt!"); } } } Beispiel 1: Das Program "Hallo Welt!" Ein Doppelklick auf ein beliebiges Element der Baumstruktur öffnet ein neues Fenster und zeigt die Implementierung des Elements in IL Code. Die Main-Methode des Beispiels sehen Sie in Abbildung 4. Der Befehl ldstr legt den String "Hallo Welt!" auf dem Stack ab. Anschließend wird die Methode System.Console.WriteLine aufgerufen, die den String auf dem Stack als Parameter nutzt. In Abbildung 5 sehen Sie die in ILDASM geöffnete Klasse System.Collections.ArrayList aus mscorelib.dll. Die Datei befindet sich im Installationsverzeichnis des .NET Framework. 2.1 ILDASM Das Programm ILDASM.exe (Intermediate Language Disassembler) ist bereits in der Installation des .NET Framework SDK enthalten. Es öffnet bereits kompilierte Module oder Assemblys und zeigt die darin enthaltenen Öffnet man zum Beispiel die Methode InsertRange 2/15 Abbildung 5: Die Klasse System.Collection.ArrayList in ILDASM Abbildung 6: Ausschnitt der InsertRange Methode der Klasse System.Collection.ArrayList der Klasse ArrayList sieht man deutlich komplexeren Code. Abbildung 6 zeigt einen Ausschnitt der Methode. Die Funktionalität des Codes nachzuvollziehen, ist nicht mehr so leicht wie im ersten Beispiel, aber mit Hilfe der bereitgestellten Metadaten und einer CIL Befehlsreferenz keinesfalls unmöglich. tor ist der des IL Disassemblers sehr ähnlich, jedoch gehen die Fähigkeiten des Programms weit darüber hinaus. In einer Dropdownliste in der Menüleiste von Reflector kann die gewünschte Sprache einstellt werden, in die Reflector den Code der Assembly rückübersetzt. Die Auswahl einer Methode zeigt deren Signatur im unteren Abschnitt von Reflector an und ein Doppelklick führt zur Darstellung des Codes in der oben ausgewählten Sprache. Abbildung 7 zeigt dies am Beispiel von "Hallo Welt". Die in Abbildung 8 gezeigte InsertRange Methode der ArrayList-Klasse demonstriert noch einmal deutlicher wie mächtig dieses Werkzeug ist. Der 2.2 Reflector Reflector wurde von Lutz Roeder [11], einem Entwickler bei Microsoft, geschrieben und kann von dessen Website kostenlos bezogen werden. Die Arbeitsweise von Reflec- 3/15 Abbildung 7: "Hallo Welt" in Reflector geöffnet Abbildung 8: Anzeige der InsertRange Methode in Reflector 3 Obfuscatingtransformationen hier gezeigte Code könnte praktisch per Copy&Paste übernommen werden. Um die Funktionsweise eines Programms vor Angreifern zu verbergen, muss ein Obfuscator den Code des Programms verändern. Erreicht wird dieses Ziel durch die Anwendung verschiedener Transformationen, die auch als Obfuscatingtransformationen bezeichnet werden. Dieses Kapitel beschäftigt sich mit unterschiedlichen Formen von Obfuscatingtransformationen und ihrer Funktionsweise. 2.3 Fazit Selbst mit einfachen, frei erhältlichen Programmen lassen sich beachtliche Ergebnisse bei der Deassemblierung bzw. Dekompilierung von .NET Assemblys erzielen. Dies gilt selbst dann, wenn man sich zuvor nicht oder nur sehr wenig mit den Methoden des Reverse Engineering beschäftigt hat, wie die obigen Beispiele verdeutlichen. Maßnahmen zum Schutz der eigenen Programme vor Spionage zu ergreifen, ist also keineswegs übertriebener Aufwand. Eine Transformation ist nur zulässig, wenn das Programm nach der Veränderung das selbe, beobachtbare Verhalten zeigt, wie zuvor. „Beobachtbares Verhalten“ bedeutet in diesem Zusammenhang „aus der Sicht des Benutzers äquivalent“. Das Programm darf also nach der 4/15 a = b.a(true); a.a(); a(a); Anwendung von Transformationen durchaus neue Seiteneffekte, wie zum Beispiel, das Erstellen von Dateien enthalten, die ursprünglich nicht vorhanden waren. Dies gilt allerdings nur unter der Bedingung, dass diese Effekte vor dem Benutzer verborgen werden. Weiterhin wird nicht verlangt, dass das Programm nach seiner Veränderung mit der gleichen Effizienz, wie zuvor, arbeitet. Tatsächlich wird es in vielen Fällen langsamer arbeiten oder mehr Speicher benötigen, da mit den meisten der nachfolgenden Transformationen eine Erhöhung der Rechenlast oder des Speicherbedarfs einhergeht. Man bezeichnet dies auch als die „Kosten“ einer Obfuscatingtransformation. } } Beispiel 3: Deassemblierter Code mit Obfuscation [3] 3.2 Kontrolltransformationen Unter die Kategorie der Kontrolltransformationen fallen Transformationen, die den Kontrollfluss eines Programms verschleiern. Man kann Kontrolltransformationen in drei Kategorien unterteilen: ● Aggregationstransformationen trennen Berechnungen, die logisch zusammen gehören, oder fügen Berechnungen zusammen, die keinen Bezug zueinander haben. ● Ordnungstransformationen ändern die Reihenfolge, in der Anweisungen ausgeführt werden und fügen, falls möglich, ein Element des Zufalls hinzu. ● Berechnungstransformationen fügen redundanten oder toten Code ein oder nehmen Änderungen an Algorithmen vor. Im Wesentlichen können die Transformationen in vier Hauptkategorien eingeteilt werden: ● Layouttransformationen ● Kontrolltransformationen ● Datentransformationen ● präventive Maßnahmen Eine weitere Unterteilung nach der Art der Obfuscation, die die jeweilige Transformation auf dem Zielprogramm ausführt, ist möglich. Bei allen Transformationen, die den Kontrollfluss verändern, muss mit einer Erhöhung der Laufzeit oder des Speicherbedarfs des Programms gerechnet werden. Oft ist es nötig, einen Kompromiss zwischen dem Grad der Effizienz eines Programms und dem Grad der Verschleierung des Codes einzugehen. 3.1 Layouttransformationen Als triviale Layouttransformation bezeichnet man das Entfernen von Formatierungen und Kommentaren aus dem Code. Diese Transformationen sind nicht sehr wirksam, verursachen allerdings auch keine Erhöhung der Laufzeit oder des Speicherbedarfs. Für .NET Sprachen sind triviale Transformationen unbedeutend, da der CILCode weder Formatierungen noch Kommentare enthält. Die Funktion vieler Transformationen beruht auf der Existenz opaker Variablen und Prädikate (boolesche Ausdrücke). Ein opakes Konstrukt zeichnet sich dadurch aus, dass es eine bestimmte Eigenschaft oder einen bestimmten Wert hat, der zum Zeitpunkt der Transformation feststeht, aber für einen Angreifer nicht leicht zu ermitteln ist. Ein einfaches Beispiel für ein solches Konstrukt ist 7y² - 1 == x². Dieser boolesche Ausdruck wird, unter der Bedingung, dass x und y Elemente der ganzen Zahlen sind, immer zu false evaluieren. Dies ist jedoch nicht ohne weiteres ersichtlich. Das Umbenennen von Bezeichnern ist eine weitere Layouttransformation. Im Gegensatz zu den trivialen Transformationen ist diese jedoch wesentlich effektiver. Die meisten Programmierer geben ihren Methoden aussagekräftige Namen, welche die von der Methode verrichtete Arbeit beschreiben. Diese Information hilft einem Angreifer enorm, da sie ihm einen Einblick in die Arbeitsweise des Programms verschafft. Viele Obfuscatoren versuchen zum Beispiel möglichst vielen Methoden den Namen „a“ zu geben. Dies verschlechtert die Lesbarkeit des Codes und provoziert zudem Missverständnisse und falsche Zuordnungen. Die Beispiele 2 und 3 zeigen die selbe Methode im ursprünglichen Quellcode bzw. mit umbenannten Bezeichnern. Natürlich kann die Eigenschaft dieses Beispiels durch eine statische Analyse relativ leicht ermittelt werden. Es existieren jedoch auch Wege, opake Konstrukte zu erzeugen, die um ein Vielfaches komplexer sind. Eine Möglichkeit besteht darin, eine komplexe Datenstruktur (einen Baum oder Graph) zu erzeugen und eine Reihe von Zeigern, die auf Elemente dieser Struktur verweisen, zu verwalten. Während des Programmablaufs werden, sowohl die Datenstruktur (hinzufügen neuer Elemente, aufspalten in Teilbäume,...), als auch die Zeiger möglichst willkürlich geändert. Die Änderungen müssen jedoch einigen wenigen Bedingungen genügen, wie etwa "Zeiger p und q dürfen nie auf das gleiche Element zeigen". Diese Bedingungen sind einfach zu programmieren, aber für den Angreifer schwer nachzuvollziehen. Mit Hilfe der Elemente der Datenstruktur und der Kenntnis der Bedingungen können, dann im Programm opake Konstrukte erstellt werden. Für dieses Verfahren bietet sich auch die Verwendung mehrerer Threads an, die die Datenstruktur gleichzeitig modifizieren und das System private void CalcPayroll(SpecialList employeeGroup) { while (employeeGroup.HasMore()) { employee = employeeGroup.GetNext(true); employee.updateSalary(); DitributeCheck(employee); } } Beispiel 2: Deassemblierter Code ohne Obfuscation [3] private void a(a b) { while (b.a()) { 5/15 public static int ggt(int a, int b) { int c = 0; while (b != 0) { c = a % b; a = b; b = c; } return a; } Beispiel 5: Methode größter gemeinsamer Teiler dadurch noch undurchsichtiger machen. 3.2.1 Aggregationstransformationen Ein gängiges Mittel um der Komplexität eines Programmierproblems Herr zu werden, ist das Einführen von Abstraktionen. Methoden stellen eine sehr wichtige Form der Abstraktion dar, sie bündeln zusammengehörigen Code und grenzen ihn gegen andere Programmteile ab. Aus diesem Grund ist es wichtig, Methodenaufrufe so weit wie möglich zu verschleiern. Die grundlegende Idee ist also Code, den der Programmierer in einer Methode zusammengefasst hat, aufzubrechen und über das gesamte Programm zu verteilen und gleichzeitig Codeteile, die keinen logischen Zusammenhang haben, in Methoden zusammenzufassen. Im Folgenden stellen wir einige Möglichkeiten vor, um Methoden und deren Aufrufe zu verschleiern. public static int calc(bool s, int a, int b) { int c = 0; int d = 0; if (s) { while (b != 0) { c = a % b; a = b; b = c; } return a; } else { c = calc(true, a, b); d = a * b / c; Methodentransformationen Inlining und Outlining sind Techniken, die eigentlich von Compilern zur Optimierung eingesetzt werden. Beim Inlining wird der Aufruf einer Methode einfach durch deren Rumpf ersetzt. Dies ist auch für den Zweck der Verschleierung äußerst hilfreich, da gleichzeitig die mit der Methode verbundene Abstraktion aus dem Code entfernt wird. Outlining bezeichnet das Umwandeln einer Folge von Anweisungen in eine Subroutine. Mit Hilfe von Outlinig ist es möglich gefälschte Abstraktionen zu erzeugen. return d; } } Beispiel 6: Verschatelte Methoden Durch das Erstellen mehrerer unterschiedlicher Versionen ein und der selben Methode können die Aufrufpunkte und die Signatur einer Methode verschleiert werden. Bei dieser als klonen bezeichneten Technik werden auf die Originalmethode unterschiedliche Sets von Obfuscatingtransformationen angewendet, um so verschiedene Klone zu erzeugen. Es wird der Anschein erweckt, dass unterschiedliche Methoden aufgerufen werden, obwohl dies tatsächlich nicht der Fall ist. Diese beiden Techniken lassen sich sehr gut in Kombination einsetzen. Dazu wird zuerst eine Folge von Methodenaufrufen mittels Inlining in eine Sequenz von Anweisungen verwandelt. Danach werden mittels Outlining aus zufällig gewählten Anweisungssequenzen neue Methoden erzeugt. Methoden, die in derselben Klasse deklariert sind, lassen sich mit relativ wenig Aufwand verschachteln. Dies geschieht, indem man die Rümpfe und Parameterlisten der Methoden zusammenführt und dann durch einen zusätzlichen Parameter oder eine globale Variable unterscheidet. Ideale Ergebnisse erzielt man mit diesem Verfahren, wenn die Methoden sich sehr ähnlich sind und z.B. dieselben Parameter nutzen. Das Entdecken von verschachteltem Code wird dann für einen Angreifer äußerst schwierig. Schleifentransformation Die meisten Schleifentransformationen wurden mit dem Ziel entworfen, die Performance eines Programms, vor allem bei numerischen Berechnungen, zu verbessern. Da einige dieser Transformationen jedoch auch die Komplexität des Codes erhöhen, lassen sie sich auch zum Zweck der Verschleierung einsetzen. Zur Demonstration verwenden wir die einfache Schleife in Beispiel 7, welche die Fibonacci Folge bis zum n-ten Element und den Wert von x² berechnet. Die Beispiele 4 und 5 zeigen die Methoden zur Berechnung des größten gemeinsamen Teilers und des kleinsten gemeinsamen Vielfachen. Beispiel 6 zeigt die verschachtelten Methoden. Die boolesche Variable s dient zur Unterscheidung des Methodenaufrufs. [0] = 0; a[1] = 1; b[0] = 0; b[1] = 1; public static int kgv(int a, int b) { int c = ggt(a, b); int d = a * b / c; public static void original(long[] a, long[] b, int n) { for (int i = 2; i < n; i++) { a[i] = a[i - 2] + a[i - 1]; b[i] = i * i; } } Beispiel 7: Ausgangsmethode zur Schleifentransformation return d; } Beispiel 4: Methode kleinstes gemeinsames Vielfaches 6/15 Loop blocking (Beispiel 8) wird benutzt, um den Iterationsraum einer Schleife so aufzuteilen, dass die Daten, die in der Schleife benutzt werden, solange im Cache bleiben, wie sie von der Schleife benötigt werden. tionen jedoch in Kombination eingesetzt, erhöht sich die von ihnen erzeugte Verwirrung drastisch. Die Anwendung aller drei Transformationen auf die Beispielmethode in Kombination zeigt Beispiel 11. public static void blocking(long[] a, long[] b, int n) { for (int I = 2; I < n; I += 10) for (int i = I; i < Math.Min(I + 10, n); i++) { a[i] = a[i - 2] + a[i - 1]; b[i] = i * i; } } Beispiel 8: Loop blocking public static void kombiniert(long[] a, long[] b, int n) { for (int I = 2; I < n; I += 10) { for (int i = I; i < Math.Min(I + 9, n - 1); i += 2) { a[i] = a[i - 2] + a[i - 1]; a[i + 1] = a[i - 1] + a[i]; } for (int i = I; i < Math.Min(I + 9, n - 1); i += 2) { b[i] = i * i; b[i + 1] = (i + 1) * (i + 1); } } if (n % 2 != 0) { a[n - 1] = a[n - 2] + a[n - 3]; b[n - 1] = (n - 1) * (n - 1); } } Beispiel 11: Kombinierte Transformationen Loop unrolling (Beispiel 9) erhöht die Cache Hit Rate und verringert die Anzahl von Sprüngen, indem der Schleifenkörper mehrfach dupliziert wird. Die Schleifenbedingung muss somit weniger oft ausgewertet werden. Das komplette Entrollen einer Schleife ist möglich, wenn zum Zeitpunkt des Kompilierens die Grenzen der Schleife feststehen. public static void unrolling(long[] a, long[] b, int n) { for (int i = 2; i < (n - 1); i += 2) { a[i] = a[i - 2] + a[i - 1]; b[i] = i * i; a[i+1] = a[i - 1] + a[i]; b[i+1] = (i+1) * (i+1); } if (n % 2 != 0) { a[n - 1] = a[n - 2] + a[n - 3]; b[n - 1] = (n - 1) * (n - 1); } } Beispiel 9: Loop unrolling 3.2.2 Ordnungstransformationen Programmierer organisieren Quellcode im Allgemeinen so, dass die Lokalität möglichst maximal ist. Das bedeutet, dass Codeteile, die einen starken logischen Bezug zueinander haben, sich auch im Code möglichst nahe beieinander befinden. Lokalität existiert auf jeder Ebene des Quellcodes, also zum Beispiel zwischen Anweisungen in Basisblöcken, Basisblöcken in Methoden, Methoden in Klassen, usw. Alle diese Arten der Lokalität geben einem Angreifer nützliche Hinweise. Deshalb sollte die Platzierung aller Elemente in der jeweiligen Ebene möglichst zufällig erfolgen. Für manche Arten ist dies relativ einfach, wie z.B. Methoden innerhalb von Klassen. In anderen Fällen, wie etwa Anweisungen innerhalb von Basisblöcken, ist dies keineswegs trivial und ohne vorherige Abhängigkeitsanalyse nicht möglich. Loop fission (Beispiel 10). Ziel dieser Transformation ist es eine Schleife in mehrere neue Schleifen aufzuteilen, die jeweils über die gleiche Indexlänge laufen, jedoch nur einen Arbeitsschritt der ursprünglichen Schleife übernehmen. Dadurch wird eine Erhöhung der Datenlokalität erreicht. Diese Arten der Transformation machen den Code an sich zwar für Menschen nicht wesentlich schwerer zu verstehen, erhöhen jedoch die Widerstandsfähigkeit gegen automatische Angriffe beträchtlich. In vielen Fällen sind diese Transformationen nicht umkehrbar. Ist die Ordnung erst einmal zerstört, kann sie nicht wieder hergestellt werden. public static void fission(long[] a, long[] b, int n) { for (int i = 2; i < n; i++) { a[i] = a[i - 2] + a[i - 1]; } for (int i = 2; i < n; i++) { b[i] = i * i; } } Beispiel 10: Loop fission 3.2.3 Berechnungstransformationen Dieses Papier beschreibt drei unterschiedliche Formen von Berechnungstransformationen. Alle drei Transformationen erhöhen die Anzahl der Codezeilen und die Anzahl der auszuwertenden Bedingungen eines Programms und damit auch dessen Komplexität. Alleine eingesetzt, bietet jede dieser Transformationen nur einen geringen Schutz. Es ist z.B. nicht weiter schwierig, eine entrollte Schleife zu erkennen und die Umwandlung um zukehren. Werden die Transforma- 7/15 ● Transformationen, die den tatsächlichen Kontrollfluss hinter irrelevanten, nicht zu den aktuellen Berechnungen beitragenden, Anweisungen verstecken. ● Transformationen, die Kontrollflussabstraktionen entfernen und gefälschte Abstraktionen einfügen und ● Entfernen von Bibliotheksaufrufen Transformationen, die Codesequenzen im Objektlevel-Code einfügen, für die keine entsprechenden Konstrukte in der Hochsprache existieren. Die meisten Programme enthalten viele Aufrufe von Methoden aus Standardbibliotheken. Die Semantik dieser Methoden ist bekannt und die Methodennamen können nicht verschleiert werden, da sie für den Aufruf der Methode benötigt werden. Daher bietet sich die Analyse der verwendeten Standardfunktionen als Einstiegspunkt für einen Angriff an. Die letzte Form ist nur in Sprachen anwendbar, bei denen der Objektcode mächtiger ist, als die Hochsprache. Dies ist in Java der Fall, nicht jedoch im .NET Framework. Diese Art der Transformation ist hier nur der Vollständigkeit halber erwähnt. Durch das Bereitstellen eigener Versionen der Standardbibliotheken ist der Obfuscator in der Lage, sowohl die aussagekräftigen Namen, als auch den Code der Standardfunktionen zu verschleiern. Diese Transformation erhöht vor allem den Speicherbedarf einer Applikation, da die neu erzeugten Bibliotheken zusammen mit dem Programm bereitgestellt werden müssen. Die Laufzeit wird dagegen kaum erhöht. Man kann sich leicht unterschiedliche Einsatzbereiche für jede Form der Berechnungstransformation vorstellen. Im Folgenden werden dazu mehrere Umsetzungen präsentiert. Verwendung opaker Konstrukte Die Komplexität eines Programmteils steigt mit der Anzahl der in ihm enthaltenen Verzweigungen und Bedingungen. Mit Hilfe opaker Konstrukte ist es relativ einfach Transformationen zu entwerfen, die diese Eigenschaft ausnutzen. Tabelleninterpretation Hierbei handelt es sich um eine der effektivsten und gleichzeitig auch am schwierigsten umzusetzenden Transformationen. Bei der Tabelleninterpretation werden Teile des Codes in Code einer anderen virtuellen Maschine übersetzt. Der Applikation muss dann ein Interpreter hinzugefügt werden, der die Ausführung des Codes zur Laufzeit übernimmt. Eine Applikation kann auch mehrere unterschiedliche Interpreter enthalten, die jeweils für einen Teil des Codes verantwortlich sind. Diese Transformation sollte nur für kleine Codeabschnitte verwendet werden, die ein hohes Maß an Schutz benötigen. Es ist mit einer signifikanten Erhöhung der Laufzeit und des Speicherbedarfs eines Programms zu rechnen. So kann man zum Beispiel innerhalb eines Basisblocks ein opakes Prädikat einfügen, welches den Block teilt und immer zu true evaluiert. Der restliche Code des ursprünglichen Blocks befindet sich danach auf dem true-Pfad. Das Prädikat wird als irrelevanter Code bezeichnet, da es den Ablauf des Programms nie ändert. Der im false-Pfad enthaltene Code wird nur mit der Absicht erzeugt, den Angreifer in die Irre zu führen. Es kann sich dabei durchaus um Code handeln, der bei einem Test Fehler verursacht, da er ja im Anwendungsfall nie zur Ausführung kommt. Man bezeichnet dies auch als toten Code. Um die Abbruchbedingung einer Schleife zu verschleiern, wird die Schleifenbedingung mit einem Prädikat so ergänzt, dass sich die Anzahl der Schleifendurchläufe nicht ändert. Das Beispiel 12 zeigt eine while Schleife, deren Abbruchbedingung um den booleschen Ausdruck (x³-x) % 3 == 0 erweitert wurde. Da dieses Prädikat immer zu true evaluiert, ändert sich der Ablauf der Schleife nicht. Code parallelisieren int x = 0, counter = 0; Zur Verschleierung des Kontrollflusses existieren zwei Varianten. Die erste Möglichkeit besteht darin einen oder mehrere Attrappen-Threads zu erzeugen, die Code enthalten, der allein zur Täuschung dient. Bei der zweiten Variante wird ein ursprünglich sequentieller Bereich des Programms in mehrere Abschnitte aufgeteilt, die parallel jeweils in einem eigenen Thread ausgeführt werden. Ein Bereich ohne Datenabhängigkeiten ist sehr leicht zu parallelisieren, sind jedoch Abhängigkeiten vorhanden, muss beim Aufspalten des Bereichs auf die Synchronisation der Threads geachtet werden. Der Code wird dann auch nach der Transformation sequentiell ausgeführt, aber der Kontrollfluss wechselt von einem Thread zum nächsten. Parallelisieren von Code eignet sich hervorragend zum Verschleiern von Programmabläufen. Die Transformation bietet einen guten Schutz vor statischen Analysen, denn die Anzahl der möglichen Ausführungspfade steigt exponentiell mit der Anzahl der auszuführenden Prozesse. Zudem ist das Verstehen von parallelem Code wesentlich schwieriger als das von sequentiellem Code. while (x < 50 && (x * x * x - x) % 3 == 0) { counter += x; x++; } Beispiel 12: Erweiterte Schleifenbedingung Arithmetische Ausdrücke lassen sich durch Hinzufügen überflüssiger Operanden verschleiern. Am Besten funktioniert dieser Vorgang mit Integer-Berechnungen, da hier nicht auf numerische Genauigkeit geachtet werden muss. Ziel dieser Transformation ist es, konstante Werte durch Berechnungen darzustellen oder zusätzliche Berechnungsschritte in arithmetische Ausdrücke einzufügen, die das Ergebnis nicht verändern. 8/15 3.3 Datentransformationen Statische in prozedurale Daten konvertieren In diesem Abschnitt beschreiben wir Transformationen, die entworfen wurden, um im Quellprogramm enthaltene Datenstrukturen zu verschleiern. Unterschieden wird hier zwischen Transformationen, welche die Speicherung, Codierung, Aggregation oder Ordnung der Daten beeinflussen. Statische Daten, wie zum Beispiel Strings, liefern dem Angreifer nützliche Hinweise über die Funktion einer Methode. Eine einfache Möglichkeit, um einen String zu verbergen, besteht darin, eine Methode zu erstellen, die den String produziert. Damit steht der Inhalt des Strings erst zur Laufzeit zur Verfügung und ist nicht direkt im Code einzusehen. Es ist allerdings nicht sinnvoll, die Erzeugung des Strings in einer Methode zu kapseln. Stattdessen kann der Code aber in kleine Komponenten aufgeteilt und dann im Kontrollfluss des Programms verteilt werden. Damit wird er wesentlich schwieriger zu durchschauen. 3.3.1 Speicherungs- und Codierungstransformationen Für die meisten Daten existieren bereits (Grund-) Typen, um die entsprechenden Werte aufzunehmen. Beispielsweise werden die meisten Programmierer für die Zählvariable einer Schleife einen Integertyp verwenden. Zwar können auch andere Typen verwendet werden, aber diese Möglichkeit wird kaum genutzt, da ein Integertyp für diese Aufgabe die logische Wahl ist. Ebenso ist mit einem Variablentyp auch immer eine bestimmte Interpretation des Bitmusters verbunden. Ein 8 Bit Integer mit dem Bitmuster 00001001 wird für gewöhnlich die Zahl 9 darstellen. Stringverschlüsselung ist eine weitere Variante, um den Inhalt von Zeichenketten zu verbergen. Grundsätzlich bietet das Verschlüsseln von Strings jedoch weniger Schutz, als die oben vorgestellte Konvertierung, denn um die Strings zur Laufzeit zu entschlüsseln, muss eine entsprechende Methode im Code vorhanden sein, die immer dann verwendet wird, wenn ein String benötigt wird. Die Entschlüsselungmethode ist dadurch für einen Angreifer sehr leicht zu identifizieren und kann sofort benutzt werden, um die im Code enthaltenen Strings zu enschlüsseln. Um Daten zu verschleiern, wählen Speicherungstransformationen unübliche Typen, um dynamische oder statische Daten aufzunehmen und Codierungstransformationen wählen unübliche Codierungen für bestehende Datentypen. 3.3.2 Aggregationstransformationen In objektorientierten Programmen werden Kontrollfunktionen um Datenstrukturen organisiert. Eine wichtige Aufgabe des Reverse Engineering besteht daher darin, die ursprüngliche Datenstruktur eines Programms zu rekonstruieren. Es ist also notwendig, auch Datenstrukturen zu verschleiern. Codierung ändern Die Codierung einer Variable kann durch einen arithmetischen Ausdruck transformiert werden. Eine Variable a wird beispielsweise unter Verwendung der Gleichung a' = a * b + c in ihre neue Form a' transformiert, wobei b und c konstant sind. Bei dieser Art der Transformation ist eine vorherige Analyse notwendig, um Überläufe oder Rundungsfehler auszuschließen. Die Erhöhung der Laufzeit und Sicherheit gehen bei dieser Transformation Hand in Hand. Die oben genannte Beispieltransformation wird die Laufzeit nur unwesentlich beeinflussen, die Codierung der Variable jedoch auch nicht sehr stark verschleiern. Skalare Variablen verschmelzen Zwei oder mehr skalare Variablen können in einer Variable mit größerer Reichweite gespeichert werden. Zum Beispiel können zwei 32 Bit Integer Werte in einer 64 Bit Integer Variablen gespeichert werden. Eine Variante dieser Transformation besteht darin, mehrere skalare Variablen in einem Array zu speichern und so den Eindruck einer Datenstruktur zu erwecken, die eigentlich nicht vorhanden ist. Variablen spalten Durch Aufspalten kann man eine Variable durch eine Kombination von zwei oder mehr Variablen darstellen. Um eine Variable V in k Variablen p1,...,pk aufzuspalten, werden drei Informationen benötigt. Eine Funktion f(p1,..,pk), welche die Werte der k Variablen auf den entsprechenden Wert von V abbildet, eine Funktion g(V), welche den Wert von V auf die Variablen p1,..,pk abbildet und neue Operationen auf p1,..,pk, welche den auf V möglichen Operationen entsprechen. Je größer die Anzahl der Variablen ist, auf die V abgebildet wird, um so effektiver verschleiert diese Transformation die ursprüngliche Variable. Gleichzeitig steigen mit der Erhöhung der Anzahl, jedoch auch die von dieser Transformation verursachten Kosten bezüglich Laufzeit und Speicherbedarf. Arrays neu strukturieren Arrays werden häufig eingesetzt um Daten zu aggregieren. Es gibt einige sehr einfache Transformationen, um ihre Struktur zu verändern. Arrays können aufgespalten, zusammengefügt, gefaltet (Erhöhung der Dimension) oder abgeflacht (Verringerung der Dimension) werden. Beispielsweise kann ein Array A in zwei Arrays A1 und A2 aufgespalten werden, wobei A1 alle Elemente von A mit geradem Index enthält und A2 alle Elemente von A mit ungeradem Index. Auf den ersten Blick scheinen diese Transformationen wenig effektiv zu sein, weil die Elemente nur umsortiert werden. Der eigentliche Nutzen dieser Transformationen liegt jedoch im Verbergen der Struktur und nicht ihrer Elemente. 9/15 Um beispielsweise eine Matrix zu speichern, ist ein zweidimensionales Array die logische Wahl. Wird dieses Array nun abgeflacht und damit in ein eindimensionales Array verwandelt, ist es bereits wesentlich schwieriger, die Matrix auch als solche zu erkennen. Die Anwendung weiterer Arraytransformationen kann diesen Effekt noch verstärken. Ebenso ist es natürlich möglich, durch Umstrukturieren eines Arrays den Eindruck einer Struktur zu erwecken, die nie im ursprünglichen Code vorhanden war. Beide Methoden eignen sich hervorragend, um Datenstrukturen zu verbergen. Das in Beispiel 13 gezeigte zweidimensionale Array wird in Beispiel 14 abgeflacht, indem alle Elemente spaltenweise in das neue eindimensionale Array eingetragen werden. Unter den Abbildungen finden Sie jeweils den Code für eine einfache Matrix-Vektor Multiplikation. Die Vektoren b und c sind jeweils eindimensionale Arrays und werden nicht transformiert: a1,0 a1,1 a1,2 a2,0 a2,1 a2,2 for (int i = 0; i < 3; i++) for (int j = 0; j < 3; j++) { c[i] += a[i, j] * b[j]; } Beispiel 13: ursprüngliches Array a0,0 a1,0 a2,0 a0,1 a1,1 a2,1 a0,2 a1,2 a2,2 1 2 3 4 5 6 7 Wie bereits früher erwähnt, ist es sinnvoll, die Berechnungsreihenfolge so weit wie möglich zufällig anzuordnen. Dies gilt auch für die Anordnung von Methoden und Instanzvariablen in Klassen, sowie von Parametern in Methoden. In vielen Fällen ist es auch möglich, die Elemente innerhalb eines Arrays neu zu ordnen. 3.4 Präventive Maßnahmen Im Gegensatz zu Kontroll- und Datentransformationen liegt die Hauptaufgabe von präventiven Maßnahmen nicht darin, das Programm in eine für Menschen möglichst schwer verständliche Form zu überführen. Vielmehr liegt ihr Zweck darin, bereits bekannte automatische Deobfuscatortechniken zu erschweren oder Fehler in bekannten Deobfuscatoren oder Decompilern auszunutzen. Inhärente präventive Maßnahmen werden vor allem eingesetzt, um die Widerstandsfähigkeit bereits angewendeter Transformationen gegen automatische Angriffe zu erhöhen. Nehmen wir beispielsweise an, eine Schleife wurde nach vorheriger Datenanalyse so umgeordnet, dass sie nun rückwärts läuft. Ein automatischer Deobfuscator kann nun ebenfalls eine Datenanalyse ausführen, feststellen, dass keine Datenabhängigkeiten vorhanden sind und die Transformation rückgängig machen. Um das zu verhindern, werden nach der Schleifentransformation gefälschte Datenabhängigkeiten in die Schleife eingebettet. Der Deobfuscator ist nun nicht mehr ohne weiteres in der Lage, die Transformation zu erkennen und umzukehren. Der Schutz, den inhärente präventive Maßnahmen bereitstellen, addiert sich also immer zu einer bereits bestehenden Transformation. a0,0 a0,1 a0,2 Neuer Index: 0 3.3.3 Ordungstransformationen 8 for (int i = 0; i < 9; i++) { c[i%3] += a[i] * b[i/3]; } Beispiel 14: Arraytransformation Vererbungsbeziehungen modifizieren Das Klassenkonzept in objektorientierten Programmiersprachen ist das wichtigste Mittel zur Modularisierung und Abstraktion. Zwei Klassen A und B können auf zwei unterschiedliche Arten miteinander in Verbindung stehen. Entweder A aggregiert B, enthält also eine Instanzvariable vom Typ B, oder A erbt von B, A erweitert also die Funktionalität von B. Die Komplexität einer Klasse wächst mit ihrer Tiefe, der Entfernung zur Wurzel der Vererbungshierarchie, und der Anzahl ihrer direkten Nachkommen. Es gibt im Wesentlichen zwei Wege um die Komplexität einer Klasse zu erhöhen, das Aufspalten von Klassen und das Einfügen von neuen gefälschten Klassen. Im Normalfall werden diese beiden Techniken kombiniert eingesetzt, denn es ist zum Beispiel nicht schwierig, aufgespaltene Klassen wieder zusammenzufügen. Gerichtete präventive Maßnahmen zielen darauf ab, Schwächen in bekannten Dekompilern und Deobfuscatoren auszunutzen und so das Ausspionieren von Assemblies zu verhindern. Ein Beispiel dafür ist das Einfügen einer Methode mit einem etwa 75 Kilobyte großen Bezeichner, die bei ILDASM zu einem Absturz führt [12]. Methodennamen dieser Länge sind im Quellcode nicht erlaubt, weshalb die Methode erst nach dem Kompilieren in die Assembly eingefügt werden kann. Die Methode hat keine Funktion und kann im normalen Programmablauf auch nicht aufgerufen werden. Wird die Assembly jedoch mittels Reflection von ILDASM untersucht, verursacht der Name der Methode einen Pufferüberlauf und dies führt zum Abbruch des Deassemblierers. 4 Bewertung In diesem Abschnitt stellen wir zunächst einige formale Kriterien vor, die von Collberg, Thomborson und Low in [2] entworfen wurden und anhand derer die in Abschnitt 3 erläuterten Transformationen bewertet werden können. Tabelle 1 bietet eine Übersicht der einzelnen Transformationen und ihrer Bewertungen nach den einzelnen Kriterien. Abschließend möchten wir noch auf einige allgemeine Punkte hinweisen, die Sie bei der Anschaffung eines Obfuscators in Betracht ziehen sollten. 10/15 4.1 Kriterien zur Bewertung von Transformationen Die Gesamtqualität einer Transformation ergibt sich dann aus der Kombination dieser Werte. Das Prädikat "einweg" nimmt eine gesonderte Stellung ein. Transformationen, die als "einweg" gekennzeichnet sind, können nicht rückgängig gemacht werden. Der Grund hierfür liegt im Allgemeinen in dem Entfernen von Informationen aus dem Programm, die zum Zeitpunkt der Erstellung des Programms sehr hilfreich waren, aber für die korrekte Ausführung nicht benötigt werden. Die in der Tabelle aufgeführten Transformationen besitzen schwache (+) oder starke (+ + ) Widerstandsfähigkeit oder sie sind als einweg (+ + +) klassifiziert. Transformationen mit trivialer oder vollständiger Widerstandsfähigkeit sind in diesem Papier nicht enthalten. Wirkung Kosten Die Wirkung einer Transformation ist ein Maß dafür, wie schwer es für einen menschlichen Angreifer ist den Code zu verstehen. Dieses Kriterium lässt sich natürlich nicht mathematisch präzise definieren, da die Wirkung auch immer von den Fähigkeiten der Person abhängt, die den Angriff ausführt. Allerdings hat die Komplexität von Code einen großen Einfluss darauf, wie leicht ein Programm verstanden werden kann. Die Kosten einer Transformation setzen sich aus dem erhöhten Speicherbedarf und der Erhöhung der Laufzeit zusammen, welche die jeweilige Transformation durch ihre Anwendung verursacht. Einige triviale Transformationen sind kostenfrei, die meisten Transformationen sind jedoch mit einer variablen Menge an Kosten verbunden. Zu beachten ist auch, dass Kosten kontextsensitiv sind. Eine Transformation, die auf Code innerhalb einer Schleife angewendet wird, hat natürlich höhere Kosten als dieselbe Transformation, angewendet auf Code, der nur einmal ausgeführt wird. Verschleiernde Transformationen können nach vier Kriterien bewertet werden: ● Wirkung ● Widerstandsfähigkeit ● Kosten ● Heimlichkeit Um die Wirkung zu messen, können Metriken aus der Softwaretechnik hinzugezogen werden, die entwickelt wurden um die Komplexität von Code zu bestimmen. Diese Metriken basieren auf statischen Analysen von Quellcode und benennen Eigenschaften des Codes, die einen Einfluss auf dessen Komplexität haben (Anzahl der Anweisungen und Prädikate, Verschachtelungstiefe von if-Anweisungen, Tiefe und Breite des Vererbungsbaums, ...). Die Wirkung einer Transformation ergibt sich aus der Erhöhung einer oder mehrerer dieser Eigenschaften. In der Tabelle unterscheiden wir nach niedriger (+), mittlerer (+ +) und hoher (+ + +) Wirkung einer Transformation. Widerstandsfähigkeit Auf den ersten Blick scheint es relativ einfach zu sein, die Wirkung einer Transformation zu erhöhen. Beispielsweise könnten immer zusätzliche if-Anweisungen mit konstanten Bedingungen eingefügt werden. Eine solche Maßnahme ist allerdings von einem automatischen Deobfuscator leicht rückgängig zu machen. Die Widerstandsfähigkeit setzt sich aus Programmieraufwand und Deobfuscatoraufwand zusammen und misst, wie viel Widerstand eine Transformation einem automatischen Deobfuscator liefern kann. Der Programmieraufwand bezeichnet dabei den Aufwand, der benötigt wird, um einen automatischen Deobfuscator zu konstruieren, der in der Lage ist, die Wirkung einer Transformation zu verringern. Der Deobfuscatoraufwand bezeichnet die Zeit, die von diesem automatischen Deobfuscator aufgewendet werden muss, um die Wirkung einer Transformation zu verringern. Die Einteilung der Kosten erfolgt nach vier Stufen. Der Aufwand bezeichnet hier jeweils den Mehraufwand an Ressourcen (Rechenzeit, Speicherbedarf), der durch die Anwendung der Transformation an der entsprechenden Stelle im Programm entsteht. Kostenfreie Transformationen verursachen keinen oder nur konstanten O(1) Aufwand, günstige Transformationen verursachen eine lineare O(n) Erhöhung des Aufwands und teure Transformationen führen zu einer polynomiellen Erhöhung des Aufwands. Aufwendige Transformationen erzeugen eine exponentielle Erhöhung des Aufwands. Für die Darstellung der Bewertung wird in der Tabelle ein Minuszeichen verwendet, da Kosten ein Negativkriterium darstellen. Für die vorgestellten Transformationen reichen die Kosten von frei (-) über günstig (- -) bis teuer ( - - -). Heimlichkeit Transformationen, die eine hohe Widerstandsfähigkeit besitzen, werden zwar von automatischen Deobfuscatoren nicht leicht erkannt, dies muss jedoch nicht für einen menschlichen Angreifer gelten. Wenn eine Transformation Code einführt, der sich sehr von dem umgebenden Code unterscheidet, so ist dies für einen Reverse Engineer leicht zu erkennen. Heimlichkeit ist ein Maß dafür, wie gut sich der verschleierte Code mit dem ursprünglichen Code mischt bzw. ihn ersetzt. Natürlich ist dieses Kriterium hochgradig kontextsensitiv. Es lässt sich demzufolge nur im tatsächlichen Anwendungsfall einer Transformation feststellen, ob der erzeugte Code heimlich ist oder nicht. Die Bewertung der Widerstandsfähigkeit erfolgt in den Stufen trivial, schwach, stark, vollständig und einweg. 11/15 Obfuscation Ziel Kriterien Transformation Wirkung Widerstand sfähigkeit Kosten ++ +++ - Layout Bezeichner umbenennen Kontrollfluss Inlining 1 ++ +++ - Outlining 1 ++ ++ - Methoden verschachteln 1 Methoden klonen 1 Schleifentransformationen 1 + + -/-- Ordnungstransformationen 2 + +++ --- Einfügen von totem oder irrelevantem Code 3 Erweitern von Schleifenbedingungen 3 Überflüssige Operanden 3 Tabelleninterpretation 3 +++ ++ --- Entfernen von Bibliotheksaufrufen 3 ++ ++ variabel Code parallelisieren 3 +++ ++ --- Codierung ändern 4 Hängt von der Komplexität der Codierungsfunktion ab. Variablen spalten 4 Hängt von der Anzahl der Variablen ab, in welche die ursprüngliche Variable aufgespalten wird statische in prozedurale Daten Konvertieren 4 Hängt von der Komplexität der generierten Funktion ab. skalare Variablen verschmelzen 1 + + - Arrays neu strukturieren 1 variabel + -/-- Vererbungsbeziehungen modifizieren 1 ++ + - Ordnungstransformationen 2 + +/+++ - Daten Hängt von der Qualität der verwendeten opaken Konstrukte ab Hängt von der Qualität der verwendeten opaken Konstrukte ab und von der Schachtelungstiefe, in der die Transformation eingefügt wurde. Tabelle 1:Obfusatingtransformationen und Bewertungen 12/15 In Tabelle 1 sind alle in diesem Papier präsentierten Transformationen zusammen mit ihrer jeweiligen Bewertung nach den Kriterien Wirkung, Widerstandsfähigkeit und Kosten aufgeführt. Die Transformationen sind nach ihren Zielbereichen Layout, Kontrollfluss und Daten gruppiert. Die Zahl hinter der jeweiligen Transformation gibt an, welcher Unterkategorie sie angehört. Aggregationstransformationen sind mit einer 1 gekennzeichnet, Ordnungstransformationen mit einer 2, Berechnungstransformationen mit einer 3 und Speicherungs- und Codierungstransformationen mit einer 4. 4.2 Allgemeine Kriterien Nachfolgend finden Sie einige allgemeine Kriterien, die beim Kauf eines Obfuscators in Betracht gezogen werden können. Auch wenn diese Punkte prinzipiell auf jede Art von Software zutreffen, heißt dies keineswegs, dass sie bei der Anschaffung eines Obfuscators vernachlässigbar sind. Die beste Möglichkeit, um herauszufinden, ob ein Produkt für die eigenen Zwecke geeignet ist, besteht darin es selbst zu testen. Gerade bei Obfuscatoren ist dies besonders wichtig, denn kein Hersteller wird detailliert erklären, welche Transformationen in seinem Produkt nach welchen Regeln angewendet werden (dies wäre in Hinblick auf die Aufgabe des Obfuscators auch nicht sehr sinnvoll). Sie sollten darauf achten, ob für ein gegebenes Programm Testversionen zur Verfügung stehen und welchen Einschränkungen diese Software unterliegt. Wie lange kann getestet werden? Welche Funktionen sind in der Testversion enthalten und welche nicht? Ist mit den gegebenen Einschränkungen überhaupt ein aussagekräftiger Test möglich? Eine gute Dokumentation ist nicht immer selbstverständlich oder überhaupt verfügbar. Sie sollten sich informieren, ob und in welcher Form (Handbuch, pdf, Visual Studio Hilfe) das Produkt dokumentiert ist. Im optimalen Fall können Sie die Dokumentation zumindest teilweise schon vor dem Kauf des Produktes einsehen. Auch die Verfügbarkeit von Supportleistungen ist ein nicht zu unterschätzendes Kriterium. Üblicherweise wird Support in Form von E-Mail, per Telefon, oder über ein Online Forum angeboten. Wichtig ist hier auch, ob der Support bereits für Testversionen zur Verfügung steht und ob oder in welcher Form Support in einer Lizenz enthalten ist. Sind Supportleistungen mit zusätzlichen Kosten verbunden oder zeitlich nur begrenzt nutzbar? Existieren unterschiedliche Abstufungen, je nach erworbener Lizenz? Ist der Obfuscator aus einem Freeware- oder Open Source Projekt hervorgegangen, lohnt sich die Frage, ob das Projekt noch aktiv weiterentwickelt wird. Sind Dokumentation und Support überhaupt verfügbar? Ist es eventuell lohnenswert den Obfuscator selbst zu erweitern (sofern erlaubt)? Bei kommerzieller Software besteht die Möglichkeit, sich genauer über den Hersteller zu informieren. Hat sich die Firma auf den Bereich Softwaresicherheit spezialisiert und wie lange ist sie schon in diesem Bereich tätig? Sind Lizenzen für Ihren Bedarf verfügbar? Gibt es ande- re Produkte des Herstellers, die für Sie von Interesse sein könnten, oder ist für den Betrieb des Obfuscators ein anderes Produkt des Herstellers nötig? 5 Marktübersicht Im folgenden Abschnitt finden Sie eine Zusammenstellung momentan erhältlicher Obfuscatoren. Dies stellt jedoch keine komplette Auflistung aller verfügbaren Programme dar. Die Programme wurden im Rahmen dieses Papiers nicht getestet und die Beschreibungen sind auch in keiner Weise wertend. Wenn Sie sich näher für eines der genannten Produkte interessieren, sollten Sie daher keinesfalls auf eigene Tests verzichten. Aspose.Obfuscator [13] Version: 1.6 Lizenz: Freeware Aspose.Obfuscator unterstützt die Sprachen C#, Visual Basic.NET und JScript.NET. Das Programm benennt private und internal deklarierte Mitglieder mittels eines alphanumerischen Schemas um. Falls möglich werden Methoden überladen. Das Umbenennen von als public gekennzeichneten Mitgliedern ist nicht möglich, jedoch können Mitglieder auf Wunsch von der Umbenennung ausgeschlossen werden. Der Obfuscator ist Freeware und wird von Aspose nicht mehr weiterentwickelt. Support und Dokumentation sind über das Forum von Aspose verfügbar. Demeanor for .NET [14] Version: 4.0 Lizenz: pro CPU; Enterprise Edition, $799 Demeanor operiert direkt auf .NET Assemblys ohne Umwege über Round Tripping Verfahren zu verwenden. Zu den Features zählen das Verschleiern von Namen, Verschlüsseln von Strings und Umordnen von Assemblies. Auch das Verschleiern von verwendeten Ressourcen (sofern sie managed Code verwenden) und generische Methoden und Typen werden unterstützt. Demeanor kann über die Kommandozeile, per Visual Studio Add-in oder über die standalone GUI gesteuert werden. Über Eigenschaften lässt sich das Verhalten des Obfuscators für jede Assembly separat einstellen. So können beispielsweise automatisch auch als public gekennzeichnete Mitglieder umbenannt werden, wenn es sich bei der zu erstellenden Assembly um eine .exe Datei handelt. Die an Assemblys vorgenommen Änderungen können in einer XML-Datei protokolliert werden, dies ermöglicht das inkrementelle Verscheiern von Assemblys. Für Testzwecke kann eine auf zwei Wochen beschränkte Testversion des Obfuscators bezogen werden. Support wird per E-Mail und Telefon angeboten. 13/15 Dotfuscator [15] Version: 3.0 Lizenz: Community Edition, - ; Professional Edition, $1890 (1 Benutzer); Network Edition, $4950 (1 - 5 Benutzer) Die Community Edition des Dotfuscators ist bereits in Visual Studio enthalten, bietet jedoch im Vergleich zur Professional Edition nur einen sehr begrenzten Funktionsumfang. Eine detaillierte Übersicht über die Unterschiede zwischen den einzelnen Editionen ist auf der Homepage von preemtive verfügbar. Zu den Features zählen das Umbenennen von Mitgliedern, Kontrollflussverschleierung, Stringverschlüsselung und das Überladen von Methoden. Zusätzlich können Applikationen, die aus mehreren Assemblys bestehen, zu einer einzigen Assembly verbunden werden. Die Auswahl der Mitglieder, die verschleiert werden sollen, erfolgt über Regeln und Attribute, das .NET Framework 2.0 wird vollständig unterstützt. Für inkrementelle Verschleierungen und verbesserte Fehleranalyse werden XML-Dateien generiert. Dotfuscator ist über eine standalone GUI, Visual Studio, die Kommandozeile oder einen MS Build Task ausführbar. Registrierte Benutzer erhalten für ein Jahr Zugang zu gesonderten Supportseiten. Erweiterte Supportverträge werden zum Verkauf angeboten. Salamander .NET Obfuscator [16] die Konsole. Die Integration in Visual Studio ist mittels einer optionalen Erweiterung möglich. Der Obfuscator benennt Mitglieder wahlweise nach einem eigenen Algorithmus um oder nach einer durch den Benutzer bereitgestellten Tabelle. Die Umbenennungen können auch über das User Interface oder Attribute im Code kontrolliert werden. IL-Obfuscator 2.0 [18] Version: 2.0 Beta 7 Lizenz: $79, DotNet-Lab Der IL-Obfuscator ist Bestandteil des Lesser-Software DotNet-Lab, das unter anderem auch einen ReflectionBrowser und Decompiler für C# und VB.NET enthält. Der Obfuscator arbeitet direkt auf Assemblys und unterstützt das Umbenennen von Mitgliedern, Kontrollflussverschleierung und Stringverschlüsselung. Bei Programmen, die aus mehreren Assemblys bestehen, wird auch das Umbenennen von internal Mitgliedern unterstützt. Auch ein Schutz gegen das Decompilieren mit ILDASM ist enthalten. Die Auswahl der zu verschleiernden Mitglieder erfolgt über die GUI. DotNet-Lab wird zum Download auf der Lesser-Software Homepage angeboten und ist ohne Registrierung nur mit eingeschränkter Funktionalität nutzbar. 6 Fazit Version: 2.0 Lizenz: $799 (1-5 Benutzer), $1399 (6-10 Benutzer) Der Obfuscator wird entweder über eine standalone GUI oder die Kommandozeile bedient. Im GUI Modus ist zusätzlich ein Explorer für .NET Applikationen enthalten, der zusätzlich auch als Decompiler verwendet werden kann. Das Programm arbeitet direkt auf bereits kompilierten Assemblys und kann auch Applikationen verschleiern, die für das Compact Framework erstellt wurden. Die Einstellungen für die jeweilige Applikation werden in XML Dateien gespeichert, die Konfiguration durch Attribute ist jedoch ebenfalls möglich. Satellitenund Ressourcenassemblys werden automatisch verschleiert. Das Handbuch behandelt auch Fälle, bei denen Probleme auftreten können, wie beispielsweise Reflection oder Serialisation und zeigt anhand von Beispielen, wie Probleme in diesen Bereichen umgangen werden können. Eine Testversion mit vollem Funktionsumfang ist verfügbar. Eine Lizenz enthält ein Jahr kostenlosen Online Support und Upgrades. Spices.Obfuscator [17] Version: 5.1.04 Lizenz: $692.95 (1 Benutzer); Team Pack $2192.95 (4 5 Computer) Auch der Spices Obfuscator arbeitet direkt mit Assemblys und unterstützt die Frameworks 1.0 – 2.0, das Compact Framework und managed C++ Code. Gesteuert wird der Obfuscator entweder über eine graphische Shell oder In diesem Übersichtspapier wird gezeigt, dass Programme in .NET sehr leicht mit Methoden des Reverse Engineering angegriffen werden können und Schutzmaßnahmen nötig sind. Dieser Schutz kann durch Obfuscatoren und die von ihnen verwendeten Transformationen bereit gestellt werden. Es werden die unterschiedlichen Kategorien von Obfuscatingtransformationen erläutert und es stellt mehrere Transformationen aus den jeweiligen Kategorien vor. Mit den Kriterien wird ein Bewertungssystem eingeführt, das es ermöglicht, die Transformationen hinsichtlich ihrer Effektivität und Kosten zu bemessen, und es wird deutlich gemacht, dass der Schutz eines Programms mittels Obfuscation immer einen Kompromiss zwischen dem Grad des erreichten Schutzes und dem Mehraufwand für die Programmausführung bedeutet. Abschließend bleibt zu erwähnen, dass Obfuscation, ebenso wie Verschlüsselung, nur Schutz mit einer begrenzten Dauer bieten kann. Denn mit steigender Rechenleistung und Fortschritten im Bereich der automatischen Deobfuscation, wird ein einmal verschleiertes Programm leichter angreifbar. Das Potential der Obfuscation ist zum momentanen Zeitpunkt jedoch keineswegs ausgeschöpft. Die Entwicklung völlig neuer Transformationsarten und die Variantenbildung bereits bekannter Techniken stellen nur eine Möglichkeit dar, die Fähigkeiten von Obfuscatoren zu erweitern. Auch ein besseres Verständnis der Zusammenhänge zwischen Kosten und Wirkung/Widerstandsfähigkeit einer Transformation, oder die Optimierung der Kombination einzelner Transformationen bieten viel Raum für Verbesserungen. 14/15 Interessant ist auch, dass sich mit dem Einsatz von Obfuscatoren auch andere Schutzfunktionen, wie beispielsweise, Wasserzeichen implementieren lassen. Es ist möglich, etwa über das Einfügen von totem Code, Wasserzeichen in einem Programm zu verstecken. Diese Wasserzeichen können dann benutzt werden, um den Diebstahl eines Algorithmus zweifelsfrei nachzuweisen oder die Herkunft von Raubkopien zu bestimmen. [4] Gabriel Torok, Bill Leach: "Obfuscate It: Thwart Reverse Engineering of Your Visual Basic .NET or C# Code", MSDN® Magazine, http://msdn.microsoft.com/msdnmag/issues/03/11/ NETCodeObfuscation, Zugriff am 26.09.06 [5] Brian Long: "Reverse Engineering To Learn .NET Better", http://www.blong.com/Conferences/DCon2003/ ReverseEngineering/ReverseEngineering.htm, Zugriff am 26.09.06 Glossar [6] "Code protection - Obfuscation", madgeek SharpToolbox, http://sharptoolbox.com/categories/ code-protectors-obfuscators, Zugriff am 05.10.06 Basisblock: Eine Folge von Anweisungen, die in ihrer Mitte keine Sprünge oder Sprungziele enthält. Als Sprungziel ist nur der Anfang eines Basisblocks erlaubt und ein Sprung aus dem Basisblock darf nur an dessen Ende stehen. [7] Sonali Gupta: "Code Obfuscation", Palisade, http://palisade.plynt.com/issues/2005Aug/codeobfuscation/, Zugriff am 10.10.06 Reflection: [8] Sonali Gupta: "Code Obfuscation - Part 2: Obfuscating Data Structures", Palisade, http://palisade.plynt.com/issues/2005Sep/codeobfuscation-continued/, Zugriff am 10.10.06 Ein Prozess, der es unter anderem ermöglicht die Metadaten einer Anwendung abzufragen. Mit Hilfe von Reflection können Anwendungen zur Laufzeit auch Erkenntnisse über ihre eigene Struktur gewinnen. [9] Sonali Gupta: "Code Obfuscation Part 3 - Hiding Control Flows", Palisade, http://palisade.plynt.com/issues/2005Oct/hidingcontrol-flows/, Zugriff am 10.10.06 Reverse Engineering: Bezeichnet in der Softwaretechnik die Rückgewinnung des Quellcodes von einem ausführbaren Programm oder einer Programmbibliothek. Die üblichen Werkzeuge sind Dekompiler und Disassembler. Round Tripping: Ein Verfahren, um bereits kompilierte Assemblys zu verändern. Zuerst wird die Assembly mittels eines Disassemblers in IL Quellcodes umgewandelt. Der so entstandene Quellcode kann editiert und danach wieder in ein ausführbares Programm zurückübersetzt werden. Satellitenassembly: Satellitenassemblys enthalten Informationen, die zur Lokalisierung eines Programms in eine andere Sprache benötigt werden. Sie enthalten keinen ausführbaren Code und sind immer der Assembly zugeordnet, die die Informationen benötigt. Literaturverzeichnis [1] Christian Collberg, Clark Thomborson, Douglas Low: "Manufactoring Cheap, Resilient, and Stealthy Opaque Construkts", Department of Computer Sience, The University of Auckland, Januar 1998 [2] Christian Collberg, Clark Thomborson, Douglas Low: "A Taxonomy of Obfuscating Transformations", Technical Report #148, Department of Computer Sience, The University of Auckland, Juli 1997 [3] Andrew Binstock: "Obfuscation: Cloaking your Code from Prying Eyes", Destination.NET, http://www.devx.com/microsoftISV/Article/11351, Zugriff am 26.09.06 [10] Mike Gunderloy: "How-To-Select an Obfuscation Tool for .NET™", How-ToSelect™-Guides, http://www.howtoselectguides.com/dotnet/ obfuscators/#sc_obfuscators, Zugriff am 09.11.06 [11] Lutz Roeder: Lutz Roeder's Programming.NET, http://www.aisto.com/roeder/dotnet/, Zugriff am 02.10.06 [12] OWASP: http://www.owasp.org/index.php/ Buffer_OverFlow_in_ILASM_and_ILDASM Zugriff am 27.10.06 [13] Aspose: Aspose.Obfuscator, http://www.aspose.com/Products/ Aspose.Obfuscator/, Zugriff am 09.11.06 [14] Wiseowl: Demeanor for .NET: http://www.wiseowl.com/products/products.aspx, Zugriff am 09.11.06 [15] Preemtive: Dotfuscator, http://www.preemptive.com/products/dotfuscator/ index.html, Zugriff am 09.11.06 [16] Remotesoft: Salamander.NET Obfuscator, http://www.remotesoft.com/salamander/ obfuscator.html, Zugriff am 09.11.06 [17] 9rays.net: Spices.Obfuscator, http://www.9rays.net/products/Spices.Obfuscator/, Zugriff am 09.11.06 [18] Lesser Software: IL-Obfuscator 2.0, http://www.lesser-software.com/, Zugriff am 09.11.06 15/15