【入門編】 tx.originの使用によるフィッシング攻撃 – IoT・OT(制御システム) & ブロックチェーンセキュリティ防御ガイド

こんにちは!IoTデバイスのリバースエンジニアリングやブロックチェーンの最先端セキュリティを研究している者です。

今日は、新人エンジニアや「これからブロックチェーンの開発に挑戦するぞ!」という方の頭を悩ませるスマートコントラクトの定番の落とし穴、tx.origin(ティーエックス・オリジン)を使ったフィッシング攻撃について、身近な「合鍵」の防犯にたとえながら、とっても優しく紐解いていきたいと思います。

セキュリティの世界は一見すると難しそうに思えますが、基本の仕組みさえ分かってしまえば怖くありません。一歩ずつ、一緒に学んでいきましょう!

—

1. 家の鍵で例える「認証」の仕組み:msg.sender と tx.origin の違い

ブロックチェーン(Ethereumなど)の世界では、スマートコントラクトというプログラムが銀行の金庫番のような役割を果たしています。この金庫番が「お、この人にお金を渡していいんだな」と判断するための仕組みが認証です。

ここに登場するのが、よく似た名前の2つのキーワードです。

  • msg.sender(メッセージ・センダー):

いま目の前であなたに話しかけてきている「直前の相手」です。
家でたとえるなら、「直接あなたの玄関のインターホンを鳴らして、荷物を届けに来た宅配業者さん」です。

  • tx.origin(ティーエックス・オリジン):

その取引(トランザクション)の「すべての元凶(一番最初の発信者)」です。
家でたとえるなら、「ネット通販でその商品を発注したあなた自身(オーナー)」です。

一見すると、「一番最初の親玉である tx.origin を確認しておけば、本当の持ち主が分かって安全なんじゃないの?」と思いがちですよね。実は、ここにサイバー攻撃者が付け入る最大の盲点があるんです。

—

2. 泥棒はこうして侵入する!tx.origin フィッシングのメカニズム

もし、あなたが「宅配業者さん(msg.sender)」ではなく、「ネットで注文した人(ズバリ tx.origin)」だけを信じるおバカな金庫番(スマートコントラクト)を作ってしまったとしたら、どうなるでしょうか?

ここで、悪意あるハッカー(攻撃者)の巧妙な手口を見てみましょう。

1. あなた(被害者)を誘い出す:
攻撃者は、「これを押したら100万円もらえるお得なゲームです!」といった甘い言葉で、あなたを巧妙な偽サイト(悪意あるDApps)へと誘導します。
2. あなたがボタンを押す(トランザクションの発生):
あなたがそのサイトで「参加する」ボタンを押すと、あなたのウォレットからブロックチェーンへ「取引(トランザクション)」が発信されます。この時、ブロックチェーンの記録上、すべての始まりである tx.origin には、あなたのウォレットアドレスがしっかりと刻まれます。
3. 悪意あるコントラクトを経由する:
しかし、この取引は直接お金をやり取りするのではなく、一度「攻撃者が用意した中継用のコントラクト(罠のプログラム)」を挟み撃ちにする形で実行されます。
4. 金庫番が騙される:
金庫番のコントラクトは、プログラムの中で require(tx.origin == owner); (「一番最初にこの取引を始めたのはオーナー本人か?」)というチェックを行っています。
ここで金庫番は、「おっ、一番最初に動かしたのはオーナーのあなたですね!じゃあ、この中継コントラクトの言う通りにお金を全額引き渡します!」と盛大に勘違いしてしまうのです。

結果として、あなたのウォレットにある大切なお金やトークンが、見知らぬ攻撃者の口座へとスルリと吸い取られてしまいます。これが、tx.origin を認証に使ってしまったがゆえに起こるフィッシング攻撃の正体です。

—

3. 実際のコードで違いを見てみましょう

百聞は一見に如かず。実際に危険なコードと、安全なコードを見比べてみましょう。

【危険なコード】tx.origin を使ってしまった例

以下のコントラクトは、絶対に書いてはいけない「アンチパターン」です。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract VulnerableBank {
    address public owner;

    constructor() {
        // コントラクトをデプロイした人をオーナーとして記録します
        owner = msg.sender;
    }

    // 【危険!】tx.originを認証に使っています
    function withdrawAll(address payable _to) public {
        // 取引の最初の発信者がオーナーであれば送金してしまう
        require(tx.origin == owner, "Unauthorized: あなたはオーナーではありません!");
        
        // 残高をすべて送金する
        _to.transfer(address(this).balance);
    }
}

このコードでは、もしあなたが攻撃者の作った「罠のコントラクト」を経由して withdrawAll を呼び出してしまった場合、tx.origin はあなた(オーナー)を指しているため、チェックがスルスルと通り抜けてしまいます。

—

【安全なコード】msg.sender に置き換えた例

それでは、これをどのように修正すれば安全になるでしょうか?答えは簡単、msg.sender を使うことです。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract SecureBank {
    address public owner;

    constructor() {
        owner = msg.sender;
    }

    // 【安全!】msg.sender(直前の呼び出し元)をチェックします
    function withdrawAll(address payable _to) public {
        // 直前にこの関数を呼び出したのが直接のオーナー本人であるかを厳格に確認する
        require(msg.sender == owner, "Unauthorized: あなたはオーナーではありません!");
        
        // 残高をすべて送金する
        _to.transfer(address(this).balance);
    }
}

このように、認証には常に msg.sender を使用するように心がけましょう。これにより、間に別のコントラクトが挟まった場合、msg.sender は「直前に挟まった悪意あるコントラクトのアドレス」になってしまうため、不正なアクセスをしっかりと弾く(防衛する)ことができるのです。

—

4. まとめ:今日から実践できるセキュリティの心得

スマートコントラクトやIoT・制御システムのセキュリティにおいて、「誰がその指示を出したのか」を正しく把握することは、システムの生死を分ける極めて重要なポイントです。

  • ルールその1:認証や権限チェックに tx.origin は絶対に使わない!
  • ルールその2:常に直前の接続元である msg.sender を検証する!

身の回りの防犯と同じように、「見知らぬ人(中継コントラクト)から渡された伝言をそのまま信じない」という泥臭い疑う心が、あなたの大切な資産やシステムを守る最強の盾となります。

セキュリティの世界は奥が深いですが、こうして一つずつ仕組みを理解していけば確実にスキルアップできます。一緒に安全な開発ライフを楽しんでいきましょう!

コメント

タイトルとURLをコピーしました