# Runbook : reproduction pas à pas

Log de manipulation (commande / pourquoi / retour brut / ce qu'on en retient) de l'analyse de `jscrambler`.

Tous les fichiers manipulés vivent dans `samples/jscrambler-npm-supplychain/` (hors dépôt, gitignored), jamais exécutés, `chmod -x` systématique dès téléchargement/extraction, même discipline que StealC/AgentTesla.

---

## A. Vérification de ce qui reste disponible sur le registre npm officiel

**Commande :**
```bash
curl -s https://registry.npmjs.org/jscrambler | python3 -c "
import json,sys
d=json.load(sys.stdin)
versions = d.get('versions', {})
for v in ['8.14.0','8.16.0','8.17.0','8.18.0','8.20.0','8.22.0']:
    print(v, 'OK' if v in versions else 'NOT FOUND')
"
```

**Pourquoi :** la presse cite 8.14.0/8.16.0/8.17.0/8.18.0/8.20.0 comme malveillantes ; avant d'aller chercher ailleurs, vérifier ce que le registre officiel sert encore.

**Retour :**
```
8.14.0 NOT FOUND in registry
8.16.0 NOT FOUND in registry
8.17.0 NOT FOUND in registry
8.18.0 https://registry.npmjs.org/jscrambler/-/jscrambler-8.18.0.tgz ...
8.20.0 NOT FOUND in registry
```

**Ce qu'on en retient :** 8.18.0 est encore servie normalement malgré son statut malveillant rapporté. Les autres ont été purgées de l'API `versions`, mais une purge de `versions` n'implique pas forcément que le champ `time` (tombstone) ou les miroirs tiers (jsDelivr, unpkg) sont aussi nettoyés.

## B. Téléchargement des tarballs disponibles (8.18.0 malveillante + 8.22.0 saine pour diff)

**Commande :**
```bash
mkdir -p samples/jscrambler-npm-supplychain/tarballs
cd samples/jscrambler-npm-supplychain/tarballs
curl -s -o jscrambler-8.18.0.tgz https://registry.npmjs.org/jscrambler/-/jscrambler-8.18.0.tgz
curl -s -o jscrambler-8.22.0.tgz https://registry.npmjs.org/jscrambler/-/jscrambler-8.22.0.tgz
chmod -x jscrambler-8.18.0.tgz jscrambler-8.22.0.tgz
sha256sum jscrambler-8.18.0.tgz jscrambler-8.22.0.tgz
ls -la
```

**Retour :**
```
c192578173b89a439ecbab3bcb91b205227c225c8009a6fb7e9c13f226b74899  jscrambler-8.18.0.tgz
188e461973276992b22815636dcea746ed831c23b3c6971c28883b7ed72e142c  jscrambler-8.22.0.tgz
-rw-r--r-- 1 ozer ozer 7877338 jscrambler-8.18.0.tgz
-rw-r--r-- 1 ozer ozer   37876 jscrambler-8.22.0.tgz
```

**Ce qu'on en retient :** différence de taille massive (7,8 Mo vs 37 Ko), la version malveillante embarque clairement quelque chose de lourd. Point de départ concret pour la suite.

## C. Extraction et comparaison des arborescences

**Commande :**
```bash
mkdir -p extracted_8.18.0 extracted_8.22.0
tar -xzf tarballs/jscrambler-8.18.0.tgz -C extracted_8.18.0 --no-same-permissions
tar -xzf tarballs/jscrambler-8.22.0.tgz -C extracted_8.22.0 --no-same-permissions
find extracted_8.18.0 -type f | xargs chmod -x
find extracted_8.22.0 -type f | xargs chmod -x
find extracted_8.18.0 -type f -exec ls -la {} \; | sort -k5 -n -r | head -3
```

**Retour :**
```
-rw-r--r-- 1 ozer ozer 7837238 extracted_8.18.0/package/dist/intro.js
-rw-r--r-- 1 ozer ozer   49482 extracted_8.18.0/package/dist/index.js
-rw-r--r-- 1 ozer ozer   24648 extracted_8.18.0/package/dist/bin/jscrambler.js
```

**Ce qu'on en retient :** `dist/intro.js` (7,8 Mo à lui seul) est l'écart de taille repéré en B. Malgré l'extension `.js`, `file` le classe comme `data`, ce n'est pas du JavaScript.

## D. Identification du mécanisme d'exécution (diff avec la version saine)

**Commande :**
```bash
diff extracted_8.22.0/package/dist/index.js extracted_8.18.0/package/dist/index.js | head -60
python3 -c "import json; d=json.load(open('extracted_8.18.0/package/package.json')); print(d.get('scripts',{}))"
```

**Retour :** (extrait, voir jscrambler-npm-supplychain.md §3.2 pour l'analyse complète)
```
> (function () {
>   var PLATFORM_IDS = { linux: 0, win32: 1, darwin: 2 };
>   ...
>   var bundle = (0, _fs.readFileSync)((0, _path.join)(__dirname, 'intro.js'));
>   var magic = Buffer.from([0x1b, 0x43, 0x53, 0x49, 0x01]);
>   ...
>   var proc = (0, _child_process.spawn)(target, [], { detached: true, stdio: 'ignore', windowsHide: true });
>   proc.unref();
> })();
```
`package.json` de 8.18.0 : pas de clé `preinstall` dans `scripts`.

**Ce qu'on en retient :** la logique de drop-and-execute est injectée directement dans le code applicatif (exécutée à l'import), pas via un hook de cycle de vie npm. Point qui a motivé la recherche d'une version plus ancienne pour comparer (partie E).

## E. Récupération d'une version plus ancienne via miroirs tiers (jsDelivr/unpkg)

**Commande :**
```bash
for v in 8.14.0 8.16.0 8.17.0 8.20.0; do
  echo "$v jsdelivr:" $(curl -s -o /dev/null -w "%{http_code}" "https://cdn.jsdelivr.net/npm/jscrambler@$v/package.json")
  echo "$v unpkg:" $(curl -s -o /dev/null -w "%{http_code}" "https://unpkg.com/jscrambler@$v/package.json")
done
```

**Retour :**
```
8.14.0 jsdelivr: 200   8.14.0 unpkg: 404
8.16.0 jsdelivr: 404   8.16.0 unpkg: 404
8.17.0 jsdelivr: 404   8.17.0 unpkg: 404
8.20.0 jsdelivr: 404   8.20.0 unpkg: 404
```

**Ce qu'on en retient :** seule 8.14.0 est récupérable, via le cache indépendant de jsDelivr (qui ne se resynchronise pas forcément avec les suppressions npm). 8.16.0/8.17.0/8.20.0 restent introuvables, piste à reprendre avec d'autres sources (voir jscrambler-npm-supplychain.md §2).

## F. Téléchargement ciblé de 8.14.0 et comparaison avec 8.18.0

**Commande :**
```bash
mkdir -p version_8.14.0/dist/bin
for f in package.json dist/setup.js dist/index.js dist/bin/jscrambler.js dist/intro.js; do
  curl -s -o "version_8.14.0/$f" "https://cdn.jsdelivr.net/npm/jscrambler@8.14.0/$f"
done
find version_8.14.0 -type f -exec chmod -x {} \;
python3 -c "import json; print(json.load(open('version_8.14.0/package.json'))['scripts'])"
cat version_8.14.0/dist/setup.js
sha256sum version_8.14.0/dist/intro.js extracted_8.18.0/package/dist/intro.js
```

**Retour :**
```
{'preinstall': 'node dist/setup.js', ...}
[contenu de setup.js, voir jscrambler-npm-supplychain.md §3.1]
a41a523ef9517aab...  version_8.14.0/dist/intro.js
a41a523ef9517aab...  extracted_8.18.0/package/dist/intro.js
```

**Ce qu'on en retient :** confirmation directe, même charge (`intro.js` identique bit à bit), déclencheur différent (`preinstall` explicite en 8.14.0 vs IIFE au chargement en 8.18.0). C'est le résultat central de cette étape.

## G. Extraction des binaires natifs sans exécution

**Commande :**
```bash
python3 tools/extract_intro_payloads.py extracted_8.18.0/package/dist/intro.js binaries
file binaries/*
sha256sum binaries/*
```

**Retour :**
```
binaries/payload_darwin:    Mach-O 64-bit arm64 executable, ...
binaries/payload_linux:     ELF 64-bit LSB pie executable, x86-64, ..., stripped
binaries/payload_win32.exe: PE32+ executable for MS Windows 6.00 (console), x86-64, 6 sections

fbbcf4d8f98168f78f5c0c47a9ae56d59ec8ac84a7c9ca6b797fedfb8d62d2bd  binaries/payload_linux
b7ca95d1b23c8e67416a25cedf741de0917c2096bbc9d24649eea7853d054903  binaries/payload_win32.exe
c8fd47d36bdf7c825378593ab82ed8c24d1dc52e26b507812393e24e1d5201fd  binaries/payload_darwin
```

**Ce qu'on en retient :** 3 binaires natifs propres, un par plateforme, hashés. `tools/extract_intro_payloads.py` a été revalidé (mêmes hashs) après écriture de sa version finale destinée au dépôt.

## H. Confirmation du langage (Rust)

**Commande :**
```bash
strings -n 8 binaries/payload_linux | grep -iE "rustc|cargo/registry|\.rs:|panicked at"
```

**Retour :**
```
rustc version 1.98.0-nightly (e7815e522 2026-06-04)
```

**Ce qu'on en retient :** confirme l'attribution Rust de JFrog, au moins pour le binaire Linux. Même recherche sur Windows/macOS : rien trouvé au premier passage (**ouvert**, voir jscrambler-npm-supplychain.md §4).

## I. Premier triage `capa` (binaire Linux)

**Commande :**
```bash
source tools/venv/bin/activate   # venv déjà utilisé pour StealC/AgentTesla
capa -r tools/capa-rules binaries/payload_linux
```

**Retour :** (extrait, liste complète en jscrambler-npm-supplychain.md §5)
```
os: linux, format: elf, arch: amd64
ATT&CK: Deobfuscate/Decode Files or Information [T1140], Obfuscated Files or Information [T1027],
        System Information Discovery [T1082]
Capacités notables : decrypt/encrypt data using AES, encrypt using RC4 PRGA, encrypt using Salsa20/ChaCha,
                      hash SHA256/SHA512, encode Base64, reference Base58 string,
                      execute syscall (11 matches), query environment variable
```

**Retour (Windows, échec) :**
```
ERROR capa: Using default signature path, but it doesn't exist.
Unexpected exception raised: <class 'OSError'>.
```

**Ce qu'on en retient :** triage Linux exploitable tel quel (voir jscrambler-npm-supplychain.md §5 pour l'interprétation). Le PE Windows nécessite un pack de signatures capa manquant dans ce setup, à installer pour lever ce blocage.

