(0) exportieren Drucken
Alle erweitern
Erweitern Minimieren

Decompiler und Obfuscator

Veröffentlicht: 21. Apr 2006
Von Manuel Lengert und Andrea Oebels

Wer Software erstellt und die Kosten für das Design, die Entwicklung und die Qualität eines Produkts tragen muss, ist zu Recht am Schutz seiner Investitionen interessiert. Wie können Ideen, Algorithmen und Executables vor neugierigen Blicken geschützt werden? Wie sicher ist ein solcher Schutz? Wer crackt den Schutz und mit welchen Motiven? Am Ende der Diskussion muss durchaus die Frage nach dem Sinn zahlreicher Tools gestellt werden, die mit dem Argument der „Codedokumentierung“ die Internas von Assemblies offen legen.

dot.net magazin


Diesen Artikel können Sie dank freundlicher Unterstützung von dot.net magazin auf MSDN Online lesen. dot.net magazin ist ein Partner von MSDN Online. Ausgabe: 4.06


Zur Partnerübersichtsseite von MSDN Online

.NET-Assemblies sind für jedermann offen lesbar. Selbst Obfuskatoren können eine Software nicht wirksam verschlüsseln. Wie kann geistiges Eigentum dann vor dem Zugriff Dritter geschützt werden? Ist ein solcher Schutz überhaupt sinnvoll? Dieser Artikel zeigt, wie eine im Release-Modus erstellte Assembly disassembliert und dekompiliert werden kann. Es wird dargestellt, wie leicht es ist, den ursprünglichen Sourcecode zu rekonstruieren. Anschließend wird diskutiert, auf welche Weise Software-Hersteller sich vor der Offenlegung ihrer Produkte schützen, und dass es ein Leichtes ist, die angewandten Schutzmechanismen wieder zu brechen. Als Fallbeispiel wird die Methode Validate() einer Klasse IbanValidator betrachtet, ein Algorithmus, der die Prüfziffer einer International Bank Account Number (IBAN) berechnet und verifiziert, ob es sich um eine grundsätzlich gültige Kontonummer handelt (siehe Kasten „IBAN“).

Auf dieser Seite

 ILDasm
 Decompiler
 Fragwürdige Gründe
 Es geht auch gratis...
 Obfuscation
 Können Obfuskatoren Software schützen?
 Ein kurzer Ausflug in die PKI
 Schutz versus Aufwand
 Abschließende Diskussion
 Fazit
 Links & Literatur
 Die Autoren

International Bank Account Number (IBAN)

ILDasm

Der erste Weg, Informationen über den Inhalt einer Assembly zu erhalten, führt über den IL-Disassembler (ILDasm), welcher Teil des .NET Framework SDK ist. Er wertet via Reflection die in der Assembly enthaltenen Metadaten aus und zeigt sie außerdem in einer Baumstruktur an (Abbildung 1). Listing A zeigt beispielhaft den disassemblierten IL-Code der Methode IbanValidator.Validate().

Disassemblierung einer Assembly via IlDasm
Abb. 1: Disassemblierung einer Assembly via IlDasm

Listing A: Disassemblierter IL-Code

.method /*060000A5*/ public hidebysig instance void 
        Validate(string strIban) cil managed
