作者:Al1ex@七芒星实验室
原文链接:https://mp.weixin.qq.com/s/dNZr8OPZVr60bSIONJbHyw
计算机中整数变量有上下界,如果在算术运算中出现越界,即超出整数类型的最大表示范围,数字便会如表盘上的时针从12到1一般,由一个极大值变为一个极小值或直接归零,此类越界的情形在传统的软件程序中很常见,但是否存在安全隐患取决于程序上下文,部分溢出是良性的(如tcp序号等),甚至是故意引入的(例如用作hash运算等)。
以太坊虚拟机(EVM)为整数指定固定大小的数据类型,这意味着一个整型变量只能有一定范围的数字表示,例如,一个uint8 ,只能存储在范围[0,255]的数字,若试图存储256到一个uint8将变成0,不加注意的话,只要没有检查用户输入又执行计算,导致数字超出存储它们的数据类型允许的范围,Solidity中的变量就可以被用来组织攻击。
类型描述:管理员通过修改合约中的参数来制造溢出漏洞,导致用户提币转出token之后,却收不到ETH(或收到极少量ETH),造成用户经济损失。
漏洞实例:合约Internet Node Token (INT)
合约地址:https://cn.etherscan.com/address/0x0b76544f6c413a555f309bf76260d1e02377c02a#code
漏洞位置:L179
漏洞危害:用户提币之后,无法得到对应数额的ETH;
漏洞原理:sellPrice被修改为精心构造的大数后,可导致amount sellPrice的结果大于整数变量(uint256)最大值,发生整数溢出,从而变为一个极小值甚至归零。该值在程序语义中是用于计算用户提币应得的ETH数量,并在L179进行了校验,但该值被溢出变为极小值之后可以逃逸L179的校验,并导致用户售出token后只能拿到少量的(甚至没有)ETH。
类型描述:管理员在特定条件下,通过调用合约中有漏洞的发币函数制造下溢,从而实现对自身账户余额的任意增加。
漏洞实例:合约Bitcoin Red(BTCR)
合约地址:https://cn.etherscan.com/address/0x6aac8cb9861e42bf8259f5abdc6ae3ae89909e11#code
漏洞位置:L45
漏洞危害:管理员执行了一个正常向某个地址进行发币的操作,实际已经暗中将自身账户的余额修改为了一个极大的数;
漏洞原理:distributeBTR()函数的本意是管理员给指定地址发放一定数额的token,并从自身账户减少对应的token数量。减少管理员账户余额的操作为balances[owner] -= 2000 108 ,运算的结果将被存到balances[owner]中,是一个无符号整数类型。当管理员余额本身少于2000 * 108时,减法计算结果为负值,解释为无符号整数即一个极大值。
类型描述:管理员调用铸币函数给某个地址增加token时,利用溢出漏洞可以突破该函数只能增加token的限制,实际减少该地址的token数量,从而实现对任一账户余额的任意篡改(增加或减少)。
漏洞实例:合约PolyAi (AI)
合约地址:https://cn.etherscan.com/address/0x5121e348e897daef1eef23959ab290e5557cf274#code
漏洞位置:L136
漏洞危害:管理员可以绕过合约限制,任意篡改所有地址的token余额;
漏洞原理:攻击者通过构造一个极大的mintedAmount,使得balanceOf[target] + mintedAmount发生整数溢出,计算结果变为一个极小值。
类型描述:管理员通过构造恶意参数,可以绕过程序中规定的token发行上限,实现超额铸币。合约Playkey (PKT)存在此类漏洞,导致合约中的铸币上限形同虚设,从而发行任意多的token。此外,我们还发现Nexxus (NXX)、Fujinto (NTO)两个合约存在类似漏洞,这两个合约没有铸币上限限制,但同样的手段,可以溢出合约中一个用于记录已发币总量(totalSupply)的变量值,使其与市场中实际流通的总币数不一致。
漏洞实例:合约Playkey (PKT)
合约地址:https://cn.etherscan.com/address/0x2604fa406be957e542beb89e6754fcde6815e83f#code
漏洞位置:红色标注的行L241
漏洞危害:管理员可以篡改已发币总量(totalSupply)为任意值,并绕过合约中的铸币上限超额发行token;
漏洞原理:_value在函数调用时被设置为精心构造的极大值,使得totalSupply + _value计算结果溢出后小于tokenLimit,从而轻易绕过L237行的铸币上限检测。
类型描述:管理员通过制造溢出来绕过合约中对单地址发币的最大上限,可以对指定地址分配超额的token,使得对单地址的发布上限无效。
漏洞实例:合约LGO (LGO)
合约地址:https://cn.etherscan.com/address/0x123ab195dd38b1b40510d467a6a359b201af056f#code
漏洞位置:红色标注的行L286
漏洞危害:管理员绕过合约中规定的单地址发币上限,给指定地址分配超额的token;
漏洞原理:一个极大的_amount可以使得算数加法运算holdersAllocatedAmount + _amount发生整数溢出,变为一个极小值,从而绕过L286的检测。
漏洞描述:买家如果拥有足够多的ETH,可以通过发送大量token制造溢出,从而绕过ICO发币上限,达到超额购币。
漏洞实例:合约EthLend (LEND)
合约地址:https://cn.etherscan.com/address/0x80fB784B7eD66730e8b1DBd9820aFD29931aab03#code
漏洞位置:红色标注的行L236
漏洞危害:调用者绕过合约中规定ICO的token容量上限,获得了超额购币;
漏洞原理:一个极大的_newTokens可以使得算数加法运算totalSoldTokens + newTokens发生整数溢出,变为一个极小值,从而绕过L236的检测。
案例代码如下:
pragma solidity ^0.4.22; contract TokenExample { address public owner; mapping(address => uint256) public balances; mapping(address =>mapping(address =>uint256)) public allowed; event Transfer(address _from,address _to,uint256 _value); modifier onlyOwner{ require(msg.sender == owner); _; } constructor() public { owner = msg.sender; balances[owner] = 2000*10**8; } function distribute(address[] addresses) public onlyOwner{ for(uint i=0;i < addresses.length;i++){ balances[owner] -= 2000*10**8; balances[addresses[i]] +=2000*10**8; emit Transfer(owner,addresses[i],2000*10**8); } } }
如上图所示,在智能合约中的distribute函数的功能是从owner账户向指定的地址列表传入代币,但是在对balance[owner]的账户做减法运算的时候,未使用SafeMath函数进行数值运算操作,而且也没有判断合约的owner是否有足够的代币,直接一个循环对owner进行减法处理,这里如果转出的代币总量大于owner账户余额,那么balance[owner]将会发生下溢,变成一个极大的值,下面在remix中演示操作一下:
调用distribute函数传入地址数组: ["0x14723a09acff6d2a60dcdf7aa4aff308fddc160c","0x4b0897b0513fdc7c541b6d9d7e929c4e5364d2db"],使用owner分别向这两个地址发送代币。 执行之前owner的余额:
执行distribute函数:
交易日志记录如下:
执行之后owner的余额:
可以从上面的结果当中看到合约的owner在执行完distribute函数之后,按理来说转账操作应该会使得合约的owner的代币减少,但是这里去不减反增了,故这里的“下溢”确实存在。
案例: GEMCHAIN
合约地址:https://cn.etherscan.com/address/0xfb340423dfac531b801d7586c98fe31e12a32f31#code
如上上图所示,该智能合约中的mintToken函数用于增发代币,但是在增发代币的过程中对于加法操作没有使用SafeMath函数进行数值运算操作,而且也没有使用require对是否发生溢出进行检查,故这里存在溢出风险,如果合约的owner给target增发较多数量的mintedAmount那么将会导致溢出问题的发生。 使用remix演示如下:
第一次铸币: 首先,我们先调用mintToken函数向地址“0x14723a09acff6d2a60dcdf7aa4aff308fddc160c”铸币,铸币的数量为: “0x8000000000000000000000000000000000000000000000000000000000000000”即2的255次方
交易日志:
铸币之后地址“0x14723a09acff6d2a60dcdf7aa4aff308fddc160c”的余额为:
为了让其发生溢出,我们还需要向地址“0x14723a09acff6d2a60dcdf7aa4aff308fddc160c”铸币,铸币的数量仍然为:“0x8000000000000000000000000000000000000000000000000000000000000000”即2的255次方,目的就是为了让2的255次方+2的255次方发生溢出,超出uint256的最大范围。下面具体看操作
第二次铸币:
交易日志:
查看余额:
从上面的结果我们可以发现确实发生了溢出!可想而知,如果合约的owner在不校验溢出问题的情况下向某一地址铸币,那么该地址如果发生溢出,那么代币数量将会发生变化,时而出现减少的情况(因为发生溢出)。
有时候你会发现虽然我们看到一个合约当中有整型溢出的风险,例如在transfer函数中未使用require进行溢出校验,同时也没有使用SafeMath函数进行数值运算防护的情形,但是该合约当中已经规定了token的总量(totalSupply),而且没有铸币函数(mintToken)另外增加代币,那么合约总体来说是安全的,不存在整型溢出,为什么这样说呢?因为你永远都不会发生两个数值相加超过uint256的情况,但是在这中情况下你就应该将目光放到“乘法溢出”或“减法下溢”的问题上来进行查找,审计是否真的不存在“整型溢出”问题。
那么如何防范这种整型溢出问题呢?官方给出的建议是使用OpenZepplin提供的SafeMath函数库进行数值运算操作,使用SafeMath库函数可以有效的对溢出问题进行检查与防范,SafeMath函数库源代码如下:
https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/math/SignedSafeMath.sol
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; /** * @title SignedSafeMath * @dev Signed math operations that revert on error. */ library SignedSafeMath { /** * @dev Returns the multiplication of two signed integers, reverting on * overflow. * * Counterpart to Solidity's `*` operator. * * Requirements: * * - Multiplication cannot overflow. */ function mul(int256 a, int256 b) internal pure returns (int256) { return a * b; } /** * @dev Returns the integer division of two signed integers. Reverts on * division by zero. The result is rounded towards zero. * * Counterpart to Solidity's `/` operator. * * Requirements: * * - The divisor cannot be zero. */ function div(int256 a, int256 b) internal pure returns (int256) { return a / b; } /** * @dev Returns the subtraction of two signed integers, reverting on * overflow. * * Counterpart to Solidity's `-` operator. * * Requirements: * * - Subtraction cannot overflow. */ function sub(int256 a, int256 b) internal pure returns (int256) { return a - b; } /** * @dev Returns the addition of two signed integers, reverting on * overflow. * * Counterpart to Solidity's `+` operator. * * Requirements: * * - Addition cannot overflow. */ function add(int256 a, int256 b) internal pure returns (int256) { return a + b; } }
应用了SafeMath函数的智能合约实例: https://etherscan.io/address/0xB8c77482e45F1F44dE1745F52C74426C631bDD52#code
可以看到在上面的智能合约当中对于数值运算都使用了SafeMath函数进行操作,而且也使用了require对溢出校验进行防护,总体较为安全。
整型溢出问题发生的根源还是在于合约的开发者在开发合约时未考虑到“整型溢出”问题,作为审计人员的我们在看到合约时也要保持清醒,对于存在疑惑的地方应该采用“调试、验证”的方法去排除疑虑,而且在审计的过程中也要十分的认真、细心才可以,不要放过任何一个有可能存在问题的地方,例如修饰器/修饰词对应的权限问题、逻辑处理问题等等。
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1581/