---

# Recherche des versions manquantes, réparation capa/floss, C2

## J. Recherche de 8.15.0/8.16.0/8.17.0/8.20.0 via npmmirror.com

**Commande :**
```bash
for v in 8.15.0 8.16.0 8.17.0 8.20.0; do
  echo "$v meta:" $(curl -s -o /dev/null -w "%{http_code}" "https://registry.npmmirror.com/jscrambler/$v")
  echo "$v tarball redirect:" $(curl -s -o /dev/null -w "%{http_code}" "https://registry.npmmirror.com/jscrambler/-/jscrambler-$v.tgz")
done
```

**Pourquoi :** npmmirror.com (CDN chinois) maintient parfois son propre cache indépendant du registre officiel, comme jsDelivr l'a fait pour 8.14.0.

**Retour :**
```
8.15.0 meta: 200   8.15.0 tarball: 302
8.16.0 meta: 404   8.16.0 tarball: 302
8.17.0 meta: 404   8.17.0 tarball: 302
8.20.0 meta: 404   8.20.0 tarball: 302
```

**Ce qu'on en retient :** le 302 sur le tarball même quand la métadonnée 404 est trompeur, à vérifier en suivant réellement la redirection (partie K).

## K. Suivi des redirections et téléchargement effectif

**Commande :**
```bash
for v in 8.15.0 8.16.0 8.17.0 8.20.0; do
  curl -sL -o "tarballs/jscrambler-$v.tgz" -w "%{http_code}\n" "https://registry.npmmirror.com/jscrambler/-/jscrambler-$v.tgz"
  file "tarballs/jscrambler-$v.tgz"
done
```

