几乎我们所有人都使用过 Google 表格或 Microsoft Excel 输入数据进行某些计算。假设您希望输入员工的姓名、电话号码、职务和薪水。
在表格或 Excel 中,记录或案例的最简单形式如下所示:
如您所知,员工姓名和职务均由文本组成,而电话号码和薪水则由一系列数字组成。
因此,从语义的角度来看,作为人类,我们理解这些字段在现实世界中的含义,并且可以区分它们。
显然,虽然您不需要计算机科学学位也能分辨出差异,但编译器或解释器如何处理这些数据?
这就是数据类型的用武之地,程序员要么花时间指定,要么不指定,这取决于他们编写代码的编程语言。
换句话说,员工姓名和职位下的数据点称为字符串。当然,由于没有小数点,工资显然是一个整数。简而言之,这些是您在编码时必须声明的数据类型,以便仅执行与该数据类型关联的正确操作。
这就是我们在 Solidity 中声明整数数据类型的方式:
也就是说,上面电子表格中的电话号码字段包含一个数据点,该数据点将用作唯一字符串,但该讨论将在另一天进行。现在,我们的重点将放在我们都执行过基本算术的原始数据类型上。
是的,我们谈论的是整数数据类型,虽然它对关键算术运算很重要,但对任何计算都有有限的范围。
现实世界中最常见的整数溢出示例可能发生在车辆上。也称为里程表,这些设备通常跟踪车辆行驶了多少英里。
那么,一旦行驶的英里数达到六位里程表中的无符号整数值 999999,会发生什么情况?
理想情况下,再增加一英里,这个值应该达到 1000000 吧?但这不会发生,因为有第七位数字的规定。
相反,行驶里程的值重置为 000000,如下所示:
根据定义,由于第七位数字不可用,这会导致“溢出”,因为没有表示准确的值。
你明白了,对吧?
相反,即使这种情况并不常见,也可能发生相反的情况。换句话说,当记录的值小于范围内可用的最小值时,这也称为“下溢”。
众所周知,计算机会将整数作为其二进制等价物存储在内存中。现在,为了简单起见,假设您正在使用一个 8 位寄存器。
= 2⁸*1 + 2⁷*1 + 2⁶*1 + 2⁵*1 + 2⁴*1 + 2³*1 + 2²*1 + 2¹*1 + 2⁰*1
= 256 + 128 + 64 + 32 + 16 + 8 + 4 + 2 + 1
= 111111111
每个位都是 1,正如您所知,您不能存储更高的值。
另一方面,如果你想在 8 位寄存器中存储数字 0,它看起来是这样的:
= 2⁸*0 + 2⁷*0 + 2⁶*0 + 2⁵*0 + 2⁴*0 + 2³*0 + 2²*0 + 2¹*0 + 2⁰*0
= 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
= 000000000
每个位都是 0 的地方,这应该告诉你,你不能存储一个更低的值。
换句话说,这种 8 位寄存器允许的整数范围是 0-511。那么,是否可以将整数 512 或 -1 存储在这样的寄存器中?
当然不是。因此,您将存储一个类似于里程表示例中行驶里程的重置值的值,但为二进制值。
显然,您需要更多位的寄存器才能舒适地容纳这样的数字。否则,可能会再次出现溢出的情况。
对于有符号整数,我们也存储负整数。因此,当我们尝试存储一个小于可接受范围或小于零的数字时,如上所示,就会发生下溢。
再一次,由于进行任何计算的目的都是为了获得确定性的结果,这充其量可能是烦人的,但在最坏的情况下会导致数百万美元的损失。特别是,当这些整数溢出或下溢错误发生在智能合约中时。
虽然整数上溢和下溢已经存在了几十年,但它们作为智能合约中的一个漏洞的存在已经提高了风险。当攻击者利用此类错误时,他们可以耗尽智能合约中的大量代币。
可能第一次出现这种类型的错误是在区块 74638 中,它为三个地址创造了数十亿比特币。通过软分叉解决这个错误需要几个小时,软分叉丢弃了块,从而使交易无效。
一方面,价值超过 2100 万比特币的交易被拒绝。这与溢出交易没有什么不同,就像向上述三个账户发送大量资金的交易一样。
然而,以太坊智能合约也经历了整数溢出和下溢,BeautyChain 就是一个突出的例子。
在这种情况下,智能合约包含一行错误的代码:
因此,攻击者理论上能够获得无限量的 BEC 代币,理论上这可能达到 (2²⁵⁶)-1 的价值。
现在,让我们来看另一个发生整数下溢/溢出的智能合约示例。
乍一看,此示例中有两个交互的合约,它演示了在整数溢出的情况下会发生什么。
正如您在下面看到的,合约 TimeLock 允许您存入和提取资金,但有一个区别:您只能在一段时间后执行后者。在这种情况下,您只能在一周内提取资金。
但是,一旦调用了 Attack 合约中的 attack 函数,时间锁定就不再有效,这就是为什么攻击者可以立即提取余额的原因。
也就是说,由于type(uint).max+1-timeLock.locktime(address(this))语句导致整数溢出,时间锁被淘汰。
例如,一旦您使用上面的代码部署了两个智能合约,您可以通过调用 TimeLock 合约中的存款和取款函数来测试时间锁是否成立,如下所示:
如您所见,通过选择 2 Ether 的数量,我们得到如上所示的 2 Ether 的智能合约余额:
具体来说,可以通过在balances函数的字段中添加地址,然后点击balances按钮来查看持有2个以太币余额的具体地址:
但是,如上所述,由于时间锁定,您还不能提取这些资金。当您在点击撤回功能后查看控制台时,您会发现一个由红色“x”符号指示的错误。正如你在下面看到的,这个错误的原因是由合约提供的是“Lock time not expired”:
现在,让我们看一下已部署的 Attack 合约,如下所示:
现在,为了调用攻击功能,您需要存入 1 个或更多的以太币。因此,在本例中,我们选择了 2 个以太币,如下所示:
在此之后,点击“攻击”。你会发现你存入的 2 Ether 将立即被提取并添加到攻击合约中,如下面的 2 Ether 余额所示:
显然,这不应该发生,因为长时间锁定应该在您存款后立即生效。当然,正如我们所知,type(uint).max+1-timeLock.locktime(address(this)) 语句通过使用 increaseLockTime 函数来减少锁定时间。这正是我们能够立即提取以太币余额的原因。
这给我们带来了一个明显的问题:有没有办法修复整数溢出和下溢漏洞?
认识到整数溢出/下溢漏洞可能是毁灭性的,已经推出了针对此错误的几个修复程序。让我们看看这两个修复程序以及它们如何解决此类错误:
Open Zeppelin 作为一个组织,在网络安全技术和服务方面提供了很多, SafeMath 库是其智能合约开发存储库的一部分。该存储库包含可以导入到您的智能合约代码中的合约,SafeMath 库就是其中之一。
让我们看看 SafeMath.sol 中的一个函数如何检查整数溢出:
现在,一旦计算了 a+b,就会检查 c<a 是否发生。当然,这只有在整数溢出的情况下才是正确的。
随着 Solidity 的编译器版本达到 0.8.0 及更高版本,现在内置了整数溢出和下溢的检查。因此,无论是在使用语言还是使用这个库时,仍然可以使用这个库来检查这个漏洞。当然,如果你的智能合约需要编译器版本低于0.8.+,那你就得使用这个库来避免溢出或下溢。
现在,如前所述,如果您的智能合约使用的是 0.8.0 及更高版本的编译器,则此版本具有针对此类漏洞的内置检查器。
事实上,只是为了验证它是否适用于上面的智能合约,当将编译器版本更改为“^0.8.0”并重新部署时,收到以下'revert'错误:
当然,没有进行2个以太币的存入,这是因为检查了时间锁值的溢出。因此,由于一开始就没有存入资金,因此无法提款。
毫无疑问,Attack.attack() 函数调用在这里没有起作用,所以一切都很好!
如果您应该从这篇冗长的博客文章中收集到任何信息,那就是忽略这个来自 BEC 攻击的漏洞可能代价高昂。您还可以看出,如果不加以检查,很容易发生非恶意错误。或者只是简单的让黑客利用这个漏洞。
说到这一点,并利用我们对 BEC 攻击如何发生的理解,认识到这个漏洞可以大大有助于在编写智能合约时防止任何攻击,这要归功于提供的修复。即使还有其他几个智能合约漏洞等着绊倒你。