作者:昏鴉@知道創宇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的結果,導致傳入地址余額無代價增加。
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函數作演示,測試合約代碼如下:
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部署合約,擁有代幣總量,授權0x28deb6CA32C274f7DabF2572116863f39b4E65D9500代幣額度

通過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漏洞等進行攻擊和入侵,給區塊鏈項目的管理運營團隊及用戶造成巨大的經濟損失。知道創宇十余年安全經驗,憑借多重防護+云端大數據技術,為區塊鏈應用提供專屬安全解決方案。
參考
https://bzx.network/blog/incident
https://twitter.com/k06a/status/1305223411615117322
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1334/
暫無評論