场景描述
2018 年 4 月 25 日,SMT 币因为整型溢出漏洞被攻击,火币 Pro 随即暂停了所有币种的充值提取业务进行大排查。
合约列表
- SMT 合约地址:
0x55F93985431Fc9304077687a35A1BA103dC1e081
- SMT 合约代码: https://etherscan.io/address/0x55f93985431fc9304077687a35a1ba103dc1e081#code
- SMT 攻击记录: https://etherscan.io/tx/0x1abab4c8db9a30e703114528e31dee129a3a758f7f8abc3b6494aad3d304e43f
攻击如何运作
Solidity 智能合约容易受到整数溢出或下溢的影响。当变量超过它使用的数据类型的最大值或最小值时,它们就会发生。发生这种情况时,该值分别环绕最小或最大范围的另一端。如下是一段 Solidity 代码演示:
// SPDX-License-Identifier: MIT
pragma solidity ^0.4.20;
contract AnbangDemo {
uint256 public min = 0;
uint256 public max = 2 ** 256 - 1;
function add(uint256 _x) public view returns (uint256) {
return max + _x;
}
function minus(uint256 _y) public view returns (uint256) {
return min - _y;
}
}
max 的值是 2^256 - 1
- 当执行
add(1)
,得到的结果是0
- 当执行
add(2)
,得到的结果是1
- 当执行
add(3)
,得到的结果是2
min 的值是 0
- 当执行
minus(1)
,得到的结果是115792089237316195423570985008687907853269984665640564039457584007913129639935
- 当执行
minus(2)
,得到的结果是115792089237316195423570985008687907853269984665640564039457584007913129639934
- 当执行
minus(3)
,得到的结果是115792089237316195423570985008687907853269984665640564039457584007913129639933
这就是 Solidity 的整数溢出。
推荐做法
推荐使用 SafeMath 操作,而不是自己做检验。自己做操作可能会出现遗漏的情况。
下面是对整型的数据进行处理的演示操作。
// SPDX-License-Identifier: MIT
pragma solidity ^0.4.20;
contract AnbangDemo {
uint256 public min = 0;
uint256 public max = 2**256 - 1;
function add(uint256 _x) public view returns (uint256) {
// ❌ bad
// return max + _x;
// ✅ good
uint256 c = max + _x;
assert(c >= max);
return c;
}
function minus(uint256 _y) public view returns (uint256) {
// ❌ bad
// return min - _y;
// ✅ good
assert(_y <= min);
return min - _y;
}
// 乘法
function mul(uint256 a, uint256 b) internal pure returns (uint256) {
if (a == 0) {
return 0;
}
uint256 c = a * b;
assert(c / a == b);
return c;
}
// 除法
function div(uint256 a, uint256 b) internal pure returns (uint256) {
// assert(b > 0); // Solidity automatically throws when dividing by 0
uint256 c = a / b;
// assert(a == b * c + a % b); // There is no case in which this doesn't hold
return c;
}
}
solidity 0.8.0 版本已经修复了此问题,推荐使用新版本。如果使用的是之前的老版本,推荐继续使用 SafeMath 。
SmartMesh (SMT) 合约内被攻击的函数 ƒ
function transferProxy(address _from, address _to, uint256 _value, uint256 _feeSmt,
uint8 _v,bytes32 _r, bytes32 _s) public transferAllowed(_from) returns (bool){
if(balances[_from] < _feeSmt + _value) revert();
uint256 nonce = nonces[_from];
bytes32 h = keccak256(_from,_to,_value,_feeSmt,nonce);
if(_from != ecrecover(h,_v,_r,_s)) revert();
if(balances[_to] + _value < balances[_to]
|| balances[msg.sender] + _feeSmt < balances[msg.sender]) revert();
balances[_to] += _value;
Transfer(_from, _to, _value);
balances[msg.sender] += _feeSmt;
Transfer(_from, msg.sender, _feeSmt);
balances[_from] -= _value + _feeSmt;
nonces[_from] = nonce + 1;
return true;
}
攻击者在交易中利用了这个功能在交易 0x1abab4c8db9a30e703114528e31dee129a3a758f7f8abc3b6494aad3d304e43f 中进行攻击
Function: transferProxy(
address _from, address _to, uint256 _value,
uint256 _feeSmt, uint8 _v, bytes32 _r, bytes32 _s
)
MethodID: 0xeb502d45
[0]: 000000000000000000000000df31a499a5a8358b74564f1e2214b31bb34eb46f
[1]: 000000000000000000000000df31a499a5a8358b74564f1e2214b31bb34eb46f
[2]: 8fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
[3]: 7000000000000000000000000000000000000000000000000000000000000001
[4]: 000000000000000000000000000000000000000000000000000000000000001b
[5]: 87790587c256045860b8fe624e5807a658424fad18c2348460e40ecf10fc8799
[6]: 6c879b1e8a0a62f23b47aa57a3369d416dd783966bd1dda0394c04163a98d8d8
该交易发生在区块 5499035 ,此时在区块链中:
balances[_from]
(balances[0xdf31a499a5a8358b74564f1e2214b31bb34eb46f]
) 的余额为 0balances[_to]
(balances[0xdf31a499a5a8358b74564f1e2214b31bb34eb46f]
) 的余额为 0balances[msg.sender]
(balances[0xd6a09bdb29e1eafa92a30373c44b09e2e2e0651e]
) 的余额为 0
transferProxy 的参数中
_from
是0xdf31a499a5a8358b74564f1e2214b31bb34eb46f
_to
是0xdf31a499a5a8358b74564f1e2214b31bb34eb46f
_value
是0x8fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
_freeSmt
是0x7000000000000000000000000000000000000000000000000000000000000001
.- 注意
0x8fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
+0x7000000000000000000000000000000000000000000000000000000000000001
是 0 .这将有助于快速理解。
- 注意
// SPDX-License-Identifier: MIT
pragma solidity ^0.4.20;
contract AnbangDemo {
uint256 public a = 0x8fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff;
uint256 public b = 0x7000000000000000000000000000000000000000000000000000000000000001;
uint256 public c = a + b; // 结果是 0
}
现在,让我们看一下 transferProxy
中
第一个 if 语句 if(balances[_from] < _feeSmt + _value) revert();
。因为 balances[_from]
是 0 和 _feeSmt + _value
也是 0, 此时 if 的条件的结果是 false
( 0 < 0 的表达式返回 false ) ,此时 revert()
没有被触发,参数通过了这个检查。
第二个 if 语句是判断地址的,这个地址是正确的,所以可以通过检查。
第三个 if 语句: 同样是拦截失败的,合约作者是打算在这里执行溢出检查的,然而却没有发生溢出。 balances[_to]
是 0 因此 balances[_to] + _value < balances[_to]
是 false。 balances[msg.sender]
是 0 因此 balances[msg.sender] + _feeSmt < balances[msg.sender]
是 false。 因此,这两个条件都是 false 和 revert()
没有被触发。
随后就是合约内记账了,没有任何检查了,余额的状态变量将被修改为:
balances[_to] += _value;
导致地址所有者 _to
获得大量代币
balances[_from] -= _value + _feeSmt;
导致所有者 _from
失去 0 个代币
OKO 可以查看状态的改变
0x55f9…e081 | Before | After | Diff |
---|---|---|---|
balanceOf[0xdf31a499a5a8358b74564f1e2214b31bb34eb46f] |
0 | 0x8fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff | 0x8fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff |
nonce[0xdf31a499a5a8358b74564f1e2214b31bb34eb46f] |
0 | 1 | 1 |
balanceOf[0xd6a09bdb29e1eafa92a30373c44b09e2e2e0651e] |
0 | 0x7000000000000000000000000000000000000000000000000000000000000001 | 0x7000000000000000000000000000000000000000000000000000000000000001 |
https://oko.palkeo.com/0x1abab4c8db9a30e703114528e31dee129a3a758f7f8abc3b6494aad3d304e43f/
小结
这里的 SMT 已经做了溢出检查的逻辑,应该也做了单元测试,但是极端情况下,被绕过去了。 建议使用 SafeMath 库操作,不要自己写,否则可能因为考虑不周被攻击。
相关攻击
- BEC 美蜜合约出现重大漏洞:https://www.secrss.com/articles/2194