// SIG: 20 01 01 0E
{
  // Method begins at RVA 0x8884
  // Code size       192 (0xc0)
  .maxstack  6
  .locals /*11000027*/ init (string V_0,
           string V_1,
           string V_2,
           string V_3,
           string V_4,
           unsigned int8 V_5,
           unsigned int8 V_6,
           string V_7,
           string[] V_8)
  IL_0000:  /* 02   |                  */ ldarg.0
  IL_0001:  /* 03   |                  */ ldarg.1
  IL_0002:  /* 28   | (06)0000A8       */ call       instance void 
                                                     de.icommit.IbanValidator
                                                     /* 02000006 */
                                                     ::CheckIban(string) 
                                                     /* 060000A8 */
  IL_0007:  /* 02   |                  */ ldarg.0
  IL_0008:  /* 03   |                  */ ldarg.1
  IL_0009:  /* 28   | (06)0000A9       */ call       instance void 
                                                     de.icommit.IbanValidator
                                                     /* 02000006 */
                                                     ::ValidateRegExIban(string) 
                                                     /* 060000A9 */
  IL_000e:  /* 02   |                  */ ldarg.0
  IL_000f:  /* 03   |                  */ ldarg.1
  IL_0010:  /* 12   | 00               */ ldloca.s   V_0
  IL_0012:  /* 12   | 01               */ ldloca.s   V_1
  IL_0014:  /* 12   | 02               */ ldloca.s   V_2
  IL_0016:  /* 12   | 03               */ ldloca.s   V_3
  IL_0018:  /* 28   | (06)0000AA       */ call       instance void 
                                                     de.icommit.IbanValidator
                                                     /* 02000006 */
                                                     ::SplitIban(string,
                                                                 string&,
                                                                 string&,
                                                                 string&,
                                                                 string&) 
                                                     /* 060000AA */
  IL_001d:  /* 02   |                  */ ldarg.0
  IL_001e:  /* 03   |                  */ ldarg.1
  IL_001f:  /* 28   | (06)0000AB       */ call       instance string 
                                                     de.icommit.IbanValidator
                                                     /* 02000006 */
                                                     ::MoveCountryCodeToEndAndSet
                                                       ChecksumTo00(string) 
                                                     /* 060000AB */
  IL_0024:  /* 13   | 04               */ stloc.s    V_4
  IL_0026:  /* 02   |                  */ ldarg.0
  IL_0027:  /* 11   | 04               */ ldloc.s    V_4
  IL_0029:  /* 28   | (06)0000AC       */ call       instance string 
                                                     de.icommit.IbanValidator
                                                     /* 02000006 */
                                                     ::TransformIbanCharacters
                                                       (string) 
                                                     /* 060000AC */
  IL_002e:  /* 13   | 04               */ stloc.s    V_4
  IL_0030:  /* 02   |                  */ ldarg.0
  IL_0031:  /* 11   | 04               */ ldloc.s    V_4
  IL_0033:  /* 7E   | (04)000040       */ ldsfld     unsigned int8 
                                                     de.icommit.IbanValidator
                                                     /* 02000006 */
                                                     ::cbytIbanDivisor 
                                                     /* 04000040 */
  IL_0038:  /* 28   | (06)0000AD       */ call       instance unsigned int8 
                                                     de.icommit.IbanValidator
                                                     /* 02000006 */
                                                     ::GetRemainder(string,
                                                                    unsigned int8) 
                                                     /* 060000AD */
  IL_003d:  /* 13   | 05               */ stloc.s    V_5
  IL_003f:  /* 7E   | (04)000041       */ ldsfld     unsigned int8 
                                                     de.icommit.IbanValidator
                                                     /* 02000006 */
                                                     ::cbytIbanSubtraction 
                                                     /* 04000041 */
  IL_0044:  /* 11   | 05               */ ldloc.s    V_5
  IL_0046:  /* 59   |                  */ sub
  IL_0047:  /* D2   |                  */ conv.u1
  IL_0048:  /* 13   | 06               */ stloc.s    V_6
  IL_004a:  /* 11   | 06               */ ldloc.s    V_6
  IL_004c:  /* 1F   | 0A               */ ldc.i4.s   10
  IL_004e:  /* 2F   | 15               */ bge.s      IL_0065
  IL_0050:  /* 72   | (70)001D0C       */ ldstr      "0" /* 70001D0C */
  IL_0055:  /* 12   | 06               */ ldloca.s   V_6
  IL_0057:  /* 28   | (0A)000015       */ call       instance string 
                                                     [mscorlib/* 23000001 */]
                                                     System.Byte/* 01000014 */
                                                     ::ToString() /* 0A000015 */
  IL_005c:  /* 28   | (0A)000031       */ call       string [mscorlib/* 23000001 */]
                                                     System.String/* 01000010 */
                                                     ::Concat(string,
                                                              string) /* 0A000031 */
  IL_0061:  /* 13   | 07               */ stloc.s    V_7
  IL_0063:  /* 2B   | 09               */ br.s       IL_006e
  IL_0065:  /* 12   | 06               */ ldloca.s   V_6
  IL_0067:  /* 28   | (0A)000015       */ call       instance string 
                                                     [mscorlib/* 23000001 */]
                                                     System.Byte/* 01000014 */
                                                     ::ToString() /* 0A000015 */
  IL_006c:  /* 13   | 07               */ stloc.s    V_7
  IL_006e:  /* 11   | 07               */ ldloc.s    V_7
  IL_0070:  /* 07   |                  */ ldloc.1
  IL_0071:  /* 6F   | (0A)000022       */ callvirt   instance int32 
                                                     [mscorlib/* 23000001 */]
                                                     System.String/* 01000010 */
                                                     ::CompareTo(string) 
                                                     /* 0A000022 */
  IL_0076:  /* 2D   | 02               */ brtrue.s   IL_007a
  IL_0078:  /* 2B   | 00               */ br.s       IL_007a
  IL_007a:  /* 11   | 07               */ ldloc.s    V_7
  IL_007c:  /* 07   |                  */ ldloc.1
  IL_007d:  /* 6F   | (0A)000022       */ callvirt   instance int32 
                                                     [mscorlib/* 23000001 */]
                                                     System.String/* 01000010 */
                                                     ::CompareTo(string) 
                                                     /* 0A000022 */
  IL_0082:  /* 2C   | 3B               */ brfalse.s  IL_00bf
  IL_0084:  /* 1B   |                  */ ldc.i4.5
  IL_0085:  /* 8D   | (01)000010       */ newarr     [mscorlib/* 23000001 */]
                                                     System.String/* 01000010 */
  IL_008a:  /* 13   | 08               */ stloc.s    V_8
  IL_008c:  /* 11   | 08               */ ldloc.s    V_8
  IL_008e:  /* 16   |                  */ ldc.i4.0
  IL_008f:  /* 72   | (70)000726       */ ldstr      "parity failure -> computed 
                                                      checksum = '" /* 70000726 */
  IL_0094:  /* A2   |                  */ stelem.ref
  IL_0095:  /* 11   | 08               */ ldloc.s    V_8
  IL_0097:  /* 17   |                  */ ldc.i4.1
  IL_0098:  /* 11   | 07               */ ldloc.s    V_7
  IL_009a:  /* A2   |                  */ stelem.ref
  IL_009b:  /* 11   | 08               */ ldloc.s    V_8
  IL_009d:  /* 18   |                  */ ldc.i4.2
  IL_009e:  /* 72   | (70)0019F2       */ ldstr      "' <> '" /* 700019F2 */
  IL_00a3:  /* A2   |                  */ stelem.ref
  IL_00a4:  /* 11   | 08               */ ldloc.s    V_8
  IL_00a6:  /* 19   |                  */ ldc.i4.3
  IL_00a7:  /* 07   |                  */ ldloc.1
  IL_00a8:  /* A2   |                  */ stelem.ref
  IL_00a9:  /* 11   | 08               */ ldloc.s    V_8
  IL_00ab:  /* 1A   |                  */ ldc.i4.4
  IL_00ac:  /* 72   | (70)001A78       */ ldstr      "' = given checksum" /* 70001A78 */
  IL_00b1:  /* A2   |                  */ stelem.ref
  IL_00b2:  /* 11   | 08               */ ldloc.s    V_8
  IL_00b4:  /* 28   | (0A)000010       */ call       string [mscorlib/* 23000001 */]
                                                     System.String/* 01000010 */
                                                     ::Concat(string[]) 
                                                     /* 0A000010 */
  IL_00b9:  /* 73   | (06)0000A3       */ newobj     instance void de.icommit.
                                                     IbanValidationException
                                                     /* 02000005 */::.ctor(string) 
                                                     /* 060000A3 */
  IL_00be:  /* 7A   |                  */ throw
  IL_00bf:  /* 2A   |                  */ ret
} // end of method IbanValidator::Validate

