
Ein automatischer Entpacker und Logger für auf das DotNet Framework abzielende Dateien
Ein automatischer Unpacker und Logger für .NET Framework-basierte Dateien! Dieses Tool wurde auf der Black Hat USA 2022 vorgestellt. Auf der Black Hat Asia 2023 wurden DotDumperGUI und DotDumperNative zusammen mit Version 1.1-stable von DotDumper veröffentlicht. Diese drei Tools sind für den gemeinsamen Einsatz gedacht, wobei DotDumper 1.1-stable die DLLs von DotDumperNative benötigt, während DotDumperGUI als grafische Benutzeroberfläche zum Öffnen und Filtern der JSON-Ausgabe von DotDumper-Durchläufen dient.
Die automatische Erkennung und Klassifizierung einer beliebigen Datei auf zuverlässige Weise wird oft als der heilige Gral der Malware-Analyse angesehen. Die Hindernisse und Mühen, dorthin zu gelangen, sind zahlreich, weshalb die Erstellung eines solchen Systems hoch geschätzt wird. Bei .NET-basierten Binärdateien zielt unser neues Open-Source-Tool DotDumper darauf ab, bei mehreren entscheidenden Schritten auf dem Weg zu helfen: Protokollierung von (In-Memory-)Aktivitäten, Dumping interessanter Speichersegmente und Extrahieren von Merkmalen aus der gegebenen Probe.
Kurz gesagt, das manuelle Unpacking ist ein mühsamer Prozess, der für Analysten unverhältnismäßig viel Zeit in Anspruch nimmt. Obfuskierte Binärdateien erhöhen die Zeit, die ein Analyst für das Unpacking einer Datei aufwenden muss, weiter. Betrachtet man dies in großem Maßstab, benötigen Unternehmen zahlreiche Analysten, die täglich Malware sezieren, wahrscheinlich in Kombination mit einer skalierbaren Sandbox. Die wertvolle Zeit, die verloren geht, könnte stattdessen genutzt werden, um sich in interessante Kampagnen oder Proben zu vertiefen, um neue Bedrohungen aufzudecken, anstatt sich mit der alltäglichen, weit verbreiteten generischen Malware zu befassen. Schließlich suchen Analysten nach den wenigen Nadeln im Heuhaufen.
Welchen Unterschied macht DotDumper also? Die Ausführung einer .NET-basierten Malware-Probe über DotDumper liefert Protokolldateien von entscheidenden, kontextualisierenden und häufigen Funktionsaufrufen in drei Formaten (menschenlesbares Klartext, JSON und XML) sowie Kopien nützlicher In-Memory-Segmente. Dadurch kann ein Analyst das Funktionsaufruf-Protokoll überfliegen. Zudem können die gedumpten Dateien gescannt werden, um sie zu klassifizieren, was zusätzliche Einblicke in die Malware-Probe und die darin enthaltenen Daten bietet. Dies verkürzt die für Triage- und Incident-Response-Prozesse entscheidende Zeit und schafft Zeit für SOC-Analysten und Forscher für anspruchsvollere Analyseanforderungen.
Um die kontextualisierenden Funktionsaufrufe und deren Ergebnisse zu protokollieren und zu dumpen, verwendet DotDumper eine Mischung aus Reflexion und verwalteten Hooks, die alle in reinem C# geschrieben sind. Im Folgenden werden die wichtigsten Funktionen hervorgehoben und erläutert, in Kombination mit Auszügen aus den Ergebnissen von DotDumper zu einer gepackten AgentTesla-Stealer-Probe, deren Hashes unten aufgeführt sind.
| Hash-Typ | Hash-Wert |
|---|---|
| SHA-256 | b7512e6b8e9517024afdecc9e97121319e7dad2539eb21a79428257401e5558d |
| SHA-1 | c10e48ee1f802f730f41f3d11ae9d7bcc649080c |
| MD-5 | 23541daadb154f1f59119952e7232d6b |
DotDumper ist über eine Befehlszeilenschnittstelle mit einer Vielzahl von Argumenten zugänglich. Das folgende Bild zeigt das Hilfemenü. Beachten Sie, dass nicht alle Argumente besprochen werden, sondern hauptsächlich die am häufigsten verwendeten.

