Runbook : reproduction pas à pas (INC Ransom, chaîne Ghost, Phantom et RevSocks)

Point de départ et contraintes#

Tout part de l'échantillon f6fbe168...0c60, signalé sur MalwareBazaar le 2026-10-20 par SOCRadarSTRU avec un renvoi vers l'article de SOCRadar sur une infrastructure d'affiliés INC et Gentlemen. L'objectif était de remonter toute la chaîne en statique, en émulation et en OSINT passif. Trois règles tenues de bout en bout : aucun échantillon exécuté, aucune connexion vers l'infrastructure de l'opérateur (ni adresse, ni domaine, ni API Telegram), aucun usage d'un identifiant récupéré dans un binaire.

Acquisition#

Pivots sur MalwareBazaar (clé dans .mb_api_key) à partir des étiquettes de l'échantillon :

for t in "INC Ransomware" "The Gentlemen Ransomware" STRU; do
  curl -s -X POST https://mb-api.abuse.ch/api/v1/ -H "Auth-Key: $(cat .mb_api_key)" \
       --data-urlencode "query=get_taginfo" --data-urlencode "tag=$t" -d "limit=1000"
done

141 échantillons uniques, dont 114 déposés le 2026-10-20 entre 12:11 et 12:24 UTC. Les 27 autres (PivotC2, ICMacOS, Operation_Conflict_Compass) appartiennent à d'autres campagnes du même rapporteur, je les ai écartés. Les pivots par imphash et par TLSH ne ramènent qu'un seul résultat chacun : l'échantillon lui-même.

Téléchargement par query=get_file, mot de passe d'archive infected, un dossier par SHA-256, vérification de chaque hash. Piège rencontré au passage : la dernière ligne de mon fichier de hashs n'avait pas de saut de ligne final, donc while read l'ignorait. J'ai eu 113 fichiers au lieu de 114 jusqu'à ce que je comprenne pourquoi.

Triage en masse#

python3 tools/triage_cluster.py binaries/cluster osint/triage

Le script produit un CSV et un JSON par fichier : type, taille, imphash, horodatage PE, signature, langage probable (Go, Rust, MinGW, MSVC), adresses, URL, domaines et mutex en clair. Lecture : 34 binaires MinGW, 17 Go, 8 Rust, 4 MSVC, 45 ELF, plus 2 documents Word, un script Python, un script PowerShell, une archive zip et une DLL non classée. L'inventaire consolidé est cluster_inventory.csv.

Métadonnées de build des binaires Go#

Le format buildinf de Go (marqueur \xff Go buildinf:) contient en clair, pour les versions récentes, la version du compilateur, le chemin du module et les paramètres de build (-ldflags, GOOS, CGO_ENABLED). Lecture directe des octets, sans exécuter le binaire. C'est de là que viennent les noms de modules (mcp-agent, lsass-clean, hostprobe, rks3, fileseeker2) et les versions de Go (de 1.22.4 à 1.26.5) du tableau du §7 du billet. Les versions de l'éditeur de liens, les en-têtes Rich et les langues de ressources des 64 exécutables Windows sont dans osint/attribution_metadata.csv.