Anstatt des hexadezimalen Byte-Codes bereitet ILDasm den IL-Code in einem les baren Format auf. Ohne die Einzelheiten der Intermediate Language (IL) genau kennen zu müssen, kann die grundsätzliche Arbeitsweise der Funktion am Listing abgelesen werden, da die Verarbeitung aus dem Aufruf weiterer Unterfunktionen besteht, die ihrerseits disassembliert werden können:

  • IL_0002: IbanValidator.CheckIban() – überprüft, ob eine nicht-leere IBAN übergeben wurde

  • IL_0009: IbanValidator.ValidateReg-ExIban() – überprüft, ob die übergebene IBAN den regulären Ausdruck ^[A-Z]{2}[0-9]{2}[0-9A-Z]{1,30}$ erfüllt

  • IL_0018: IbanValidator.SplitIban() – unterteilt die übergebene IBAN in vier Substrings (Ländercode, Prüfziffer, BLZ, Kontonummer)

  • IL_001f: IbanValidator.MoveCountry-CodeToEndAndSetChecksumTo00() – verschiebt die Positionen der Zeichen innerhalb der übergebenen IBAN

  • IL_0029: IbanValidator.Transform-IbanCharacters() – ersetzt den Ländercode zeichenweise gemäß einer vorgegebenen Umsetzungstabelle

  • IL_0033 - IL_0082: Berechnung der Prüfziffer und Vergleich mit der in der übergebenen IBAN enthaltenen Prüfziffer

  • IL_0084 - IL_00bf: Fehlerbehandlung bei Nicht-Übereinstimmung der Prüfziffer

Um das eigentliche Prüfziffernberechnungsverfahren nachvollziehen zu können, ist ein tieferer Einstieg in die IL-Syntax erforderlich. Jedoch kann die grundsätzliche Arbeitsweise der Methode mithilfe von ILDasm bereits „gelesen“ werden.

Decompiler

IL ist eine idealisierte Programmiersprache, die auf eine relativ einfach strukturierte virtuelle Maschine zugeschnitten ist. Als solche ähnelt IL einem Assembler-Code, wie man ihn von Intel-x386-Architekturen her kennt, ist zugleich aber wesentlich näher an den Klassen und Methoden des .NET Framework angelehnt und kennt die in C# und VB verfügbaren objektorientierten Sprachkonstrukte (Vererbung, Polymorphie). IL hat einen wesentlich höheren Abstraktionsgrad als Maschinencode und verzichtet auf bloße Registeroperationen. Deshalb ist es viel einfacher, IL-Code in C# oder Visual Basic zurück zu übersetzen. Genau diese Aufgabe erfüllen sog. Decompiler. Einen exzellenten Decompiler bietet die Firma Remotesoft mit ihrem Salamander .NET Decompiler [2]. Listing B zeigt das Ergebnis einer Dekompilierung der IL-Methode IbanValidator.Validate() aus Listing A.

Listing B: Dekompilierter IL-Code der Methode IbanValidator.Validate()

