Reverse Engineering von Zwischensprachen wie .NET

 

Was ist Reverse Engineering?

Unter Reverse Engineering versteht man das Analysieren von Software oder Technologie, um zu verstehen, wie sie funktioniert, auch wenn man den ursprünglichen Quellcode nicht kennt. Das bedeutet, den Code sorgfältig zu analysieren und seine Funktionen auf der Grundlage von Beobachtungen und Programmierkenntnissen zu rekonstruieren.
 

Warum ist Reverse Engineering notwendig?

Reverse Engineering ist wichtig, weil wir manchmal verstehen müssen, wie eine Software funktioniert, ohne Zugang zu ihrem ursprünglichen Code zu haben. Wenn Entwicklerinnen und Entwickler Software schreiben, benennen sie normalerweise ihre Variablen und Funktionen deutlich, damit andere leicht verstehen können, was das Programm tut.

Stellen Sie sich zum Beispiel eine Online-Shopping-Website vor. Eine Entwicklerin oder ein Entwickler könnte eine Variable namens „Einkaufswagen“ verwenden, um eine Liste von Artikeln zu speichern, die eine Kundin oder ein Kunde kaufen möchte. Es könnte auch eine Funktion namens produkt_zu_einkaufswagen_hinzufügen geben, die das richtige Produkt findet und es der Liste hinzufügt. Dies macht es anderen Entwicklerinnen und Entwicklern leicht, den Code zu verstehen und ihn bei Bedarf zu ändern.

Unternehmen, die Software verkaufen, oder Hacker, die bösartige Programme erstellen, möchten jedoch möglicherweise nicht, dass andere sehen, wie ihr Code funktioniert. Sie verwenden Techniken, um die ursprüngliche Struktur des Codes zu verbergen, so dass er schwer zu lesen ist. An dieser Stelle wird Reverse Engineering nützlich. Wenn man der Quelle eines Programms nicht trauen kann, ist es gefährlich, es auszuführen, ohne zu verstehen, was es tut. Sicherheitsexpertinnen und -experten, Softwareentwicklerinnen und -entwickler und Forscherinnen und Forscher nutzen Reverse Engineering, um unbekannte Software zu analysieren, Sicherheitsbedrohungen zu erkennen und sogar bestehende Programme zu verbessern.

Ein einfaches Beispiel: Das verschlossene Kästchen

Stellen Sie sich vor, Sie finden ein verschlossenes Kästchen mit mehreren Knöpfen, aber keiner Anleitung, wie Sie es öffnen können. Sie wissen nicht, was sich darin befindet, aber Sie sind neugierig. Sie fangen an, die Knöpfe in unterschiedlicher Reihenfolge zu drücken und beobachten, was jedes Mal passiert. Vielleicht geht ein Licht an, wenn Sie einen Knopf drücken, oder Sie hören ein Klicken, wenn Sie einen anderen drücken. Durch Experimentieren finden Sie langsam heraus, welche Tasten in welcher Reihenfolge gedrückt werden müssen, um die Box zu entriegeln.

Das Reverse Engineering von Software funktioniert auf ähnliche Weise. Anstelle von Knöpfen analysieren Softwareingenieurinnen und -ingenieure, wie ein Programm auf verschiedene Eingaben reagiert. Durch Tests, Beobachtung von Mustern und fundierte Vermutungen können sie herausfinden, wie ein Programm aufgebaut ist und wie es funktioniert – selbst, wenn sie keinen Zugang zum ursprünglichen Quellcode haben.

Verschleierung: Verstecken der Funktionsweise von Software

Manche Softwareentwicklerinnen oder -entwickler wollen nicht, dass ihr Code verstanden wird und verwenden daher eine Technik, die sie Obfuskation bzw. Verschleierung nennen, um die Analyse zu erschweren. Das ist so, als würde man die Beschriftung der Knöpfe auf einem verschlossenen Kasten entfernen, so dass jemand, der sie findet, nicht verstehen kann, wie man den Kasten öffnet. Manchmal werden sogar Knöpfe ohne Funktion hinzugefügt, um Ihre Zeit zu verschwenden, in der Hoffnung, dass Sie aufgeben. Später werden wir sehen, dass es eine grundlegende Einsicht gibt, die es uns ermöglicht, die Funktionalität jedes beliebigen Programms zu erschließen, wenn wir bereit sind, die Zeit zu investieren, um es zu verstehen.

