Ethernaut完走の感想 その2
はじめに
この記事はEthernaut完走の感想の続きです。ネタバレがあるのでご注意ください。
6. Delegation
instances
で取得できるコントラクトはDelegation
の方です。このコントラクトのowner
を奪取するのが課題です。さて、fallback
を見てみるとdelegatecallback
でDelegate
コントラクトの関数が呼び出せます。そしてDelegate
コントラクトを見るとちょうどDelegation
コントラクトと同じスロットにowner
があり、さらにこれを書き換えるためのpwn()
という関数が用意されています。
さて、ここで大事なのがdelgatecall
の挙動の理解です。delegatecall
でコントラクトを呼び出すと以下の点について通常の呼び出し(call
)と異なります。
-
msg.sender
が上書きされない - 呼び出し元のスロットが使用される
つまり、Delegate
コントラクトのowner
を変更すると、実際には呼び出し元のDelegation
コントラクトのowner
が書き換えられるということですね。これはSWC-112として知られる脆弱性です。以降のレベルでも何度か使うテクニックなので覚えておきましょう。
また、この問題ではDelegate
コントラクトの関数はmsg.data
で指定します。EVMには「関数シグネチャをkeccak256
でハッシュし、その最初の4バイトで関数を指定して呼び出し」というルールがあります。ここではpwn()
をkeccak256でハッシュし先頭4バイトを算出しますが、abi.encodeWithSignature("pwn()")
で手っ取り早く取得できます。
あとはデベロッパーツールのコンソールで以下のように入力すればOKです。
await sendTransaction({from: YOUR_ADDRESS, to: INSTANCE_ADDRESS, data: "0xdd365b8b"})
7. Force
通常、payable
関数のないコントラクトへの送金はできません。ただし例外があり、selfdestruct
の宛先と指定された場合には送金できます。なので、攻撃用コントラクトにselfdestruct
を実行する関数を定義して実行しましょう。
8. Vault
password
がprivate
だから読めない、と思わせておいて実は読めます。Web3.jsのgetStrageAt
やethers.jsのgetStrageAt
を使用します。この脆弱性はSWC-136として知られています。
9. King
コントラクトには0.01ETHが入っていて、prize
も同じ値が設定されています。なので、このコントラクトに0.01 ETHを送りつけるとreceive()
関数が呼ばれてKingになれます。
10. Re-entrancy
これぞSolidtyバグの代名詞、リエントランシー!これをやるためにEthernaut始めたと言っても過言ではない!さくっと解いて次にいこう!と考えていましたが、意外とハマってしまってしまいました。
最初コントラクトは0.001ETH持っているので、これを全部引き出したいです。balances[msg.sender] >= _amount
という条件があるので、最初にいくらかdonate
で入金しておき、withdraw
でそれを引き出しつつ、receive()
関数内部で更に0.001ETHをwithdraw
するコードを書けば良いです。残高が0のときはwithdraw
しないで抜ける処理を忘れないようにしましょう(そうしないと2度目のreceive()
実行時にトランザクションがfailします)。
リエントランシーの脆弱性はSWC-107に記載されているのでご確認ください。また、予防策としてChecks-Effects-Interactionsパターンとリエントランシーロックが紹介されています。前者は、外部コントラクトを呼ぶ際に「条件チェック」「ステートの更新」「外部コントラクトの実行」の順番に処理を行うパターンです。後者は、処理の入り口にMutexロックのような仕組み(boolのステートで管理)を置くことで処理が一回しか行われないことを保証します。
Mastering Ethereumには「transfer
を使用する」という予防策も紹介されていますが、これは現在ではアンチパターンなので使わないほうが良いでしょう。
11. Elevator
Building building = Building(msg.sender);
が脆弱です。つまり、isLastFloor
を実装してしているコントラクトがmsg.sender
となっていればその中身は何でもOKです。なので、一回目はtue、二回目はfalseを返すようなisLastFloor
を実装した攻撃用コントラクトを作成し、そこからgoTo
を呼び出せるようにします。
12. Privacy
これもVaultと同様、getStrageAt
をつかってprivateを覗き見します。
13. Gatekeeper One
これは辛い問題でした。特にgateTwo()
のrequire(gasleft() % 8191 == 0);
が鬼門です。gasleft()
実行時に残っているガスが8191の倍数である必要があるのですが、そうそう狙えるものではありません。いくつかのサイトで「Remix上でデバッグしてGAS
実行時の残ガスから使用量を見積もる」という方法が紹介されていたので試しましたが、どうもうまくいきませんでした。
困っていたところブルートフォースでいけるという記事を発見し、その通りパスしました。
14. Gatekeeper Two
Gatekeeper Oneに比べるとそこまで辛くないです。ポイントはgateTwo()
のextcodesize
です。これはデプロイされているコードサイズを取得する命令で、通常コントラクトから呼び出された際は0よりも大きな値が返されます。gateOne()
の条件からコントラクトから呼び出す必要があり、gateTwo()
の条件でコードサイズが0じゃないといけないという矛盾に悩みますが解決策があります。コンストラクタの中で呼ばれたextcodesize
は0を返すので、攻撃用コントラクトのコンストラクタの中に攻撃コードを書けば両方の条件を満たせます。
15. Naught Coin
ERC-20トークンはトークンの持ち主の他にapprove
された人も代理で送金できます。その人ならなんの縛りもなくtranferFrom
で送れます(lockTokens
も関係ないですね)。
16. Preservation
脆弱性以前にバグってるコードです。setFirstTime
内部でdelegatecall
でライブラリを呼んでいるので、address public timeZone1Library;
がuint _timeStamp
の値で上書きされます。ということで、以下の手順でOKです。
-
setTime
関数を実装し、その中でスロット2に自分のアドレスを書き込む攻撃用コントラクトをデプロイ - 1.のアドレスを引数にして
setFirstTime
を実行 - もう一回
setFirstTime
を実行
17. Recovery
Explorer等でinstanceを確認するとContract Creation
というトランザクションがあり、それが「忘れちゃったトークンのアドレス」です。destroy
でサルベージしてください。
Discussion