场景描述
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