您的位置: > 欧 易 OKX 区块链新闻> 正文

打印本文             

By:小白@慢雾安全团队

背景概述

上周写了智能合约安全审计入门篇 —— 重入漏洞,这次我们接着来说一个同样很经典的漏洞 —— 溢出漏洞

前置知识

首先我们还是先来看看溢出是什么:

算术溢出(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 时溢出会报错。所以当我们看到 0.8 版本以下的合约时,就要注意这个合约可能出现溢出问题。

漏洞示例

看了前置知识我相信大家对溢出漏洞都有一定的了解了,下面我们来结合合约代码来深入了解溢出漏洞:

  •  

// SPDX-License-Identifier: MITpragma 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 函数存入该合约并锁定,且至少一周内不能提现。当然用户也可以通过 increaseLockTime 函数来增加存储时间,用户在设定的存储期限到期前是无法提取 TimeLock 合约中锁定的代币的。首先我们发现这个合约中的 increaseLockTime 函数和 deposit 函数具有运算功能,并且合约支持的版本是:0.7.6 向上兼容,所以这个合约在算数溢出时是不会报错的,所以我们这里就可以判断这个合约是可能存在溢出漏洞的,这里可利用的函数有两个,一个是 increaseLockTime 函数,一个是 deposit 函数。我们先来分析这两个函数内参数可影响的范围再来决定如何发起攻击:

1. deposit 函数存在两个运算操作,第一个是影响用户存入的余额 balances 的,这里传入的参数是可控的所以这里会有溢出的风险,另一个是影响用户的锁定时间 lockTime 的,但是这里的运算逻辑是每次调用 deposit 存入代币时会给 lockTime 增加一周,由于这里的参数不可控所以这个运算不会存在溢出风险。

2. increaseLockTime 函数是根据用户传入的 _secondsToIncrease 参数来进行运算从而改变用户的存入代币的锁定时间的,由于这里的 _secondsToIncrease 参数是可控的,所以这里有溢出的风险。

综上所述,我们发现可利用的参数有两个,分别为 deposit 函数中的 balances 参数 increaseLockTime 函数中的 _secondsToIncrease 参数

我们先来看 balances 参数,如果要让这个参数溢出我们需要有足够的资金存入才可以(需要 2^256 个代币存入才能导致 balances 溢出并归零),如果要利用这个溢出漏洞的话,我们把大量资金存入自己的账户并让自己的账户的 balances 溢出并归零从而清空自己的资产,我觉得在坐的各位没有人会这么做吧。所以这个参数可以认为在攻击者的角度是不可用的。

我们再看 _secondsToIncrease 参数,这个参数是我们调用 increaseLockTime 函数来增加存储时间时传入的,这个参数可以决定我们什么时候可以将自己存入并锁定的代币从合约中取出,我们可以看到这个参数在传入之后是直接与账户对应的锁定时间 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(); }}

这里我们将使用 Attack 攻击合约先存入以太后利用合约的溢出漏洞在存储未到期的情况下提取我们在刚刚 TimeLock 合约中存入并锁定的以太:

1. 首先部署 TimeLock 合约;

2. 再部署 Attack 合约并在构造函数中传入 TimeLock 合约的地址;

3. 调用 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 ;

4. 这时 Attack.attack 再调用 TimeLock. withdraw 函数将成功通过 block.timestamp > lockTime[msg.sender] 这项检查让我们能够在存储时间未到期的情况下成功提前取出我们刚刚在 TimeLock 合约中存入并锁定的那个以太。

下面是攻击流程图:

solidity

修复建议

到这里相信大家对溢出漏洞都有自己的理解了,那么下面我们就以开发者和审计者的角度来分析如何预防溢出漏洞和如何快速找出溢出漏洞:

(1)作为开发者

1. 使用 SafeMath 来防止溢出;

2. 使用 Solidity 0.8 及以上版本来开发合约并慎用 unchecked 因为在 unchecked 修饰的代码块里面是不会对参数进行溢出检查的;

3. 需要慎用变量类型强制转换,例如将 uint256 类型的参数强转为 uint8 类型由于两种类型的取值范围不同也可能会导致溢出。

(2)作为审计者

1. 首先查看合约版本是否在 Solidity 0.8 版本以下或者是否存在 unchecked 修饰的代码块,如果存在则优先检查参数的溢出可能并确定影响范围;

2. 如果合约版本在 Solidity 0.8 版本以下则需要查看合约是否引用了 SafeMath;

3. 如果使用了 SafeMath 我们需要注意合约中有没有强制类型转换,如果有的话则可能会存在溢出的风险;

4. 如果没有使用 SafeMath 且合约中存在算术运算的我们就可以认为这个合约是可能存在溢出风险的,在实际审计中还要结合实际代码来看。


