Ethernautとは、Solidityで書かれたコントラクトを攻撃することで脆弱性を学べるWebサイトです。 OpenZeppelinがホストしています。 攻撃とは言っても、中には単なるパズルもあり、SolidityやEVMについて理解を深めることができます。
この資料では、実際にFoundryを使ってEthernautの問題を解いてみることで、forge script
コマンドを利用したオンチェーンのコントラクトとの対話・攻撃の流れを学びます。
目次
(Foundryについて知りたい方は、このリポジトリのcourse/foundryにより詳しい説明があるので、そちらを参照してください。)
まず、Ethernautは、問題ごとにプレイヤー専用のコントラクト(インスタンスと呼ばれる)が、オンチェーンにデプロイされます。 厳密には、そのインスタンスを生成するトランザクションはプレイヤーが発行します。
そして、プレイヤーは一つあるいは複数のトランザクションを発行して、インスタンスに攻撃を行います。 この際、必要ならばコントラクトのデプロイも行う必要があります。
そのため、おすすめの問題を解く流れとしては、次のようになります。
forge test
コマンドで、問題コントラクトに対してローカルで攻撃が成功するかテストする- 成功したら、
forge script
コマンドで、その攻撃トランザクションをオンチェーンに発行する
forge script
はSolidityで書かれたスクリプトを元にオンチェーンにトランザクションを発行するコマンドです。
基本的には、作成した分散型アプリケーションのコントラクト群をデプロイしたり初期化したりするために使われます。
デプロイする前に、現在のステートをダウンロードして、オンチェーンでのトランザクションの実行をシミュレーションできたり、EIP-1559を利用するかどうかを設定できたりなどの機能が揃っています。 詳しくは、Foundryドキュメントの「Solidity Scripting」や「forge script」を参照してください。
今回はGatekeeperシリーズを解きます。 Gatekeeperシリーズは、Gatekeeper One、Gatekeeper Two、Gatekeeper Threeの3問で構成されています。
3問全てのコントラクトにはenter
関数があり、このenter
関数へのコールを成功させることがゴールです。
ただし、enter
関数には、gateOne
, gateTwo
, gateThree
という3つのモディファイアが修飾されており、これらモディファイアの条件を満たさないとenter
関数の実行を成功できません。
つまり、1つの問題にゲートという形の小問が3つある構成になっています。
テストとスクリプトの雛形は用意してあります。
後は、Gatekeeper(One|Two|Three)Exploit.sol
の
////////// YOUR CODE GOES HERE //////////
////////// YOUR CODE END //////////
の部分を埋めて攻撃を完成させるだけです。
どの問題も初見で解くのは中々難しいです。 そのため、1問目と2問目には各ゲートごとにヒントを用意しています。 詰まったらこのヒントを利用することをおすすめします。 補足には知らなくても解けるけど知っておいたほうが良い情報を書いています。
問題リンク: https://ethernaut.openzeppelin.com/level/0x2a2497aE349bCA901Fea458370Bd7dDa594D1D69
テスト:
forge test --match-contract GatekeeperOneExploitTest -vvv
スクリプト:
export PRIVATE_KEY=<PRIVATE_KEY>
export RPC_URL=<RPC_URL>
export FOUNDRY_ETH_RPC_URL=$RPC_URL
export INSTANCE_ADDRESS=<INSTANCE_ADDRESS>
forge script GatekeeperOneExploitScript --private-key $PRIVATE_KEY --fork-url $RPC_URL --broadcast --sig "run(address)" $INSTANCE_ADDRESS -vvv
gateOneのヒント1
tx.origin
: トランザクションの発行者アドレス。msg.sender
: コントラクトコールの呼び出しアドレス。
gateOneのヒント2
EOAからentry
関数を呼び出すとmsg.sender
とtx.origin
がEOAのアドレスになってしまう。
ということは……?
gateOneの補足
tx.origin
はEVMのORIGIN
命令にコンパイルされ、msg.sender
はCALLER
命令にコンパイルされる。
gateTwoのヒント1
gasleft()
は残りのガスを返す。
闇雲にentry
を呼び出しても1/8191の確率でしか成功しない。
gateTwoのヒント2
コントラクトコールの際にガスを指定することで攻略できないだろうか。
gateTwoのヒント3
例えば1000ガスで関数foo
を呼び出すには、foo{gas: 1000}()
とすれば良い。
gateTwoのヒント4
enter
関数の実行からgasleft()
の実行までの間のガス消費量は一定だと予測できる。
ということは、{gas: amount}
構文を使って、gasleft() % 8191 == 0
を満たせるamount
を全探索すればいい
gateTwoのヒント5
entry{gas: amount}
を使って全探索すると、entry
関数がリバートしたときトランザクションもリバートしてしまう。
entry
関数が失敗しても処理を続行するためには……?
gateTwoの補足
gasleft()
はGAS
命令にコンパイルされる。
GAS
命令を実行されると、GAS
命令実行後の残りのガスがスタックにプッシュされる。
gateThreeのヒント1
uint64(_gateKey)
が0x1122334455667788
だったときを考えてみよう。
uint32(uint64(_gateKey)): 0x0000000055667788
uint16(uint64(_gateKey)): 0x0000000000007788
gateThreeのヒント2
tx.origin
はトランザクション発行者のアドレスで20バイト(160ビット)。
uint160(tx.origin)
はそれを非負整数に直すということ。
その値をuint16
に変換した値とuint32(uint64(_gateKey))
を一致させるには……?
問題リンク: https://ethernaut.openzeppelin.com/level/0xf59112032D54862E199626F55cFad4F8a3b0Fce9
テスト:
forge test --match-contract GatekeeperTwoExploitTest -vvv
スクリプト:
export INSTANCE_ADDRESS=<INSTANCE_ADDRESS>
forge script GatekeeperTwoExploitScript --private-key $PRIVATE_KEY --fork-url $RPC_URL --broadcast --sig "run(address)" $INSTANCE_ADDRESS -vvv
gateTwoのヒント1
assembly { ... }
はインラインアセンブリブロックと呼ばれる。
括弧の中はYul言語で記述され、EVMのニーモニックを使用できるようになる。
(詳しくはSolidityドキュメントの「インラインアセンブリ」を参照。)
gateTwoのヒント2
extcodesize(address)
でaddress
のコードサイズを取得する。
caller()
はコントラクトコールの呼び出しアドレスを取得する。
つまり、extcodesize(caller())
でコントラクトコールの呼び出しアドレスのコードサイズを取得している。
gateTwoのヒント3
gateOne
を満たすためには、コントラクトからentry
関数を呼ばなくてはいけなかった。
でも、普通にコントラクトからentry
関数を呼ぶと、extcodesize(caller())
が0
にならない。
では、どうしたらいいか……?
gateTwoのヒント4
EXTCODESIZE
命令の仕様を詳しく調べてみよう。
gateThreeのヒント1
abi.encodePacked(msg.sender)
は、
それのkeccak256
ハッシュを取得している
gateThreeのヒント2
bytes32
の値をbytes8
に変換すると先頭8バイトが得られる。
gateThreeのヒント3
_gateKey
を逆算するにはどうしたらよいか……?
問題リンク: https://ethernaut.openzeppelin.com/level/0x03aFA729959cDB6EA3fAD8572b718E88df0594af
テスト:
forge test --match-contract GatekeeperThreeExploitTest -vvv
スクリプト:
export INSTANCE_ADDRESS=<INSTANCE_ADDRESS>
forge script GatekeeperThreeExploitScript --private-key $PRIVATE_KEY --fork-url $RPC_URL --broadcast --sig "run(address)" $INSTANCE_ADDRESS -vvv