研究报告【Fin

你有没有考虑过如何审计一个智能合约来找出安全漏洞?

你可以自己学习,或者你可以使用这份便利的一步步的指南来准确地知道在什么时候该做什么,并对合约进行审计。

我已经研究过很多智能合约的审计,并且我已经找到了从任何合约中提取所有重要信息的最常规步骤。

在本文中,你将会学到以下内容:

  • 生成对一个智能合约的完整审计报告所需的所有步骤。
  • 作为以太坊智能合约审计人员需要了解的最重要的攻击类型。
  • 应该在合约中寻找什么,和一些你不会在其他任何地方找到的有用的提示。

让我们直接开始审计合约吧:

如何审计一个智能合约

为了教会你如何进行审计,我会审计我自己写的一份合约。这样,你可以看到可以由你自行完成的真实世界的审计。

现在你也许会问:智能合约的审计到底是指什么?

智能合约审计就是仔细研究代码的过程,在这里就是指在把Solidity合约部署到以太坊主网络中并使用之前发现错误、漏洞和风险;因为一旦发布,这些代码将无法再被修改。这个定义仅仅是为了讨论目的。

请注意,审计不是验证代码安全的法律文件。没有人能100%确保代码不会在未来发生错误或产生漏洞。这仅仅是保证你的代码已被专家校订过,基本上是安全的。

讨论可能的改进,主要是为了找出那些可能会危害到用户的以太币的风险和漏洞。

好了,现在我们来看看一份智能合约审计报告的结构:

  1. 免责声明: 在这里你会说审计不是一个具有法律约束力的文件,它不保证任何东西。这只是一个讨论性质的文件。
  2. 审计概览和优良特性: 快速查看将被审计的智能合约并找到良好的实践。
  3. 对合约的攻击: 在本节中,你将讨论对合约的攻击以及会产生的结果。这只是为了验证它实际上是安全的。
  4. 合约中发现的严重漏洞: 可能严重损害合约完整性的关键问题。那些会允许攻击者窃取以太币的严重问题。
  5. 合约中发现的中等漏洞: 那些可能损害合约但危害有限的漏洞。比如一个允许人们修改随机变量的错误。
  6. 低严重性的漏洞: 这些问题并不会真正损害合约,并且可能已经存在于合约的已部署版本中。
  7. 逐行评注: 在这部分中,你将分析那些具有潜在改进可能的最重要的语句行。
  8. 审计总结: 你对合约的看法和关于审计的最终结论。

将这份结构说明保存在一个安全的地方,这是你安全地审计智能合约时需要做的全部内容。它将确实地帮助你找到那些难以发现的漏洞。

我建议你从第7点“逐行评注”开始,因为当逐行分析合约时,你会发现最重要的问题,你会看到缺少了什么,以及哪些地方应该修改或改进。

在后文中,我会给你展示一个免责声明,你可以把它作为审计的第一步。你可以从第1点开始看下去,直到结束。

接下来,我将向你展示使用这样的结构完成的审计结果,这是我针对我自己写的一个合约来做的。你还将在第3点中看到对于智能合约可能受到的最重要的攻击的介绍。

赌场合约审计

你可以在我的Github上看到审计的代码:https://github.com/merlox/casino-ethereum/blob/master/contracts/Casino.sol 对应的合约代码:

以下就是我的合约Casino.sol的审计报告:

序言

在这份智能合约审计报告中将包含以下内容:

  1. 免责声明
  2. 审计概览和优良特性
  3. 对合约的攻击
  4. 合约中发现的严重漏洞
  5. 合约中发现的中等漏洞
  6. 低严重性的漏洞
  7. 逐行评注
  8. 审计总结

1、免责声明

审计不会对代码的实用性、代码的安全性、商业模式的适用性、商业模式的监管制度或任何其他有关合约适用性的说明以及合约在无错状态的行为作出声明或担保。审计文档仅用于讨论目的。

2、概述

Casino.sol

该项目使用了一个中心化的服务实现了Oraclize API,来在区块链上生成真正的随机数字。

译者注: Oraclize是一种为智能合约和区块链应用提供数据的独立服务,官网:【http://www.oraclize.it】。因为类似于比特币脚本或者以太坊智能合约这样的区块链应用无法直接获取链外的数据,所以就需要一种可以提供链外数据并可以与区块链进行数据交互的服务。Oraclize可以提供类似于资产/财务应用程序中的价格信息、可用于点对点保险的天气信息或者对赌合约所需要的随机数信息。 这里是指在这个项目的源代码中引入了一个实现了Oraclize API的开源的Solidity代码库。

在区块链上生成随机数字是一个相当困难的课题,因为以太坊的核心价值之一就是可预测性,其目标是确保没有未定义的值。

译者注: 这里之所以说在区块链上生成随机数很困难,是因为,无论采用何种算法,都需要使用时间戳作为生成随机数的“种子”(因为时间戳是计算机领域内唯一可以理论上保证“不会重复”的数值);而在智能合约中取得时间戳只能依赖某个节点(矿工)来做到。这就是说,合约中取得的时间戳是由运行其代码的节点(矿工)的计算机本地时间决定的;所以这个节点(矿工)的可信度就成了最大的问题。理论上,这个本地时间是可以由恶意程序伪造的,所以这种方法被认为是“不安全的”。通行的做法是采用一个链外(off-chain)的第三方服务,比如这里使用的Oraclize,来获取随机数。因为Oraclize是一种公共基础服务,不会针对特定的合约“作假”,所以这可以认为是“相对安全的”。