a16z 合伙人:Web3 的新思维、新策略和新指标   

每个公司都面临着不同的“冷启动问题”:你如何从无到有?如何获取客户?如何创造网络效应——即当更多的人使用你的产品或服务时,你的产品或服务对客户而言变得更有价值——从而激励更多的用户到来?

简而言之,你如何“进入市场”并说服潜在客户在你的产品或服务上花费他们的金钱、时间和注意力?

Web2 是由大型中心化产品/服务定义的互联网时代,比如亚马逊、eBay、Facebook 和 Twitter,其中绝大多数的价值是由平台本身而不是用户产生。大多数 Web2 组织对于上述问题的回应是对销售和营销团队进行大量投资,这是传统的进入市场 (go-to-market, GTM) 策略的一部分,GTM 的重点是创造潜在客户、获取和留住客户。

但近年来,一种全新的组织建设模式出现了。这种新模式利用去中心化的技术,通过被称为代币 (Token) 的数字原语,将用户带入所有者的角色,而不是由企业控制——即由中心化的领导来制定有关产品或服务的所有决策,甚至在使用用户数据和免费的、用户生成的内容时也是如此。

这种被称为 Web3 的新模式,彻底改变了这些新型公司的 GTM (进入市场) 理念。虽然一些传统的用户获取框架仍然是相关的,但 Token 的引入和新的组织结构 (如去中心化自治组织(DAO))需要各种 GTM 策略。由于 Web3 对很多人来说还是个新事物,但在这一领域有巨大的发展空间,在这篇文章中,我将分享一些在这一背景下思考 GTM 的新框架,以及这个生态系统中可能存在的不同类型的组织。我还将提供一些技巧和战术,以帮助那些希望伴随 Web3 的不断发展,创建自己的 Web3 GTM 策略的构建者。

01. 全新 GTM 策略的催化剂:代币

用户获取漏斗的概念是 GTM 的核心,大多数企业都非常熟悉这个概念:从漏斗顶部的意识和潜在用户生成 (Awareness & Lead Generation),到漏斗底部的转化 (Conversion) 和留住用户 (Retention),如下图所示。因此,传统的 Web2 GTM 通过这个非常线性的用户获取视角来解决冷启动问题,包括定价、市场营销、合作伙伴、销售渠道映射和销售队伍优化等领域。成功的衡量标准包括网站点击率、每个客户为企业带来的收入等等。

Web3 改变了启动新网络的整个方法,因为 Token (代币) 为解决冷启动问题提供了一种替代传统方式的方法。核心开发团队可以使用 Token 来吸引早期用户,而不是在传统营销上花费资金来吸引和获取潜在用户,在网络效应还不明显或尚未开始时,这些用户可以因为他们的早期贡献而获得奖励。这些早期用户不仅是将更多的人带入该网络的传道者 (他们也希望通过自己的贡献获得回报),而且从本质上说,这使得 Web3 中的早期用户比 Web2 中的传统业务开发或销售人员带来更强大的影响力

比如借贷协议 Compound (披露,a16z 是该协议以及本文讨论的其他组织的投资方),该协议使用代币来激励早期的放贷人 (lenders) 和借款人 (borrowers),也即以 COMP 代币的形式为参与其「流动性挖矿」的用户提供额外的奖励,从而“引导流动性”。该协议的任何用户,无论是放贷人还是借款人,都会获得 COMP 代币奖励。该流动性挖矿计划于 2020 年启动后,Compound 协议中的总锁仓价值 (TVL) 从 1 亿美元跃升至 6 亿美元。值得注意的是,虽然代币奖励能够吸引用户,但这种方式本身并不足以让用户变得具有“粘性”,对此稍后再详细介绍。虽然传统公司确实通过股权激励员工,但它们很少在经济上长期激励客户。

总结一下:在 Web2 中,GTM (进入市场) 的主要利益相关者是客户,通常是通过销售和市场营销获得的;而在 Web3 中,某个组织的 GTM 利益相关者不仅包括他们的客户/用户,还包括他们的开发者、投资者和合伙人。因此,许多 Web3 公司发现社区比销售和市场营销角色更重要。

02. Web3 中的 GTM 矩阵

对于 Web3 组织而言,根据其组织结构 (中心化 vs 去中心化) 和经济激励 (没有 Token vs. 使用 Token),其 GTM (进入市场) 策略取决于该组织在下图矩阵中的位置:

a16z 合伙人:Web3 的新思维、新策略和新指标

撰文:Maggie Hsu,a16z 合伙人编辑:南风   ...

关于我们

支持iOS|android|windows等平台

  • 用户支持
  • 帮助中心
  • 服务条款
微信二维码
欧 易 (OKX) 数字货币交易平台 Powered by OKX
QR code