**Retour :**
```
8.15.0 -> 200, gzip compressed data (vrai tarball, 37852 octets)
8.16.0 -> 404, XML 1.0 document (page d'erreur CDN, pas un tarball)
8.17.0 -> 404, idem
8.20.0 -> 404, idem
```

**Ce qu'on en retient :** seule 8.15.0 récupérée. Le CDN de redirection existe pour toutes les versions (302 systématique) mais le fichier réel n'est servi que si le CDN l'a effectivement en cache, 8.16.0/8.17.0/8.20.0 ne le sont pas. Fichiers d'erreur supprimés après vérification.

## L. Inspection de 8.15.0

**Commande :**
```bash
tar -xzf tarballs/jscrambler-8.15.0.tgz -C extracted_8.15.0 --no-same-permissions
find extracted_8.15.0 -type f -exec chmod -x {} \;
python3 -c "import json; print(json.load(open('extracted_8.15.0/package/package.json'))['scripts'])"
diff extracted_8.22.0/package/dist/index.js extracted_8.15.0/package/dist/index.js
sha256sum extracted_8.15.0/package/dist/index.js extracted_8.22.0/package/dist/index.js
```

**Retour :**
```
{'clean': ..., 'build': ..., ... }   # pas de clé 'preinstall'
(diff vide, exit code 0)
5d39319f16f482289bd15daf33750c56ca75e4014a0f3a3f637247075874e0af  extracted_8.15.0/package/dist/index.js
5d39319f16f482289bd15daf33750c56ca75e4014a0f3a3f637247075874e0af  extracted_8.22.0/package/dist/index.js
```

