flatmap-stream-Nutzlast A
Im November wurde das npm-Paket event-stream über eine bösartige Abhängigkeit namens flatmap-stream kompromittiert. Der gesamte Vorfall wurde hier dokumentiert. In diesem Beitrag soll er als Fallstudie für das Reverse Engineering von JavaScript dienen. Die drei mit flatmap-stream verbundenen Payloads sind einfach genug, um sie verständlich zu beschreiben, und zugleich komplex genug, um interessant zu sein. Um diesen Beitrag zu verstehen, ist es zwar nicht unbedingt erforderlich, die Hintergründe des Vorfalls zu kennen, doch ich werde einige Annahmen treffen, die möglicherweise nicht offensichtlich sind, wenn Sie mit den Einzelheiten nicht zumindest einigermaßen vertraut sind.
Das Reverse Engineering von JavaScript ist meist unkomplizierter als das von Binärdateien, die auf einem Desktop-Betriebssystem ausgeführt werden – schließlich liegt der Quellcode direkt vor einem. JavaScript-Code, der bewusst schwer verständlich gestaltet wurde, durchläuft jedoch häufig mehrere Verschleierungsschritte, um seine eigentliche Absicht zu verbergen. Ein Teil dieser Verschleierung entsteht durch die sogenannte „Minifizierung“. Dabei wird die Gesamtgröße des Quellcodes so weit wie möglich reduziert, um Speicherplatz zu sparen. Dazu werden Variablennamen auf einzelne Zeichen verkürzt und Ausdrücke wie true in kürzere, aber gleichwertige Formen wie !0 umgewandelt. Die Minifizierung ist aufgrund der Ursprünge von JavaScript im Webbrowser weitgehend eine Besonderheit des JavaScript-Ökosystems. Wegen der Wiederverwendung entsprechender Werkzeuge findet man sie gelegentlich auch in node-Paketen; als Sicherheitsmaßnahme ist sie jedoch nicht gedacht. Für eine grundlegende Rückgängigmachung gängiger Minifizierungs- und Verschleierungstechniken empfiehlt sich das unminify-Tool von Shape. Spezielle Verschleierungsschritte können mithilfe eigens dafür entwickelter Tools oder manuell durch den Entwickler vorgenommen werden.
Der erste Schritt besteht darin, den isolierten Quellcode für die Analyse zu beschaffen. Das Paket flatmap-stream wurde gezielt so gestaltet, dass es harmlos wirkt. Eine Schadnutzlast war lediglich in einer einzigen Paketversion enthalten, nämlich Version 0.1.1. Die Änderungen am Quellcode lassen sich schnell erkennen, indem man Version 0.1.2 und Version 0.1.1 vergleicht oder einfach in zwei Tabs zwischen den URLs wechselt. Im weiteren Verlauf des Beitrags bezeichnen wir den angehängten Quellcode als Nutzlast A. Nachfolgend ist der formatierte Quellcode von Nutzlast A zu sehen.

Das Wichtigste zuerst: FÜHREN SIE NIEMALS SCHADCODE AUS (außer in isolierten Umgebungen). Ich habe eigene Tools entwickelt, mit denen ich Code mithilfe der Shift-Suite aus Parsern und JavaScript-Transformatoren dynamisch umstrukturieren kann. Sie können jedoch auch eine IDE wie Visual Studio Code verwenden, um die Schritte in diesem Beitrag nachzuvollziehen.
Beim Reverse Engineering von JavaScript ist es hilfreich, die geistige Belastung so gering wie möglich zu halten. Das bedeutet, alle Ausdrücke oder Anweisungen zu entfernen, die keinen unmittelbaren Mehrwert bieten, und außerdem das DRY-Prinzip bei automatisch oder manuell optimiertem Code rückgängig zu machen. Da wir JavaScript statisch analysieren und die Ausführung gedanklich nachvollziehen, steigt mit der Tiefe des mentalen Stacks auch die Wahrscheinlichkeit, den Überblick zu verlieren.
Eine der einfachsten Maßnahmen besteht darin, die Minifizierung von Variablen rückgängig zu machen, denen globale Eigenschaften wie require und process zugewiesen werden, wie etwa in den Zeilen 3 und 4.
Dies ist mit jeder IDE möglich, die Refactoring-Funktionen bietet (in der Regel durch Drücken von „F2“, während sich der Cursor auf einem Bezeichner befindet, den Sie umbenennen möchten). Anschließend sehen wir eine Funktionsdefinition namens e, die offenbar lediglich eine Hexadezimalzeichenfolge dekodiert.

