← Blog

Le 28 juin 2026, le monitor MUAD'DIB a détecté le package npm longzy-basic-ui@2.0.3 (score 100/100, sévérité CRITICAL). Son script postinstall (.prepare.cjs) implémente une vingtaine de gardes anti-analyse. L'une d'elles teste la variable d'environnement MUADDIB_GVISOR, le marqueur injecté par la sandbox de MUAD'DIB, dont le nom est reconstruit par String.fromCharCode(...) pour ne pas apparaître en clair dans le source.

Le package appartient à une campagne attribuée au mode opératoire nord-coréen « Contagious Interview », comprenant 5 packages répartis sur 3 scopes npm. Ce rapport documente la détection, l'analyse du payload, une perte de couverture liée au délestage mémoire du monitor, et le traitement retenu pour le marqueur MUADDIB_GVISOR.

OpenSourceMalware (OSM)

OpenSourceMalware (OSM) est une base communautaire de menaces de la supply chain logicielle : packages (npm, PyPI…), dépôts, extensions, domaines et IP malveillants confirmés, documentés avec leurs indicateurs de compromission et vérifiés par la communauté avant publication. Elle est maintenue par Paul McCarty (Head of Research chez Safety, fondateur de SecureStack de 2017 à 2023). MUAD'DIB l'utilise comme source d'IOC, avec Aikido et le feed OSV osv-malicious.

L'API publique d'OSM expose un unique endpoint query-latest renvoyant les ~100 dernières menaces d'un écosystème, soit une fenêtre glissante d'environ deux jours, sans recherche par package ni backfill. L'ingestion se limite au contenu de cette fenêtre au moment de la collecte ; un package catalogué hors fenêtre n'est pas récupéré par ce canal. Ce point intervient dans la section sur le délestage.

La liste complète des scopes de l'opérateur a été communiquée par Paul McCarty, ce qui a permis de rattacher les packages à une même campagne.

Message LinkedIn de Paul McCarty : « Heya @Kéwin Poszalski, your scanner must be working cuz bad guys are adding detections for it. »
Paul McCarty (OpenSourceMalware) signale la campagne : « your scanner must be working cuz bad guys are adding detections for it ».

La campagne et son opérateur

Le package.json de longzy-basic-ui@2.0.3 contient les métadonnées suivantes :

"author": "yangguisheng <yangguisheng@szlongzy.com>",
"homepage": "http://192.168.100.4:9088/app/lzy-basic-module.git",
"repository": { "url": "http://192.168.100.4:9088/app/lzy-basic-module.git" }

Elles indiquent un host de build interne (192.168.100.4:9088, instance Git privée) et un domaine d'auteur szlongzy.com. Selon l'intel communiquée par Paul McCarty (OSM), la même adresse 192.168.100.4 figure dans les indicateurs de @longzy/react-native-polyfill, ce qui rattache les scopes à un même opérateur.

L'opérateur répartit les packages sur plusieurs namespaces afin qu'un takedown d'un scope n'interrompe pas l'ensemble. Les 5 packages (3 scopes), tous catalogués par OSM, OSV ou Aikido :

PackageVersionCatalogué par
longzy-basic-ui2.0.3Aikido + OSM
@longzy/react-native-polyfill1.0.1OSV (osv-malicious)
@digitalcnzz/embedded-sdk1.0.7OSM (infostealer, install-hook)
@digitalcnzz/commonmodule1.0.3OSM + Aikido
@sailing-package/core10.0.18OSM + Aikido

Les noms imitent des packages react-native courants, ce qui correspond au mode opératoire « Contagious Interview » (dépendances piégées présentées lors de faux entretiens techniques).

Détection de longzy-basic-ui

Le 28 juin à 19:42 UTC, MUAD'DIB attribue au package un score de 100/100 (CRITICAL). Règles déclenchées :

RègleSévéritéObjet
MUADDIB-FLOW-001CRITICALFlux credential→réseau dans .prepare.cjs
MUADDIB-COMPOUND-007CRITICALHook de cycle de vie exécutant un fichier malveillant (lifecycle_file_exec)
MUADDIB-COMPOUND-009HIGHHook de cycle de vie + flux de données suspect (lifecycle_dataflow)
MUADDIB-PKG-001MEDIUMScript de cycle de vie suspect (lifecycle_script)

FLOW-001 détecte un flux reliant une lecture de données sensibles (process.env, os.hostname, os.userInfo, os.networkInterfaces) à un envoi réseau (https.request). La détection est comportementale et ne dépend d'aucune signature. Le ledger du scan indique sandbox: none : la sandbox dynamique n'a pas été exécutée, le score provient uniquement de l'analyse statique.

Les quatre règles sont comportementales (flux de données, hooks de cycle de vie) et ne dépendent d'aucune base d'IOC. Le package n'est entré dans notre base d'IOC que le 29 juin (05:32, via OSM), soit après ce scan : la détection du 28 juin repose entièrement sur le comportement.