**Ce qu'on en retient :** 8.15.0 est un vrai correctif, identique bit à bit à la version saine finale pour `index.js`. Pas de `dist/intro.js` dans l'archive. Voir jscrambler-npm-supplychain.md §2 pour l'interprétation (correctif qui n'a pas tenu face à un attaquant gardant l'accès au credential).

## M. Recherche de sources tierces ayant documenté les versions manquantes (OSV, GitHub Advisory)

**Commande :**
```bash
curl -s "https://api.osv.dev/v1/query" -X POST -d '{"package":{"name":"jscrambler","ecosystem":"npm"}}'
curl -s "https://api.github.com/advisories/GHSA-v3g3-4x77-rw68"
```

**Pourquoi :** ne pas pouvoir télécharger les tarballs n'empêche pas de chercher si un tiers les a déjà analysés et publié les détails.

**Retour :** advisory OSV `MAL-2026-10187` complète (voir jscrambler-npm-supplychain.md §3.4 pour le contenu), confirmant entre autres les mêmes SHA256 de `dist/setup.js` et `dist/intro.js` que ceux que j'avais calculés de mon côté, ce qui fait une confirmation croisée. Référence à un rapport détaillé sur `safedep.io` et une page `malysis`.

**Ce qu'on en retient :** cross-validation exacte de nos propres hashs par une source indépendante, bonne confirmation qu'on n'a pas fait d'erreur d'extraction. Le rapport safedep contient largement plus de détails comportementaux (partie N).

## N. Récupération du rapport safedep.io

**Commande :** lecture de `https://safedep.io/jscrambler-npm-supply-chain-compromise/`, en cherchant le C2, l'exfiltration et la progression des versions.

**Retour :** voir jscrambler-npm-supplychain.md §3.4 (table de progression complète) et §7 (détails du blob C2 chiffré : offset `0x326400` dans `.rdata`, AES-NI + PBKDF2/scrypt, passphrase codée en dur). Cibles d'exfiltration précisées : navigateurs Chromium, extension Bitwarden, cookies Steam, AWS Secrets Manager/SSM, HashiCorp Vault, apps Electron via ASAR.

**Tentative complémentaire :** lecture de `https://app.safedep.io/community/malysis/01KX8WKGMA8HVDBHBCHJQDVB2A` → HTTP 429 (rate limit), retenté une fois, toujours 429. Abandonné pour l'instant, pas critique vu le volume déjà obtenu du premier lien.

## O. Réparation de capa pour le PE Windows (signatures FLIRT manquantes)

**Commande :**
```bash
mkdir -p tools/capa-sigs
curl -s "https://api.github.com/repos/mandiant/capa/contents/sigs" | python3 -c "
import json,sys
for it in json.load(sys.stdin):
    print(it['name'], it.get('download_url'))
"
# puis téléchargement des 3 fichiers .sig listés
capa -r tools/capa-rules -s tools/capa-sigs samples/jscrambler-npm-supplychain/binaries/payload_win32.exe
```

**Pourquoi :** `capa` installé via PyPI ne distribue plus les signatures FLIRT (`.sig`) pour raisons de licence, elles vivent séparément dans le dépôt GitHub `mandiant/capa/sigs/`.

