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