Analyse du payload (.prepare.cjs)

Le fichier est un script prepare/postinstall obfusqué par String.fromCharCode. Déobfusqué, il présente la structure d'un infostealer à exécution différée.

1. Gardes anti-analyse

Le script exécute une vingtaine de process.exit(0) conditionnels destinés à interrompre l'exécution en environnement d'analyse. Conditions observées : credentials AWS égaux aux exemples de la documentation AWS (AKIAIOSFODNN7EXAMPLE) ; token correspondant à /honey|dummy|F4k3/i ; NODE_OPTIONS contenant --require ; hostname de type detonat|cuckoo|sandbox ; présence de variables d'environnement propres à divers outils d'analyse.

Liste des variables d'environnement de solutions d'analyse testées par le payload : DetonationLogFilePath, PYPI_POISON_HONEY_TOKEN, PYPI_POISON_AUDIT_LOG_NODE, PP_ARTIFACT_SHA256, THREAT_ANALYZER_MODEL, ASPECT_TLOG, MUADDIB_GVISOR, préfixes SANDYCLAW_/OPENCLAW_/PERMISO_/CHAINRADAR_.
Marqueurs d'environnement d'analyse testés par le payload, dont MUADDIB_GVISOR, avec les outils auxquels ils sont attribués. Table de déobfuscation communiquée par Paul McCarty (OpenSourceMalware).

La ligne 60 teste env['MUADDIB_GVISOR'], nom reconstruit par charcode :

// .prepare.cjs, ligne 60 (déobfusquée)
if (env['MUADDIB_GVISOR']) process.exit(0);

L'obfuscation par String.fromCharCode vise une recherche textuelle (grep) et reste sans effet sur une détection comportementale : c'est le flux credential→réseau (FLOW-001) qui déclenche l'alerte, indépendamment de la forme du marqueur.

2. Exécution différée

Le payload est armé par setTimeout(..., 15000 + random*30000), soit un délai de 15 à 45 secondes destiné à dépasser les timeouts des sandbox d'analyse.

3. Reconnaissance et exfiltration

Après les gardes et le délai, le script collecte les interfaces réseau non internes (os.networkInterfaces()), le hostname, l'utilisateur, la plateforme, la version Node et le registry, puis sérialise l'intégralité de process.env. Le rapport utilise des libellés en chinois (包名 / 主机 / 用户 / 平台 / 时间).

4. Command & control

Le host et le path sont encodés séparément (host : tableau inversé puis soustraction de 7 sur chaque code de caractère ; path : XOR avec la clé Zk9x). Valeur décodée :

https://open.larksuite.com/open-apis/bot/v2/hook/813ab7c8-27fc-4df5-8704-74bd504c47c3

Il s'agit d'un webhook de bot Lark/Feishu : infrastructure légitime utilisée comme canal d'exfiltration (pas de domaine dédié à bloquer, TLS valide).

Perte de couverture : délestage mémoire

Le 28 juin entre 22:06:34 et 22:06:37 UTC, deux packages de la campagne (@longzy/react-native-polyfill et @digitalcnzz/embedded-sdk) sont entrés dans la file de scan du monitor et en ont été retirés par le mécanisme de délestage mémoire (mem_emergency_spill), sans être scannés. Un troisième nom présent dans la même rafale, @longzy/react-native-audio-recorder-player, est un artefact de file (détaillé plus bas), non un package publié.

La cause n'est pas un délestage insensible aux IOC. Au moment du délestage, aucun de ces noms n'était référencé dans un feed : react-native-polyfill apparaît dans les feeds le 29 juin, embedded-sdk est catalogué par OSM le 29 juin à 07:20, et l'ensemble n'entre dans notre base d'IOC que le 1er juillet, soit 9 heures à 3 jours après le délestage. Les packages retirés étaient alors inconnus. Il s'agit d'une limite de débit, non de détection.

Ce troisième nom, @longzy/react-native-audio-recorder-player, n'apparaît que dans la file du monitor (champ version vide, sans tarball) et dans aucun catalogue (OSM ne référence que 2 packages sous longzy*). Il correspond à un artefact de file, non à un package publié. La campagne compte 5 packages.

En l'absence des archives (aujourd'hui en 404), les packages retirés ne peuvent pas être scannés a posteriori. Ils relèvent du même opérateur et du même scope, et ont été confirmés par les feeds après coup (OSM classe embedded-sdk en infostealer / install-hook). Aucun score par-package ne peut toutefois être produit sans les fichiers.

Correctif : anti-spill IOC-aware

Le délestage protège déjà les items marqués isIOCMatch. Ce marquage est effectué à l'enqueue et devient obsolète si le package n'était pas connu à ce moment, ou si la base d'IOC est rafraîchie pendant l'attente en file.

