Bei Shape stoßen wir auf viele skizzenhafte Teile von JavaScript. Wir finden Skripte, die in böswilliger Absicht in Seiten eingeschleust wurden. Diese könnten von einem Kunden mit der Bitte um Rat gesendet worden sein, oder unsere Sicherheitsteams könnten im Internet auf eine Ressource stoßen, die sich scheinbar speziell auf bestimmte Aspekte unseres Dienstes bezieht. Es gehört zu unserer täglichen Routine, dass wir uns intensiv mit den Skripten befassen, um zu verstehen, was sie tun und wie sie funktionieren. Sie sind normalerweise minimiert, oft verschleiert und erfordern immer mehrere Änderungsebenen, bevor sie wirklich für eine gründliche Analyse bereit sind.
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 möchten, indem wir eine Liste von RequestPatterns an setRequestInterception übermitteln. Sie können entweder in der Phase „Anforderung“ oder in der Phase „HeadersReceived“ eingreifen. Um eine Antwort tatsächlich zu ändern, müssen wir auf „HeadersReceived“ warten. Der Ressourcentyp entspricht den Typen , die Sie normalerweise im Netzwerkbereich der Devtools sehen.
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 einen geänderten Textkörper nicht einfach allein weitergeben, da der Inhalt möglicherweise mit den Headern in Konflikt steht, die mit der Originalressource gesendet wurden. Da Sie aktiv testen und optimieren, möchten Sie wahrscheinlich mit den Grundlagen beginnen, bevor Sie sich zu viele Gedanken über andere Header-Informationen machen, die Sie übermitteln müssen. Sie können bei Bedarf über responseHeaders auf die Antwortheader zugreifen, die an den Ereignishandler übergeben werden. Zunächst erstellen wir jedoch einfach unseren eigenen minimalen Satz in einem Array, um ihn später einfach manipulieren und bearbeiten 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!