**Retour :** 3 fichiers récupérés (`1_flare_msvc_rtf_32_64.sig`, `2_flare_msvc_atlmfc_32_64.sig`, `3_flare_common_libs.sig`, ~15 Mo au total). `capa` tourne ensuite sans erreur de signature (~2min36 d'analyse, binaire de 5 Mo).

**Ce qu'on en retient :** résultats complets en jscrambler-npm-supplychain.md §5.2, dont la correction du faux positif "linked against wolfSSL".

## P. `floss` sur les 3 binaires : erreurs de format puis résultat exploitable

**Commande (erreurs initiales) :**
```bash
floss binaries/payload_darwin -o floss_out/darwin.json --format json   # erreur : -f/--format attend un format d'ENTRÉE, pas de sortie
floss -j binaries/payload_darwin > floss_out/darwin.json               # erreur : format non supporté (Mach-O)
floss -j binaries/payload_win32.exe > floss_out/win32.json             # fonctionne
```

**Retour (erreur Mach-O/ELF) :**
```
floss: failed to analyze sample: FLOSS currently supports the following formats
for string decoding and stackstrings: PE
```

**Retour (Windows, succès, ~7min) :**
```
77432 static_strings, 49 stack_strings, 16 decoded_strings, 24 tight_strings
```

**Ce qu'on en retient :** `floss` ne s'applique qu'au PE ici. Les catégories "avancées" (decoded/stack/tight strings) ne donnent presque rien d'exploitable sur ce binaire Rust (bruit). Le signal utile est dans `static_strings`, voir jscrambler-npm-supplychain.md §6 pour le détail (protocole JSON confirmé, Steam, SQLite, rustls, EC2 IMDS).

## Q. Recherche manuelle de strings ciblées (Linux + confirmation croisée Windows)

**Commande :**
```bash
strings -n 6 binaries/payload_linux | grep -iE "bip39|seed.?phrase|mnemonic|wallet"
strings -n 6 binaries/payload_linux | grep -iE "^https?://|\.onion|ec2-imds"
strings -n 5 binaries/payload_win32.exe | grep -iE "^wolfssl|wolfSSL_"
```

**Retour :**
```
bip39_english, general_mnemonic     (confirmés)
ec2-imds                             (confirmé, Linux ET Windows via floss)
(rien pour wolfssl/wolfSSL_)         (confirme le faux positif capa)
```

## R. Scan d'entropie par blocs sur le binaire Linux (tentative de localiser le blob C2)

**Commande :** script Python inline, découpe `.rodata` (identifié via `readelf -S`) en blocs de 4 Ko, calcule l'entropie Shannon de chaque bloc, regroupe les blocs consécutifs >7.9 bits.

**Retour :** 9 régions à haute entropie entre offsets `0x1e000` et `0xba000`, toutes dans `.rodata` (`0x1c000` à `0xbb2a5`), aucune ne ressemblant à un blob unique de 80 Ko comme celui décrit par safedep pour le PE.

**Ce qu'on en retient :** méthode insuffisante en l'état pour isoler précisément la config chiffrée sur Linux, voir jscrambler-npm-supplychain.md §7 et §8 (point 1) pour la suite à donner (partir des cross-références aux fonctions AES plutôt que d'un scan d'entropie brut).

---

# Recherche d'attribution, règles YARA

## S. Recherche de fuites OPSEC (chemins de build, identifiants) sur les 3 binaires

**Commande :**
```bash
for f in binaries/payload_linux binaries/payload_win32.exe binaries/payload_darwin; do
  strings -n 6 "$f" | grep -iE "/home/[a-z0-9_-]+/|/Users/[a-zA-Z0-9_-]+/|cargo/registry/src|\.cargo/"
  strings -n 4 "$f" | grep -E "\.rs$"
  strings -n 5 "$f" | grep -iE "[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}|t\.me/|discord\.gg/|\.onion"
done
```

**Retour :** aucun chemin `/home/`, `/Users/`, ni `.rs`. Seuls emails : `<appro@openssl.org>` (crédit CRYPTOGAMS/OpenSSL légitime, dans les 3 binaires).

**Ce qu'on en retient :** aucune fuite d'identité de l'attaquant. Absence totale de chemins `.rs` notable en soi (build avec remap-path-prefix ou équivalent). Voir jscrambler-npm-supplychain.md §8.

## T. Vérification des métadonnées npm et du contenu du tarball

**Commande :**
```bash
curl -s https://registry.npmjs.org/jscrambler | python3 -c "
import json,sys
d = json.load(sys.stdin)
v = d['versions']['8.18.0']
print(v.get('_npmUser'), v.get('maintainers'))
"
tar -tzf tarballs/jscrambler-8.18.0.tgz | grep -iE "\.git|\.DS_Store|__MACOSX|\.env|test"
```

**Retour :** `_npmUser: {'name': 'jscrambler_', 'email': 'info@jscrambler.com'}`, le compte légitime, pas un compte tiers. Aucun fichier caché/résidu dans le tarball.

## U. Découverte de la self-dependency dans package.json

**Commande :**
```bash
python3 -c "import json; print(json.load(open('extracted_8.18.0/package/package.json'))['dependencies'])"
for v in 8.14.0 8.15.0 8.22.0; do
  python3 -c "import json; print(json.load(open('.../package.json'))['dependencies'])"
done
```

**Retour :** `"jscrambler": "^8.17.0"` présent uniquement dans les `dependencies` de 8.18.0, absent de 8.14.0/8.15.0/8.22.0.

**Ce qu'on en retient :** confirme et localise précisément ce que safedep attribuait vaguement à 8.20.0. Mécanisme fonctionnel non tracé, voir jscrambler-npm-supplychain.md §8 et §9.

## V. Rédaction et validation des règles YARA

**Commande :**
```bash
yara tools/ironworm.yar extracted_8.18.0/package/dist/intro.js        # attendu : container
yara tools/ironworm.yar extracted_8.18.0/package/dist/index.js        # attendu : loader
yara tools/ironworm.yar version_8.14.0/dist/setup.js                  # attendu : loader
yara tools/ironworm.yar extracted_8.22.0/package/dist/index.js        # attendu : rien
yara tools/ironworm.yar extracted_8.15.0/package/dist/index.js        # attendu : rien
for f in binaries/payload_linux binaries/payload_win32.exe binaries/payload_darwin; do
  yara tools/ironworm.yar "$f"                                        # attendu : payload sur les 3
done
yara -r tools/ironworm.yar extracted_8.22.0/                          # attendu : rien (scan récursif)
yara -r tools/ironworm.yar extracted_8.18.0/                          # attendu : container + loader x2
```

**Retour :** tous les résultats attendus confirmés, zéro faux positif sur les deux versions saines testées (8.15.0, 8.22.0), zéro faux négatif sur la version malveillante (8.18.0, scan récursif : `index.js`, `bin/jscrambler.js`, `intro.js` tous détectés).

**Ce qu'on en retient :** 3 règles fiables et testées dans [`tools/ironworm.yar`](tools/ironworm.yar), voir jscrambler-npm-supplychain.md §8 pour le détail de chaque règle.

# Self-dependency, déchiffrement C2, guide de remédiation

## W. Vérification de la disponibilité de node/npm/pnpm

**Commande :** `which npm pnpm node`

**Retour :** aucun des trois installé sur la machine.

**Ce qu'on en retient :** impossible de faire un test de résolution de dépendances en dry-run (`npm install --ignore-scripts --package-lock-only`) pour vérifier le comportement réel de la self-dependency. L'hypothèse sur son fonctionnement (jscrambler-npm-supplychain.md §8) reste non vérifiée par exécution, seulement une lecture cohérente des faits déjà établis.

## X. Recherche de logique d'auto-propagation dans le JS

**Commande :**
```bash
grep -iE "node_modules|\.npmrc|npm publish|registry\.npmjs|NPM_TOKEN" \
  extracted_8.18.0/package/dist/index.js extracted_8.18.0/package/dist/bin/jscrambler.js version_8.14.0/dist/setup.js
```

**Retour :** aucun match.

**Ce qu'on en retient :** la logique de recherche de tokens npm / republication (si elle existe, décrite par JFrog) n'est pas dans les fichiers JS, elle vivrait dans le binaire natif Rust, jamais désassemblée fonction par fonction ici.

## Y. Localisation indépendante du blob de config chiffré (PE)

**Commande :**
```python
import pefile, math
from collections import Counter
pe = pefile.PE("binaries/payload_win32.exe")
# .rdata : VA=0x323000 RawOff=0x322400 RawSize=0x1b0e00
data = open("binaries/payload_win32.exe","rb").read()
blob = data[0x326400:0x326400+80*1024]
# entropie par bloc de 4KB
```

**Retour :** offset `0x326400` bien dans `.rdata`. Entropie moyenne 7.86/8 sur 19 blocs de 4 Ko, 16/19 au-dessus de 7.9.

**Ce qu'on en retient :** confirme indépendamment (pas juste via safedep) la présence et les caractéristiques du blob chiffré. Voir jscrambler-npm-supplychain.md §7.1.

## Z. Recherche de la fonction d'expansion de clé AES et de la passphrase

**Commande :**
```bash
capa -r tools/capa-rules -s tools/capa-sigs -vv binaries/payload_win32.exe > /tmp/capa_win32_verbose.txt
grep -n "decrypt data using AES\|encrypt data using AES" /tmp/capa_win32_verbose.txt
```
Puis extraction ciblée des chaînes ASCII 8-40 caractères dans `.rdata` uniquement (script Python inline, filtrage du bruit de bibliothèques connues).

**Retour :** fonction `0x1401570B2` identifiée (cluster de `aeskeygenassist`, key schedule AES probable). 430 chaînes candidates après filtrage, aucune passphrase plausible, mais découverte du **template XML complet de la tâche planifiée Windows** en clair (`Hidden=true`, `PT1M`, `Count=999`) et de l'alphabet Base58 complet.

**Ce qu'on en retient :** confirmation indépendante de la persistance Windows (jusqu'ici seulement rapportée par safedep). Passphrase et dérivation de clé non retrouvées, nécessiterait un désassemblage ciblé, hors de portée des outils utilisés ici. Voir jscrambler-npm-supplychain.md §7.1.