Pour les binaires Go dont les chaînes sont masquées (garble), les outils tools/decompile_go_main.py, tools/decompile_tail_funcs.py, tools/decompile_at.py et tools/go_pclntab_funcs.py ciblent la décompilation sur main et sur les fonctions de fin de binaire, et tools/decode_garble_literals.py et tools/decode_garble_shuffle.py recalculent les chaînes à partir du texte décompilé, sans rien exécuter. C'est ainsi que sortent les chemins de compilation /opt/c2/ghost-api-tg/... et /root/chat/..., le mode ghostSend de BackupConsole, les douze messages de log de Ghost_dump_ppl et l'identité de son pilote BYOVD (Dell DBUtil_2_3.sys, embarqué à l'offset 0xa358c0).

Certificats de signature#

Extraction de la structure Authenticode de chaque PE signé (répertoire de sécurité, indice 4) puis openssl pkcs7 -print_certs. Les cinq binaires « Microsoft Corporation » ont un sujet égal à l'émetteur et une validité d'un an à la seconde près à partir de l'horodatage PE. Les trois outils de sauvegarde portent un « SysTool Software Ltd. » auto-signé, valable du 2026-06-25 au 2036-06-22, avec un contre-horodatage DigiCert.

Livraison#

Modèles Word : unzip, lecture de docProps/core.xml et app.xml, extraction du projet VBA avec olevba (oletools). Le projet VBA contient une seule procédure, Document_Open, qui crée un processus par WMI. Les métadonnées fines (racines d'identifiants de révision, version d'Office, langue, format de page) se lisent dans les mêmes fichiers XML.

Cradle PowerShell et beacon Python : lecture directe. Pour le cradle, la vérification du contournement AMSI consiste à comparer les décalages écrits dans le tampon de contexte (0x350, 0x378) à ceux de Dr0 et Dr7 dans une structure CONTEXT x64 (0x48, 0x70).

Chargeur Ghost_loader et Meterpreter#

Clé et IV sont en clair dans .data. Le déchiffrement du blob de 250 016 octets se rejoue avec OpenSSL en AES-256-CBC, et l'émulation du WinMain sous Unicorn jusqu'à CryptDecrypt donne le même résultat. La DLL obtenue se compare au metsrv.x64.dll du gem metasploit-payloads 2.0.245 téléchargé sur rubygems.org : identique à 37 octets d'amorce près, suivie du bloc de configuration de 2 202 octets (URL, en-tête Host, User-Agent, expiration).

Implants Ghost : décompilation et émulation des chaînes#

python3 tools/decompile_all.py <echantillon> cluster_decomp/<pre>.c.txt

Les chaînes de configuration sont chiffrées par une routine vectorisée SSE. Plutôt que de la réécrire, j'appelle la fonction du binaire sous Unicorn avec le tampon chiffré en entrée :

# G6B77FE12 : decodeur a 2 arguments (tampon, longueur)
python3 tools/emulate_string_decoder.py <G6B77FE12.exe> 140001bf0 \
   "qw:970605a333b1ce61,a6c7b0e69d9c2d26,84c8caba9e764a52:24" \
   "qw:d02910b17fb7d247,eb96f6f7d6d42172,84ca8bb6833f4f07:24" \
   "rva:b010:21"
# -> Global\31ae8e4090b03c3e ; api.system-strategy.org ; /api/v3/be13a10b476f

Pour les générations Telegram, le décodeur prend un contexte en premier argument (ctx, tampon, longueur). extract_encoded_strings.py relit la décompilation, reconstitue les tampons posés sur la pile, résout les alias de pointeurs puis émule le décodeur à chaque appel :

python3 tools/extract_encoded_strings.py <G00F9FCB5.exe> cluster_decomp/118c38f1.c.txt FUN_1400048f0 FUN_1400040c0 14000f040

Le build stealth utilise deux contextes (0x140013000 et 0x140013010) et des écritures d'octets isolés sur la pile, que l'outil gère. Limite connue : quand un pointeur n'est pas un alias direct d'un tampon de pile, l'outil retombe sur une heuristique qui produit parfois de faux décodages (octets illisibles). Ces lignes sont à écarter à la main.

Le jeton Telegram complet apparaît à l'écran lors de ce passage. Il n'est reporté dans aucun fichier, seul l'identifiant numérique du bot l'est. Le PE extrait de Ghost_OneDriveSync (voir plus bas) a été traité de la même façon, par émulation de FUN_1400043b0 : même mutex, même bot 8986910514, même conversation 8809711703 que l'implant G7CDD6836 du lot.

Ghost_OneDriveSync et blob Donut#

python3 -I tools/decrypt_onedrivesync.py <Ghost_OneDriveSync.exe> payload.bin   # AES-256 CTR, cle derivee d'une chaine du binaire
python3 -I tools/extract_donut_module.py payload.bin donut_out/                   # instance Chaskey CTR, puis module embarque

payload.bin (114 388 octets) est un blob Donut, ce que Triage et VMRay avaient aussi identifié. Le module extrait est un PE x64 de 94 824 octets, implant Ghost de troisième génération, absent de MalwareBazaar. La règle YARA Ghost_OneDriveSync_DonutShellcode vise le blob déchiffré.

RevSocks#

Les 16 builds ont été décompilés un par un. C'est ce passage qui a fait sortir le second serveur de livraison, 141.98.83.180, dont l'URL est masquée par un XOR 0xcb et n'apparaît dans aucune extraction de chaînes. La règle structurelle du fichier YARA reconnaît les 16 builds.

Pour les agents v4, la dérivation de clé lue dans la décompilation a été confirmée par émulation, et le format des trames est implémenté dans un codec :

python3 tools/emulate_v4_kdf.py .          # émule FUN_140001910 et FUN_140001990 sur le préfixe XOR 0x5a et le secret, compare à hashlib
python3 tools/revsocks_codec.py selftest   # clé, poignée de main, scellement et ouverture des trames, détection d'une étiquette altérée
python3 tools/revsocks_codec.py decrypt --secret <secret> flux.bin   # déchiffre une session capturée

Le tunnel de l'agent ELF RevSocks_vmware-usbarbitrator se lit avec tools/decompile_refs.py en ciblant la chaîne chacha20_tunnel_v3_ : la constante de 32 caractères est présente deux fois dans le binaire.

Encrypteurs INC#

Builds Windows#

Les six builds de 2 206 208 octets se comparent octet par octet (53 à 62 octets d'écart par paire). Dump des zones qui diffèrent, décodage de l'horodatage Unix des 8 premiers caractères du premier identifiant, extraction des crates Rust depuis les chemins registry/src/ du binaire.

Builds Linux et ESXi#

Sans exécuter aucun binaire :

# champs patchés par lot : clé base64 de 44 caractères et identifiant de 24 hex (voir osint/inc_victim_ids.csv)
# note de rançon : base64 stocké dans le binaire, décalage de 2 caractères
# rôle de la clé : décompiler seulement les fonctions qui référencent la chaîne
python3 -I tools/decompile_refs.py <elf_x86_64_gnu> cluster_decomp/inc_elf_x86_64_gnu_refs.txt 'rCdf4MoDNZi3/S'

La fonction trouvée tire 32 octets aléatoires, applique le masquage de scalaire Curve25519, calcule la clé publique éphémère, décode la chaîne base64 et exige 32 octets : échange X25519 avec une clé publique d'opérateur propre à chaque victime. Les trois lots (kit1, kit2, kit3) correspondent à trois des six builds Windows personnalisés. Les 6 builds PowerPC ne contiennent pas l'identifiant en clair.

Le mode de chiffrement est un paramètre d'exécution, pas un champ patché : les chaînes d'aide d'un ELF gnu et du build ESXi donnent les options directory, file, mode, motd, esxi, skip, daemon et delay. L'extension INC se trouve juste avant la clé base64 dans les 39 ELF (recherche de la clé dans chaque fichier, puis test des octets qui la précèdent).

RDAP ne couvre pas .su, et les journaux de transparence n'ont aucun certificat pour incblog.su.

Kits historiques, Lynx et Sinobi#

Les 54 autres hachages étiquetés INC sur MalwareBazaar ont été téléchargés (get_file, mot de passe infected) et lus de la même manière. Le détail est dans osint/inc_candidates_analysis.md. L'ESXi antérieur au lot (86d04474...) a été comparé par diff binaire à un build ESXi du lot : 67 octets d'écart.

Parenté de code : extraction des chaînes imprimables de huit caractères et plus de cinq échantillons (INC C++ ELF 2024, INC C++ PE32 2024, Lynx PE32 2024, INC Rust Windows 2026, INC Rust ELF 2026), puis indice de Jaccard par paire. INC C++ PE32 et Lynx : 0,467 (274 chaînes communes). Les paires avec les builds Rust restent à 0,05 ou moins. C'est une mesure sur des chaînes, pas sur des fonctions.

Identifiants de victime et blog de fuite#

Relevé des publications des groupes inc ransom (967), lynx et sinobi par l'API publique de l'agrégateur ransomlook, sans aucun contact avec les blogs eux-mêmes. Extraction de l'identifiant de 24 caractères hexadécimaux de chaque lien de publication, découpage en horodatage (4 octets), valeur aléatoire (5 octets) et compteur (3 octets), regroupement par valeur aléatoire (osint/inc_objectid_process_values.csv), puis comparaison avec les identifiants des kits historiques et des trois lots de 2026.

Ghost_AetherPulse#

python3 tools/ghidra_api_callgraph.py <Ghost_AetherPulse.dll> sortie.txt <dossier_projet_ghidra>

Le script calcule les chemins d'appel depuis les trois exports vers les API Windows. Il atteint une routine de copie puis d'écrasement de fichier (FILE_FLAG_DELETE_ON_CLOSE) et une routine de création de fichier dans le dossier temporaire. Les deux blocs d'entropie élevée de .rdata sont des tables zlib et CRC-32 et un pool de constantes, pas des charges. tools/ghidra_function_fingerprint.py et tools/ghidra_refs_bytes.py ont servi aux comparaisons fonction par fonction (implants Ghost) et aux recherches d'octets référencés.

OSINT passif#

Source Requête
Certificats https://crt.sh/?q=%25.<domaine>&output=json
Domaines https://rdap.org/domain/<domaine>
Blocs IP https://rdap.org/ip/<adresse>
Flux abuse.ch ThreatFox search_ioc et URLhaus host pour chaque adresse et domaine
Historique web Wayback Machine (API CDX), urlscan.io (recherche publique)
Exposition Shodan InternetDB, RIPEstat, AlienVault OTX, GreyNoise communautaire

Aucune de ces requêtes ne contacte l'infrastructure de l'opérateur. Chaque requête et chaque résultat, y compris les négatifs, est consigné dans osint/pivots_log.md. Les réponses brutes (RDAP, urlscan, Wayback) contiennent des contacts de personnes physiques et ne sont pas publiées.

GitHub#

Recherche de code en lecture seule avec ma session gh (l'API search/code exige une session authentifiée) sur les chemins de compilation, les noms d'utilisateur et les chaînes uniques relevés dans les binaires, dont le runner runneradmin. Journal complet dans osint/github_search.md : aucun dépôt de l'opérateur, seulement des listes de blocage de défenseurs.

VirusTotal#

Clé dans ../.vt_api_key, hors du dépôt et jamais écrite dans les sorties. Passes successives, toutes en lecture seule, à 4 requêtes par minute :

python3 tools/vt_pull.py          # résolutions, fichiers qui communiquent, sous-domaines, objets
python3 tools/vt_pull_pivots.py   # contacts des fichiers hors lot et adresses additionnelles de mondeikin.com
python3 tools/vt_pull_leads.py    # pistes de domaine issues des résolutions

Rien n'est soumis. Le tableau des fichiers vus hors du lot est osint/vt_hors_lot.csv, les réponses brutes restent hors paquet.

Triage#

Clé dans ../.env (TRIAGE_API_KEY), jamais écrite dans les sorties. Le service exige un User-Agent explicite. L'outil masque les jetons de bots Telegram à l'écriture.

python3 tools/triage_pull.py --no-pcap <id_echantillon> ...   # rapports comportementaux
python3 tools/triage_pull.py <id_echantillon>                  # avec capture réseau

Les captures sont analysées hors ligne avec un lecteur de paquets Python : flux TCP vers les serveurs de la campagne, octets échangés, messages de poignée de main TLS et certificats lus en clair (TLS 1.2). Rapports et captures restent hors paquet. Les rapports publics consultés pour Ghost_loader : Triage 261020-pw4hkshv61 (score 3, tag discovery), CAPE 93070 (Meterpreter), Intezer 49a39783-487a-4c27-b62d-302147486844, VMRay _mb/f6fbe1681165. Les API d'ANY.RUN, OpenTIP, FileScan et mwdb.cert.pl refusent l'accès sans compte, je ne les ai pas utilisées.

Article du rapporteur#

Lu le 2026-10-24 dans sa version mise à jour du 2026-10-23. Les neuf hashs INC qu'il cite sont tous dans le lot. Les domaines qu'il ajoute via ThreatFox (mondeikin.com, ghost-relay.stk-cdn-07.workers.dev, karabas.corp) ont été examinés passivement : journaux de transparence, RDAP, DNS par résolveur public, urlscan.io, Wayback Machine, ThreatFox.

Règles YARA et tests négatifs#

ghost_phantom_toolchain.yar compte 18 règles. Contrôle fichier par fichier sur les 114 échantillons (yara -w sur chaque fichier) : 90 fichiers touchés par 17 règles, la dix-huitième visant le blob Donut déchiffré, hors lot. Les deux implants Telegram étaient déjà couverts avant l'ajout de la règle Ghost_Telegram_Implant_Commands.

Tests négatifs : yara -r -f sur /usr/bin, les paquets Python, les bibliothèques .NET, Ghidra, le JDK et le SDK Android, puis yara -f sur chacun des 6 579 fichiers .exe et .dll du disque hors du dépôt. Aucune correspondance, hormis une copie de Ghost_loader qui traînait dans le répertoire de travail.

yara -r ghost_phantom_toolchain.yar binaries/cluster

Export STIX#

python3 tools/build_stix.py       # bundle STIX 2.1 a partir de iocs.csv et cluster_inventory.csv

Le script écrit le bundle sous disclosure/ dans mon arborescence de travail ; c'est ce fichier qui est publié ici sous le nom stix_bundle.json. Il s'importe directement dans MISP ou OpenCTI.

Versions des outils#

Python 3.14.8, Ghidra 12.1.2 (pyghidra), Unicorn 2.1.4, pefile 2024.8.26, pycryptodome 3.23.0, Capstone 5.0.7, oletools, OpenSSL, YARA.

Contrôles avant publication#

Un contrôle complet du dossier a recalculé les chiffres du billet, relancé les règles YARA et corrigé les affirmations trop fortes ou périmées. Trois corrections de fond en sont sorties : le contournement AMSI par point d'arrêt matériel du cradle PowerShell n'arme rien (décalages de registres de débogage faux), DelegateExecute appartient à la commande uac de l'implant Ghost et non à persist, et quatre domaines sur sept seulement résolvent vers Cloudflare. Une seconde relecture des sections OSINT a trouvé huit écarts de dates, tous corrigés (le site légitime de system-strategy.org est archivé jusqu'au 2026-01-17 et non 2025-09-13, 39 certificats distincts pour 13rzip.ru, bloc de 91.199.163.67 créé le 2026-02-19, indice Shodan sur l'origine de 89.185.83.8).

Avant publication : grep sur l'ensemble du dossier pour vérifier l'absence de tiret cadratin, de jeton et de clé d'accès complète.

Ce qui reste ouvert#

Le rôle fonctionnel de Ghost_AetherPulse.dll (copie et écrasement de fichiers, aucune charge ni configuration, rattachement à la campagne non établi), le contenu de stage1.cmd (absent du lot, hub injoignable pendant les exécutions publiques), la destination exacte de l'envoi HTTP de Ghost_dump_ppl (passée en argument), la primitive noyau de l'« exploit » CVE-2025-62215, la disposition exacte du trailer du chiffreur Lynx (déduite de l'ordre des variables locales, à confirmer au désassemblage), le côté serveur du protocole RevSocks v4 et le canal DNS des implants Ghost annoncé par l'article, que les échantillons ne montrent pas.

Ce qui n'est pas publié#

La liste des victimes relevée sur les blogs de fuite, les noms des organisations tierces dont la marque est imitée ou dont le nom apparaît dans les métadonnées, les réponses OSINT brutes, les rapports et captures Triage, les sorties brutes des outils d'émulation (qui contiennent les jetons Telegram), le dossier de divulgation et les notes de travail intermédiaires. Les jetons des bots Telegram et la clé AES de l'agent Athena ne sont écrits nulle part, ni en public ni en restreint : ils se recouvrent au besoin avec les outils fournis, à partir des échantillons publics.