WTF Solidity 合约安全: S05. 整型溢出
我最近在重新学 Solidity,巩固一下细节,也写一个“WTF Solidity 合约安全”,供小白们使用(编程大佬可以另找教程),每周更新 1-3 讲。
所有代码和教程开源在 github: github.com/AmazingAng/WTF-Solidity
这一讲,我们将介绍整型溢出漏洞(Arithmetic Over/Under Flows)。这是一个比较经典的漏洞,Solidity 0.8版本后内置了Safemath库,因此很少发生。
整型溢出
以太坊虚拟机(EVM)为整型设置了固定大小,因此它只能表示特定范围的数字。例如 uint8
,只能表示 [0,255] 范围内的数字。如果给 uint8
类型变量的赋值 257
,则会上溢(overflow)变为 1
;如果给它赋值-1
,则会下溢(underflow)变为255
。
攻击者可以利用这个漏洞进行攻击:想象一下,黑客余额为0
,他凭空花 $1
之后,余额突然变成了 $2^256-1
。2018年的土狗项目 PoWHC
因为这个漏洞被盗了 866 ETH
。
漏洞合约例子
下面这个例子是一个简单的代币合约,参考了 Ethernaut
中的合约。它有 2
个状态变量:balances
记录了每个地址的余额,totalSupply
记录了代币总供给。
它有 3
个函数:
- 构造函数:初始化代币总供给。
transfer()
:转账函数。balanceOf()
:查询余额函数。
由于solidity 0.8.0
版本之后会自动检查整型溢出错误,溢出时会报错。如果我们要重现这种漏洞,需要使用 unchecked
关键字,在代码块中临时关掉溢出检查,就像我们在 transfer()
函数中做的那样。
这个例子中的漏洞就出现在transfer()
函数中,require(balances[msg.sender] - _value >= 0);
这个检查由于整型溢出,永远都会通过。因此用户可以无限转账。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;
contract Token {
mapping(address => uint) balances;
uint public totalSupply;
constructor(uint _initialSupply) {
balances[msg.sender] = totalSupply = _initialSupply;
}
function transfer(address _to, uint _value) public returns (bool) {
unchecked{
require(balances[msg.sender] - _value >= 0);
balances[msg.sender] -= _value;
balances[_to] += _value;
}
return true;
}
function balanceOf(address _owner) public view returns (uint balance) {
return balances[_owner];
}
}
Remix
复现
- 部署
Token
合约,将总供给设为100
。 - 向另一个账户转账
1000
个代币,可以转账成功。 - 查询自己账户的余额,发现是一个非常大的数字,约为
2^256
。
预防办法
Solidity
0.8.0
之前的版本,在合约中引用 Safemath 库,在整型溢出时报错。Solidity
0.8.0
之后的版本内置了Safemath
,因此几乎不存在这类问题。开发者有时会为了节省gas使用unchecked
关键字在代码块中临时关闭整型溢出检测,这时要确保不存在整型溢出漏洞。
总结
这一讲,我们介绍了经典的整型溢出漏洞,由于solidity 0.8.0 版本后内置 Safemath
的整型溢出检查,这类漏洞已经很少见了。