Anstatt eindeutige Variablennamen wie Einkaufswagen und produkt_zu_einkaufswagen_hinzufügen zu verwenden, könnte ein verschleiertes Programm sie in zufällige, bedeutungslose Namen wie X1A und F2G umbenennen. Anstelle von lesbaren Anweisungen werden möglicherweise unnötige oder verwirrende Schritte verwendet, die es schwerer machen, dem Programm zu folgen. Manche Programme erkennen sogar, wenn sie analysiert werden, und ändern ihr Verhalten, um nicht verstanden zu werden.

Lassen Sie uns dies anhand eines Beispiels veranschaulichen. Hier ist ein einfaches C#-Programm, das eine Datei liest und ihren Inhalt ausgibt:

using System; 

using System.IO; 

 

class ReadAndPrintFile 

    static void Main() 

    { 

        string filePath = "secret.txt"; 

        string content = ReadSecretFile(filePath); 

        Console.WriteLine(content); 

    } 

 

    static string ReadSecretFile(string path) 

    { 

         return File.ReadAllText(path); 

     } 

}

und hier ist die verschleierte Version, bei der wir mehrere Dinge geändert haben:

  • Der Programmname lautet jetzt einfach X3 statt ReadAndPrintFile.
  • Der Dateiname ist in Base64 kodiert ("U2VjcmV0LnR4dA==" wird zu "secret.txt" dekodiert) und muss entschlüsselt werden.
  • Die Funktionsnamen werden geändert (ReadSecretFile → P).
  • Nutzloser Code (bedeutungslose Bedingungen) wird hinzugefügt, um Reverse Engineers zu verwirren.  

using System; 

using System.IO; 

 

class X3 

    static void Main() 

    { 

        string y = Decrypt("U2VjcmV0LnR4dA==");   // Encrypted filename 

        string z = P(y); 

        Console.WriteLine(z); 

    } 

 

    static string P(string x) 

    { 

        if (x.Length > 0) { }  // Junk code 

        return File.ReadAllText(x); 

    } 

 

    static string Decrypt(string s) 

    { 

        byte[] b = Convert.FromBase64String(s); 

        return System.Text.Encoding.UTF8.GetString(b); 

    } 

Das Programm tut immer noch genau das Gleiche, aber es ist für jemanden, der es analysiert, weniger lesbar. 

Wenn wir das Programm nun wieder entschlüsseln würden, kämen wir zu einem Programm wie diesem: 

using System; 

using System.IO; 

using System.Text; 

 

class Program 

    static void Main() 

    { 

        string filePath = DecodeBase64("U2VjcmV0LnR4dA=="); // Looked up externally and found to decode to 'secret.txt' 

        string content = ReadSecretFile(filePath); 

        Console.WriteLine(content); 

    } 

 

    static string ReadSecretFile(string path) 

    { 

        return File.ReadAllText(path); 

    } 

 

    static string DecodeBase64(string input) 

    { 

        byte[] data = Convert.FromBase64String(input); 

        return Encoding.UTF8.GetString(data); 

    } 

Deobfuskierung: Die Verschleierung rückgängig machen

Genauso wie Reverse Engineers die korrekte Tastenfolge auf dem verschlossenen Kästchen herausfinden können, können sie auch Software entschleiern - die Tricks rückgängig machen, mit denen die Logik versteckt wurde. Dazu gehört oft die Umbenennung von Variablen in etwas Sinnvolles, die Vereinfachung komplexer Codestrukturen und das Entfernen unnötiger Schritte. 

Wenn ein Reverse Engineer beispielsweise eine Funktion mit dem Namen F2G findet, könnte sie oder er herausfinden, dass diese Funktion eigentlich ein Produkt zu einem Einkaufswagen hinzufügt, also benennt sie oder er diese Funktion in produkt_zu_einkaufswagen_hinzufügen um, um das Programm verständlicher zu machen. Mit der Zeit können sie mit genügend Aufwand die ursprüngliche Logik der Software rekonstruieren, genau wie beim Lösen des Rätsels des verschlossenen Kästchens.

Verschleierung kann nicht alles verbergen

Auch wenn Verschleierung den Code schwerer lesbar macht, hat sie eine grundlegende Schwäche: Ein Programm muss immer noch echte Anweisungen auf einem Computer ausführen. Egal, wie viel Code verschlüsselt ist, irgendwann muss er mit dem Betriebssystem interagieren - hier kommen die Systemaufrufe (syscalls) ins Spiel. 