Die erste interessante Codezeile scheint eine Datei zu importieren, deren Pfad sich aus der Dekodierung der Zeichenfolge „2e2f746573742f64617461“ durch die Funktion e ergibt.
Bei absichtlich verschleiertem JavaScript ist es äußerst üblich, sämtliche literalen Zeichenfolgenwerte unkenntlich zu machen, damit niemand schon bei einem flüchtigen Blick durch besonders verdächtige Zeichenfolgen oder offen sichtbare Eigenschaften alarmiert wird. Den meisten Entwicklern ist klar, dass dies nur eine sehr niedrige Hürde darstellt. Daher findet man häufig Codierungen, die sich mit minimalem Aufwand rückgängig machen lassen – und hier ist es nicht anders. Die Funktion e kehrt lediglich Hexadezimalzeichenfolgen um. Das können Sie manuell mit einem Online-Tool oder mit einer eigenen Hilfsfunktion erledigen. Selbst wenn Sie sicher sind, verstanden zu haben, was die Funktion e tut, sollten Sie sie nicht mit Eingaben aus einer schädlichen Datei ausführen (auch dann nicht, wenn Sie die Funktion extrahieren). Schließlich können Sie nicht ausschließen, dass der Angreifer eine Sicherheitslücke gefunden hat, die durch diese Daten ausgelöst wird.
Nachdem wir diese Zeichenfolge umgekehrt haben, sehen wir, dass das Skript eine Datendatei namens './test/data' einbindet, die sich im veröffentlichten npm-Paket befindet.

Nachdem wir n in data umbenannt und die Aufrufe von e(n[2]) bis e(n[9]) entschleiert haben, wird allmählich deutlicher, womit wir es hier zu tun haben.

