Ethernaut完走の感想 その3
はじめに
この記事はEthernaut完走の感想 その2の続きです。ネタバレがあるのでご注意ください。なお、このあたりからだんだんと難しくなってきます。
18. MagicNumber
呼ばれたら42
を返すwhatIsTheMeaningOfLife()
関数をつくりなさい、ただし10opcodes以内で、という無茶振りです。これは全くわからなかったので解説を見ました。
解説を理解する上でいくつか予備知識が必要です。
- コンストラクタではreturnによりruntime bytecodeのmemory上の場所を返す(そしてEVMはこの情報をもとにruntime bytecodeをデプロイすると思われます)
- 関数セレクタはSolidityのコードをコンパイルした際にコードの先頭に自動生成される。つまりEVMが処理しているわけではなく、コントラクトが
call
のパラメータを見て自分で実行するコードにジャンプしている。ということで、セレクタのないコントラクトをデプロイするとcall
されたらその引数に関係なくデプロイされたruntime bytecodeの先頭から順番に実行する
これを踏まえると解説サイトの以下の解答は理解できますね。
constructor() {
assembly {
mstore(0x0, 0x602a60005260206000f3) //memoryの0x0の場所に0x60...というコードを配置。このコードは`42`をmemoryに配置するコード
return(0x16, 0x0a) //mstoreは32バイト単位で使用されるため、0パディングされている0x16をoffsetとし、コード長である0x0aを返す
}
}
そしてデプロイされたruntime bytecodeの0x602a60005260206000f3
がcall
からの呼び出しに対して実行されます。
むずっ!
参考資料:
19. Alien Codex
ownerを奪取する課題です。ぱっと見ownerはありませんが、こちらのコードで宣言されています。
ということでスロット0に_owner
がいらっしゃるのでサクッと上書きしたいところですが、いつものようなdelegatecall
で上書きのようなテクニックは使えません。だた、solidityのバージョを見ると0.5.0
と古いので、オーバーフローとか使えるんじゃないのかなぁ?と予想できます。
実際、codex
が空のときに以下を実行するとcodex
の長さが2の256乗-1となります。
function retract() contacted public {
codex.length--;
}
また公式ドキュメントに記載されているように、動的配列(ここではcodex
)はそのストレージが宣言されているスロットに配列長、keccak256(p)
スロットから順番に配列の中身が格納されます。なので0-keccak256(p)
がスロット0になるのでcodex[0-keccak256(p)]
に自分のアドレスを書き込めば、_owner
が上書きできます。作業としてはretract
を一回呼んで長さをMAXにしてからrevise
でスロット0の場所にアドレスを書き込むでOKです。
と偉そうにいってますが、この解説サイトをカンニングしました。
ちなみに、Solidity v0.6.0のBreaking Changeでlengthがリードオンリーになったため、このコードはv0.5.0となっていました。
20. Denial
自分がpartner
となり、owner
がwitdraw
できない状態にすることが課題です(0.001ETHを頂くことが課題ではありません)。攻撃用コントラクトを準備しpartner
に設定します。このコントラクトにfallback
がreceive
を準備し、その中で無限ループを実行するとwithdraw
は必ずfailするようになり、目的達成です。
SWC-113として知られる脆弱性です。
21. Shop
11. Elevatorの亜種です。攻撃用コントラクトにprice()
を実装し、一回目と二回目で違う応答にすればよいのですが、今回はview
が指定されているため攻撃用コントラクト側でストレージに呼ばれた回数を保存するようなコードは書けません。
しかし、Shop
側にisSold
というそれっぽい変数があり、しかも一回目と二回目で値が変わるのでこれを見てprice
の応答を変えればOKです。
22. Dex
タイトルからして、なんだか現実の課題に近づいてきたような気がします!この課題、コードの脆弱性を突くのではなくgetSwapPrice
の実装上の問題(バグ)を利用します。token1/2共にDexが100、自分が10持っているのですが、以下のような交換を続けるだけでトークンが増えていきます。
操作 | Dex token1 | Dex token2 | 自分 token1 | 自分 token2 |
---|---|---|---|---|
持っているtoken1すべてををtoken2にswap | 110 | 90 | 0 | 20 |
持っているtoken2すべてををtoken1にswap | 86 | 110 | 24 | 0 |
持っているtoken1すべてををtoken2にswap | 110 | 80 | 0 | 30 |
持っているtoken2すべてををtoken1にswap | 69 | 110 | 41 | 0 |
持っているtoken1すべてををtoken2にswap | 110 | 45 | 0 | 65 |
23. Dex Two
Dexとほとんど同じコードで課題が異なります。今回はtoken1/2両方をすべて頂かないといけないのでDexの解法ではダメです。そこで、Dexとのコードの違いを見るとswap
から以下のコードが削除されています。
require((from == token1 && to == token2) || (from == token2 && to == token1), "Invalid tokens");
これは、スワップするトークンは何でも良いということになります。つまり、自分で作ったトークンとtoken1/2をswapできます。そこで、常にbalanceOf
が100を返すオレオレトークンをデプロイし、from: オレオレトークン, to: token1/2, amount: 100でスワップすると全部GETできます。
Discussion