我最近在重新学solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。
所有代码和教程开源在github: github.com/AmazingAng/WTFSolidity
这一讲,我们将介绍整型溢出漏洞(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()
:转账函数。
balnceOf()
:查询余额函数。
由于solidity 0.8.0
版本之后会自动检查整型溢出错误,溢出时会报错。如果我们要重现这种漏洞,需要使用 unchecked
关键字,在代码块中临时关掉溢出检查,就像我们在 transfer()
函数中做的那样。
这个例子中的漏洞就出现在transfer()
函数中,require(balances[msg.sender] - _value >= 0);
这个检查由于整型溢出,永远都会通过。因此用户可以无限转账。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
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];
}
}
部署 Token
合约,将总供给设为 100
。
向另一个账户转账 1000
个代币,可以转账成功。
查询自己账户的余额,发现是一个非常大的数字,约为2^256
。
Solidity 0.8.0
之前的版本,在合约中引用 Safemath 库,在整型溢出时报错。
Solidity 0.8.0
之后的版本内置了 Safemath
,因此几乎不存在这类问题。开发者有时会为了节省gas使用 unchecked
关键字在代码块中临时关闭整型溢出检测,这时要确保不存在整型溢出漏洞。
这一讲,我们介绍了经典的整型溢出漏洞,由于solidity 0.8.0 版本后内置 Safemath
的整型溢出检查,这类漏洞已经很少见了。