Es ist auch leicht nachzuvollziehen, warum diese Zeichenfolgen verborgen wurden: Hinweise auf Entschlüsselung in einer einfachen Flatmap-Bibliothek wären ein untrügliches Zeichen dafür, dass etwas ganz und gar nicht stimmt.
Hier 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 von createDecipher, hier o, das zur Entschlüsselung verwendete Passwort ist. Nun können wir dieses Argument und die nachfolgenden Rückgabewerte anhand der API sinnvoll benennen. Jedes Mal, wenn wir ein neues Puzzleteil finden, ist es wichtig, diese Erkenntnis durch ein Refactoring oder einen Kommentar festzuhalten, selbst wenn es sich nur um die Umbenennung einer scheinbar unbedeutenden Variable handelt. Wenn man sich stundenlang durch fremden Code arbeitet, passiert es leicht, dass man den Überblick verliert, abgelenkt wird oder wegen eines fehlerhaften Refactorings zurückgehen muss. Auch git zum Speichern von Zwischenständen während eines Refactorings zu verwenden, ist sinnvoll, aber diese Entscheidung überlasse ich Ihnen. Der Code sieht nun wie folgt aus, wobei die Funktion e gelöscht wurde, da sie nicht mehr verwendet wird, ebenso wie die Anweisung if (!o) {..., da sie keinen Mehrwert für die Analyse bietet.

Außerdem werden Sie feststellen, dass ich f in newModuleInstance umbenannt habe. Bei einem so kurzen Code ist das nicht entscheidend, aber bei Code, der Hunderte von Zeilen lang sein kann, ist es wichtig, dass alles so klar wie möglich ist.
Nun ist Payload A weitgehend deobfuskiert, und wir können den Code Schritt für Schritt durchgehen, um zu verstehen, was er bewirkt.
Zeile 3 importiert unsere externen Daten.
Zeile 4 liest ein Passwort aus der Umgebung aus. process.env ermöglicht den Zugriff auf Variablen innerhalb eines Node-Skripts, und npm_package_description ist eine Variable, die npm, der Paketmanager von Node, setzt, wenn Sie in einer package.json-Datei definierte Skripte ausführen.
Zeile 5 erstellt eine Decipher-Instanz mit dem Wert aus npm_package_description als Passwort. Das bedeutet, dass die verschlüsselte Nutzlast nur entschlüsselt werden kann, wenn dieses Skript über npm und für ein bestimmtes Projekt ausgeführt wird, dessen package.json ein spezifisches Beschreibungsfeld enthält. Das dürfte schwierig werden.

Die Zeilen 6 und 7 entschlüsseln das erste Element in unserer externen Datei und speichern es in der Variablen „decrypted“.
Die Zeilen 8–11 erstellen ein neues Modul und übergeben anschließend die entschlüsselten Daten an die undokumentierte Methode _compile. Dieses Modul exportiert dann das zweite Element unserer externen Datendatei. module.exports ist der Mechanismus von Node, um Daten aus einem Modul für ein anderes bereitzustellen. Somit stellt newModuleInstance.exports(data[1]) eine zweite verschlüsselte Nutzlast bereit, die sich in unserer externen Datendatei befindet.

An diesem Punkt haben wir verschlüsselte Daten, die sich nur mit einem Passwort entschlüsseln lassen, das irgendwo in einer package.json zu finden ist. Die entschlüsselten Daten werden anschließend an die Methode _compile übergeben. Damit stehen wir vor einem Problem: Wie entschlüsselt man Daten, wenn das Passwort unbekannt ist? Das ist keine triviale Frage. Ließe sich eine aes256-Verschlüsselung einfach durch Ausprobieren knacken, hätten wir größere Probleme als die Übernahme eines npm-Pakets. Glücklicherweise haben wir es nicht mit einer völlig unbekannten Menge möglicher Passwörter zu tun, sondern lediglich mit einer beliebigen Zeichenfolge, die irgendwo in eine package.json eingetragen wurde. package.json-Dateien entstanden ursprünglich als Dateiformat für die Metadaten von npm-Paketen, daher können wir genauso gut beim offiziellen npm-Registry beginnen. Glücklicherweise gibt es ein npm-Paket, das uns einen Datenstrom mit sämtlichen Paketmetadaten liefert.

Es gibt keine Garantie dafür, dass sich unsere Zieldatei in einem npm-Paket befindet. Viele Projekte außerhalb von npm verwenden package.json, um Konfigurationen für Node-basierte Tools zu speichern, und die Beschreibungen in package.json können sich von Version zu Version ändern. Dennoch ist dies ein guter Ausgangspunkt. Diese Nutzlast lässt sich möglicherweise mit mehreren Schlüsseln entschlüsseln, wobei unverständlicher Zeichensalat entsteht. Daher benötigen wir eine Möglichkeit, unsere entschlüsselte Nutzlast während dieses Brute-Force-Vorgangs zu validieren. Da wir es mit etwas zu tun haben, das an Module.prototype._compile übergeben wird, das es wiederum an weiterleitet vm.runInThisContext, können wir vernünftigerweise davon ausgehen, dass die Ausgabe JavaScript ist, und zur Validierung der Daten einen beliebigen JavaScript-Parser verwenden. Wenn unser Passwort nicht funktioniert oder zwar funktioniert, unser Parser jedoch einen Fehler ausgibt, müssen wir mit der nächsten package.json fortfahren. Praktischerweise hat Shape Security eigene JavaScript-Parser für den Einsatz in JavaScript- und Java-Umgebungen entwickelt. Das verwendete Brute-Force-Skript ist hier zu finden:

Nachdem das Programm 92,1 Sekunden lang ausgeführt und 740543 Pakete verarbeitet hat, erhalten wir unser Passwort – „A Secure Bitcoin Wallet“ –, mit dem sich die unten aufgeführte Nutzlast erfolgreich entschlüsseln lässt:

Das war ein Glücksfall. Was zu einem gewaltigen Brute-Force-Problem hätte werden können, erforderte letztlich weniger als eine Million Durchläufe. Bei dem betroffenen Paket mit dem fraglichen Schlüssel handelte es sich um die Client-Anwendung der Bitcoin-Wallet Copay. Die nächsten beiden Payloads dringen tiefer in die Anwendung selbst vor. Da die Zielanwendung auf die Aufbewahrung von Bitcoins ausgerichtet ist, können Sie sich vermutlich denken, worauf das hinausläuft.
Wenn du Themen wie dieses interessant findest und eine Analyse der beiden anderen Payloads oder zukünftiger Angriffe lesen möchtest, dann gib diesem Beitrag unbedingt ein „Gefällt mir“ oder schreib mir auf Twitter unter @jsoverson.
Über den Autor
Ähnliche Blogbeiträge
F5 und Equinix bündeln ihre Kräfte, um die sichere Anwendungsbereitstellung in Multicloud-Umgebungen zu vereinfachen
F5 und Equinix ermöglichen es Unternehmen, sich weltweit sicher und zuverlässig mit ihren Partnern zu vernetzen.

API-Sicherheitsrisiken und -herausforderungen
Erfahren Sie, warum APIs für die moderne App-Entwicklung unverzichtbar sind und wie Sie sie vor den häufigsten Sicherheitslücken schützen. Profitieren Sie von Expertentipps und direkt umsetzbaren Strategien.
Ein Meilenstein für die Community Impact Grants von F5
F5 feiert das fünfjährige Bestehen seines innovativen Community Impact Grant-Programms, das gemeinnützige Organisationen, bei denen sich F5-Mitarbeitende ehrenamtlich engagieren, mit Fördermitteln in Höhe von 5.000 US-Dollar unterstützt.
Mit moderner, anpassungsfähiger und sicherer Gesundheits-IT Leben retten
Die IT-Investitionen im Gesundheitswesen tragen dazu bei, die schnellsten Wege zu einer besseren Versorgung zu finden, indem sie die medizinische Betreuung näher an die Patientinnen und Patienten bringen. Viele Gesundheitsunternehmen nutzen riesige Mengen an klinischen und anderen Gesundheitsdaten, um Entwicklungen zu verfolgen und übergreifende Trends in der gesamten Gesundheitslandschaft zu erkennen. Gleichzeitig nähern sie sich einer möglichst präzisen, individuell zugeschnittenen Versorgung an.
Verbesserung der Benutzer- und Administrationserfahrung durch sicheren hybriden Zugriff
Diese Kombination aus BIG-IP Access Policy Manager und Azure Active Directory ermöglicht Identitätsföderation, zentralisierten Benutzerzugriff (über Single Sign-on) und erhöhte Anwendungssicherheit (durch Multi-Faktor-Authentifizierung) für geschäftskritische Anwendungen sowie cloudbasierte und SaaS-Anwendungen. Dieser Ansatz vereinfacht die Einrichtung und Bereitstellung, reduziert den Verwaltungsaufwand und verbessert die gesamte Administration.
Das Internet der Sicherheitsdinge
Nein, das ist keine Tirade über die Sicherheit des IoT. Es ist eine Geschichte über Veränderung – genauer gesagt über Veränderung und ihre Auswirkungen auf die Sicherheit. Veränderung ist allgegenwärtig. Es gibt unzählige Axiome und Sprichwörter über Veränderung, also ist es ...