Rétro-ingénierie de JavaScript par l’exemple

Écosystème F5 | 02 janvier 2019

Charge utile A de flatmap-stream

En novembre, le paquet npm event-stream a été compromis par l’intermédiaire d’une dépendance malveillante, flatmap-stream. Toute l’affaire a été relatée ici ; cet article vise à en faire une étude de cas sur la rétro-ingénierie de JavaScript. Les 3 charges utiles associées à flatmap-stream sont suffisamment simples pour être faciles à présenter, tout en étant assez complexes pour être intéressantes. Bien qu’il ne soit pas indispensable de connaître le contexte de cet incident pour comprendre cet article, je partirai de certains présupposés qui pourraient ne pas être évidents si vous ne connaissez pas déjà un peu les détails de l’affaire.

La rétro-ingénierie de la plupart des codes JavaScript est plus simple que celle des exécutables binaires que vous pouvez lancer sur le système d’exploitation de votre ordinateur — après tout, le code source se trouve juste sous vos yeux —, mais le code JavaScript conçu pour être difficile à comprendre passe souvent par plusieurs étapes d’obfuscation afin d’en dissimuler l’objectif. Une partie de cette obfuscation provient de ce que l’on appelle la « minification », un processus qui consiste à réduire autant que possible le nombre total d’octets du code source afin de gagner de l’espace. Cela implique notamment de raccourcir les noms de variables en identifiants d’un seul caractère et de remplacer des expressions comme true par des équivalents plus courts, tels que !0. La minification est principalement propre à l’écosystème JavaScript en raison de ses origines liées aux navigateurs web. On la rencontre parfois dans les paquets node du fait de la réutilisation d’outils, mais elle n’est pas destinée à servir de mesure de sécurité. Pour annuler simplement les techniques courantes de minification et d’obfuscation, consultez l’outil unminify de Shape. Les étapes d’obfuscation spécifiques peuvent être réalisées à l’aide d’outils conçus à cet effet ou manuellement par le développeur.

La première étape consiste à récupérer le code source isolé afin de l’analyser. Le paquet flatmap-stream a été spécialement conçu pour paraître inoffensif, à l’exception d’une charge utile malveillante incluse dans une seule version du paquet, la version 0.1.1. Vous pouvez rapidement repérer les modifications apportées au code source en comparant la version 0.1.2 et la version 0.1.1 ou même simplement en passant d’une URL à l’autre dans deux onglets. Dans la suite de cet article, nous désignerons le code source ajouté sous le nom de charge utile A. Vous trouverez ci-dessous le code source mis en forme de la charge utile A.

Rétro-ingénierie par l’exemple – Image 1

Tout d’abord : N’EXÉCUTEZ JAMAIS DE CODE MALVEILLANT (sauf dans des environnements isolés). J’ai développé mes propres outils pour m’aider à remanier dynamiquement du code à l’aide de la suite Shift d’analyseurs syntaxiques et d’outils de transformation JavaScript mais vous pouvez utiliser un environnement de développement comme Visual Studio Code pour suivre les étapes décrites dans cet article.

Lors de la rétro-ingénierie de code JavaScript, il est utile de réduire au minimum la gymnastique mentale. Cela implique de supprimer les expressions ou les instructions qui n’apportent aucune valeur immédiate, mais aussi de défaire le principe DRY dans tout code ayant été optimisé automatiquement ou manuellement. Puisque nous analysons le JavaScript de manière statique et suivons mentalement son exécution, plus votre pile mentale s’alourdit, plus vous risquez de perdre le fil.

L’une des choses les plus simples à faire consiste à remplacer les noms minifiés des variables auxquelles sont affectées des propriétés globales telles que require et process, comme aux lignes 3 et 4.

Rétro-ingénierie par l’exemple – Image 2

Vous pouvez le faire avec n’importe quel IDE proposant des fonctionnalités de refactorisation (généralement en appuyant sur « F2 » lorsque le curseur se trouve sur l’identifiant que vous souhaitez renommer). Ensuite, nous voyons une définition de fonction, e, qui semble simplement décoder une chaîne hexadécimale.

Rétro-ingénierie par l’exemple – Image 3

La première ligne de code intéressante semble importer un fichier dont le chemin est obtenu à partir du résultat de la fonction e, qui décode la chaîne « 2e2f746573742f64617461 ».

Rétro-ingénierie par l’exemple – Image 4

Il est extrêmement courant que du JavaScript délibérément obscurci masque toutes les chaînes de caractères littérales afin qu’une personne qui y jette un rapide coup d’œil ne soit pas alertée par des chaînes ou des propriétés particulièrement inquiétantes affichées en clair. La plupart des développeurs savent qu’il s’agit d’un obstacle très facile à surmonter. Vous trouverez donc souvent un encodage trivialement réversible, et c’est également le cas ici. La fonction e se contente d’inverser les chaînes hexadécimales, ce que vous pouvez faire manuellement à l’aide d’un outil en ligne ou de votre propre fonction utilitaire. Même si vous êtes certain de comprendre ce que fait la fonction e, il est préférable de ne pas l’exécuter (même après l’avoir extraite) avec des données provenant d’un fichier malveillant, car rien ne garantit que l’attaquant n’a pas découvert une faille de sécurité susceptible d’être déclenchée par ces données.