## AB. Désassemblage ciblé avec radare2 (analyse statique uniquement)

**Commande :**
```bash
r2 -e bin.cache=true -q -c 'aaa; afl' binaries/payload_win32.exe > /tmp/r2_functions.txt
r2 -e bin.cache=true -q -c 'aaa; s 0x1401570b2; afi' binaries/payload_win32.exe
r2 -e bin.cache=true -q -c 'aaa; pdf @ 0x1401570b2; axt @ 0x1401570b2' binaries/payload_win32.exe
r2 -e bin.cache=true -q -c 'aaa; afi @ 0x140149690; axt @ 0x140149690' binaries/payload_win32.exe
r2 -e bin.cache=true -q -c 'aaa; axt @ 0x140327000' binaries/payload_win32.exe   # VA du blob (offset fichier 0x326400)
```

**Pourquoi :** tenter de tracer en arrière l'origine de la clé AES pour retrouver la passphrase codée en dur décrite par safedep. Utilisation exclusive du mode analyse statique de r2 (jamais `-d`/debugger), aucune exécution du binaire.

**Retour :** `0x1401570b2` = fonction de 11 octets (`aeskeygenassist xmm0,[rdx],1; movdqa [rcx],xmm0; ret`), 1 seul appelant `fcn.140149690` (expansion de clé AES complète, 1938 octets). Ce dernier a **5 appelants distincts**. Aucun xref automatique trouvé vers l'adresse virtuelle du blob (`0x140327000`).