因为使用Oraclize可以在链外生成随机数字,所以使用它来产生可信的数字被认为是一种很好的做法。 它实现了修饰符和一个回调函数,用于验证信息是否来自可信实体。

此智能合约的目的是参与随机抽奖,人们在1到9之间下注。当有10个人下注时,奖金会自动分配给赢家。每个用户都有一个最低下注金额。

每个玩家在每局游戏中只能下一次注,并且只有在参与者数量达到要求时才会产生赢家号码。

优秀特性

这个合约提供了一系列很好的功能性代码:

  • 使用Oraclize生成安全的随机数并在回调中进行验证。
  • 修改器检查游戏结束条件,阻止关键功能,直到奖励得以分配。
  • 做了较多的检查来验证bet函数的使用是合适的。
  • 只有在下注数达到最大条件时才安全地生成赢家号码。

3、对合约进行的攻击

为了检查合约的安全性,我们测试了多种攻击,以确保合约是安全的并遵循了最佳实践。

重入攻击(Reentrancy attack)
call.value()balance

当你调用一个函数将以太币发送给合约时,你可以使用fallback函数再次执行该函数,直到以太币被从合约中提取出来。

transfer()call.value()

因为transfer函数只会在每局游戏结束,向赢家分发奖励时才会被调用一次,所以重入式攻击在这里不会导致任何问题。

distributePrizes()
distributePrizes()
数值溢出(Over and under flows)
uint256

例如,如果你想把一个unit类型的变量赋予大于2**256的值,它会简单地变为0,这是危险的。

另一方面,当你从0值中减去一个大于0的数字时,则会发生下溢出(underflow)。例如,如果你用0减去1,结果将是2**256,而不是-1。

在处理以太币的时候,这非常危险;然而在这个合约中并不存在减法操作,所以也不会有下溢出的风险。

bet()totalBet

有人可能会发送大量的以太币而导致累加结果超过2**256,这会使totalBet变为0。这当然是不大可能发生的,但风险是有的。

所以我推荐使用类似于[OpenZeppelin’s SafeMath.sol]这样的库。它可以使你的计算处理更安全,免去发生溢出(overflow或者underflow)的风险。

.mul().add().sub().div()
重放攻击(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)中来与智能合约参与者进行“竞争”,从而使攻击者有机会将自己的信息存储到合约中。

bet()playerBetsNumber
distributePrizes()

因为这个函数起作用的条件在其结束之前才会被重置,所以这就有了重排攻击(reordering attack)的风险。

distributePrizes()
短地址攻击(Short address attack)

这种攻击是由Golem团队发现的针对ERC20代币的攻击:

  • 一个用户创建一个空钱包,这并不难,它只是一串字符,例如:【0xiofa8d97756as7df5sd8f75g8675ds8gsdg0】
  • 然后他使用把地址中的最后一个0去掉的地址来购买代币:也就是用【0xiofa8d97756as7df5sd8f75g8675ds8gsdg】作为收款地址来购买1000代币。
  • 如果代币合约中有足够的余额,且购买代币的函数没有检查发送者地址的长度,以太坊虚拟机会在交易数据中补0,直到数据包长度满足要求
  • 以太坊虚拟机会为每个1000代币的购买返回256000代币。这是一个虚拟机的bug,并且仍未被修复。所以如果你是一个代币合约的开发者,请确保对地址长度进行了检查。

但我们这个合约因为并不是ERC20代币合约,所以这种攻击并不能适用。

你可以参考这篇文章【http://vessenes.com/the-erc20-short-address-attack-explained/】来获得更多关于这种攻击的信息。

4、合约中发现的严重漏洞

审计中并未发现严重漏洞。

5、合约中发现的中等漏洞

checkPlayerExists()

应该把它改为常态函数来避免昂贵的消耗gas的执行。

checkPlayerExists()playerBetsNumberconstant

6、低严重性的漏洞

__callback()pay()assert()require()
assert()require()
  • 你定义了一个合约级别的变量players,但没有任何地方使用它。如果你不打算使用它,就把它删除。

7、逐行评注

0.4.11

这不是一个好实践。因为大版本的变化可能会使你的代码不稳定,所以我推荐使用一个固定的版本,比如‘0.4.11’。

uinttotalBettotalBetstotalBetplayercheckPlayerExists()constantconstant

即使函数默认是public类型,但显式地给函数指定类型仍然是一个好实践,它可以避免任何困惑。这里可以在这个函数声明的末尾确切地加上public声明。

playerrequire(player != address(0));bet()publicrequire()assert()
require()assert()require()
msg.valuegenerateNumberWinner()internal
internalprivate
oraclize_newRandomDSQuery()__callback()external
publicexternalpublicexternalpublic
assert()require()sha3()keccak256()distributePrizes()internal
generateNumberWinner()internalprivate
  • 第129行:尽管你在这里用了一个变长数组的大小来控制循环次数,但其实也没有多糟糕,因为获胜者的数量被限制为小于100。

8、审计总结

总体上讲,这个合约的代码有很好的注释,清晰地解释了每个函数的目的。

下注和分发奖励的机制非常简单,不会带来什么大问题。

assertrequirekeccak

这是一个安全的合约,可以在其运行期间保证资金安全。


结论

以上就是我使用我在开篇介绍过的结构所进行的审计。希望你确实学到了一些东西并且可以对其他智能合约进行安全审计了。

请继续学习合约安全知识、编码最佳实践以及其他实用知识,并努力提高。