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