Chez Shape, nous rencontrons de nombreux morceaux de JavaScript peu fiables. Nous trouvons des scripts injectés de manière malveillante dans des pages, ils peuvent être envoyés par un client pour obtenir des conseils, ou nos équipes de sécurité peuvent trouver une ressource sur le Web qui semble faire spécifiquement référence à certains aspects de notre service. Dans le cadre de notre routine quotidienne, nous plongeons dans les scripts pour comprendre ce qu'ils font et comment ils fonctionnent. Ils sont généralement minifiés, souvent obscurcis et nécessitent toujours plusieurs niveaux de modification avant d’être réellement prêts pour une analyse approfondie.
Jusqu’à récemment, le moyen le plus simple de réaliser cette analyse consistait soit à utiliser des configurations mises en cache localement qui permettent l’édition manuelle, soit à utiliser des proxys pour réécrire le contenu à la volée. La solution locale est la plus pratique, mais les sites Web ne se traduisent pas toujours parfaitement dans d'autres environnements et cela conduit souvent les gens à se lancer dans un terrier de dépannage juste pour devenir productifs. Les proxys sont extrêmement flexibles, mais sont généralement encombrants et peu portables. Chacun a sa propre configuration personnalisée pour son environnement et certaines personnes sont plus familières avec un proxy qu'avec un autre. J'ai commencé à utiliser Chrome et son protocole devtools afin de me connecter aux requêtes et aux réponses au fur et à mesure qu'elles se produisent et de les modifier à la volée. Il est portable sur n'importe quelle plateforme dotée de Chrome, contourne toute une série de problèmes et s'intègre bien aux outils JavaScript courants. Dans cet article, je vais vous expliquer comment intercepter et modifier JavaScript à la volée à l'aide du protocole devtools de Chrome .
Nous utiliserons Node, mais une grande partie du contenu est portable dans la langue de votre choix, à condition que les hooks devtools soient facilement accessibles.
Tout d’abord, si vous n’avez jamais exploré les scripts Chrome, Eric Bidelman a rédigé un excellent guide de démarrage pour Chrome sans tête . Les conseils qui y sont présentés s'appliquent à la fois à Chrome Headless et GUI (avec une particularité que j'aborderai dans la section suivante).
Nous utiliserons la bibliothèque chrome-launcher de npm pour faciliter cette tâche.
chrome-launcher fait exactement ce que vous pensez qu'il ferait et vous pouvez transmettre les mêmes commutateurs de ligne de commande auxquels vous êtes habitué à partir du terminal sans changement (une excellente liste est conservée ici ). Nous allons passer aux options suivantes :
–taille-de-fenêtre=1200,800
–ouverture automatique des outils de développement pour les onglets
–user-data-dir=/tmp/chrome-testing
Essayez d’exécuter votre script pour vous assurer que vous pouvez ouvrir Chrome. Vous devriez voir quelque chose comme ceci :
Ceci est également appelé « protocole de débogage Chrome », et les deux termes semblent être utilisés de manière interchangeable dans les documents de Google. Tout d’abord, installez le package chrome-remote-interface via npm qui nous donne des méthodes pratiques pour interagir avec le protocole devtools. Assurez-vous d'avoir la documentation du protocole à portée de main si vous souhaitez approfondir le sujet.
Pour utiliser le CDP, vous devez vous connecter au port du débogueur et, comme nous utilisons la bibliothèque chrome-launcher , celui-ci est facilement accessible via chrome.port .
De nombreux domaines du protocole doivent d’abord être activés, et nous allons commencer par le domaine Runtime afin de pouvoir nous connecter à l’API de la console et transmettre tous les appels de console du navigateur à la ligne de commande.
Désormais, lorsque vous exécutez votre script, vous obtenez une fenêtre Chrome entièrement fonctionnelle qui génère également tous ses messages de console sur votre terminal. C'est génial en soi, surtout à des fins de test !
Tout d’abord, nous devons enregistrer ce que nous voulons intercepter en soumettant une liste de RequestPatterns à setRequestInterception . Vous pouvez intercepter soit à l'étape « Requête », soit à l'étape « En-têtes reçus » et, pour réellement modifier une réponse, nous devrons attendre « En-têtes reçus ». Le type de ressource correspond aux types que vous verrez généralement dans le volet réseau des outils de développement.
N'oubliez pas d'activer le domaine Réseau comme vous l'avez fait avec Runtime , ci-dessus, en ajoutant Network.enable() au même tableau.
L'enregistrement du gestionnaire d'événements est relativement simple et chaque requête interceptée est accompagnée d'un interceptionId qui peut être utilisé pour interroger des informations sur la requête ou éventuellement émettre une continuation. Ici, nous intervenons simplement et enregistrons chaque requête que nous interceptons sur le terminal.
Pour modifier les requêtes, nous devrons installer certaines bibliothèques d'aide qui encoderont et décoderont les chaînes base64. Il existe de nombreuses bibliothèques disponibles ; n'hésitez pas à choisir la vôtre. Nous utiliserons atob et btoa .
L'API pour gérer les réponses est un peu maladroite. Pour gérer les réponses, vous devez inclure toute votre logique de réponse dans l'interception de la requête (au lieu de simplement intercepter une réponse, par exemple), puis vous devez interroger le corps à l'aide de l'ID d'interception. En effet, le corps peut ne pas être disponible au moment où votre gestionnaire est appelé et cela vous permet d'attendre explicitement ce que vous recherchez. Le corps peut également être codé en base64, vous devrez donc le vérifier et le décoder avant de le transmettre aveuglément.
À ce stade, vous êtes libre de vous déchaîner sur JavaScript. Votre code vous place au milieu d'une réponse vous permettant à la fois d'accéder au JavaScript complet qui a été demandé et de renvoyer votre réponse modifiée. Génial! Nous allons simplement modifier le JS en ajoutant un console.log à la fin de celui-ci afin que notre terminal reçoive un message lorsque notre code modifié est exécuté dans le navigateur.
Nous ne pouvons pas simplement transmettre un corps modifié seul, car le contenu pourrait entrer en conflit avec les en-têtes qui ont été envoyés avec la ressource d'origine. Étant donné que vous effectuez des tests et des ajustements actifs, vous souhaiterez probablement commencer par les bases avant de trop vous soucier des autres informations d'en-tête que vous devez transmettre. Vous pouvez accéder aux en-têtes de réponse via responseHeaders transmis au gestionnaire d'événements si nécessaire, mais pour l'instant, nous allons simplement créer notre propre ensemble minimal dans un tableau pour une manipulation et une édition faciles ultérieurement.
L'envoi de la nouvelle réponse nécessite la création d'une réponse HTTP complète codée en base64 (y compris la ligne d'état HTTP) et son envoi via une propriété rawResponse dans l'objet passé à continueInterceptedRequest .
Maintenant, lorsque vous exécutez votre script et naviguez sur Internet, vous verrez quelque chose comme ce qui suit dans votre terminal pendant que votre script intercepte JavaScript et également pendant que votre JavaScript modifié s'exécute dans le navigateur et que les console.log() remontent à travers le hook que nous avons créé au début du didacticiel.
Le code de travail complet pour l'exemple de base est ici :
Vous pouvez commencer par imprimer joliment le code source, ce qui est toujours un moyen utile de commencer à faire de la rétro-ingénierie sur quelque chose. Oui, bien sûr, vous pouvez le faire dans la plupart des navigateurs modernes, mais vous souhaiterez contrôler chaque étape de modification vous-même afin de maintenir la cohérence entre les navigateurs et les versions de navigateur et de pouvoir relier les points lorsque vous analysez la source. Lorsque je fouille dans du code étranger et obscur, j'aime renommer les variables et les fonctions au fur et à mesure que je commence à comprendre leur objectif. Modifier JavaScript en toute sécurité n'est pas un exercice trivial et cela fait l'objet d'un article de blog à part entière, mais pour l'instant, vous pouvez utiliser quelque chose comme unminify pour annuler les techniques courantes de minification et d'obscurcissement.
Vous pouvez installer unminify via npm et envelopper votre nouveau corps JavaScript avec un appel à unminify afin de le voir en action :
Nous approfondirons davantage les transformations dans le prochain article. Si vous avez des questions, des commentaires ou d’autres astuces intéressantes, n’hésitez pas à me contacter via Twitter !