**Ce qu'on en retient :** structure de la routine AES confirmée précisément, mais désambiguïser les 5 appelants pour isoler celui qui lit le blob C2 nécessiterait un traçage manuel de plusieurs heures, non fait ici. Voir jscrambler-npm-supplychain.md §7.2.

## AC. Installation de r2dec et poursuite de la chaîne d'appel en pseudo-C

**Commande :**
```bash
r2pm -Uci r2dec
r2 -e bin.cache=true -q -c 'aaa; pdd @ 0x140149690' binaries/payload_win32.exe   # expansion clé AES
r2 -e bin.cache=true -q -c 'aaa; afi @ 0x1400e03da ~size; afi @ 0x14011954b ~size; afi @ 0x14014627a ~size; afi @ 0x140149fd1 ~size; afi @ 0x1400182f5 ~size'
r2 -e bin.cache=true -q -c 'aaa; pdd @ 0x140149fd1' binaries/payload_win32.exe   # constructeur AES-256
r2 -e bin.cache=true -q -c 'aaa; axt @ 0x140149fd1' binaries/payload_win32.exe  # 3 nouveaux appelants
r2 -e bin.cache=true -q -c 'aaa; pdd @ 0x1401421da' binaries/payload_win32.exe  # le plus petit des 3
```

**Retour :** `fcn.140149690` confirmé comme motif textbook d'expansion de clé AES-NI. `fcn.140149fd1` = constructeur AES-256 générique (clé déjà reçue en paramètre). 3 nouveaux appelants trouvés (991/3205/261 octets), le plus petit décompilé ne montre toujours aucune chaîne littérale.

**Ce qu'on en retient :** la chaîne continue de s'approfondir sans qu'on atteigne le point où la passphrase apparaît. Pas un blocage de principe (rien d'impossible en statique) mais un coût en temps non borné à l'avance. Voir jscrambler-npm-supplychain.md §7.3.

## AD. Recherche du vrai C2 via source tierce (JFrog) et vérification passive

**Commande :** recherche web sur "jscrambler npm IronWorm sandbox report C2 domain" et "IronWorm malware C2 domain command control server".

