BLOG

Reverse Engineering von JS anhand von Beispielen

F5 Miniaturansicht
F5
Veröffentlicht am 02. Januar 2019

flatmap-stream Nutzlast A

Im November wurde das NPM-Paket Event-Stream über eine bösartige Abhängigkeit, Flatmap-Stream, ausgenutzt. Die ganze Angelegenheit wurde hier niedergeschrieben und der Schwerpunkt dieses Beitrags liegt darauf, sie als Fallstudie für das Reverse Engineering von JavaScript zu verwenden. Die 3 mit Flatmap-Stream verknüpften Payloads sind einfach genug, um leicht darüber zu schreiben, und komplex genug, um interessant zu sein. Obwohl es für das Verständnis dieses Beitrags nicht unbedingt erforderlich ist, die Vorgeschichte dieses Vorfalls zu kennen, werde ich Annahmen treffen, die möglicherweise nicht offensichtlich sind, wenn man mit den Einzelheiten nicht einigermaßen vertraut ist.

Das Reverse Engineering des Großteils von JavaScript ist einfacher als bei den binären ausführbaren Dateien, die Sie möglicherweise auf Ihrem Desktop-Betriebssystem ausführen – schließlich liegt der Quellcode direkt vor Ihnen –, doch JavaScript-Code, der so konzipiert ist, dass er schwer zu verstehen ist, durchläuft häufig mehrere Verschleierungsdurchgänge, um seine Absicht zu verschleiern. Ein Teil dieser Verschleierung ist auf die sogenannte „Minimierung“ zurückzuführen. Dabei handelt es sich um den Vorgang, die Gesamtbytezahl Ihres Quellcodes aus Platzgründen so weit wie möglich zu reduzieren. Dabei werden Variablen auf einstellige Bezeichner gekürzt und Ausdrücke wie „true“ in etwas Kürzeres, aber Gleichwertiges wie „!0“ übersetzt. Die Minimierung ist aufgrund ihres Ursprungs in Webbrowsern größtenteils nur im JavaScript-Ökosystem zu beobachten und kommt aufgrund der Wiederverwendung von Tools gelegentlich in Node-Paketen vor. Sie ist nicht als Sicherheitsmaßnahme gedacht. Eine grundlegende Umkehrung gängiger Minimierungs- und Verschleierungstechniken finden Sie mit dem Unminify -Tool von Shape. Spezielle Verschleierungsdurchläufe können von Tools stammen, die zur Verschleierung entwickelt wurden, oder werden manuell vom Entwickler durchgeführt.

Der erste Schritt besteht darin, die isolierte Quelle zur Analyse in die Hände zu bekommen. Das Flatmap-Stream-Paket wurde speziell so gestaltet, dass es harmlos aussieht, abgesehen von einer bösartigen Nutzlast, die nur in einer Version des Pakets enthalten ist, nämlich in der Version 0.1.1. Sie können die Änderungen an der Quelle schnell sehen, indem Sie zwischen Version 0.1.2 und Version 0.1.1 unterscheiden oder einfach zwischen den URLs in zwei Registerkarten wechseln. Im weiteren Verlauf des Beitrags bezeichnen wir die angehängte Quelle als Nutzlast A. Unten sehen Sie die formatierte Quelle der Nutzlast A.

Reverse Engineering anhand von Beispielen Bild 1

Das Wichtigste zuerst: Führen Sie niemals schädlichen Code aus (außer in isolierten Umgebungen). Ich habe meine eigenen Tools geschrieben, die mir dabei helfen, Code mithilfe der Shift-Suite aus Parsern und JavaScript-Transformatoren dynamisch zu refaktorisieren. Sie können zum Verfolgen dieses Beitrags jedoch auch eine IDE wie Visual Studio Code verwenden.

Beim Reverse Engineering von JavaScript ist es sinnvoll, den Aufwand für gedankliches Jonglieren auf ein Minimum zu beschränken. Dies bedeutet, dass Sie alle Ausdrücke oder Anweisungen entfernen, die keinen unmittelbaren Mehrwert bieten, und dass Sie die DRYness von Code, der automatisch oder manuell optimiert wurde, rückgängig machen. Da wir das JavaScript statisch analysieren und die Ausführung in unseren Köpfen verfolgen, ist es umso wahrscheinlicher, dass Sie den Überblick verlieren, je tiefer Ihr mentaler Stapel wird.

Eine der einfachsten Möglichkeiten besteht darin, die Minimierung von Variablen aufzuheben, denen globale Eigenschaften wie „require“ und „process“ zugewiesen werden, wie in den Zeilen 3 und 4.

 

Reverse Engineering anhand von Beispielen Bild 2

 

