BLOG

Reverse engineering de JS par l'exemple

Miniature F5
F5
Publié le 2 janvier 2019

charge utile A de flatmap-stream

En novembre, le package npm event-stream a été exploité via une dépendance malveillante, flatmap-stream. L'ensemble de l'épreuve a été décrit ici et l'objectif de cet article est de l'utiliser comme étude de cas pour la rétro-ingénierie JavaScript. Les 3 charges utiles associées à flatmap-stream sont suffisamment simples pour être faciles à écrire et suffisamment complexes pour être intéressantes. Bien qu'il ne soit pas essentiel de comprendre l'histoire de cet incident pour comprendre cet article, je ferai des hypothèses qui pourraient ne pas être évidentes si vous n'êtes pas quelque peu familier avec les détails.

La rétro-ingénierie de la plupart des fichiers JavaScript est plus simple que les exécutables binaires que vous pouvez exécuter sur votre système d'exploitation de bureau (après tout, la source est juste devant vous), mais le code JavaScript conçu pour être difficile à comprendre passe souvent par quelques passes d'obscurcissement afin de masquer son intention. Une partie de cette obscurcissement provient de ce qu’on appelle la « minification », qui est le processus de réduction du nombre total d’octets de votre source autant que possible à des fins d’économie d’espace. Cela implique de raccourcir les variables en identifiants à caractère unique et de traduire des expressions comme true en quelque chose de plus court mais équivalent comme !0. La minification est principalement propre à l'écosystème JavaScript en raison de ses origines de navigateur Web et est parfois observée dans les packages de nœuds en raison d'une réutilisation d'outils et n'est pas destinée à être une mesure de sécurité. Pour une inversion de base des techniques courantes de minification et d'obscurcissement, consultez l'outil de déminification de Shape. Les passes d'obscurcissement dédiées peuvent provenir d'outils conçus pour obscurcir ou sont effectuées manuellement par le développeur

La première étape consiste à mettre la main sur la source isolée pour l’analyse. Le package flatmap-stream a été conçu spécifiquement pour paraître innocent, à l'exception d'une charge utile malveillante incluse dans une seule version du package, la version 0.1.1. Vous pouvez rapidement voir les modifications apportées à la source en comparant la version 0.1.2 et la version 0.1.1 ou même simplement en alternant entre les URL dans deux onglets. Pour le reste de l'article, nous ferons référence à la source ajoutée sous le nom de charge utile A. Vous trouverez ci-dessous la source formatée de la charge utile A.

Ingénierie inverse par l'exemple Image 1

Tout d’abord, commençons par le commencement : N’EXÉCUTEZ JAMAIS DE CODE MALVEILLANT (sauf dans des environnements isolés). J'ai écrit mes propres outils pour m'aider à refactoriser le code de manière dynamique à l'aide de la suite d'analyseurs Shift et de transformateurs JavaScript , mais vous pouvez utiliser un IDE comme Visual Studio Code pour suivre cet article.

Lors de la rétro-ingénierie de JavaScript, il est important de réduire au minimum les jonglages mentaux. Cela signifie se débarrasser de toutes les expressions ou déclarations qui n’ajoutent pas de valeur immédiate et également inverser le caractère DRY de tout code qui a été optimisé automatiquement ou manuellement. Étant donné que nous analysons le JavaScript de manière statique et que nous suivons son exécution dans nos têtes, plus votre pile mentale s’approfondit, plus vous risquez de vous perdre.

L’une des choses les plus simples que vous puissiez faire est de déminifier les variables auxquelles sont attribuées des propriétés globales telles que require et process, comme sur les lignes 3 et 4.

 

Ingénierie inverse par l'exemple Image 2

 

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

 

Ingénierie inverse par l'exemple Image 3

 

La première ligne de code intéressante semble importer un fichier qui provient du résultat de la fonction e décodant la chaîne « 2e2f746573742f64617461 »

 

Ingénierie inverse par l'exemple Image 4

Il est extrêmement courant que du JavaScript délibérément obscurcisse toute valeur de chaîne littérale afin que quiconque y jette un coup d'œil ne soit pas alerté par des chaînes ou des propriétés particulièrement inquiétantes en vue claire. La plupart des développeurs reconnaissent qu'il s'agit d'un obstacle très faible, vous trouverez donc souvent un codage trivialement annulable en place et ce n'est pas différent ici. La fonction e inverse simplement les chaînes hexadécimales et vous pouvez le faire manuellement via un outil en ligne ou avec votre propre fonction de commodité. Même si vous êtes sûr de comprendre ce que fait la fonction e, c'est toujours une bonne idée de ne pas l'exécuter (même si vous l'extrayez) avec une entrée trouvée dans un fichier malveillant, car vous n'avez aucune garantie que l'attaquant n'a pas trouvé une vulnérabilité de sécurité déclenchée par les données.

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

 

Ingénierie inverse par l'exemple Image 5

Après avoir renommé n en données et désobscurci les appels à e(n[2]) en e(n[9]), nous commençons à avoir une meilleure idée de ce à quoi nous avons affaire ici.

 

Ingénierie inverse par l'exemple Image 6

Il est également facile de comprendre pourquoi ces chaînes ont été masquées. Trouver des références au décryptage dans une simple bibliothèque flatmap serait un signe évident que quelque chose ne va pas.

