Reverse Engineering in .NET |
|
....und wie man es verhindert
Der nachfolgende Beitrag, im Original nachzulesen hier, wurde freundlicherweise zur Veröffentlichung auf Kopierschutzsteme.de freigegeben.
Einleitung
Dieser Artikel befasst sich mit den Möglichkeiten des Reverse Engineerings unter Microsofts .NET Framework. Zuerst erkläre ich ein paar grundlegende Dinge und Eigenschaften von .NET. Im Anschluss daran zeige ich, wie man aus bestehenden, ungeschützten .NET-Anwendungen beliebigen Programmcode generiert und abschließend stelle ich einige Methoden vor, mit denen man seine eigenen Anwendungen vor Reverse Engineering schützen kann.
Grundlegendes
Bis 1996 bzw. 2002 war das Reverse Engineering von PC-Anwendungen ein schwieriges Unterfangen. Jeder, der sich damit beschäftigte, brauchte ausreichende Kenntnisse in Assembler und der jeweiligen Systemarchitektur, um brauchbare Ergebnisse zu erzielen. Das direkte Übersetzen eines bestehenden Programmes in eine Hochsprache wie C++ war ohne große Mühen nicht machbar – bei großen Programmen nahezu unmöglich.
Durch die Einführung von .NET und Java hat sich dieser Umstand jedoch geändert. Beide Plattformen kompilieren ihre Anwendungen nicht in nativen Maschinencode, sondern benutzen einen Zwischencode (bzw. eine Zwischensprache), der zur Laufzeit des Programmes in nativen Code übersetzt wird. Im Falle von .NET ist dies die Microsoft Intermediate Language (MSIL). Durch die MSIL wird es möglich die Programme plattformunabhängig und optimal auf die jeweilige Systemarchitektur angepasst laufen zu lassen. Die MSIL erinnert an Assembler, enthält jedoch weit mehr Informationen über das Programm als nativer Code. Die folgende Abbildung zeigt den Inhalt einer .NET-Anwendung, die mit dem MSIL-Disassembler[1] dekompiliert wurde.
Abbildung 1: Eine Beispiel-Methode "SayHello()" in MSIL
Wie man in Abbildung 1 erkennen kann, wird der Methodenname "SayHello", die Sichtbarkeitsmodifizierer "private static void" und die Klasse "Program" in der jeweiligen .NET-Assembly (so nennt man eine kompilierte .NET-Anwendung) gespeichert. Außerdem enthält jede .NET-Assembly Informationen über alle verwendeten Datenstrukturen und die Namen aller benutzten Objekte. Nur so ist es möglich, dass das .NET-Framework Objekte dynamisch zur Laufzeit laden und damit umgehen kann. Außerdem wird so die Interoperabilität zwischen einzelnen .NET-Sprachen sichergestellt.
Reverse Engineering in Aktion
Obwohl die MSIL auf der einen Seite klare Vorteile mit sich bringt, gibt es auf der anderen Seite Probleme: Das Rückführen einer .NET-Assembly ist viel trivialer, als bei nativem Maschinencode. Die zusätzlichen Informationen, die enthalten sind, lassen genauere Rückschlüsse auf den ursprünglichen Programmcode zu und ermöglichen ein leichteres Reverse Engineering bis hin zur kompletten Anwendung, die in jeder beliebigen .NET-Sprache wieder dargestellt werden kann. Die Abbildung 2 zeigt an einem Beispiel, wie man aus einer ungeschützten .NET-Assembly wieder .NET-Code generiert. Hierfür wird das Programm .NET-Reflector[2] verwendet.
Abbildung 2: Rückführen einer .NET-Assembly
Wie man sieht, ist es möglich kompilierten C#-Code in VB.NET-Code zu dekompilieren. Theoretisch kann man in jede verfügbare .NET-Sprache dekompilieren. Der Vorgang, der in Abbildung 2 beschrieben wird, dauert wenige Sekunden und kann von nahezu jeder Person durchgeführt werden, die die enstprechenden Programme kennt. Spezielle Kenntnisse werden nicht benötigt.
Gegenmaßnahmen
Wir haben gesehen, dass eine ungeschützte .NET-Assembly ihren eigenen Source-Code quasi direkt mitliefert. Nun gibt es drei Möglichkeiten die eigene Anwendung vor Reverse Engineering zu schützen:
– Obfuscation (Verschleierung)
– Verschlüsselte Assemblies
– Vorkompilierte Assemblies
Obfuscation (Verschleierung)
Bei der Obfuscation wird versucht den Zwischencode (MSIL) so zu verschleiern und zu ändern, dass eine Rückführung in eine Hochsprache (C#, VB.NET, etc.) erschwert wird und der resultierende Code schwer lesbar oder ganz unlesbar ist. Hierbei gibt es drei verschiedene Ansätze, wie der Code verschleiert wird:
1. Wie oben erwähnt, sind in einer .NET-Assembly die kompletten Namen der verwendeten Objekte, Methodennamen etc. gespeichert. Beim ersten Ansatz werden diese Namen verändert. Eine Methode, die z.B. "SayHello" heißt, könnte nach der Obfuscation "xfa14c485ff12d7be4d" heißen.
2. Der zweite Ansatz ändert den Kontrollfluß eines Programmes. Die eigentlichen Funktionen des Codes bleiben dabei erhalten, es ist jedoch schwieriger diesen in eine lesbare Hochsprache zu konvertieren. Zum Beispiel können umgelenkte Sprungbefehle innerhalb des Codes oder so genannte Junk-Bytes die Konvertierung erschweren und so dafür sorgen, dass schlecht lesbarer Code ensteht.
3. Beim dritten Ansatz möchte man das Disassemblieren an sich verhindern. Durch die gezielte Änderung der Metadaten einer .NET-Assembly, werden die Programme, die zur Disassemblierung benutzt werden, zum Fehlverhalten oder zum Absturz gebracht. Wichtig hierbei ist, dass sich das Laufzeitverhalten innerhalb der .NET-Runtime nicht ändert. Aus diesem Grund ist der Ansatz nicht immer geeignet.
Wie eine verschleierte .NET-Assembly aussehen könnte, wenn man sie wieder in C# konvertiert, zeigt folgende Abbildung.
Abbildung 3: Beispiel einer Obfuscation
Verschlüsselte Assemblies
Eine weitere Möglichkeit zum Schutz von .NET-Assemblies ist das Verschlüsseln. Bei diesem Schutz wird der Zwischencode der Anwendung mit verschiedenen Verschlüsselungsalgorithmen (z.B. RC4, MD5) bearbeitet. Dabei treten jedoch Probleme auf:
1. Das Entschlüsseln wird über native DLL-Bibliotheken realisiert. Das heißt, dass die Plattformunanbhängigkeit nicht mehr gegeben ist.
2. Der Key, der zur Entschlüsselung gebraucht wird, muss mitgeliefert werden, was eine potentielle Sicherheitslücke darstellt.
Trotz dieser Schwächen kann man das Verschlüsseln von Assemblies als ergänzende Maßnahme zu anderen Schutzmechanismen einsetzen. Eventuelle Geschwindigkeitseinbußen können bei der Entschlüsselung auftreten, sind im Allgemeinen jedoch vernachlässigbar, da dies nur einmal während der Laufzeit passiert.
Vorkompilierte Assemblies
Eine weitere Methode, wie man seine .NET-Anwendung schützen kann, ist das Kompilieren zu nativem Code, der sich sehr schwer in eine Hochsprache konvertieren lässt. Auch das ist eine in der Praxis benutzte Methode, die aber Schwächen zeigt:
Die Metadaten der .NET-Assembly bleiben erhalten, was das Reverse Engineering unter Umständen erleichtern könnte. Und man sollte im Hinterkopf behalten, dass auch nativer Code keine hundertprozentige Sicherheit bietet!
Außerdem wird dem Just-in-Time-Compiler, der den MSIL-Code zur Laufzeit in nativen Code übersetzt, die Möglichkeit genommen, die Anwendung auf die vorliegende CPU zu optimieren.
Fazit
Wir haben gesehen, dass das Reverse Engineering von ungeschützten .NETAnwendungen sehr leicht ist und wenig Zeit in Anspruch nimmt. Jeder der kommerzielle Software entwickelt, sollte sich dessen bewusst sein und sicherstellen, dass seine Anwendungen mit einer der oben vorgestellten Möglichkeiten geschützt wird. Auch Kombinationen dieser sind möglich!
Mit den Bordmitteln von Visual Studio und dem .NET-Framework lassen sich bereits gute Ergebnisse erzielen. Hier kann man die Kompilierung von MSIL in nativen Code einsetzen (NGEN[3]) oder den mitgelieferten Dotfuscator[4] benutzen. Außerdem bieten Dritthersteller weitere Software an, z.B. Remotesoft[5], XenoCode[6], oder PreEmptiveSolutions[7].
Referenzen
[1]MSIL-Disassembler: Programm, das von Microsoft zu Debuggingzwecken im .NET Framework SDK mitgeliefert wird.
[2].NET-Reflector: Programm zum Dekompilieren von .NET-Assemblies: http://www.aisto.com/roeder/dotnet/
[3]NGEN: Programm zum Kompilieren von MSIL in nativen Code. Siehe .NET Framework SDK Dokumentation bzw. MSDN: http://msdn.microsoft.com
[4]Dotfuscator: Programm, das Bestandteil von Visual Studio ist und Obfuscation-Funktionen bietet.
[5]Remotesoft: www.remotesoft.com
[6]XenoCode: http://www.xenocode.com
[7]PreEmptive Solutions: http://www.preemptive.com
|