Skip to content

Latest commit

 

History

History

ethernaut

Ethernaut with Foundry

Ethernautとは、Solidityで書かれたコントラクトを攻撃することで脆弱性を学べるWebサイトです。 OpenZeppelinがホストしています。 攻撃とは言っても、中には単なるパズルもあり、SolidityやEVMについて理解を深めることができます。

この資料では、実際にFoundryを使ってEthernautの問題を解いてみることで、forge scriptコマンドを利用したオンチェーンのコントラクトとの対話・攻撃の流れを学びます。

目次

Foundryを使用したEthernautの解法

(Foundryについて知りたい方は、このリポジトリのcourse/foundryにより詳しい説明があるので、そちらを参照してください。)

Foundryで問題を解く流れ

まず、Ethernautは、問題ごとにプレイヤー専用のコントラクト(インスタンスと呼ばれる)が、オンチェーンにデプロイされます。 厳密には、そのインスタンスを生成するトランザクションはプレイヤーが発行します。

そして、プレイヤーは一つあるいは複数のトランザクションを発行して、インスタンスに攻撃を行います。 この際、必要ならばコントラクトのデプロイも行う必要があります。

そのため、おすすめの問題を解く流れとしては、次のようになります。

  1. forge testコマンドで、問題コントラクトに対してローカルで攻撃が成功するかテストする
  2. 成功したら、forge scriptコマンドで、その攻撃トランザクションをオンチェーンに発行する

forge scriptについて

forge scriptはSolidityで書かれたスクリプトを元にオンチェーンにトランザクションを発行するコマンドです。 基本的には、作成した分散型アプリケーションのコントラクト群をデプロイしたり初期化したりするために使われます。

デプロイする前に、現在のステートをダウンロードして、オンチェーンでのトランザクションの実行をシミュレーションできたり、EIP-1559を利用するかどうかを設定できたりなどの機能が揃っています。 詳しくは、Foundryドキュメントの「Solidity Scripting」や「forge script」を参照してください。

解いてみる

Gatekeeperシリーズ

今回は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問目には各ゲートごとにヒントを用意しています。 詰まったらこのヒントを利用することをおすすめします。 補足には知らなくても解けるけど知っておいたほうが良い情報を書いています。

13. Gatekeeper One

問題リンク: 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.sendertx.originがEOAのアドレスになってしまう。 ということは……?

gateOneの補足

tx.originはEVMのORIGIN命令にコンパイルされ、msg.senderCALLER命令にコンパイルされる。

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://github.com/minaminao/ctf-blockchain/blob/main/src/Ethernaut/GatekeeperOne/GatekeeperOneExploit.sol

14. Gatekeeper Two

問題リンク: 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://github.com/minaminao/ctf-blockchain/blob/main/src/Ethernaut/GatekeeperTwo/GatekeeperTwoExploit.sol

28. Gatekeeper Three

問題リンク: 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
解法

https://github.com/minaminao/ctf-blockchain/blob/main/src/Ethernaut/GatekeeperThree/GatekeeperThreeExploit.sol