Die Mindestanforderung zum Ausführen einer Probe ist die Angabe des Arguments „-file“ zusammen mit einem Dateinamen oder Dateipfad. Wenn ein vollständiger Pfad angegeben wird, wird dieser verwendet. Wenn ein Dateiname angegeben wird, wird das aktuelle Arbeitsverzeichnis sowie der Ordner des DotDumper-Ausführungsverzeichnisses überprüft.
Sofern kein Verzeichnisnamen angegeben wird, wird der Ordnername „-log“ gleich dem Dateinamen der Probe ohne Erweiterung (falls vorhanden) gesetzt. Der Ordner befindet sich im selben Ordner wie DotDumper, in dem die Protokolle und gedumpten Dateien gespeichert werden.
Im Falle einer Bibliothek oder eines alternativen Einstiegspunkts in eine Binärdatei muss der Einstiegspunkt mit „-overrideEntry true“ überschrieben werden. Zusätzlich muss die vollständig qualifizierte Klasse, einschließlich des Namespace, mit „-fqcn My.NameSpace.MyClass“ angegeben werden. Dies teilt DotDumper mit, welche Klasse ausgewählt werden soll, aus der der angegebene Funktionsname (mittels „-functionName MyFunction“) abgerufen wird.
Wenn die ausgewählte Funktion Argumente erfordert, muss die Anzahl der Argumente mit „-argc“ und die Anzahl der erforderlichen Argumente angegeben werden. Die Argumenttypen und -werte sind als „string|myValue int|9“ anzugeben. Beachten Sie, dass bei Verwendung von Leerzeichen in den Werten das Argument in der Befehlszeilenschnittstelle in Anführungszeichen gesetzt werden muss, um sicherzustellen, dass es als einzelnes Argument übergeben wird.
Andere weniger häufig verwendete Optionen wie „-raceTime“ oder „-deprecated“ sind in ihren Standardeinstellungen sicher, könnten jedoch aufgrund von Änderungen im .NET Framework in Zukunft angepasst werden müssen. Sie sind derzeit in der Befehlszeilenschnittstelle verfügbar, um bei Bedarf einfache Änderungen zu ermöglichen, selbst wenn zu diesem Zeitpunkt eine ältere Version von DotDumper verwendet wird.
Protokollierung und Dumping sind die beiden Kernfunktionen von DotDumper. Um die für die Analyse benötigte Zeit zu minimieren, sollte die Protokollierung dem Analysten Kontext bieten. Dies wird erreicht, indem dem Analysten für jeden protokollierten Funktionsaufruf die folgenden Informationen bereitgestellt werden:
Beachten Sie, dass für jede gedumpte Datei der Dateiname dem SHA-256-Hash der Datei entspricht.
Zur Verdeutlichung wird unten ein Auszug aus einem Protokoll gezeigt. Der Auszug zeigt die Details für die zuvor erwähnte AgentTesla-Probe, bei der sie die zweite Stufe mit .NETs Assembly.Load-Funktion lädt.
![Das Protokoll für einen abgefangenen Assembly.Load(byte[] rawAssembly)-Funktionsaufruf](images/assembly_load.png)
Zunächst wird die lokale Systemzeit zusammen mit dem Rückgabetyp, dem Namen und den Argumenten der ursprünglichen Funktion angegeben. Zweitens wird die Stack-Trace gezeigt, die zeigt, dass die Hauptfunktion der Probe zu einem Konstruktor führt, die Komponenten initialisiert und zwei benutzerdefinierte Funktionen aufruft. Die Assembly.Load-Funktion wurde innerhalb von „NavigationLib.TaskEightBestOil.GGGGGGGGGGGGGGGGGGGG(String str)“ aufgerufen. Dies bietet dem Analysten Kontext, um den Code um diesen Aufruf herum zu finden, falls dies von Interesse ist.
Dann werden Informationen zur Aufrufreihenfolge der Assembly gegeben. Je mehr Stufen geladen werden, desto komplexer wird es, nachzuvollziehen, über welche Stufen der Aufruf zustande kam. Normalerweise erwartet man, dass eine Stufe die nächste lädt, aber in einigen Fällen verwenden spätere Stufen frühere Stufen in einer nichtlinearen Reihenfolge. Zusätzlich werden Informationen zur ursprünglichen Assembly bereitgestellt, um die Daten für den Analysten weiter anzureichern.
Als nächstes wird der übergeordnete Hash angegeben. Die übergeordnete Stufe einer Stufe ist die vorherige Stufe, die in diesem Beispiel noch nicht vorhanden ist. Die neu geladene Stufe wird diese Stufe als ihre übergeordnete Stufe haben. Dies ermöglicht es dem Analysten, Ereignisse leichter zu korrelieren.
Schließlich werden der Rückgabetyp und -wert der Funktion sowie Typ, Name und Wert jedes Arguments, das an die gehookedte Funktion übergeben wird, gespeichert. Wenn eine Variable größer als 100 Bytes ist, wird sie stattdessen auf der Festplatte gespeichert. Ein Verweis wird dann in das Protokoll eingefügt, um auf die Datei zu verweisen, anstatt den Wert anzuzeigen. Der Schwellenwert wurde gewählt, um Probleme beim Drucken des Protokolls zu vermeiden, da einige Arrays tausende von Indizes groß sind.
Laut Microsofts Dokumentation wird Reflexion am besten zusammengefasst als „[...] stellt Objekte bereit, die Assemblys, Module und Typen kapseln“. Kurz gesagt, dies ermöglicht die dynamische Erstellung und den Aufruf von .NET-Klassen und -Funktionen aus der Malware-Probe. DotDumper enthält einen reflektiven Lader, der es einem Analysten ermöglicht, sowohl ausführbare Dateien als auch Bibliotheken zu laden und zu analysieren, solange sie auf dem .NET Framework basieren.
Um den Lader zu nutzen, muss man sich dafür entscheiden, den Einstiegspunkt in der Befehlszeilenschnittstelle zu überschreiben, die Klasse (einschließlich des Namespace, in dem sie sich befindet) und den Funktionsnamen innerhalb einer bestimmten Datei anzugeben. Optional können der angegebenen Funktion Argumente für alle nativen Typen und deren Arrays bereitgestellt werden. Beispiele für native Typen sind int, string, char und Arrays wie int[], string[] und char[]. Alle Argumente sind über die Befehlszeilenschnittstelle anzugeben, wobei sowohl der Typ als auch der Wert spezifiziert werden müssen.
Wenn der Einstiegspunkt nicht überschrieben wird, wird der Standardeinstiegspunkt verwendet. Standardmäßig wird ein leeres String-Array an die Hauptfunktion der Probe übergeben, als ob die Probe ohne Argumente ausgeführt würde.
Zusätzlich wird Reflexion oft von Loadern verwendet, um eine bestimmte Funktion in einer bestimmten Klasse der nächsten Stufe aufzurufen. Manchmal werden auch Argumente weitergegeben, die später zur Entschlüsselung einer Ressource verwendet werden. In der zuvor erwähnten AgentTesla-Probe spielt sich genau dieses Szenario ab. Die aufrufbezogenen Hooks von DotDumper protokollieren diese Vorkommnisse, wie unten zu sehen ist.

