Bei Shape stoßen wir immer wieder auf zahlreiche zweifelhafte JavaScript-Skripte. Wir finden Skripte, die bösartig in Seiten eingeschleust wurden, erhalten von Kunden zur Prüfung eingesandte Beispiele oder entdecken Ressourcen im Web, die sich gezielt mit Teilen unseres Dienstes beschäftigen. Täglich gehen wir diese Skripte detailliert durch, um zu verstehen, was sie tun und wie sie funktionieren. Sie sind meist minimiert, oft verschlüsselt und brauchen mehrere Bearbeitungsschritte, bevor sie wirklich tiefgehend analysiert werden können.
Bis vor Kurzem bestand die einfachste Möglichkeit für diese Analyse darin, entweder lokal zwischengespeicherte Setups zu verwenden, die manuelle Bearbeitungen ermöglichen, oder Proxys zu verwenden, um Inhalte im laufenden Betrieb neu zu schreiben. Die lokale Lösung ist die bequemste, aber Websites lassen sich nicht immer perfekt in andere Umgebungen übertragen und die Benutzer stürzen sich oft in eine lange Liste von Fehlerbehebungen, nur um produktiv zu werden. Proxys sind äußerst flexibel, aber normalerweise umständlich und nicht sehr portabel – jeder hat seine eigene benutzerdefinierte Konfiguration für seine Umgebung und manche Leute sind mit einem Proxy besser vertraut als mit einem anderen. Ich habe begonnen, Chrome und sein Devtools-Protokoll zu verwenden, um in Anfragen und Antworten einzusteigen, während sie auftreten, und sie im laufenden Betrieb zu ändern. Dies ist auf jede Plattform portierbar, auf der Chrome installiert ist, umgeht eine ganze Reihe von Problemen und lässt sich gut in gängige JavaScript-Tools integrieren. In diesem Beitrag erkläre ich, wie man mit dem Devtools-Protokoll von Chrome JavaScript im laufenden Betrieb abfängt und ändert.
Wir verwenden Node, aber ein Großteil des Inhalts lässt sich in die Sprache Ihrer Wahl portieren, vorausgesetzt, Sie haben einfachen Zugriff auf die Devtools-Hooks.
Wenn Sie sich noch nie mit der Skripterstellung für Chrome beschäftigt haben, sollten Sie zunächst wissen, dass Eric Bidelman eine hervorragende Anleitung für die ersten Schritte mit Headless Chrome geschrieben hat. Die dort aufgeführten Tipps gelten sowohl für Headless als auch für GUI Chrome (mit einer Eigenart, auf die ich im nächsten Abschnitt eingehen werde).
Um dies zu vereinfachen, verwenden wir die Chrome-Launcher -Bibliothek von npm.
Chrome-Launcher macht genau das, was Sie erwarten, und Sie können dieselben Befehlszeilenschalter, die Sie vom Terminal gewohnt sind, unverändert übergeben (eine ausführliche Liste wird hier gepflegt ). Wir übergeben die folgenden Optionen:
–Fenstergröße=1200,800
–auto-open-devtools-for-tabs
–user-data-dir=/tmp/chrome-testing
Versuchen Sie, Ihr Skript auszuführen, um sicherzustellen, dass Sie Chrome öffnen können. Sie sollten ungefähr Folgendes sehen:
Dies wird auch als „Chrome-Debugger-Protokoll“ bezeichnet und beide Begriffe scheinen in den Dokumenten von Google synonym verwendet zu werden. Installieren Sie zunächst das Paket „chrome-remote-interface“ über npm, das uns praktische Methoden zur Interaktion mit dem Devtools-Protokoll bietet. Halten Sie die Protokolldokumente bereit, wenn Sie tiefer in die Materie eintauchen möchten.
Um das CDP zu verwenden, müssen Sie eine Verbindung zum Debugger-Port herstellen. Da wir die Chrome-Launcher -Bibliothek verwenden, ist dieser bequem über chrome.port zugänglich.
Viele der Domänen im Protokoll müssen zuerst aktiviert werden. Wir beginnen mit der Runtime- Domäne, damit wir uns in die Konsolen-API einklinken und alle Konsolenaufrufe im Browser an die Befehlszeile weiterleiten können.
Wenn Sie Ihr Skript jetzt ausführen, erhalten Sie ein voll funktionsfähiges Chrome-Fenster, das auch alle seine Konsolenmeldungen an Ihr Terminal ausgibt. Das ist an sich schon großartig, insbesondere für Testzwecke!
Zuerst müssen wir registrieren, was wir abfangen wollen, indem wir eine Liste von RequestPatterns an setRequestInterception senden. Du kannst entweder in der „Request“-Phase oder in der „HeadersReceived“-Phase abfangen. Möchtest du die Antwort tatsächlich ändern, warten wir auf „HeadersReceived“. Der Ressourcentyp entspricht den types, die du üblicherweise im Netzwerkbereich der Devtools findest.
Vergessen Sie nicht, die Netzwerkdomäne zu aktivieren, wie Sie es oben bei Runtime getan haben, indem Sie Network.enable() zum selben Array hinzufügen.
Die Registrierung des Ereignishandlers ist relativ unkompliziert und jede abgefangene Anfrage verfügt über eine InterceptionId , die verwendet werden kann, um Informationen zur Anfrage abzufragen oder ggf. eine Fortsetzung auszugeben. Hier greifen wir einfach ein und protokollieren jede Anfrage, die wir abfangen, im Terminal.
Um Anfragen zu ändern, müssen wir einige Hilfsbibliotheken installieren, die Base64-Zeichenfolgen kodieren und dekodieren. Es stehen zahlreiche Bibliotheken zur Verfügung. Wählen Sie einfach Ihre eigene aus. Wir verwenden atob und btoa .
Die API zum Verarbeiten der Antworten ist etwas umständlich. Zum Verarbeiten von Antworten müssen Sie Ihre gesamte Antwortlogik in die Anforderungsabfangung einbeziehen (und nicht beispielsweise einfach eine Antwort abfangen) und dann den Textkörper anhand der Abfang-ID abfragen. Dies liegt daran, dass der Textkörper beim Aufruf Ihres Handlers möglicherweise nicht verfügbar ist. So können Sie explizit auf das warten, was Sie suchen. Der Textkörper kann auch Base64-codiert sein, Sie sollten ihn daher prüfen und decodieren, bevor Sie ihn blind weitergeben.
An diesem Punkt können Sie sich mit JavaScript völlig austoben. Ihr Code versetzt Sie in die Mitte einer Antwort, sodass Sie sowohl auf das vollständige angeforderte JavaScript zugreifen als auch Ihre geänderte Antwort zurücksenden können. Eindrucksvoll! Wir optimieren das JS einfach, indem wir am Ende ein console.log anhängen, sodass unser Terminal eine Nachricht erhält, wenn unser geänderter Code im Browser ausgeführt wird.
Wir können nicht einfach nur den geänderten Body weitergeben, da der Inhalt den ursprünglichen Headern widersprechen könnte. Da Sie aktiv testen und optimieren, sollten Sie mit den Grundlagen starten, bevor Sie sich zu sehr um weitere Header-Informationen kümmern. Sie können die Antwort-Header bei Bedarf über responseHeaders abrufen, die an den Ereignis-Handler übergeben werden. Für den Anfang erstellen wir jedoch unser eigenes minimales Array, um den Satz später leicht bearbeiten und anpassen zu können.
Zum Senden der neuen Antwort muss eine vollständige, Base64-codierte HTTP-Antwort (einschließlich der HTTP-Statuszeile) erstellt und über eine RawResponse- Eigenschaft im an continueInterceptedRequest übergebenen Objekt gesendet werden.
Wenn Sie nun Ihr Skript ausführen und im Internet navigieren, sehen Sie in Ihrem Terminal etwa Folgendes, da Ihr Skript JavaScript abfängt und auch Ihr geändertes JavaScript im Browser ausgeführt wird und die console.log() s durch den Hook nach oben sprudeln, den wir zu Beginn des Tutorials erstellt haben.
Der vollständige Arbeitscode für das Basisbeispiel finden Sie hier:
Sie können mit dem hübschen Ausdrucken des Quellcodes beginnen. Dies ist immer eine nützliche Möglichkeit, mit dem Reverse Engineering zu beginnen. Ja, natürlich können Sie dies in den meisten modernen Browsern tun, aber Sie möchten jeden Änderungsschritt selbst steuern, um die Konsistenz über verschiedene Browser und Browserversionen hinweg zu gewährleisten und beim Analysieren der Quelle die Zusammenhänge erkennen zu können. Wenn ich mich in fremden, verschleierten Code vertiefe, benenne ich Variablen und Funktionen gerne um, sobald ich beginne, ihren Zweck zu verstehen. Das sichere Ändern von JavaScript ist keine Kleinigkeit und würde einen eigenen Blog-Beitrag erfordern. Für den Moment können Sie jedoch etwas wie unminify verwenden, um gängige Minimierungs- und Verschleierungstechniken rückgängig zu machen.
Sie können unminify über npm installieren und Ihren neuen JavaScript-Body mit einem Aufruf von unminify umschließen, um es in Aktion zu sehen:
Wir werden im nächsten Beitrag tiefer auf die Transformationen eingehen. Wenn Sie Fragen, Kommentare oder andere tolle Tricks haben, kontaktieren Sie mich bitte über Twitter!