Sie können dies mit jeder IDE tun, die Refactoring-Funktionen bietet (normalerweise durch Drücken von „F2“ über einem Bezeichner, den Sie umbenennen möchten). Danach sehen wir eine Funktionsdefinition, e, die scheinbar einfach eine Hex-Zeichenfolge dekodiert.

 

Reverse Engineering anhand von Beispielen Bild 3

 

Die erste interessante Codezeile scheint eine Datei zu importieren, die aus dem Ergebnis der Funktion e stammt, die die Zeichenfolge „2e2f746573742f64617461“ dekodiert.

 

Reverse Engineering anhand eines Beispiels Bild 4

Es kommt äußerst häufig vor, dass absichtlich verschleiertes JavaScript jeden wörtlichen Zeichenfolgenwert verbirgt, sodass niemand, der einen flüchtigen Blick darauf wirft, durch besonders ominöse Zeichenfolgen oder Eigenschaften, die deutlich sichtbar sind, gewarnt wird. Die meisten Entwickler erkennen, dass dies eine sehr niedrige Hürde ist, sodass Sie häufig auf trivial umkehrbare Kodierungen stoßen, und das ist hier nicht anders. Die e-Funktion kehrt Hex-Strings einfach um und Sie können dies manuell über ein Online-Tool oder mit Ihrer eigenen Komfortfunktion tun. Auch wenn Sie sicher sind, dass Sie verstehen, was die Funktion e tut, ist es dennoch eine gute Idee, sie nicht mit Eingaben aus einer schädlichen Datei auszuführen (auch wenn Sie sie extrahieren), da Sie nicht garantieren können, dass der Angreifer keine Sicherheitslücke gefunden hat, die durch die Daten ausgelöst wird.

Nachdem wir diese Zeichenfolge umgekehrt haben, sehen wir, dass das Skript eine Datendatei „./test/data“ einschließt, die sich im verteilten npm-Paket befindet.

 

Reverse Engineering anhand eines Beispiels Bild 5

Nachdem wir n in data umbenannt und Aufrufe von e(n[2]) in e(n[9]) entschlüsselt haben, erhalten wir ein besseres Bild davon, womit wir es hier zu tun haben.

 

Reverse Engineering anhand eines Beispiels Bild 6

Es ist auch leicht zu erkennen, warum diese Zeichenfolgen versteckt waren. Das Auffinden von Verweisen auf die Entschlüsselung in einer einfachen Flatmap-Bibliothek wäre ein sicheres Zeichen dafür, dass etwas ganz und gar nicht stimmt.