Der Funktionsname in der ersten Zeile ist keine interne Funktion des .NET Frameworks, sondern ein Aufruf einer spezifischen Funktion in der zweiten Stufe. Die Typen und Namen der drei Argumente sind in der Funktionssignatur aufgelistet. Ihre Werte finden Sie im Abschnitt mit den Funktionsargumentinformationen. Dies würde es einem Analysten ermöglichen, die zweite Stufe in einem benutzerdefinierten Loader mit den angegebenen Werten für die Argumente zu laden oder dies sogar mit DotDumper zu tun, indem er die zuvor gedumpte Stufe lädt und die Argumente bereitstellt.
Bevor wir auf verwaltete Hooks eingehen, muss man verstehen, wie Hooks funktionieren. Es gibt zwei Hauptvariablen, die hier zu berücksichtigen sind: die Zielfunktion und eine kontrollierte Funktion, die als Hook bezeichnet wird. Vereinfacht gesagt wird der Speicher an der Zielfunktion (z. B. Assembly.Load) so verändert, dass er stattdessen zum Hook springt. Dadurch wird der Ausführungsfluss des Programms umgeleitet. Der Hook kann dann beliebige Aktionen ausführen, optional die ursprüngliche Funktion aufrufen, wonach er die Ausführung zusammen mit einem Rückgabewert (falls erforderlich) an den Aufrufer zurückgibt. Das folgende Diagramm veranschaulicht diesen Prozess.