À partir de là, nous voyons que le script importe la bibliothèque « crypto » de node.js et, après avoir recherché les API , nous constatons que le deuxième argument de createDecipher, o ici, est le mot de passe utilisé pour décrypter. Nous pouvons maintenant renommer cet argument et les valeurs de retour suivantes avec des noms sensés basés sur l'API. Chaque fois que nous trouvons une nouvelle pièce du puzzle, il est important de l'immortaliser via un refactor ou un commentaire, même s'il s'agit d'une variable renommée qui semble triviale. Il est très courant, lorsque l'on plonge pendant des heures dans un code étranger, de perdre son chemin, d'être distrait ou d'avoir besoin de revenir en arrière à cause d'une refactorisation erronée. Utiliser git pour enregistrer les points de contrôle lors d'une refactorisation est également utile, mais je vous laisse cette décision. Le code ressemble maintenant à ceci, avec la fonction e supprimée car elle n'est plus utilisée avec l'instruction if (!o) {... car elle n'ajoute pas de valeur à l'analyse.

 

Ingénierie inverse par l'exemple Image 7

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

La charge utile A est désormais largement démasquée et nous pouvons la parcourir pour comprendre ce qu'elle fait.

La ligne 3 importe nos données externes.

 

Ingénierie inverse par l'exemple Image 8

La ligne 4 récupère un mot de passe dans l'environnement. process.env vous permet d'accéder aux variables à partir d'un script de nœud et npm_package_description est une variable que npm, le gestionnaire de packages de node, définit lorsque vous exécutez des scripts définis dans un fichier package.json.

 

Ingénierie inverse par l'exemple Image 9

La ligne 5 crée une instance de déchiffrement avec 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 est exécuté pour un projet particulier qui a, dans son package.json, un champ de description spécifique. Ça va être dur.

 

Ingénierie inverse par l'exemple Image 10

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

 

Ingénierie inverse par l'exemple Image 11

 

Les lignes 8 à 11 créent un nouveau module, puis alimentent les données déchiffrées dans 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 du nœud permettant d’exposer les données d’un module à un autre, donc newModuleInstance.exports(data[1]) expose une deuxième charge utile cryptée trouvée dans notre fichier de données externe.

 

Ingénierie inverse par l'exemple Image 12

 

À ce stade, nous avons des données chiffrées qui ne peuvent être déchiffrées qu'avec un mot de passe trouvé dans un package.json quelque part et dont les données déchiffrées sont introduites dans la méthode _compile. Nous sommes maintenant confrontés à un problème : comment décrypter des données dont le mot de passe est inconnu ? C'est une question non triviale, s'il était facile de forcer le cryptage aes256, nous aurions plus de problèmes qu'un paquet npm pris en charge. Heureusement, nous n'avons pas affaire à un ensemble de mots de passe possibles complètement inconnu, mais simplement à n'importe quelle chaîne qui a été saisie dans un package.json quelque part . Les fichiers package.json sont à l'origine du format de fichier pour les métadonnées du package npm, nous pouvons donc aussi bien commencer par le registre officiel npm. Heureusement, il existe un package npm qui nous donne un flux de toutes les métadonnées du package .

Ingénierie inverse par l'exemple Image 13

Il n'y a aucune garantie que notre fichier cible se trouve dans un package npm, de nombreux projets non npm utilisent package.json pour stocker la configuration des outils basés sur des nœuds, et les descriptions package.json peuvent changer d'une version à l'autre, mais c'est un bon point de départ. Il est possible de décrypter cette charge utile avec plusieurs clés, ce qui donne lieu à un charabia confus. Nous avons donc besoin d'un moyen de valider notre charge utile décryptée pendant ce processus de forçage brut. Étant donné que nous traitons quelque chose qui est envoyé à Module.prototype._compile qui alimente à son tour vm.runInThisContext, nous pouvons raisonnablement supposer que la sortie est JavaScript et nous pouvons utiliser n'importe quel nombre d'analyseurs JavaScript pour valider les données. Si notre mot de passe échoue ou s'il réussit mais que notre analyseur génère une erreur, nous devons passer au package.json suivant. Heureusement, Shape Security a créé son propre ensemble d’analyseurs JavaScript à utiliser dans les environnements JavaScript et Java. Le script de force brute utilisé est ici :

 

Ingénierie inverse par l'exemple Image 14

Après avoir exécuté ceci pendant 92,1 secondes et traité 740543 paquets, nous obtenons notre mot de passe – « Un portefeuille Bitcoin sécurisé » – qui décode avec succès la charge utile incluse ci-dessous :

 

Ingénierie inverse par l'exemple Image 15

C'était une chance. Ce qui aurait pu être un problème de force brute monstrueux a fini par nécessiter moins d’un million d’itérations. Le package affecté avec la clé en question s’est avéré être l’application cliente du portefeuille Bitcoin Copay. Les deux charges utiles suivantes plongent plus profondément dans l'application elle-même et, étant donné que l'application cible est centrée sur le stockage de bitcoins, vous pouvez probablement deviner où cela pourrait aller.

Si vous trouvez des sujets comme celui-ci intéressants et que vous souhaitez lire une analyse des deux autres charges utiles ou des attaques futures, assurez-vous d’aimer cet article ou de me le faire savoir sur Twitter à @jsoverson .