Von hier aus sehen wir, dass das Skript die „Crypto“-Bibliothek von node.js importiert, und nachdem wir die APIs nachgeschlagen haben , stellen wir fest, dass das zweite Argument für createDecipher, hier o, das zum Entschlüsseln verwendete Kennwort ist. Jetzt können wir dieses Argument und die folgenden Rückgabewerte basierend auf der API in sinnvolle Namen umbenennen. Jedes Mal, wenn wir ein neues Puzzleteil finden, ist es wichtig, es durch ein Refactoring oder einen Kommentar zu verewigen, selbst wenn es sich um eine umbenannte Variable handelt, die trivial erscheint. Wenn man sich stundenlang durch fremden Code wühlt, kommt es sehr häufig vor, dass man die Stelle verliert, abgelenkt wird oder aufgrund einer fehlerhaften Umgestaltung zurückgehen muss. Die Verwendung von Git zum Speichern von Prüfpunkten während einer Refaktorierung ist ebenfalls wertvoll, aber ich überlasse diese Entscheidung Ihnen. Der Code sieht nun wie folgt aus, wobei die Funktion e gelöscht wurde, da sie zusammen mit der Anweisung if (!o) {... nicht mehr verwendet wird, da sie keinen Mehrwert für die Analyse bietet.

 

Reverse Engineering anhand eines Beispiels Bild 7

Ihnen wird auch auffallen, dass ich f in newModuleInstance umbenannt habe. Bei so kurzem Code ist das nicht kritisch, aber bei Code, der Hunderte von Zeilen lang sein kann, ist es wichtig, dass alles so klar wie möglich ist.

Jetzt ist Nutzlast A weitgehend deobfuskiert und wir können sie durchgehen, um zu verstehen, was sie tut.

Zeile 3 importiert unsere externen Daten.

 

Reverse Engineering anhand eines Beispiels Bild 8

Zeile 4 holt ein Passwort aus der Umgebung. „process.env“ ermöglicht Ihnen den Zugriff auf Variablen innerhalb eines Node-Skripts und „npm_package_description“ ist eine Variable, die npm, der Paketmanager von Node, festlegt, wenn Sie Skripte ausführen, die in einer Datei „package.json“ definiert sind.

 

Reverse Engineering anhand von Beispielen Bild 9

Zeile 5 erstellt eine Decipher-Instanz mit dem Wert aus npm_package_description als Passwort. Dies bedeutet, dass die verschlüsselte Nutzlast nur entschlüsselt werden kann, wenn dieses Skript über npm ausgeführt wird und für ein bestimmtes Projekt ausgeführt wird, das in seinem package.json über ein bestimmtes Beschreibungsfeld verfügt. Das wird hart.

 

Reverse Engineering anhand eines Beispiels Bild 10

Die Zeilen 6 und 7 entschlüsseln das erste Element in unserer externen Datei und speichern es in der Variable „decrypted“

 

Reverse Engineering anhand eines Beispiels Bild 11

 

Die Zeilen 8–11 erstellen ein neues Modul und speisen die entschlüsselten Daten dann in die nicht dokumentierte Methode _compile ein. Dieses Modul exportiert dann das zweite Element unserer externen Datendatei. module.exports ist der Mechanismus des Knotens zum Offenlegen von Daten von einem Modul für ein anderes, daher legt newModuleInstance.exports(data[1]) eine zweite verschlüsselte Nutzlast offen, die in unserer externen Datendatei gefunden wurde.

 

Reverse Engineering anhand eines Beispiels Bild 12

 

An diesem Punkt haben wir verschlüsselte Daten, die nur mit einem Passwort entschlüsselt werden können, das sich irgendwo in einer Datei „package.json“ befindet und deren entschlüsselte Daten in die Methode _compile eingespeist werden. Nun stehen wir vor einem Problem: Wie entschlüsselt man Daten, deren Passwort unbekannt ist? Dies ist keine triviale Frage. Wenn es einfach wäre, die AES256-Verschlüsselung mit Brute-Force-Methode zu erzwingen, hätten wir mehr Probleme als die Übernahme eines NPM-Pakets. Glücklicherweise haben wir es nicht mit einem völlig unbekannten Satz möglicher Passwörter zu tun, sondern nur mit einer beliebigen Zeichenfolge, die zufällig irgendwo in ein package.json eingegeben wurde. package.json-Dateien sind ursprünglich das Dateiformat für NPM-Paketmetadaten, daher können wir auch gleich beim offiziellen NPM-Register beginnen. Glücklicherweise gibt es ein NPM-Paket, das uns einen Stream aller Paketmetadaten liefert.

Reverse Engineering anhand eines Beispiels Bild 13

Es gibt keine Garantie, dass sich unsere Zieldatei in einem NPM-Paket befindet. Viele Nicht-NPM-Projekte verwenden package.json, um Konfigurationen für node-basierte Tools zu speichern, und die Beschreibungen von package.json können sich von Version zu Version ändern, aber es ist ein guter Ausgangspunkt. Es ist möglich, diese Nutzlast mit mehreren Schlüsseln zu entschlüsseln, was zu unverständlichem Kauderwelsch führen würde. Daher benötigen wir eine Möglichkeit, unsere entschlüsselte Nutzlast während dieses Brute-Force-Prozesses zu validieren. Da es sich um etwas handelt, das an Module.prototype._compile weitergeleitet wird, das wiederum an vm.runInThisContext weitergeleitet wird, können wir vernünftigerweise davon ausgehen, dass die Ausgabe JavaScript ist und wir eine beliebige Anzahl von JavaScript-Parsern zum Validieren der Daten verwenden können. Wenn unser Passwort fehlschlägt oder wenn es erfolgreich ist, unser Parser aber einen Fehler ausgibt, müssen wir zum nächsten package.json wechseln. Praktischerweise hat Shape Security einen eigenen Satz von JavaScript-Parsern für die Verwendung in JavaScript- und Java-Umgebungen erstellt. Das verwendete Brute-Force-Skript ist hier:

 

Reverse Engineering anhand eines Beispiels Bild 14

Nachdem wir dies 92,1 Sekunden lang ausgeführt und 740543 Pakete verarbeitet haben, erhalten wir unser Passwort – „Eine sichere Bitcoin-Wallet“ –, das die unten enthaltene Nutzlast erfolgreich dekodiert:

 

Reverse Engineering anhand eines Beispiels Bild 15

Das war ein Glück. Was ein monströses Brute-Force-Problem hätte sein können, erforderte letztendlich weniger als eine Million Iterationen. Bei dem betroffenen Paket mit dem fraglichen Schlüssel handelte es sich letztendlich um die Client-Anwendung der Bitcoin-Wallet Copay. Die nächsten beiden Payloads tauchen tiefer in die Anwendung selbst ein, und da es bei der Zielanwendung um die Speicherung von Bitcoins geht, können Sie wahrscheinlich erraten, worauf das hinausläuft.

Wenn Sie solche Themen interessant finden und eine Analyse der anderen beiden Payloads oder zukünftiger Angriffe lesen möchten, dann markieren Sie diesen Beitrag mit „Gefällt mir“ oder lassen Sie es mich auf Twitter unter @jsoverson wissen.