Zu wissen, was Hooks sind, ist entscheidend, um zu verstehen, was verwaltete Hooks sind. Verwalteter Code wird in einer virtuellen und verwalteten Umgebung ausgeführt, wie der .NET-Laufzeitumgebung oder der Java Virtual Machine. Die Speicheradresse, an der sich die verwaltete Funktion befindet, zu ermitteln, unterscheidet sich von einer nicht verwalteten Sprache wie C. Sobald die korrekten Speicheradressen für beide Funktionen ermittelt wurden, kann der Hook gesetzt werden, indem direkt auf den Speicher zugegriffen wird, wobei unsicheres C# zusammen mit .NETs Interoperabilitätsdienst zum Aufrufen von nativen Windows-API-Funktionen verwendet wird.
Seit DotDumper v1.1-stable kann DotDumper auch nicht verwaltete (oder native) Funktionen hooken. Die Funktionsumleitung kann jede Kombination von und zu verwalteten und nicht verwalteten Funktionen sein, mit einer wichtigen Einschränkung. Jede nicht verwaltete Funktion, die einen verwalteten Hook verwendet, kann die Werte auf dem Stack nicht richtig lesen. Daher ist eine native Komponente erforderlich (genannt DotDumperNative). Diese Komponente kommuniziert über eine Named Pipe mit DotDumper selbst und nutzt so dessen zentralisiertes Protokollierungssystem, während sie nicht verwaltete Aufrufe mit Zugriff auf die auf dem Stack gefundenen Funktionsargumente abfängt.
Da DotDumper in reinem C# ohne externe Abhängigkeiten geschrieben ist, kann man das Framework einfach mit Visual Studio erweitern. Der Code ist in diesem Blog, auf GitHub, in Klassen, in Funktionen und inline im Quellcode dokumentiert. Dies ermöglicht es in Kombination mit dem klaren Benennungsschema jedem, das Tool nach Belieben zu modifizieren und minimiert die Zeit und den Aufwand, die man benötigt, um das Tool zu verstehen. Stattdessen können sich Entwickler und Analysten gleichermaßen auf die Verbesserung des Tools konzentrieren.
Obwohl die Sandbox-Timeout-Unterstützungsfunktion weder geändert noch modifiziert wurde, wurde sie zuvor nicht dokumentiert. Da DotDumper die übergebene Datei ausführt, wird die Ausführung fortgesetzt, bis die Probe beendet wird. Malware tritt oft in einen „Warte“-Zustand ein, in dem eine bestimmte Bedingung erfüllt sein muss, bevor sie reaktiviert wird, oder die Malware setzt die Ausführung in einem anderen Prozess fort (mit Hilfe von Prozessinjektion). Zum Beispiel kehrt ein neu erstellter Thread in einem ausgehöhlten Prozess erst zurück, wenn er beendet ist.
Um Stillstand zu vermeiden, verfügt DotDumper über einen Stagnations-Handler. Jedes Mal, wenn die Ergebnisse eines Hooks protokolliert werden, wird ein Zähler erhöht. Wenn dieser Zähler nach drei aufeinanderfolgenden 20-Sekunden-Intervallen unverändert bleibt, geht DotDumper davon aus, dass ein Stagnationszustand vorliegt. In diesem Fall wird die Systemzeit auf den 30.12.2200 12:00 gesetzt, um die Sandbox zum Timeout zu zwingen, woraufhin der Analyst über den Logger benachrichtigt wird und sich das Tool selbst beendet. Das Sandbox-Timeout tritt ein, wenn die vergangene Zeit vom aktuellen Datum bis zum neu gesetzten Datum die üblichen 5 oder 10 Minuten überschreitet. Selbst wenn die Sandbox-Laufzeit auf Stunden oder Tage eingestellt ist, wird sie überschritten.
Der Grund für die vorzeitige Beendigung der Analyse ist Zeit- und Ressourceneinsparung, da eine Übermittlung mit einem 10-minütigen Timeout möglicherweise nur 2 Minuten zur Ausführung benötigt. Mit dem Stagnations-Handler könnte man also mehrere Proben im gleichen Zeitraum ausführen. Ein Überblick über die Logik des Stagnations-Handlers ist unten dargestellt.