Le correctif ajoute une vérification au moment du délestage : le nom de l'item est comparé à la base d'IOC courante (lookup de nom, sans chargement de l'AST ni de la sandbox). Un nom présent est protégé et conservé dans la file.

Portée : ce correctif n'aurait pas protégé les packages du 28 juin, alors inconnus. Il couvre les cas où le package est déjà référencé (republication, campagne active, IOC wildcard, rafraîchissement de la base pendant l'attente). La couverture des 0-day relève du débit (scan avant délestage, prefetch), traitée séparément.

Traitement du marqueur MUADDIB_GVISOR

Le renommage, la rotation ou le masquage de cette variable ont été envisagés puis écartés.

Rôle. MUADDIB_GVISOR est un flag fonctionnel. Il est injecté par la sandbox (src/sandbox/index.js:279) et lu par l'entrypoint du conteneur (docker/sandbox-runner.sh) pour sélectionner l'instrumentation : sous gVisor, strace et tcpdump sont désactivés, le runtime capturant syscalls et réseau au niveau kernel. La variable ne peut donc pas être simplement supprimée.

Limites du masquage.

  • Le script est copié dans l'image Docker ; une rotation implique un rebuild périodique, et le nom doit rester connu des deux côtés.
  • La présence de gVisor est détectable par d'autres moyens (/proc, comportement de syscalls, timing). Le marqueur n'est qu'un test parmi une vingtaine.
  • La sandbox dynamique est secondaire : en CLI, son déclenchement automatique est borné à une fenêtre de score intermédiaire [15-35] (au-delà, le verdict statique est déjà définitif) ; dans le monitor, elle alimente l'alerte et la classification « dormant » sans modifier le score statique. longzy-basic-ui a été traité avec sandbox: none.
  • Les credentials-canaris injectés par la sandbox, s'ils correspondent aux motifs AKIAIOSFODNN7EXAMPLE / honey / dummy, provoquent une sortie du payload en amont du test MUADDIB_GVISOR.

Approche retenue. La variable est conservée et traitée comme honeytoken : publique et stable, elle n'est significative que sous forme obfusquée. L'effort porte sur la détection de l'évasion elle-même, en analyse statique. Deux règles ont été ajoutées :

  • MUADDIB-AST-096, analyzer_honeytoken_reference (CRITICAL) : référence dissimulée en charcode à un marqueur d'environnement d'analyse (dont MUADDIB_GVISOR). Seule la forme obfusquée est détectée ; le code de sandbox de MUAD'DIB et l'outillage de sécurité légitime, qui référencent ces marqueurs en clair, ne déclenchent pas la règle. Le re-scan de longzy-basic-ui la déclenche.
  • MUADDIB-AST-095, anti_analysis_evasion (HIGH) : détection structurelle du « mur de gardes ». Au moins cinq process.exit() silencieux couplés à une lecture d'environnement et à de l'obfuscation charcode ou de la reconnaissance hôte. Elle repose sur la structure (comptage des gardes et signaux couplés), non sur un suivi de flux ; agnostique au marqueur testé, donc robuste aux marqueurs inédits.

La récupération de payload distant à l'exécution (dead-drop) est en grande partie déjà couverte : la variante fetch→eval/Function déclenche remote_code_load et staged_payload (CRITICAL). Le résidu identifié, le staging via disque (fetch→writeFileSyncrequire, actuellement sous-scoré), n'est pas encore traité.

Indicateurs de compromission (IOC)

TypeValeur
Packagelongzy-basic-ui@2.0.3
Package@longzy/react-native-polyfill@1.0.1
Package@digitalcnzz/embedded-sdk@1.0.7
Package@digitalcnzz/commonmodule@1.0.3
Package@sailing-package/core@10.0.18
C2open.larksuite.com/open-apis/bot/v2/hook/813ab7c8-27fc-4df5-8704-74bd504c47c3
Build host192.168.100.4:9088 (/app/lzy-basic-module.git)
Auteuryangguisheng@szlongzy.com
ATT&CKT1195.002 (supply chain), T1041 (exfiltration C2), T1552 (credentials)
Signal MUAD'DIBMUADDIB-FLOW-001 (credential→réseau, CRITICAL, comportemental)

Synthèse

  • La détection de longzy-basic-ui repose sur l'analyse comportementale (FLOW-001) et a fonctionné sur un package 0-day, inconnu des feeds au moment du scan.
  • La perte de couverture (deux packages de campagne non scannés) provient du délestage mémoire, non de la détection. Le correctif IOC-aware couvre les packages déjà référencés ; les 0-day relèvent du débit.
  • Le marqueur MUADDIB_GVISOR est conservé et traité comme honeytoken. Le masquage n'apporte pas de gain défensif ; deux règles de détection statique de l'évasion ont été ajoutées (AST-095, AST-096). La récupération de payload distant est déjà couverte, hors un résidu (staging via disque).

Sources : liste des scopes et table des marqueurs anti-analyse communiquées par Paul McCarty (OpenSourceMalware). IOC : OpenSourceMalware, OSV (osv-malicious), Aikido.