public void Validate(string strIban)
{
  string str1;
  string str2;
  string str3;
  string str4;
  string str6;

  CheckIban(strIban);
  ValidateRegExIban(strIban);
  SplitIban(strIban, out str1, out str2, out str3, out str4);

  string str5 = MoveCountryCodeToEndAndSetChecksumTo00(strIban);
  str5 = TransformIbanCharacters(str5);
  byte b1 = GetRemainder(str5, cbytIbanDivisor);
  byte b2 = (byte)(cbytIbanSubtraction - b1);

  if (b2 < 10)
  {
    str6 = String.Concat("0", b2.ToString());
  }
  else
  {
    str6 = b2.ToString();
  }

  if (str6.CompareTo(str2) != 0)
  {
    throw new IbanValidationException(String.Concat(new string[]{"parity failure 
              -> computed checksum = \'", str6, "\' <> \'", str2, "\' = given 
                 checksum"}));
  }
  else
  {
    return;
  }
}

Selbstverständlich wird die gesamte Assembly dekompiliert, sodass auch der Sourcecode der verwendeten Methoden CheckIban(), ValidateRegExIban(), SplitIban(), MoveCountryCodeToEndAndSetChecksumTo00(), TransformIbanCharacters() und GetRemainder() rekonstruiert wird. Wer es nicht bei einem Studium des Codes belassen will, kann nun ein VS-Projekt anlegen, die dekompilierten Sourcen importieren und schrittweise durch den Sourcecode debuggen. Ein Decompiler ist damit in der Lage, das Ergebnis einer wochenlangen Projektarbeit in kürzester Zeit zu entzaubern. Dabei ist es allerdings nicht möglich, die ursprünglichen Sourcen exakt zu rekonstruieren – „lediglich“ die in der kompilierten Assembly enthaltenen Informationen können zurück übersetzt werden. In Listing B drückt sich dies durch die Verwendung generischer Bezeichner für lokale Variablen aus (str1, ..., str6, b1, b2). Weitere Abweichungen zwischen Dekompilat und Original-Sourcecode sind:

  • Die Deklarationen von Methoden, Eigenschaften und Member- Variablen werden in ihrer Reihenfolge vertauscht. Das ist einleuchtend, da der Decompiler den Reflection-Mechanismus auf die Assembly anwendet und abhängig von der Reihenfolge ist, in der Enumerationen wie GetMethods(), GetProperties() und GetFields() die Klassendefinitionen auflisten. Zudem entscheidet der Programmierer des Decompilers, in welcher Reihenfolge die benötigten Reflection-Methoden aufgerufen werden.

  • Sprachspezifische Operatoren wie + (C#) oder & (VB) für eine String-Konkatenation werden bei der Kompilierung in die IL durch ihre Äquivalente ersetzt. Ein Dekompilat enthält in diesem Fall String.Concat(str1,str2) anstatt str1 + str2 bzw. str1 & str2.

  • Nicht alle Schlüsselwörter einer .NETProgrammiersprache haben ihre IL-Entsprechung. Z.B. wird das C#-Schlüsselwort foreach bei der Kompilierung in einen Enumerator umgesetzt (Listing 1).

  • Leerzeilen und Kommentare gehen bei der Kompilierung unwiderruflich verloren.

  • Debug-Anweisungen wie Trace.Assert() werden in einem Release-Kompilat nicht berücksichtigt.

Listing 1:

Anstatt

foreach(Object objKey in mobjCreditCards.Keys)
{
  CreditCardInstitute objCreditCardInstitute = (CreditCardInstitute)objKey;
  ...
}

erstellt der Microsoft C# Compiler Csc.exe den folgenden Code:

IEnumerator iEnumerator = mobjCreditCards.Keys.GetEnumerator();
try
{
  while (iEnumerator.MoveNext())
  {
    CreditCardInstitute creditCardInstitute = (CreditCardInstitute)iEnumerator.Current;
    ...
  }
}
finally
{
  IDisposable iDisposable = iEnumerator as IDisposable;
  if (iDisposable != null)
  {
    iDisposable.Dispose();
  }
}

Listing C zeigt zum Vergleich mit dem dekompilierten Code den Original-Sourcecode der Methode IbanValidator.Validate(). Ein toolbasierter Textvergleich, beispielsweise mithilfe des Tools WinDiff, ist zwischen den beiden Source-Dateien aus den zuvor genannten Gründen nicht möglich. Bei dem Vergleich der beiden Listings ist aber deutlich zu erkennen, dass das Dekompilat in Listing B dem ursprünglichen Sourcecode gefährlich nahe kommt. In den Händen eines „Crackers“ (also eines Hackers mit kriminellen Absichten) kann ein Decompiler zu einem gefährlichen Werkzeug werden, da es den Sourcecode einer Software entschlüsselt und alle darin enthaltenen „Geheimnisse“ offen legt.

Listing C: Original-Source-Code der Methode IbanValidator.Validate()

public void Validate(String strIban)
{
  // validation
  CheckIban(strIban);
  ValidateRegExIban(strIban);

  // splitting
  String strCountryIsoCode2;
  String strGivenChecksum;
  String strBlz;
  String strAccount;
  
  SplitIban(strIban,
            out strCountryIsoCode2,out strGivenChecksum,out strBlz,out strAccount);

  // algorithm
  String strModifiedIban = MoveCountryCodeToEndAndSetChecksumTo00(strIban);
  strModifiedIban = TransformIbanCharacters(strModifiedIban);
  byte bytRemainder = GetRemainder(strModifiedIban,cbytIbanDivisor);   
  
  // compute checksum
  byte bytComputedChecksum = (byte)(cbytIbanSubtraction - bytRemainder);
  
  String strComputedChecksum;
  
  if (bytComputedChecksum < 10) strComputedChecksum = "0" + 
                                                     bytComputedChecksum.ToString();
  else  strComputedChecksum = bytComputedChecksum.ToString();
  
  // compare checksums
  if (strComputedChecksum.CompareTo(strGivenChecksum) == 0) 
    Trace.WriteLine(" --> OK");
  else  
    Trace.WriteLine(" --> ERROR");

  if (strComputedChecksum.CompareTo(strGivenChecksum) != 0) 
    throw new IbanValidationException("parity failure -> computed checksum = '" + 
          strComputedChecksum + "' <> '" + strGivenChecksum + "' = given checksum");
}

Der Hersteller Remotesoft möchte nach eigener Aussage mit dem Salamander .NET Decompiler demonstrieren, wie einfach es ist, den Sourcecode einer kompilierten Assembly wiederherzustellen. Die eigentliche Produktpalette des Herstellers soll dem Schutz von Software dienen:

  • Der Salamander Obfuscator erschwert das Dekompilieren von .NET-Assemblies, indem String-Literale, Klassen-, Methoden- und Variablennamen ins nahezu Unverständliche verschlüsselt werden. Selbst der Kontrollfluss der Applikation wird mutiert, um Decompilern die Rekonstruktion lesbarer Schleifenkonstrukte zu erschweren. Eine Vollversion für bis zu fünf Entwicklerlizenzen wird für 799 US-Dollar angeboten.

  • Der Salamander Protector konvertiert den IL-Code einer Assembly in ein natives Format und erhält dabei alle .NET-Metadaten innerhalb der Assembly. Klassen-, Methoden- und andere Symbolnamen bleiben also erhalten. Dabei ergibt sich ein vergleichbarer Schutz wie bei nativ kompilierten C/C++-Applikationen. Es können Strings, Ressourcen und Codeabschnitte verschlüsselt werden. Das Werkzeug setzt das Dekompilieren einer Assembly damit grundsätzlich außer Kraft. Die Vollversion kostet 1.899 US-Dollar für fünf Entwicklerlizenzen. Ein besonders interessantes Feature des Protectors ist die Option, vor dem Verpacken der Assembly ein externes Verschlüsselungspasswort zu vergeben. Der Benutzer der Applikation muss dieses Passwort unmittelbar nach dem Aufruf der Applikation eingeben, bevor das eigentliche Programm ausgeführt werden kann. Neben der Verschlüsselung des Codes kann dadurch ein interessanter Nebeneffekt erreicht werden: Wird für jeden Kunden ein individuelles Passwort eingesetzt und sind die Kunden allesamt namentlich bekannt, kann jede Kopie der Applikation auf den ursprünglichen Erwerber zurückverfolgt werden. Im Worst Case lassen sich also auch Raubkopien zurückverfolgen (auch wenn dem rsprünglichen Erwerber keine Absicht zu unterstellen ist).

  • Der Remotesoft .NET Explorer integriert die Tool-Suite des Herstellers in einer IlDasm-ähnlichen Benutzeroberfläche. Ein Objekt-Browser ermöglicht das Navigieren durch die Inhalte einer Assembly. Kontextsensitive Menus unterstützen das Disassemblieren, Dekompilieren, Obfuskieren, Protektieren und Ausführen der betrachteten Assembly (Abbildung 2).

Remotesoft .NET Explorer
Abb. 2: Remotesoft .NET Explorer

Der Funktionsumfang des Decompilers verdeutlicht, wie umfassend der Sourcecode einer Assembly rekonstruiert werden kann und wie ausgereift das Werkzeug wirklich ist:

  • Erkennung aller .NET-sprachspezifischen Konstrukte, wie beispielsweise Attribute, Eigenschaften, Ereignisse, Felder, Methoden und verschachtelte Typen

  • Automatische Erkennung des verwendeten Compilers und Generierung von Sourcecode in der entsprechenden Sprache, wie beispielsweise C#, Managed C++ oder Visual Basic.

  • Unterstützung von Generics (.NET 2.0), Unsafe Code und Zeigerarithmetik

  • Erstellung von VS.NET-Projektdateien zwecks einfacher Rekompilierung

Laut Hersteller wurden über 10.000 Klassen, 800 Assemblies und 150.000 Dateien getestet. Der Salamander .NET Decompiler ist damit quasi das Verkaufsargument des Herstellers für die eigentlich angebotenen Schutzwerkzeuge.

Fragwürdige Gründe

Dem Autor ist es nicht gelungen, einen kommerziellen Markt für Decompiler zu identifizieren. Entwickler werden sich ihren Sourcecode kaum versehentlich löschen. Die geschätzte Leserschaft sei aufgerufen, zur Aufklärung beizutragen. Es bleibt festzustellen, dass ein Decompiler in den falschen Händen beträchtlichen Schaden anrichten kann. Software-Hersteller müssen keinen Decompiler erwerben, um zu dieser Erkenntnis zu gelangen. Vielleicht ist es beruhigend zu wissen, dass der Einstieg in ein solches Werkzeug über den hohen Preis gewisse Barrieren schafft – dennoch gibt es immer Mittel und Wege.

Es geht auch gratis...

Einen brillianten Decompiler bietet Lutz Roeder auf seiner Seite zum freien Download an [3]. Der Reflector for .NET ist ein Klassenbrowser, der die Inhalte einer Assembly analysiert und die Abhängigkeiten zwischen Assemblies, Klassen, Methoden und Member-Variablen aufzeigt (Abbildung 3).

Reflector for .NET
Abb. 3: Reflector for .NET. Klicken Sie für eine größere Darstellung auf das Bild.

Hierbei wird für jede einzelne Methode ein Graph der aufgerufenen (call graph) sowie der aufrufenden Methoden (callee graph) erzeugt. Der Benutzer kann auf diese Weise im TreeView erkennen, an welchen Stellen im Code die betrachtete Methode überall aufgerufen wird und innerhalb des Klassenbrowsers dorthin navigieren. Die analysierten Abhängigkeiten beschränken sich dabei nicht auf eine einzelne Assembly, vielmehr ist es möglich, sämtliche Assemblies einer Applikation inklusive aller Assemblies des .NET Framework einzubeziehen. Über den Eintrag Disassembler kann eine einzelne Klasse oder Methode disassembliert oder dekompiliert werden (Abbildung 4).

Dekompilieren mit Reflector for .NET
Abb. 4: Dekompilieren mit Reflector for .NET. Klicken Sie für eine größere Darstellung auf das Bild.

Die Zielsprache wird über eine ComboBox in der Toolbar des Reflectors eingestellt. Durch Änderung dieses Eintrags kann der Code „on-the-fly“ zwischen IL, C#, Visual Basic und Delphi übersetzt werden. Per Klick auf eines der im Sourcecode verwendeten Symbole navigiert der Klassenbrowser zu der entsprechenden Methode bzw. Typendefinition und zeigt deren Sourcecode in der eingestellten Sprache an. Auf diese Weise wird ein bequemes Lesen und Verstehen der untersuchten Applikation ermöglicht. Interessant ist die Arbeitsweise des Reflectors: Das Werkzeug setzt anstatt des .NET Reflection API ein eigenes Modell zum Auslesen der Metadaten, IL- Instruktionen, Resourcen und XML-Dokumentationen einer Assembly ein. Daher können Assemblies, die mit .NET 2.0 erstellt wurden, analysiert werden, ohne dass .NET 2.0 selbst auf dem Rechner installiert sein muss. Zusammenfassend lässt sich feststellen, dass der Reflector für eine schrittweise, interaktive Dekompilierung von Sourcen besonders geeignet ist, während der Salamander .NET Decompiler seine Stärke als Kommandozeilenwerkzeug zeigt und das Dekompilat einer gesamten Assembly in einem einzigen Schritt erstellt.

Obfuscation

Viele Anbieter beantworten das Problem der Dekompilierung mit dem Begriff „Obfuskatierung“ (Verschleierung). Dabei geht es darum, den kompilierten ILCode nachträglich derart zu verändern, dass es sehr schwierig wird, die modifizierte Assembly zu lesen und zu verstehen. Das Ziel besteht nicht darin, den Prozess des Reverse Engineering grundsätzlich zu verhindern – das ist auch gar nicht möglich. Vielmehr soll das Ergebnis dieses Prozesses weitgehend unbrauchbar gemacht werden. Der erste Anhaltspunkt zum Verständnis einer Applikation liegt im Studium der darin verwendeten Zeichenketten (Beispiel siehe Listing 2).

Listing 2:

.method private hidebysig specialname instance
                                         string get_ConnectionString()
    cil managed
{
.maxstack 1
.locals init (string)

IL_0000: ldarg.0
IL_0001: ldstr “Data Source=MySqlServer;
                                                 Initial Catalog=pubs;
            User Id=sa;Password=abc123;"
IL_0006: stloc.0
IL_0007: br.s IL_0009
IL_0009: ldloc.0
IL_000a: ret
}

Entsprechend fokussieren sich Obfuskatoren in erster Linie auf die Verschlüsselung aller Strings, die in der Assembly aufgeführt sind. Bei der String Encryption werden alle String-Literale nach einem vorgegebenen Algorithmus verschlüsselt und erst zur Laufzeit wieder entschlüsselt. Aus dem String “Data Source=My-SqlServer;Initial Catalog=pubs;UserId=sa;Password=abc123;“ wird ein Byte-Array, beispielsweise C3 08 51 4F 5C E2 B8 CA 11 E7 EE D1 6D EA 36 F2 B3 BC 1E 87 D8 59 7B 8B 9A 81 C6 6A 26 D8 8D 53 0E 09 B2 46 DE 94 07 6E. Ohne Kenntnisse des Verschlüsselungsalgorithmus ist es sicherlich nicht einfach, einen String wie in Listing 3 zu entschlüsseln.

Listing3:

.method private hidebysig specialname instance
                                  void string get_ConnectionString()
      cil managed
{
      .maxstack 1
      .locals init (string)

      IL_0000: ldarg.0
      IL_0001: ldstr bytearray(C3 08 51 4F 5C E2 B8 CA 11
                                                       E7 EE D1 6D EA 36 F2 B3
                        BC 1E 87 D8 59 7B 8B 9A 81 C6 6A 26 D8
                                                                              8D 53 0E 09
                        B2 46 DE 94 07 6E)
      IL_0006: stloc.0
      IL_0007: br.s IL_0009
      IL_0009: ldloc.0
      IL_000a: ret
}

Das Problem dieser Art der Obfuskation ist allerdings inhärent. Zur Laufzeit muss die Applikation das Byte-Array wieder in den ursprünglichen String zurück übersetzen. Obfuskatoren injizieren zu diesem Zweck eine entsprechende Entschlüsselungsmethode in die Ziel-Assembly, zum Beispiel:

.method public hidebysig static string Decrypt
                                                  (string inputStr) cil managed
{
...
}

Die Methode get_ConnectionString() besteht demnach in Wirklichkeit aus dem IL-Code aus Listing 4.

Listing 4:

.method private hidebysig specialname instance void
string get_ConnectionString()
      cil managed
{
      .maxstack 1
      .locals init (string)

      IL_0000: ldarg.0
      IL_0001: ldstr bytearray(C3 08 51 4F 5C E2 B8 CA 11
                                                       E7 EE D1 6D EA 36 F2 B3
                     BC 1E 87 D8 59 7B 8B 9A 81 C6 6A 26 D8
                                                                                8D 53 0E 09
                     B2 46 DE 94 07 6E)
      IL_0006: call string Decrypt(string)
      IL_000b: stloc.0
      IL_000c: br.s IL_000e
      IL_000e: ldloc.0
      IL_0010: ret
}

Der Vorgang der Obfuskation ist damit ad absurdum geführt. Der verschlüsselte String ist in der IL klar als Byte-Array lesbar. Um den gewünschten String zu erhalten, muss nun lediglich die IL-Methode Decrypt() aufgerufen werden. Bei der Eingabe des Byte-Arrays erhält man als Ausgabe dann die ursprüngliche Zeichenkette. Eine detaillierte Abhandlung zu diesem Thema kann dem Artikel „The truth about string protection by obfuscators“ entnommen werden [4].

Können Obfuskatoren Software schützen?

String Encryption wird allgemein überschätzt. Es wird nur ein geringer Schutz erreicht und dies zu Lasten einer schlechteren Performance. Daher sollten Obfuskatoren höchstens nach dem Motto „besser ein geringer Schutz als gar keiner“ eingesetzt werden. Der Salamander .NET Decompiler ist sogar in der Lage, String Encryption in einer Assembly zu erkennen, die injizierte Decrypt()-Methode zu finden, aufzurufen und an deren Stelle den entschlüsselten String in den Sourcecode einzufügen.Darüber hinaus nehmen Obfuskatoren Umbenennungen aller Symbolnamen vor. Allerdings können Verwirrungen bei der Benennung von Klassen, Methoden und Variablen mit ein wenig Zeit und Geduld durch Debugging des rekonstruierten Sourcecodes aufgelöst werden. Das Problem der derzeit am Markt angebotenen Obfuskatoren besteht darin, dass der Verschlüsselungsalgorithmus einschließlich des verwendeten Schlüssels in der „geschützten“ Assembly ausgeliefert wird und damit jedem Hacking ausgeliefert ist. Ein verschlüsselter Inhalt kann nach dem heutigem Sicherheitsverständnis jedoch nur dann wirkungsvoll vor unberechtigten Dritten bewahrt werden, wenn der Schlüssel geheim bleibt, nur dem Empfänger des Inhalts bekannt ist und losgelöst von der eigentlichen Nachricht verwaltet wird.

Ein kurzer Ausflug in die PKI

Die beschriebene Verschüsselungsproblematik wird mit der Public/Private-Key-Infrastruktur (PKI) gelöst. Der Absender einer Nachricht verschlüsselt den Inhalt der Nachricht mit dem öffentlichen Schlüssel des Empfängers. Dieser Public Key ist, wie der Name beschreibt, der Öffentlichkeit frei zugänglich. Der Empfänger wiederum besitzt einen geheimen Schlüssel, den Private Key, den nur er kennt. Beide Schlüssel, Public Key und Private Key, bilden ein Schlüsselpaar und sind so konstruiert, dass mit dem öffentlichen Schlüssel verschlüsselte Nachrichten ausschließlich von dem zugehörigen geheimen Schlüssel entschlüsselt werden können. Will ein unberechtigter Dritter die verschlüsselte Nachricht „hacken“, muss er den verwendeten Schlüssel brechen. Aufgrund der mathematischen Natur der konstruierten Schlüsselpaare, der Verwendung von sehr langen Primzahlen, benötigt ein Hochleistungsrechner viele Jahre, um eine verschlüsselte Nachricht ohne Kenntnis des privaten Schlüssels zu entschlüsseln. Dieser Sicherheitsstandard wird im heutigen Geschäftsverkehr beispielsweise zur Verschlüsselung der Kommunikation zwischen Clients und Servern eingesetzt (Secure Socket Layer, SSL): Auf dem Server wird ein Serverzertifikat installiert. Die Clients haben freien Zugriff auf den darin enthaltenen öffentlichen Schlüssel des Servers und verschlüsseln mit ihnen ihre Nachricht (zum Beispiel einen geheimen Session-Key für die weitere Kommunikation zwischen den beiden Partnern). Aufgrund der Natur des Schlüsselpaars kann die verschlüsselte Nachricht ausschließlich über den geheimen Schlüssel des Servers entschlüsselt werden. Der Server entschlüsselt den vom Client übertragenen Session-Key und verschlüsselt fortan seine Nachrichten für diesen Client mit diesem Session-Key.

Schutz versus Aufwand

Ein wirklich standhafter Schutz von Software durch Obfuskation kann nur in Verbindung mit der Installation von Client-Zertifikaten erreicht werden. Hierzu muss der Software-Hersteller einen externen Schlüssel für den Prozess der Obfuskation verwenden: Den öffentlichen Schlüssel des Clients. Die verschlüsselten Inhalte der ausgelieferten Assembly können dann ausschließlich über den geheimen Schlüssel des Kunden entschlüsselt werden. Leider mangelt es bei der derzeitigen IT-Infrastruktur an der Verwendung von Client-Zertifikaten. Im beschriebenen Obfuskationsszenario müsste entweder der Kunde vor dem Kauf der Software ein Zertifikat beschaffen und bereitstellen. Beides ist für den Kunden mit Kosten verbunden. Alternativ müsste der Software-Hersteller ein Client-Zertifikat erstellen, im Nachgang das Problem lösen, wie dieses Zertifikat sicher, und vor den Augen Dritter geschützt, den richtigen Adressaten erreicht. In jedem Fall kann die Software nicht einfach von einem Vervielfältigungsmedium (CD, Internet) installiert werden, da der Hersteller die Software vor ihrer Auslieferung kundenspezifisch obfuskieren muss. Aufgrund des beschriebenen Aufwands ist verständlich, warum zu schützendes Knowhow auf diese Weise fast usschließlich in Hochsicherheitsszenarien bewahrt wird, der betriebene Aufwand also nicht massentauglich ist.

Abschließende Diskussion

Letztlich bleibt noch die Frage, aus welchen Gründen die Hersteller ihre Produkte schützen und welchen Effekt sie dabei erzielen. Die nachfolgenden Argumente sind sicherlich subjektiv zu bewerten, daher sei der Leser herzlich zu einer kontroversen Diskussion eingeladen. Welcher Obfuscator schützt am besten? Wie lange wird es dauern, bis ein Tool verfügbar ist, den obfuskierten Code wieder zu entschlüsseln? Welche Kosten sind mit der Beteiligung an einem solchen Wettlauf verbunden? Am Ende ist der Kunde der Benachteiligte. Er allein trägt die Kosten und muss gleichzeitig die einhergehenden Nachteile in Kauf nehmen. Vergleichbare Szenarien lassen sich in der Unterhaltungsbranche ausmachen. CDs und DVDs werden mit immer ausgeklügelteren Kopierschutzverfahren ausgeliefert. Dabei wird die Oberfläche einer Disk gezielt beschädigt, um Kopierern die Arbeit zu erschweren. Das Ergebnis jedoch ist, dass es in der Regel nicht lange dauert, bis ein geeignetes Kopierwerkzeug in der Community auftaucht. Und der Käufer der CD ärgert sich darüber, dass er seine Lieblingsmusik nur zu Hause am PC und nicht im Auto hören kann. Zurück zur Software-Branche: Warum wird Software geschützt? Warum wird Sourcecode nicht einfach offen gelegt?

Vertreter der Open-Source-Community argumentieren, dass Software gekauft wird, um Zeit und Kosten zu sparen, nicht um geistiges Eigentum zu rauben. Firmen kaufen Software und versichern, die erforderlichen Lizenzen zu erwerben. Durch die Offenlegung des Sourcecode würde ersichtlich, was die Software tut. Bisher undurchsichtige APIs würden verständlich. Treten Fehler auf, sind Kunden in der Lage, die Fehlerursache zu untersuchen und zu fixen, bevor der Hersteller einen Patch oder ein Service Pack bereitstellt. Der Hersteller profitiert gleichzeitig von dem Umstand, dass viele Augen den Sourcecode untersuchen und insgesamt zu einem besseren, stabileren Produkt beitragen.

Fazit

Möchte sich ein Software-Hersteller vor dem Zugriff Anderer auf seinen Sourcecode schützen, muss er einen erheblichen Aufwand mit fragwürdigem Ergebnis betreiben. Es gibt grundsätzlich nichts, was in der .NET-Welt nicht dekompiliert werden kann (gleiches gilt übrigens auch für die Java-Welt). Wer die Lösung in nativem Maschinencode sieht, kann auf das Tool Ngen.exe der .NET-Laufzeit zurückgreifen und sicher sein, dass ein Dekompilieren kaum möglich ist, muss jedoch auch alle Nachteile in Kauf nehmen, die mit der Auslieferung von nativem Maschinencode einhergehen: Für jede potenzielle Zielplattform muss ein pre-JIT-Kompilat erstellt werden, während aber die mit .NET einhergehende Hardware-Unabhängigkeit verloren geht. Obfuskatoren verschlüsseln den IL-Code einer Assembly bis zur Unlesbarkeit und erzeugen dabei einen nicht zu vernachlässigenden Performance-Impact, ohne die Software wirksam schützen zu können. Deobfuskatoren und Decompiler können den ursprünglichen Sourcecode dennoch weitgehend rekonstruieren. Verwirrungen bei der Benennung von Klassen, Methoden und Variablen lassen sich durch Debugging des rekonstruierten Sourcecodes auflösen. Der Wettlauf zwischen Verschlüsselung und Entschlüsselung kostet Zeit und Geld – folglich gibt es nur wenige Anwendungsbereiche, in denen dieser Aufwand betrieben wird und in denen er durchaus gerechtfertigt ist. Nicht ohne Grund implementieren die betroffenen Hersteller ihre Software meist in C/C++.

Microsoft hat sich aus gutem Grund dazu entschieden, die ausgelieferten Assemblies der .NET-Framework nicht zu obfuskieren. Der Konzern hat dazu beigetragen, dass Software-Hersteller die Funktionsweise des Frameworks und seiner internen Struktur besser verstehen können. Nicht zuletzt ist daraus z.B. Mono entstanden.

Links & Literatur

[1] Prüfziffernberechnung für IBANs:
www.pruefziffernberechnung.de/I/IBAN.shtml

[2] Remotesoft:
www.remotesoft.com

[3] PROGRAMMING.NET:
www.aisto.com/roeder/dotnet/

[4] String Encryption:
www.remotesoft.com/salamander/stringencrypt/

Die Autoren

Manuel Lengert ist seit kurzem Certified Ethical Hacker (CEH) und auf die Sicherheit von Software, Hardware und Netzwerken spezialisiert – Sie erreichen ihn unter mlengert@icommit.de.

Andrea Oebels ist Geschäftsführerin der iCommit Integrationslösungen GmbH und auf das Projekt-Management kundenindividueller Lösungen spezialisiert – Sie erreichen sie unter aoebels@icommit.de.


Anzeigen:
© 2014 Microsoft