npm・PyPI・Crates.ioをまたぐTrapDoorが、Rustのbuild.rsとSui/Moveのキーストアを狙う暗号資産ステイラー
目次
TL;DR
影響 npm 21・PyPI 7・Crates.io 6の計34パッケージ(Socket追跡「TrapDoor」)。Solana/Aptos/Sui/Move開発者の鍵に加え、SSH鍵・GitHub・AWS認証情報・ウォレット拡張
発火点 npmはpostinstall、PyPIはimport時、Crates.ioはbuild.rs。依存に足してインストール・ビルドした時点で発火し、関数の呼び出しは不要
対応 入れた端末は侵害済みとして扱い、ウォレット資産を新環境へ退避。SSH鍵・GitHubトークン・AWSキーをローテーション
SocketがTrapDoorとして追跡しているサプライチェーン攻撃は、npm・PyPI・Crates.ioの3レジストリに34パッケージをまたいで置いた、珍しいクロスレジストリ型の暗号資産ステイラー(暗号資産の鍵を盗むマルウェア)だ。
注目したのはAI設定ファイルを汚す部分ではない。そこは過去の攻撃でも見てきた。今回の芯は、Rustの build.rs を発火点に使ってSui・Move系のキーストアを直接狙ってきた点と、レジストリごとに発火タイミングも暗号化方式も別々に作り込まれている点にある。
DEV Communityの短報は「Solana開発者向け」として出ていたが、Socketの調査本文を見ると主眼はもう少し広い。
Sui、Aptos、Move、DeFi、Solidity、それからビルド補助に見せた小物まで、ブロックチェーン開発者が作業中に検索しそうな名前を先回りして置いている。
レジストリごとに発火点も暗号化方式も違う
TrapDoorで一番特徴的なのは、3レジストリでコードが実行されるタイミングと、盗んだデータの暗号化方式が別々に設計されていることだ。
1つのペイロード(悪意あるコード本体)を使い回したのではなく、レジストリの性質に合わせて発火点を変えている。
| レジストリ | 確認数 | 発火点 | 暗号化方式 | 送信先 |
|---|---|---|---|---|
| npm | 21 | postinstall | Fernet(共通鍵)+ ECDH鍵交換 | 攻撃者インフラ |
| PyPI | 7 | import時のリモートJS実行 | なし(平文) | webhook |
| Crates.io | 6 | build.rs(ビルド時) | XOR(鍵 cargo-build-helper-2026) | GitHub Gist |
npm側はFernet対称暗号にECDH鍵交換まで足して通信を保護している一方、PyPI側は平文でwebhookに流すだけ、Crates.io側は固定XOR鍵という雑な暗号で、3つの完成度がバラバラだ。
同一の攻撃者が、レジストリごとに別の実装を当てている。npmの trap-core.js が48,485バイト・1,149行と作り込まれているのに対し、PyPI側はimport時に外部JSを落として node -e で実行するだけの薄い作りになっている。
flowchart TD
A["依存に追加 / ビルド"] --> B{"レジストリ"}
B --> C["npm<br/>postinstall"]
B --> D["PyPI<br/>import時"]
B --> E["Crates.io<br/>build.rs"]
C --> F["trap-core.js<br/>Fernet+ECDH"]
D --> G["外部JSをnode -e<br/>平文でwebhook"]
E --> H["キーストア探索<br/>XORでGistへ"]
F --> I["SSH鍵・GitHub・AWS・ウォレット"]
G --> I
H --> I
Rustのbuild.rsという新しい発火点
npmの postinstall は比較的知られた経路だが、Crates.ioの build.rs を突いてくるサプライチェーン攻撃をこのブログで扱うのは初めてだ。
ここが今回の新しさの中心になる。
build.rs はRustクレートのビルドスクリプトで、本来はCライブラリのリンクやコード生成のために用意された仕組みだ。
問題は実行タイミングで、そのクレートを依存関係に追加して cargo build をかけた時点で、ライブラリのAPIを一度も呼ばないうちに任意コードが実行される。
npm側には --ignore-scripts やpnpm 11の minimumReleaseAge のように、インストール時スクリプトを止める手段が一応ある(pnpm 11がminimumReleaseAgeをデフォルト化した件で触れた)。
Cargoには build.rs を一律で無効化する標準的な仕組みがなく、ビルドした時点で実行されるのを前提に避けるしかない。
TrapDoorのCrates.io側6パッケージは、名前が全部Sui・Move開発者向けに寄せてある。
sui-framework-helperssui-move-build-helpersui-sdk-build-utilsmove-analyzer-buildmove-compiler-toolsmove-project-builder
どれも「Sui SDKのビルド補助」「Moveコンパイラ周辺ツール」に見える名前で、build.rs の中でローカルのキーストアを探索し、見つけた鍵を固定XOR鍵 cargo-build-helper-2026 で暗号化してGitHub Gistの公開ファイルとして送り出す。
公開Gistを出口に使うため、攻撃者側のサーバーを用意しなくても、GitHubへの通常のHTTPS通信に紛れてデータが流出する。
狙いはブロックチェーン開発者の鍵
TrapDoorが取りに来るのは、一般的な開発者シークレット(SSH鍵、GitHubトークン、AWS認証情報)に加えて、ブロックチェーン開発者の手元にある鍵だ。
Solana、Aptos、Sui、それからMetaMaskやPhantomのようなブラウザウォレット拡張のデータも対象になっている。
ブロックチェーン開発者の端末は、攻撃者から見て効率がいい。
資産に直結する秘密鍵と、デプロイやテストに使う開発鍵が同じマシンに置かれていることが多いからだ。SocketはTrapDoorが具体的にどのパスを読むかまでは公開していないが、各チェーンのCLIが鍵を置く既定の場所はだいたい決まっている。
| チェーン | CLIが鍵を置く既定の場所(参考) |
|---|---|
| Solana | ~/.config/solana/id.json |
| Sui | ~/.sui/sui_config/sui.keystore |
| Aptos | ~/.aptos/config.yaml(プロファイル内に秘密鍵) |
ウォレット拡張の鍵やシードフレーズはブラウザのプロファイル配下にある。
mnemonic-safety-check や wallet-backup-verifier のような「バックアップ確認」「安全確認」を名乗るパッケージは、まさにそのシードフレーズを触れる位置に置かれている。「秘密鍵が漏れていないか確認するツール」を装って、確認のついでに秘密鍵を持ち出す構図だ。
AI設定ファイルの汚染は既出の手口の延長
npm側の trap-core.js は、盗んだAWS・GitHub認証情報をそれぞれのAPIで検証してから使い、Gitフック・シェルフック・systemd・cronで永続化し、SSH経由の横展開(盗んだ鍵で別ホストへ移る動き)まで仕掛ける。
この中に .cursorrules と CLAUDE.md への書き込みが含まれ、ゼロ幅Unicode文字で隠した指示を仕込む。
ただ、この手口自体は新しくない。
AI開発環境を狙う構図はSANDWORM_MODEやMini Shai-HuludがClaude Code/VS Codeに痕跡を残した件で、ゼロ幅Unicodeでの隠蔽はStegaBinで、AI設定経由で次のセッションを汚す攻撃チェーンはClinejectionで見た。
TrapDoorはこれらの既知パーツを暗号資産ステイラーに組み込んで再利用している、と読むのが近い。今回のオリジナリティはAI汚染ではなく、Crates.ioとブロックチェーン鍵の側にある。
危ない境界は「呼んだか」ではなく「取り込んだか」
該当パッケージの関数をアプリから呼んだかどうかは関係ない。
npmは postinstall、Crates.ioは build.rs、PyPIはimport時実行で、依存関係に足してインストールやビルドが実行された時点で端末上の鍵に到達する経路がある。一時的な検証リポジトリで一度試しただけ、CIで一度実行されただけでも侵害扱いになる。
npm audit や pip audit、cargo audit は主に既知脆弱性データベースを見る道具で、公開直後の悪意あるパッケージやpostinstall・ビルドスクリプト・リモート取得ペイロードの挙動を直接止めるものではない。
確認する範囲とIOC
該当パッケージを入れた可能性がある端末は、削除で止めずに侵害済みとして扱う。
SocketのIOCはログ検索に使える。
| 種別 | 値 |
|---|---|
| GitHub Pages | ddjidd564[.]github[.]io、ddjidd564[.]github[.]io/defi-security-best-practices/ |
| 公開アカウント | GitHub ddjidd564 / npm asdxzxc / PyPI asdmini67・dae5411 |
| ペイロード | trap-core.js(48,485バイト)、キャンペーンID P-2024-001 |
| XOR鍵 | cargo-build-helper-2026 |
確認する場所は端末と周辺サービスで切る。
| 場所 | 見るもの |
|---|---|
| 開発端末 | ~/.ssh、各チェーンのキーストア、ブラウザ拡張、.env、シェル設定、cron、systemd |
| Gitリポジトリ | .git/hooks/、CLAUDE.md、.cursorrules、見覚えのない設定ファイル |
| GitHub | PAT、OAuthアプリ、Actionsのシークレット、最近のGist作成・PR・コミット |
| クラウド | アクセスキー利用履歴、IAM権限、異常なAPI呼び出し |
| CI | npm/pip/cargoのインストール・ビルドログ、依存追加、シークレット利用履歴 |
ウォレットは別扱いだ。
端末にSolana・Aptos・Sui・MetaMask・Phantomの鍵やシードが残っていたなら、後から送金される前提で資産を新しい環境へ移す。秘密鍵を抜かれた時点ではオンチェーンに何も出ず、送金されてから初めて気づくことになる。
SSH鍵・GitHubトークン・AWSキーは順にローテーションする。
SSH鍵を差し替えたら、古い公開鍵をGitHub・サーバーの authorized_keys・CIのデプロイキーから消す。GitHubトークンを消しただけで、盗まれたSSH鍵が残っていればリポジトリやサーバーへの経路は塞げない。
クールダウンでは止めにくい隙間を突いている
Socketは最初のPyPIパッケージ eth-security-auditor@0.1.0 を2026年5月22日20:20:18 UTCに確認し、381件のpackage-versionを平均5分56秒・中央値5分27秒・最速58秒で検出したと書いている。
これだけ早く検出されるなら、新規公開を数分〜数時間寝かせるクールダウンだけで踏まずに済んだ環境は多い。
ただし今回の名前は、恒常的な依存というより「作業中に一度だけ検索して入れる小物」に見えるものが多い。
wallet-security-checker や move-compiler-tools のような名前は、lockfileに固定された依存ほどクールダウンでは止めにくい。新規公開から検出までの数分の窓ではなく、開発者がその場で検索して入れる瞬間を狙ってきている。