译者注:
智能合约代码的审计,目前还不是技术社区内经常会讨论的主题。今年3月6日,发表在博客网站【Schneier on Security】上的一篇博客(原文链接:【https://www.schneier.com/blog/archives/2018/03/security\_vulner\_13.html】,原文中附有一篇专业的研究报告【Finding The Greedy, Prodigal, and Suicidal Contracts at Scale】)指出,目前在以太坊中,有89%的智能合约代码都或多或少存在安全漏洞/隐患,这显然是一个非常惊人的调查结果,对社区而言也是一个巨大的风险因素。而随着智能合约的增多乃至未来可能的大规模发展,相信对各种合约代码的审计也将会变成一个专门的、专业的领域,并且是不能够、也不应该被忽视的。
本文译自Merunas Grincalaitis(一位以太坊开发者)于2017年9月18日发表在Medium上的文章,原文链接:【https://medium.com/@merunasgrincalaitis/how-to-audit-a-smart-contract-most-dangerous-attacks-in-solidity-ae402a7e7868】。本文是作者结合自己所写的一份智能合约代码来讲述智能合约审计要点的技术文章,并包含了对Solidity语言可能遇到的几种危险攻击的介绍。对于以太坊智能合约开发者而言有一定的参考和学习价值。
你有没有考虑过如何审计一个智能合约来找出安全漏洞?
你可以自己学习,或者你可以使用这份便利的一步步的指南来准确地知道在什么时候该做什么,并对合约进行审计。
我已经研究过很多智能合约的审计,并且我已经找到了从任何合约中提取所有重要信息的最常规步骤。
在本文中,你将会学到以下内容:
生成对一个智能合约的完整审计报告所需的所有步骤。
作为以太坊智能合约审计人员需要了解的最重要的攻击类型。
应该在合约中寻找什么,和一些你不会在其他任何地方找到的有用的提示。
让我们直接开始审计合约吧:
如何审计一个智能合约
为了教会你如何进行审计,我会审计我自己写的一份合约。这样,你可以看到可以由你自行完成的真实世界的审计。
现在你也许会问:智能合约的审计到底是指什么?
智能合约审计就是仔细研究代码的过程,在这里就是指在把Solidity合约部署到以太坊主网络中并使用之前发现错误、漏洞和风险;因为一旦发布,这些代码将无法再被修改。这个定义仅仅是为了讨论目的。
请注意,审计不是验证代码安全的法律文件。没有人能100%确保代码不会在未来发生错误或产生漏洞。这仅仅是保证你的代码已被专家校订过,基本上是安全的。
讨论可能的改进,主要是为了找出那些可能会危害到用户的以太币的风险和漏洞。
好了,现在我们来看看一份智能合约审计报告的结构:
免责声明:在这里你会说审计不是一个具有法律约束力的文件,它不保证任何东西。这只是一个讨论性质的文件。
审计概览和优良特性:快速查看将被审计的智能合约并找到良好的实践。
对合约的攻击:在本节中,你将讨论对合约的攻击以及会产生的结果。这只是为了验证它实际上是安全的。
合约中发现的严重漏洞:可能严重损害合约完整性的关键问题。那些会允许攻击者窃取以太币的严重问题。
合约中发现的中等漏洞:那些可能损害合约但危害有限的漏洞。比如一个允许人们修改随机变量的错误。
低严重性的漏洞:这些问题并不会真正损害合约,并且可能已经存在于合约的已部署版本中。
逐行评注:在这部分中,你将分析那些具有潜在改进可能的最重要的语句行。
审计总结:你对合约的看法和关于审计的最终结论。
将这份结构说明保存在一个安全的地方,这是你安全地审计智能合约时需要做的全部内容。它将确实地帮助你找到那些难以发现的漏洞。
我建议你从第7点“逐行评注”开始,因为当逐行分析合约时,你会发现最重要的问题,你会看到缺少了什么,以及哪些地方应该修改或改进。
在后文中,我会给你展示一个免责声明,你可以把它作为审计的第一步。你可以从第1点开始看下去,直到结束。
接下来,我将向你展示使用这样的结构完成的审计结果,这是我针对我自己写的一个合约来做的。你还将在第3点中看到对于智能合约可能受到的最重要的攻击的介绍。
赌场合约审计
你可以在我的Github上看到审计的代码:https://github.com/merlox/casino-ethereum/blob/master/contracts/Casino.sol
以下就是我的合约Casino.sol的审计报告:
序言
在这份智能合约审计报告中将包含以下内容:
免责声明
审计概览和优良特性
对合约的攻击
合约中发现的严重漏洞
合约中发现的中等漏洞
低严重性的漏洞
逐行评注
审计总结
1、免责声明
审计不会对代码的实用性、代码的安全性、商业模式的适用性、商业模式的监管制度或任何其他有关合约适用性的说明以及合约在无错状态的行为作出声明或担保。审计文档仅用于讨论目的。
2、概述
该项目只有一个包含142行Solidity代码的文件 。所有的函数和状态变量的注释都按照标准说明格式(即Ethereum Nature Specification Format,缩写为natspec,它是以太坊社区官方的代码注释格式说明,原文参考github:【https://github.com/ethereum/wiki/wiki/Ethereum-Natural-Specification-Format】,译者注)进行编写,这可以帮助我们快速地理解程序是如何工作。
该项目使用了一个中心化的服务实现了Oraclize API,来在区块链上生成真正的随机数字。
在区块链上生成随机数字是一个相当困难的课题,因为以太坊的核心价值之一就是可预测性,其目标是确保没有未定义的值。
译者注:
这里之所以说在区块链上生成随机数很困难,是因为,无论采用何种算法,都需要使用时间戳作为生成随机数的“种子”(因为时间戳是计算机领域内唯一可以理论上保证“不会重复”的数值);而在智能合约中取得时间戳只能依赖某个节点(矿工)来做到。这就是说,合约中取得的时间戳是由运行其代码的节点(矿工)的计算机本地时间决定的;所以这个节点(矿工)的可信度就成了最大的问题。理论上,这个本地时间是可以由恶意程序伪造的,所以这种方法被认为是“不安全的”。通行的做法是采用一个链外(off-chain)的第三方服务,比如这里使用的Oraclize,来获取随机数。因为Oraclize是一种公共基础服务,不会针对特定的合约“作假”,所以这可以认为是“相对安全的”。
因为使用Oraclize可以在链外生成随机数字,所以使用它来产生可信的数字被认为是一种很好的做法。 它实现了修饰符和一个回调函数,用于验证信息是否来自可信实体。
此智能合约的目的是参与随机抽奖,人们在1到9之间下注。当有10个人下注时,奖金会自动分配给赢家。每个用户都有一个最低下注金额。
每个玩家在每局游戏中只能下一次注,并且只有在参与者数量达到要求时才会产生赢家号码。
优秀特性
这个合约提供了一系列很好的功能性代码:
使用Oraclize生成安全的随机数并在回调中进行验证。
修改器检查游戏结束条件,阻止关键功能,直到奖励得以分配。
做了较多的检查来验证bet函数的使用是合适的。
只有在下注数达到最大条件时才安全地生成赢家号码。
3、对合约进行的攻击
为了检查合约的安全性,我们测试了多种攻击,以确保合约是安全的并遵循了最佳实践。
重入攻击(Reentrancy attack)
此攻击通过递归地调用ERC20代币中的 方法来提取合约中的以太币,如果用户在发送以太币之后才更新发送者的 (即账户余额,译者注)的话,攻击就会生效。
当你调用一个函数将以太币发送给合约时,你可以使用fallback函数再次执行该函数,直到以太币被从合约中提取出来。
由于该合约使用了 而不是 ,因此不存在重入攻击的风险;因为transfer函数只允许使用2300 gas,这只够用来产生事件日志数据并在失败时抛出异常。这样就无法递归调用发送者函数,从而避免了重入攻击。
因为transfer函数只会在每局游戏结束,向赢家分发奖励时才会被调用一次,所以重入式攻击在这里不会导致任何问题。
请注意,调用此函数的条件是投注次数大于或等于10次,但这个投注次数只有在 函数结束时才会被重置为0,这是有风险的;因为理论上是可以在投注次数被清零之前调用该函数并执行所有逻辑的。
所以我的建议是在函数开始时就更新条件、将投注次数设置为0,以确保 在被超出预期地多次调用时不会产生实际效果。
数值溢出(Over and under flows)
当一个 类型的变量值超出上限2**256(即2的256次方,译者注)时会发生溢出。其结果是变量值变为0,而不是更大。
例如,如果你想把一个unit类型的变量赋予大于2**256的值,它会简单地变为0,这是危险的。
另一方面,当你从0值中减去一个大于0的数字时,则会发生下溢出(underflow)。例如,如果你用0减去1,结果将是2**256,而不是-1。
在处理以太币的时候,这非常危险;然而在这个合约中并不存在减法操作,所以也不会有下溢出的风险。
唯一可能发生溢出的情况是当你调用 向某个数字下注时, 变量的值会相应增加:
有人可能会发送大量的以太币而导致累加结果超过2**256,这会使totalBet变为0。这当然是不大可能发生的,但风险是有的。
所以我推荐使用类似于[OpenZeppelin’s SafeMath.sol]这样的库。它可以使你的计算处理更安全,免去发生溢出(overflow或者underflow)的风险。
可以将其导入来使用,对uint256类型激活它,然后使用 、 、 和 这些函数。例如:
重放攻击(Replay attack)
重放攻击是指在像以太坊这样的区块链上发起一笔交易,而后在像以太坊经典这样的另一个链上重复这笔交易的攻击。(就是说在主链上创建一个交易之后,在分岔链上重复同样的交易。译者注。)
以太币会像普通的交易那样,从一个链转移到另一个链。
基于由Vitalik Buterin提出的EIP 155【https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md】,从Geth的1.5.3版本和Parity的1.4.4版本开始,已经增加了对这个攻击的防护。
译者注:
EIP,即Ethereum Improvement Proposal(以太坊改进建议),官方地址【https://github.com/ethereum/EIPs】是由以太坊社区所共同维护的以太坊平台标准规范文档,涵盖了基础协议规格说明、客户端API以及合约标准规范等等内容。
所以使用合约的用户们需要自己升级客户端程序来保证针对这个攻击的安全性。
重排攻击(Reordering attack)
这种攻击是指矿工或其他方试图通过将自己的信息插入列表(list)或映射(mapping)中来与智能合约参与者进行“竞争”,从而使攻击者有机会将自己的信息存储到合约中。
当一个用户使用 函数下注以后,因为实际的数据是存储在链上的,所以任何人都可以简单地通过调用公有状态变量 这个mapping看到所下注的数字。
这个mapping是用来表示每个人所选择的数字的,所以,结合交易数据,你就可以很容易地看到他们各自下注了多少以太币。这可能会发生在 函数中,因为它是在随机数生成处理的回调中被调用的。
因为这个函数起作用的条件在其结束之前才会被重置,所以这就有了重排攻击(reordering attack)的风险。
因此,我的建议就像我之前谈的那样:在 函数开始时就重置下注人数来避免其产生非预期的行为。
短地址攻击(Short address attack)
这种攻击是由Golem团队发现的针对ERC20代币的攻击:
一个用户创建一个空钱包,这并不难,它只是一串字符,例如:【0xiofa8d97756as7df5sd8f75g8675ds8gsdg0】
然后他使用把地址中的最后一个0去掉的地址来购买代币:也就是用【0xiofa8d97756as7df5sd8f75g8675ds8gsdg】作为收款地址来购买1000代币。
如果代币合约中有足够的余额,且购买代币的函数没有检查发送者地址的长度,以太坊虚拟机会在交易数据中补0,直到数据包长度满足要求
以太坊虚拟机会为每个1000代币的购买返回256000代币。这是一个虚拟机的bug,并且仍未被修复。所以如果你是一个代币合约的开发者,请确保对地址长度进行了检查。
但我们这个合约因为并不是ERC20代币合约,所以这种攻击并不能适用。
领取专属 10元无门槛券
私享最新 技术干货