Ein Syscall ist eine Anfrage, die ein Programm an das Betriebssystem richtet, um grundlegende Aufgaben auszuführen, wie das Lesen einer Datei, das Senden von Daten über das Internet oder die Zuweisung von Speicher. Diese Syscalls müssen strengen, vom Betriebssystem festgelegten Regeln folgen, was bedeutet, dass sie nicht verschleiert werden können. 

Dies ist eine wichtige Erkenntnis beim Reverse Engineering. Selbst wenn ein Funktionsname versteckt ist und die Logik voller unnötiger Komplexität ist, muss das Programm dennoch Syscalls ausführen, um irgendetwas Nützliches zu tun. Durch die Überwachung dieser Syscalls kann ein Reverse Engineer auf den wahren Zweck eines Programms schließen, ohne jede Zeile des verschleierten Codes verstehen zu müssen.

Zusammenhang mit .NET

Das .NET-Framework (das für C#-, VB.NET- und F#-Anwendungen verwendet wird) ist im Zusammenhang mit Obfuskation und Reverse Engineering besonders interessant, da .NET-Programme nicht direkt in Maschinencode kompiliert werden. Stattdessen werden sie in Intermediate Language (IL) kompiliert, die dann von der Common Language Runtime (CLR) zur Laufzeit ausgeführt wird.

Da .NET-Anwendungen auf IL basieren, sind sie leichter zu analysieren als nativer Maschinencode. Reverse Engineers können Tools wie dnSpy oder ILSpy verwenden, um ein .NET-Programm zu dekompilieren, und dabei oft große Teile des lesbaren Codes wiederherstellen – selbst, wenn eine gewisse Verschleierung vorgenommen wurde. 

Auch wenn ein .NET-Programm stark verschleiert ist, muss es immer noch mit dem zugrunde liegenden Windows-System über P/Invoke oder Systemaufrufe interagieren. Beispielsweise muss ein Programm, das System.IO.File.ReadAllText("secret.txt") aufruft, letztendlich einen Windows-Systemaufruf wie NtReadFile aufrufen, um die Datei zu lesen, und ein Malware-Programm, das versucht, sich in einen anderen Prozess einzuschleusen, könnte System.Diagnostics.Process.Start() verwenden, was letztendlich CreateProcessW in Windows aufruft.

Verfolgen von Systemaufrufen zur Aufdeckung von Verhaltensweisen 

Ein Reverse Engineer kann die Verschleierung völlig ignorieren und stattdessen Systemaufrufe überwachen, um herauszufinden, was ein Programm tut. Dies geschieht mit Tools wie: 

  • Process Monitor (ProcMon) - Zeigt an, mit welchen Dateien, Registry-Schlüsseln und Netzwerkverbindungen ein Programm interagiert. 
  • WinDbg - Ein Debugger, der laufende .NET-Anwendungen auf einer niedrigen Ebene untersuchen kann. 
  • API Monitor - Erfasst API-Aufrufe, die von einer .NET-Anwendung gemacht werden. 

Da alle Programme irgendwann mit dem Betriebssystem interagieren müssen, kann ein Reverse Engineer Systemaufrufe verfolgen, um herauszufinden, was wirklich passiert. Bei .NET-Anwendungen ist dies sogar noch einfacher, da der Code in einer Zwischenform verbleibt, was die Dekompilierung vereinfacht. 

Ganz gleich, wie gut ein Programm seine Logik zu verbergen versucht, es muss immer noch das Betriebssystem bitten, die eigentliche Arbeit zu erledigen - und genau hier kann ein Reverse Engineer ansetzen.

Warum dies wichtig ist

Reverse Engineering und Entschleierung sind aus vielen Gründen relevant, aber besonders wichtig sind sie in Bezug auf IT-Sicherheit. Um den zeitlichen Ablauf eines Cybersicherheitsvorfalls vollständig zu verstehen, ist es unerlässlich, die Funktionalitäten aller beteiligten Tools zu kennen. Dies ist nicht nur wichtig, um den Schaden oder die Datenexfiltrationsmöglichkeiten des Angriffs zu beurteilen, sondern vor allem in der Aufräumphase, um festzustellen, ob Hintertüren zurückgelassen wurden. 

Wenn Sie kürzlich eine Software in Ihrem Netzwerk gefunden haben, von dem Sie vermuten, dass es bösartig sein könnte, helfen Ihnen unsere geschulten Reverse-Engineering-Expertinnen und -Experten von BDO Cyber Security gerne bei der Untersuchung und geben Ihnen Empfehlungen und Maßnahmen, die Sie auf der Grundlage der von Ihnen bereitgestellten Probe ergreifen können.