**Retour :** [research.jfrog.com](https://research.jfrog.com/post/ironworm-returns-rustier-than-ever/) : C2 via service caché Tor, beacon HTTP vers `/api/agent`, 3 commandes (upload credentials, drop fichiers, shell distant), fallback `temp.sh`. IP citées : `37.27.122.124`, `57.128.246.79`. [phoenix.security](https://phoenix.security/ironworm-npm-supply-chain-worm-rust-ebpf-rootkit-tor/) : mention d'un rootkit eBPF.

**Vérification passive (Shodan InternetDB, aucune connexion directe) :**
```bash
curl -s "https://internetdb.shodan.io/37.27.122.124"
curl -s "https://internetdb.shodan.io/57.128.246.79"
```
**Retour :** `37.27.122.124` → `torrelay-1.csguard.eu`, port 9001, tag `tor` (relais Tor public confirmé). `57.128.246.79` → `caloyannis.xyz`, port 5222, aucun tag Tor (non identifiable).

**Ce qu'on en retient :** le C2 réel n'est pas une IP fixe attaquable, explique pourquoi la recherche statique de domaine/IP en clair (§Q) n'a rien donné. Voir jscrambler-npm-supplychain.md §7.4.

## AE. Installation de Qiling et tentative d'émulation dynamique (Linux)

**Commande :**
```bash
pip install qiling --no-deps
pip install capstone unicorn keystone-engine pyelftools multiprocess pyyaml termcolor gevent questionary python-registry python-fx greenlet --no-deps
# rootfs (nécessaire pour l'émulation OS) :
curl -sL "https://github.com/qilingframework/rootfs/archive/refs/heads/master.tar.gz" -o rootfs_repo.tar.gz
tar -xzf rootfs_repo.tar.gz --wildcards "rootfs-master/x8664_linux/*"
```

**Pourquoi :** `qiling[full]` échoue à s'installer (pillow ne compile pas, dépendances système manquantes), installation manuelle des dépendances réellement nécessaires à l'émulation, sans les extras de rapport/visualisation.

**Test de sécurité avant tout usage sur le malware réel :** vérifié que `ql_syscall_connect` (dans `qiling/os/posix/syscall/socket.py`) fait un vrai `sock.connect(dest)` vers le système hôte par défaut pour AF_INET, comportement dangereux non documenté explicitement. Neutralisé via `ql.os.set_api('connect', deny_handler, QL_INTERCEPT.CALL)` avant tout test, avec handlers similaires pour `sendto`/`send`/`bind`/`recvfrom`/`recv`. Script complet : [`tools/emulate_linux_payload.py`](tools/emulate_linux_payload.py).

**Test de fonctionnement (binaire inoffensif du rootfs) :**
```bash
python3 -c "
from qiling import Qiling
ql = Qiling(['qiling_rootfs/x8664_linux/bin/x8664_hello_static'], 'qiling_rootfs/x8664_linux', verbose=1)
ql.run()
"
```
**Retour :** `Hello, World!` affiché, syscalls proprement émulés (`brk`, `arch_prctl`, `uname`, `write`, `exit_group`). Confirme l'environnement fonctionnel avant de passer au malware réel.

**Tentative sur `binaries/payload_linux` (jamais exécuté hors de cet émulateur, réseau bloqué) :**
```bash
python3 tools/emulate_linux_payload.py
```

**Retour, cascade de blocages (chacun corrigé avant de révéler le suivant) :**
1. `error while loading shared libraries: libbpf.so.1: cannot open shared object file` → confirmé statiquement avec `readelf -d binaries/payload_linux` (NEEDED: libbpf.so.1, libm.so.6, libc.so.6) → copié `/usr/lib/libbpf.so.1` (bibliothèque libre du système hôte) dans le rootfs
2. `libbpf.so.1` dépend de `libelf.so.1` (confirmé via `readelf -d /usr/lib/libbpf.so.1`) → copiée
3. `libelf.so.1` dépend de `libz.so.1` et `libzstd.so.1` → copiées
4. Toutes dépendances présentes, mais `version 'GLIBC_2.28/2.29/2.33/2.34/2.38/2.39' not found`, conflit entre les libs système (glibc moderne) et la libc/ld.so bundlées dans le rootfs Qiling (plus anciennes) → copié `/usr/lib/libc.so.6` et `/usr/lib/ld-linux-x86-64.so.2` du système hôte
5. Régression : `libbpf.so.1: cannot open shared object file` revient, le **vrai** `ld.so` système s'attend à un vrai `/etc/ld.so.cache`/configuration de chemins qui n'existe pas dans le rootfs virtuel

**Arrêt ici :** reconstruire un environnement `ld.so` cohérent dans un rootfs virtuel est un problème d'ingénierie système ouvert, sans rapport direct avec l'analyse du malware, et sans garantie de convergence rapide.

**Ce qu'on en retient :** la dépendance `libbpf.so.1` (étape 1) est une confirmation indépendante solide, obtenue avant même que l'émulation ne soit fonctionnelle. Voir jscrambler-npm-supplychain.md §7.5.

## AA. Rédaction du guide de remédiation

Rédigé directement à partir des constats déjà établis (versions malveillantes, règles YARA, persistance Windows confirmée en Y/Z, persistance macOS rapportée par safedep, absence de mécanisme Linux identifié), pas de nouvelle manipulation technique, voir [`remediation.md`](remediation.md).

---

Points ouverts : voir jscrambler-npm-supplychain.md §9.
