DeFi 项目 bZx-iToken 盗币事件分析
作者:昏鸦@知道创宇404区块链安全研究团队
时间:2020年9月14日
发生了什么
iToken是bZx推出的一种代币,今天早些时候,bZx官方发推表示发现了一些iTokens的安全事件,随后有研究员对比iToken合约源码改动,指出其中存在安全问题,可被攻击用于薅羊毛。
什么是iToken
iToken是bZx推出的类似iDAI、iUSDC的累积利息的代币,当持有时,其价值会不断上升。iToken代表了借贷池中的份额,该池会随借贷人支付利息而扩大。iToken同样能用于交易、用作抵押、或由开发人员组成结构化产品,又或者用于安全价值存储。
分析
根据推文指出的代码,问题存在于_internalTransferFrom
函数中,未校验from
与to
地址是否不同。
若传入的from
与to
地址相同,在前后两次更改余额时balances[_to] = _balancesToNew
将覆盖balances[_from] = _balancesFromNew
的结果,导致传入地址余额无代价增加。
1 2 3 4 5 6 7 8 9 10 |
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;//knownsec// 变量覆盖,当_from与_to相同时 |
漏洞复现
截取transferFrom
与_internalTransferFrom
函数作演示,测试合约代码如下:
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 143 144 145 146 |
pragma solidity ^0.5.0; library SafeMath { function add(uint256 a, uint256 b) internal pure returns (uint256) { uint256 c = a + b; require(c >= a, "SafeMath: addition overflow"); return c; } function sub(uint256 a, uint256 b) internal pure returns (uint256) { return sub(a, b, "SafeMath: subtraction overflow"); } function sub(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { require(b <= a, errorMessage); uint256 c = a - b; return c; } 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; } function div(uint256 a, uint256 b) internal pure returns (uint256) { return div(a, b, "SafeMath: division by zero"); } function div(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { require(b > 0, errorMessage); uint256 c = a / b; // assert(a == b * c + a % b); // There is no case in which this doesn't hold return c; } function mod(uint256 a, uint256 b) internal pure returns (uint256) { return mod(a, b, "SafeMath: modulo by zero"); } function mod(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { require(b != 0, errorMessage); return a % b; } } contract Test { using SafeMath for uint256; uint256 internal _totalSupply; mapping(address => mapping (address => uint256)) public allowed; mapping(address => uint256) internal balances; event Transfer(address indexed from, address indexed to, uint256 value); event Approval(address indexed owner, address indexed spender, uint256 amount); constructor() public { _totalSupply = 1 * 10 ** 18; balances[msg.sender] = _totalSupply; } function totalSupply() external view returns (uint256) { return _totalSupply; } function balanceOf(address account) external view returns (uint256) { return balances[account]; } function approve(address spender, uint256 amount) external returns (bool) { require(spender != address(0)); allowed[msg.sender][spender] = amount; emit Approval(msg.sender, spender, amount); } 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;//knownsec// 变量覆盖,当_from与_to一致时 // handle checkpoint update // uint256 _currentPrice = tokenPrice(); // _updateCheckpoints( // _from, // _balancesFrom, // _balancesFromNew, // _currentPrice // ); // _updateCheckpoints( // _to, // _balancesTo, // _balancesToNew, // _currentPrice // ); emit Transfer(_from, _to, _value); return true; } } |
remix部署调试,0x1e9c2524Fd3976d8264D89E6918755939d738Ed5
部署合约,拥有代币总量,授权0x28deb6CA32C274f7DabF2572116863f39b4E65D9
500代币额度
通过0x28deb6CA32C274f7DabF2572116863f39b4E65D9
账户,调用transferFrom
函数,_from
与_to
传入地址0x1e9c2524Fd3976d8264D89E6918755939d738Ed5
,_value
传入授权的500
最后查看0x1e9c2524Fd3976d8264D89E6918755939d738Ed5
地址余额,已增加500额度,超出代币发行总量。
综上,恶意用户可创建小号,通过不断授权给小号一定额度,使用小号频繁为大号刷代币,增发大量代币薅羊毛。
总结
针对本次事件,根本原因,还是没做好上线前的代码审计工作。由于区块链智能合约的特殊性,智能合约上线前务必做好完善的代码审计、风险分析的工作。
另外通过github搜索到其他项目也同样存在这个问题,务必提高警惕。
智能合约审计服务
针对目前主流的以太坊应用,知道创宇提供专业权威的智能合约审计服务,规避因合约安全问题导致的财产损失,为各类以太坊应用安全保驾护航。
知道创宇404智能合约安全审计团队: https://www.scanv.com/lca/index.html
联系电话:(086) 136 8133 5016(沈经理,工作日:10:00-18:00)欢迎扫码咨询:
区块链行业安全解决方案
黑客通过DDoS攻击、CC攻击、系统漏洞、代码漏洞、业务流程漏洞、API-Key漏洞等进行攻击和入侵,给区块链项目的管理运营团队及用户造成巨大的经济损失。知道创宇十余年安全经验,凭借多重防护+云端大数据技术,为区块链应用提供专属安全解决方案。欢迎扫码咨询:
参考
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1334/