Après avoir inversé cette chaîne, nous constatons que le script inclut un fichier de données, './test/data', qui se trouve dans le paquet npm distribué.

Rétro-ingénierie par l’exemple — image 5

Après avoir renommé n en data et désobfusqué les appels de e(n[2]) à e(n[9]), nous commençons à mieux comprendre ce à quoi nous avons affaire ici.

Rétro-ingénierie par l’exemple — Image 6

On comprend aussi facilement pourquoi ces chaînes ont été dissimulées : trouver la moindre référence au déchiffrement dans une simple bibliothèque flatmap révélerait immédiatement que quelque chose ne tourne vraiment pas rond.

Nous voyons ici que le script importe la bibliothèque « crypto » de node.js et, après avoir consulté la documentation des API, nous constatons que le deuxième argument de createDecipher, ici o, est le mot de passe utilisé pour le déchiffrement. Nous pouvons maintenant renommer cet argument et les valeurs de retour suivantes avec des noms explicites basés sur l’API. Chaque fois que nous trouvons une nouvelle pièce du puzzle, il est important d’en garder une trace au moyen d’une refactorisation ou d’un commentaire, même s’il ne s’agit que de renommer une variable d’apparence anodine. Lorsqu’on passe des heures à explorer du code inconnu, il est très fréquent de perdre le fil, de se laisser distraire ou de devoir revenir en arrière à cause d’une refactorisation erronée. Utiliser git pour enregistrer des points de contrôle pendant une refactorisation est également utile, mais je vous laisse en décider. Le code se présente désormais comme suit : la fonction e a été supprimée, car elle n’est plus utilisée, tout comme l’instruction if (!o) {..., qui n’apporte rien à l’analyse.

Rétro-ingénierie par l’exemple – Image 7

Vous remarquerez également que j’ai renommé f en newModuleInstance. Avec un code aussi court, ce n’est pas essentiel, mais lorsqu’un code peut compter des centaines de lignes, il est important que tout soit aussi clair que possible.

À présent, la charge utile A est en grande partie désobfusquée, et nous pouvons l’examiner pas à pas pour comprendre ce qu’elle fait.

La ligne 3 importe nos données externes.

Rétro-ingénierie par l’exemple — Image 8

La ligne 4 récupère un mot de passe dans l’environnement. process.env permet d’accéder aux variables depuis un script Node, et npm_package_description est une variable que npm, le gestionnaire de paquets de Node, définit lorsque vous exécutez des scripts déclarés dans un fichier package.json.

Rétro-ingénierie par l’exemple – Image 9

La ligne 5 crée une instance de déchiffrement en utilisant la valeur de npm_package_description comme mot de passe. Cela signifie que la charge utile chiffrée ne peut être déchiffrée que lorsque ce script est exécuté via npm et pour un projet particulier dont le fichier package.json contient un champ de description spécifique. Cela ne va pas être facile.

Rétro-ingénierie par l’exemple – Image 10

Les lignes 6 et 7 déchiffrent le premier élément de notre fichier externe et le stockent dans la variable « decrypted ».

Rétro-ingénierie par l’exemple – Image 11

Les lignes 8 à 11 créent un nouveau module, puis transmettent les données déchiffrées à la méthode non documentée _compile. Ce module exporte ensuite le deuxième élément de notre fichier de données externe. module.exports est le mécanisme utilisé par Node pour exposer les données d’un module à un autre. Ainsi, newModuleInstance.exports(data[1]) expose une deuxième charge utile chiffrée présente dans notre fichier de données externe.

Rétro-ingénierie par l’exemple – Image 12

À ce stade, nous disposons de données chiffrées qui ne peuvent être déchiffrées qu’avec un mot de passe figurant quelque part dans un fichier package.json, et les données ainsi déchiffrées sont transmises à la méthode _compile. Un problème se pose alors : comment déchiffrer des données lorsque le mot de passe est inconnu ? La question est loin d’être simple : s’il était facile de casser le chiffrement aes256 par force brute, nous aurions des problèmes bien plus graves que la prise de contrôle d’un paquet npm. Heureusement, nous n’avons pas affaire à un ensemble totalement inconnu de mots de passe possibles, mais simplement à n’importe quelle chaîne de caractères ayant pu être saisie dans un fichier package.json quelque part à l’origine, les fichiers package.json servaient de format aux métadonnées des paquets npm ; autant donc commencer par le registre npm officiel. Heureusement, il existe un paquet npm qui nous fournit un flux contenant toutes les métadonnées des paquets.

Rétro-ingénierie par l’exemple – Image 13

Rien ne garantit que notre fichier cible se trouve dans un package npm. De nombreux projets non npm utilisent package.json pour stocker la configuration d’outils basés sur Node.js, et les descriptions contenues dans package.json peuvent changer d’une version à l’autre, mais c’est un bon point de départ. Il est possible de déchiffrer cette charge utile avec plusieurs clés et d’obtenir ainsi un charabia incompréhensible. Nous avons donc besoin d’un moyen de valider la charge utile déchiffrée au cours de ce processus de recherche par force brute. Puisque nous avons affaire à quelque chose qui est transmis à Module.prototype._compile qui le transmet à vm.runInThisContext nous pouvons raisonnablement supposer que le résultat est du JavaScript et utiliser l’un des nombreux analyseurs syntaxiques JavaScript pour valider les données. Si notre mot de passe échoue, ou s’il fonctionne mais que notre analyseur génère une erreur, nous devons passer au fichier package.json suivant. Fort opportunément, Shape Security a développé son propre ensemble d’analyseurs syntaxiques JavaScript destinés aux environnements JavaScript et Java. Voici le script de recherche par force brute utilisé :

Rétro-ingénierie par l’exemple — Image 14

Après avoir exécuté ce programme pendant 92,1 secondes et traité 740543 paquets, nous obtenons notre mot de passe – « A Secure Bitcoin Wallet » – qui permet de décoder correctement la charge utile ci-dessous :

Rétro-ingénierie par l’exemple – Image 15

Nous avons eu de la chance. Ce qui aurait pu se transformer en un monstrueux problème de recherche par force brute n’a finalement nécessité que moins d’un million d’itérations. Le paquet concerné par la clé en question s’est avéré être l’application cliente du portefeuille Bitcoin Copay. Les deux charges utiles suivantes explorent plus en profondeur l’application elle-même et, puisque l’application ciblée sert principalement à stocker des bitcoins, vous devinez probablement où tout cela va nous mener.

Si ce genre de sujet vous intéresse et que vous souhaitez lire une analyse des deux autres charges utiles ou de futures attaques, pensez à « aimer » cet article ou faites-le-moi savoir sur Twitter à l’adresse @jsoverson.

Partager
Mots-clés : 2021

À propos de l’auteur

Articles de blog associés

F5 NGINXaaS pour Google Cloud : des applications résilientes et évolutives
Écosystème F5 | 08/09/2025

F5 NGINXaaS pour Google Cloud : des applications résilientes et évolutives

Découvrez comment NGINXaaS s’intègre à Google Cloud pour renforcer les performances et la sécurité, et préparer vos applications critiques à un avenir propulsé par l’IA.

L’intégration de F5 Web Application Firewall (WAF) à AWS est disponible dès aujourd’hui
Écosystème F5 | 24/04/2023

L’intégration de F5 Web Application Firewall (WAF) à AWS est disponible dès aujourd’hui

En faisant appel à un partenaire de déploiement AWS WAF tel que F5, vous pouvez renforcer la sécurité de vos applications web grâce à des règles personnalisées qui bloquent les schémas d’attaque courants, comme les injections SQL ou les attaques par scripts intersites (XSS).

Appliquer le principe de Peter Parker à l’inspection TLS
Écosystème F5 | 09/12/2019

Appliquer le principe de Peter Parker à l’inspection TLS

« Un grand pouvoir implique de grandes responsabilités. » Si l’on en trouve des échos chez Voltaire et Churchill, cette citation est surtout connue grâce aux bandes dessinées Spider-Man, où elle est attribuée à l’oncle Ben. Bien entendu, si cette phrase est si répandue dans la culture populaire, c’est notamment parce qu’elle peut s’appliquer à une multitude de situations et de sujets, y compris à l’inspection TLS.

Le NetOps a besoin de défenseurs, pas d’adversaires
Écosystème F5 | 13/12/2018

Le NetOps a besoin de défenseurs, pas d’adversaires

Il est important de reconnaître que les équipes NetOps ne sont pas toujours celles qui font obstacle au déploiement de la dernière nouveauté, application ou service. Les freins à la rapidité sont souvent dus au fait que les organisations n’adoptent pas tous les principes du DevOps lorsqu’elles cherchent à transformer leurs opérations informatiques.

Mise à jour des services applicatifs : sécurité, rapidité et visiteurs suspects
Écosystème F5 | 01/03/2018

Mise à jour des services applicatifs : sécurité, rapidité et visiteurs suspects

Il est de nouveau temps de nous pencher sur les services applicatifs que les entreprises utilisent réellement pour rendre leurs applications plus rapides et plus sûres. On constate notamment une hausse du recours aux services de protection contre les bots, ainsi que la progression lente mais régulière de HTTP/2. Bien qu’il ne s’agisse pas d’une application…

Les templates prennent le contrôle du cloud
Écosystème F5 | 02/02/2017

Les templates prennent le contrôle du cloud

Les modèles s’imposent dans le cloud. Lisez le blog de F5 pour découvrir comment l’infrastructure en tant que code et les API déclaratives automatisent la mise à disposition des applications.