Nachdem Ziel und Funktionen von DotDumper klar sind, könnte es den Anschein haben, dass es Überschneidungen mit bekannten öffentlich verfügbaren Tools wie ILSpy, dnSpyEx, de4dot oder pe-sieve gibt. Beachten Sie, dass nicht die Absicht besteht, zu behaupten, dass ein Tool besser sei als ein anderes, sondern vielmehr, wie sich die Tools unterscheiden.
DotDumpers Ziel ist es, entscheidende, kontextualisierende und häufige Funktionsaufrufe aus .NET-basierten Proben zu protokollieren und zu dumpen. ILSpy ist ein .NET-Disassembler und -Decompiler, erlaubt jedoch nicht die Ausführung der Datei. dnSpyEx (und sein Vorgänger dnSpy) verwenden ILSpy als Disassembler- und Decompiler-Komponente und fügen einen Debugger hinzu. Dies ermöglicht die manuelle Inspektion und Manipulation des Speichers. de4dot wird ausschließlich zum Deobfuskatieren von .NET-Binärdateien verwendet, um die Lesbarkeit des Codes für das menschliche Auge zu verbessern. Das letzte Tool in diesem Vergleich, pe-sieve, dient der Erkennung und dem Dumping von Malware aus laufenden Prozessen, unabhängig von der verwendeten Programmiersprache. Die folgende Tabelle bietet eine grafische Übersicht der oben genannten Tools.

DotDumper wird ständig überprüft und weiterentwickelt, wobei sich alles auf zwei Hauptinteressensbereiche konzentriert: Fehlerbehebung und Hinzufügen neuer Funktionen. Während der Entwicklung wurde der Code getestet, aber aufgrund der Injektion von Hooks in die Funktionen des .NET Frameworks, die sich ändern können, ist es sehr gut möglich, dass es Fehler im Code gibt. Jeder, der auf einen Fehler stößt, wird gebeten, ein Issue im GitHub-Repository zu eröffnen, welches dann überprüft wird. Die Vorschläge neuer Funktionen sind ebenfalls über das GitHub-Repository möglich. Für diejenigen mit einem GitHub-Konto oder für diejenigen, die lieber nicht öffentlich interagieren, können Sie mir gerne eine private Nachricht auf meinem Twitter senden.
Natürlich, wenn Sie DotDumper während einer Analyse oder auf kreative Weise verwendet haben, zögern Sie nicht, sich öffentlich oder privat zu melden! Es gibt nichts Schöneres, als von der Verwendung eines selbstgebauten Tools zu hören!
Es ist noch mehr für DotDumper geplant, und eine Aktualisierung wird an die Community gesendet, sobald sie verfügbar ist!