以太坊智能合约攻击研究综述

  • 时间:
  • 浏览:70

  论文:Nicola Atzei , Massimo Bartoletti , Tiziana Cimoli

  A survey of attacks on Ethereum smart contracts - International Conference on Principles of Security & Trust - 2017

  智能合约是由节点组成的网络可以正确执行的计算机程序,不需要外部可信中介的授权。由于智能合约处理和转移的资产价值相当大,因此,除了正确执行它们之外,确保它们的实现不受窃取或篡改资产的攻击也是至关重要的。我们在Ethereum中研究了这个问题,Ethereum是迄今为止最著名和使用最多的智能契约框架。我们分析了Ethereum智能合约的安全漏洞,给出了常见程序的分类。

  比特币是一种去中心化的加密货币,自2009年问世以来,其市值已达到100亿美元。比特币的成功引发了业界和学术界的极大兴趣。简单来说,区块链是由P2P网络的节点维护的只支持追加数据的数据结构。加密货币使用区块链作为一个公共账本,在那里他们记录所有的货币转移。时至今日,区块链的使用范围不仅仅是比特币,它还涉及金融产品、服务行业、跟踪各种财产的所有权、数字身份验证、投票等各个产业。智能合约的框架有很多种,其中最出名的是以太坊。智能合约被正确执行是以太坊有效性的必要条件;否则,对手可以篡改合约执行,例如从合约参与者那里转移一些钱给攻击者自己。然而,仅仅保证执行的正确性并不足以保证智能合约的安全性。实际上,通过实际的开发经验和对Ethereum区块链上的所有合约的静态分析,我们已经发现了Ethereum智能契约中的几个安全漏洞。这些漏洞已经被一些黑客利用,其中最出名的一次攻击造成了6000万美元的财产损失。这也引发了人们对于区块链安全性的激烈讨论。

  导致智能合约在以太坊中的执行容易出错的原因有很多个。其中一个重要的原因与solidity(以太坊支持的高级编程语言)有关。Solidity的编程语法与程序员的直觉之间的不一致性导致了很多的漏洞。问题在于,尽管solid看起来类似javascript语言(除了异常和函数)。与此同时,该语言没有引入处理区域特异性方面的构造,例如,计算步骤被记录在公共区块链上,而在公共区块链上他们会bet365官方被故意的重新排序或者延迟执行。另一个含有漏洞合约依然再被扩散的主要原因是,很多关于漏洞的文档分布在不同的文档之中。

  我们的贡献是:本文首次系统地阐述了以太坊及其高级编程语言Solidity的安全漏洞。我们将造成漏洞的原因归类,目的有两方面:(i)作为智能合约开发者的参考,以了解和避免常见的陷阱;(ii)作为研究人员的指南,促进发展智能合同的分析和验证技术。对于分类法中的大多数漏洞原因,我们给出了一个实际的利用了这些漏洞进行攻击的例子(通常是在实际合约中进行的)。

  智能合约的编写:

  我们通过一个小例子(图1中的AWallet)来说明智能合约,它实现了一个与所有者关联的个人钱包。我们没有直接将其作为EVM字节码进行编程,而是使用了一种类似javascript的编程语言solidity,它可以编译为EVM字节码。直观地说,合约可以从其他用户接收以太币,它的所有者可以通过功能pay将一部分以太币发送给其他用户。一个哈希表outflow记录了它发送金钱到的所有地址,并将转账总额关联到每个地址。所有接收到的以太币都由合约保存。它的金额自动记录在余额中,这是一个特殊的变量,不能由程序员更改。

  合约由字段和函数组成。用户可以通过向Ethereum节点发送合适的事务来调用函数。交易必须包括给矿工的执行费用,并可能包括以太从调用方到合同的转账。Solidity也有例外的情况。当抛出异常时,无法捕获异常:执行停止,费用丢失,所有副作用(包括以太的转账)都被恢复。第5行中的函数AWallet是一个构造函数,在创建契约时只运行一次。函数pay将金额wei (1 wei =

  ether)从合同发送给收件人。在第8行,如果调用者(msg.sender)不是所有者,或者一些ether (msg.value)附加到调用并传输到契约,合约将抛出异常。因为异常会恢复副作用,所以这个以太会返回给调用者(但是调用者会损失执行费用)。在第9行,如果以太币数量不足,调用将终止;在这种情况下,不需要恢复异常状态。在第10行,契约更新outflow,然后将以太币传输给接收方。第11行中用于此目的的send函数出现了一些特殊限制,例如,如果收件人是合同,它可能会失败。

  执行费用:

  理想情况下,每个函数调用都由Ethereum网络中的所有矿工执行。调用函数的用户所支付的执行费用激励矿工进行此类工作。除了被用作奖励,执行费用还可以防止服务攻击,即对手试图通过请求耗时的计算来降低网络速度。需要指出的是,执行费用由gas和gas的单位价格决定的。大略地说,用户支付的费用越高,那么他的调用有越大的可能被矿工执行。

  挖矿过程:

  矿工的挖矿难度由很大因素构成。其中一个条件要求矿工解决一个中等难度的“工作证明”难题,这取决于前一个块和新块中的事务。谜题的难度是动态更新的,所以平均挖掘速度是每12秒挖矿1块。当一个采矿者解决了这个难题并向网络广播了一个新的有效块时,其他采矿者将放弃他们的尝试,通过添加新的块来更新他们的本地区块链副本,并在其上开始“挖矿”。解决这一难题的矿工将获得新区块交易费用的奖励。

  如果有两名(或两名以上)矿工几乎同时解决了这个难题,区块链将分叉为两个(或更多)分支,新块指向相同的父块。共识协议规定采矿者要扩展最长的分支。因此,即使两个分支都可以暂时继续存在,最终也会为最长的分支解析fork。只有其中的事务将成为区块链的一部分,而最短分支中的事务将被丢弃。在GHOST协议中,奖励将全部费用分配给最长分支中区块的采矿者,并将部分费用分配给开采废弃分支的根的采矿者。例如,假定块A和B有同样的父节点,一个矿工添加了一个新的节点在A上面。那么这个矿工可以贡献出一些奖励费用给B。

  编译Solidity为EVM二进制字节码:

  虽然契约以函数集的形式呈现,但是EVM字节码不支持函数。因此,solability编译器翻译合约,以便实现函数调度机制。更具体地说,每个函数都由一个签名唯一地标识,签名基于函数的名称和类型参数。在函数调用时,此签名作为输入传递给被调用的契约:如果它匹配某个函数,则执行跳转到相应的代码,否则将跳转到回调函数。回调函数是一个没有名称和参数的特殊函数,可以任意编程。回调函数也会在合同被传递一个空签名时执行:例如,当向合同发送以太币时。

  在本节中,我们将Ethereum智能合约的安全漏洞系统化。我们根据引入漏洞的级别将漏洞分为三类。此外,我们还通过一小段代码来说明每个漏洞在可靠性级别上的表现。所有这些漏洞都可以(实际上,大部分漏洞已经被利用)进行攻击,例如从合同中窃取资金。表1总结了我们的漏洞分类。

  Call to the unknown.

  在solidity中用于调用函数和传输以太币的一些原语可能具有调用被调用方/接收者的回调函数函数的副作用。我们将在下面对此进行说明。

  call调用一个函数(另一个合约的函数,或本身的函数),并将以太币传输给被调用方。例如,可以调用合同c的函数ping:

  其中,被调用的函数由其哈希签名的前4个字节标识,amount决定需要向c传输多少个wei, n是ping的实际参数。值得注意的是,如果在地址c处不存在具有给定签名的函数,那么将执行c的回退函数。

  send用于将以太币从正在运行的合同传输到某些接收者r,如r.send(amount)。传输以太币之后,send执行接收者的回退。其他与发送相关的漏洞将在“exception disorders”和“gasless send”。delegatecall与call非常相似,不同之处在于被调用函数的调用是在调用者环境中运行的。例如,执行c.delegatecall(bytes4(sha3("ping(uint256)"),n),如果ping包含变量this,它指的是调用者的地址而不是c,如果以太通过d.send(amount)传输给某个接收者d,以太从调用者余额中取出。 除了上述基本类型外,还可以使用以下直接调用:

  第一行声明Alice的合约接口,最后两行包含Bob的合约:其中,pong通过直接调用调用Alice的ping。现在,如果程序员输入错误了contract Alice的接口(例如,通过声明参数的类型为int而不是uint),并且Alice没有具有该签名的函数,那么对ping的调用实际上会导致对Alice回调函数的调用。

  Exception disorder.

  在Solidity中,有几种可能引发异常的情况,例如(i)执行耗尽气体;(ii)调用栈达到极限;(iii)执行命令抛出。然而,Solidity在处理异常的方式上并不一致:有两种不同的行为,这取决于合约如何调用彼此。例如,考虑:

  现在,假设某个用户调用Bob的pong, Alice的ping抛出一个异常。之后,执行会停止,并且整个事务的副作用会被回退。因此,字段x在事务之后包含0。现在,假设Bob通过call调用ping。在这种情况下,只有调用的副作用被恢复,call返回false,执行继续。因此,x在调用之后的值为2。

  更一般地,假设抛出异常时存在一个嵌套调用链。然后,异常处理如下:

  如果链中的每个元素都是直接调用,那么执行就会停止,所有副作用(包括以太币的转账)都会恢复。此外,原始事务分配的所有gas均被消耗;如果链中的至少一个元素是一个调用(案例delegatecall和send是类似的),那么异常将沿着链传播,恢复被调用合约中的所有副作用,直到它到达一个调用。从那时起,执行将继续,调用返回false。此外,调用分配的所有gas都被消耗掉了。

  异常处理方式的不规范可能会影响合约的安全性。例如,仅仅因为没有异常就相信以太币的传输是成功的可能会导致攻击。分析显示,~28%的契约并不控制call/send调用的返回值。

  Gasless send.

  当使用send函数将以太币传输到一个合约时,可能会导致一个out-of-gas异常。我们通过一个小例子来说明发送行为,包括合同C通过函数支付发送以太币,以及两个收件人D1、D2。

  执行pay有三种可能的情况:

  - n

  0, d = D1。send失败,原因是out-of-gas异常,2300个单位的gas不足以执行更新状态D1的回调函数

  - n

  0, d = D2。send成功.

  - n = 0, d∈{D1, D2}。对于编译器版本< 0.4.0, send会失败,出现out-of-gas异常,因为该气体不足以执行任何回调函数,甚至不能执行空回调函数。对于≥0.4.0的编译器版本,无论是d = D1还是d = D2,其行为都与前两种情况相同。

  Type casts.

  再次考虑如下情况:

  pong的签名通知编译器c符合接口Alice。但是,编译器只检查接口是否声明了函数ping,而没有检查:(i) c是契约Alice的地址;(ii) Bob声明的接口与Alice的实际接口相匹配。类似的情况也发生在显式类型转换中。实际上,直接调用是在用于编译调用的EVM字节码指令中编译的(除了异常管理)。因此,如果类型不匹配,在运行时可能会发生三种不同的情况:

  如果c不是合同地址,调用返回而不执行任何代码。如果c是具有与Alice 's ping相同签名的函数的任何合约的地址,则执行该函数。如果c是一个没有匹配Alice ping签名的函数的合约,那么执行c的回调函数。

  在很多情况下,没有错误抛出,调用者无法察觉异常的产生。

  Reentrancy.

  事务的原子性和顺序性可能会使程序员相信,当调用非递归函数时,它不能在终止之前重新输入。然而,情况并非总是如此,因为回调机制可能允许攻击者重新输入调用方函数。这可能导致意想不到的行为,也可能导致循环调用,最终消耗所有的气体。例如,假设契约

  Bob已经在区块链上了,当攻击者发布Mallory契约时:

  Bob中的ping函数使用一个空签名且没有gas限制的调用,将2 wei发送到某个地址c。现在,假设已经使用Mallory的地址调用了ping。如前所述,call具有调用Mallory的回调函数的副作用,而回调函数又调用ping。由于变量sent尚未被设置为true, Bob再次将2wei发送给Mallory,并再次调用她的回滚,从而开始循环。此循环将在执行最终耗尽资源或达到堆栈限制时结束。

  Keeping secrets.

  合约中的字段可以是公共的,即每个人都可以直接读取,也可以是私有的,即其他用户/合约不能直接读取。尽管如此,宣布字段为私有并不能保证其保密性。这是因为,要设置字段的值,用户必须向采矿者发送一个合适的事务,采矿者将在区块链上发布它。因为区块链是公共的,所以每个人都可以检查事务的内容,并推断字段的新值。为了确保字段在特定事件发生之前保持机密,合约必须使用适当的加密技术。

  Immutable bugs.

  如果一个合同包含一个bug,就没有直接的方法来修补它。因此,程序员必须预期在实现时更改或终止合同的方法——尽管这与Ethereum的原则的一致性存在争议.

  Ether lost in transfer.

  发送以太币时,必须指定接收地址,它的形式是160 bit的序列。然而,这些地址中有许多是孤立的,也就是说,它们不与任何用户或合约相关联。如果某个以太币被发送到一个孤立地址,它将永远丢失(注意,没有方法检测一个地址是否是孤立地址)。由于丢失的以太无法恢复,程序员必须手动确保接收地址的正确性。

  Stack size limit.

  每当合约调用另一个约(甚至是通过this.f()调用自己)时,与事务关联的调用堆栈将增长一帧。调用堆栈被限制到1024帧:当达到此限制时,进一步的调用将抛出异常。

  Unpredictable state.

  合约的状态由其字段的值和余额决定。通常,当用户向网络发送事务以调用某个合约时,他不能确保事务将在发送该事务时合约的相同状态下运行。

  在某些情况下,不知道事务将在何处运行可能会导致漏洞。例如,在调用可动态更新的合约时就是这种情况。

  Generating randomness.

  EVM字节码的执行是确定的:在没有不当行为的情况下,所有执行事务的矿工将得到相同结果。因此,为了模拟非确定性的选择,许多合约(例如彩票、游戏等)生成伪随机数,其中初始化种子是为所有矿工唯一选择的。然而,由于矿工控制哪些事务被放在一个块中,以及按照什么顺序放置,恶意矿工可以尝试构造自己的块,从而对伪随机数的结果产生控制。

  Time constraints.

  合约可以检索块被挖掘的时间戳;一个块中的所有事务共享相同的时间戳。这保证了执行后与合约状态的一致性,但也可能使合约暴露于攻击之下,因为创建新块的矿工可以选择具有一定任意性的时间戳。如果一个矿业公司持有一个合同的股份,他可以通过为他正在开采的区块选择合适的时间戳来获得优势。

  现在我们举例说明一些利用本节中提供的漏洞进行攻击的攻击——其中许多攻击的灵感来自真实的用例

  The DAO attack.

  我们提供了DAO的简化版本,它与原始版本有一些相同的漏洞。然后我们展示了利用它们的两种攻击。

  SimpleDAO允许参与者根据自己的选择捐赠以太币来资助合同。然后合同可以收回他们的投资。

  攻击1:这种攻击与实际DAO上使用的攻击类似,允许对手从SimpleDAO窃取所有的以太币。攻击的第一步是发布合同Mallory。

  后,对手为Mallory捐献了一些以太币,并调用了马洛里的回调函数。回退函数调用withdraw,它将以太币传输到Mallory。现在,用于此目的的函数调用具有再次调用Mallory回退的副作用(第5行),它恶意地回调withdraw。直到gas被消耗完、栈溢出或者DAO的账户余额变为0。

  攻击2:同样,我们的第二次攻击允许对手从SimpleDAO窃取所有的以太币,但是它只需要调用两次回退函数。第一步是发布Mallory2,为它提供少量以太币(例如,1 wei)。然后,对手发动攻击,捐出1wei给自己,然后收回。该函数提取用户信用是否足够的检查,如果足够,则将以太传输到Mallory2。

  与前面的攻击一样,call调用Mallory2的回调函数,后者反过来调用back withdraw。由此DAO会第二次发送1 wei给Mallory2。然而,这一次回调函数什么也不做,嵌套调用开始关闭。其结果是,Mallory2的信用被更新了两次:第一次更新为零,第二次更新为(

  - 1)wei。

  King of the Ether Throne.

  “King of the Ether Throne”是一款玩家为获得“King of the Ether Throne”称号而竞争的游戏。如果有人想成为国王,他必须向现任国王支付一定数额的以太币,并向合同支付一小笔费用。成为国王的奖赏单调地增加。我们讨论了一个简化版的游戏(具有相同的漏洞),实现为合同KotET:

  合同看上去似乎是合理的:实际上并非如此,因为不检查send的返回代码可能导致窃取以太币。事实上,由于send配备了一些gas,如果国王的地址是具有昂贵回调函数的合同地址,那么第17行上的send将失败。在这种情况下,由于send不传播异常,补偿由合同保留。

  Multi-player games.

  考虑一个合同,它实现了两个玩家之间简单的“赔率和平手”游戏。每个玩家选择一个数字:如果总和是偶数,第一个玩家获胜,否则第二个玩家获胜。

  合约记录了两名场上队员的赌注。由于该字段是私有的,其他合约不能直接读取它。要加入游戏,每个玩家在调用函数play时必须传输1以太币。如果传输的数量不同,则通过抛出异常将其发送回玩家。

  对手可以进行攻击,这总是让她赢得比赛。为此,对手模拟第二个玩家,并等待第一个玩家下注。现在,虽然场上的玩家是私有的,但是对手可以通过查看第一个玩家加入游戏的区块链交易来推断他的赌注。然后,对手可以通过调用适当的赌注来赢得比赛。这个攻击利用了”keeping secrets”这个漏洞。

  Rubixi.

  Rubixi是一种实施庞氏骗局的合约,庞氏骗局是一种欺诈性的高收益投资项目,参与者从新来者的投资中获利。此外,合同所有人可以收取一些费用,在投资时支付给合同。下面的攻击允许对手从契约中窃取一些以太币,利用“immutable bugs”漏洞。

  在合同的发展过程中,它的名字从DynamicPyramid变成了Rubixi。但是,程序员忘记了相应地更改构造函数的名称,这样构造函数就变成了任何人都可以调用的函数(相反,构造函数在创建合约时只运行一次)。动态金字塔函数设置所有者地址;业主可以通过收取费用收回利润。

  GovernMental.

  GovernMental是另一个有缺陷的庞氏骗局。参加计划的人士必须在合约内加入一定量的以太币。如果12小时内没有人参加这个计划,最后一个参与者将获得合同中所有的以太币(业主保留的费用除外)。参与者列表及其信用记录存储在两个数组中。当12小时结束时,最后一名参加者可以领取款项,数值声明如下:

  我们现在给出了Governmental的一个简化版本,它与原始合约有一些相同的漏洞:

  攻击1:这种攻击利用了“exception disorder”和“stack size limit”漏洞。

  攻击者的目标是不付钱给获胜者,这样以太币就可以按照合同保存,并在以后的时间由拥有者赎回。为了实现这个目标,所有者必须使第24行send失败。他的第一步是公布以下合同:

  然后,所有者调用Mallory的攻击,该攻击开始递归地调用自己,使堆栈增长。当调用堆栈的深度达到1022时,Mallory调用Governmental的resetInvestment,然后以堆栈大小1023执行。此时,由于调用堆栈限制(第二个发送也失败),第24行上的发送失败。由于政府没有检查发送的返回代码,执行继续,重置合同状态,然后开始另一轮。每次运行此攻击时,合约的余额都会增加,因为合法的赢家没有得到报酬。

  攻击2:在本例中,攻击者是一个矿工,他还模拟一个玩家。作为一名矿工,她可以选择不把指向政府的交易包括在区块中,除非是她自己的交易,以便在这轮交易中成为最后一名参与者。此外,攻击者可以重新排序事务,使她的事务首先出现:实际上,通过先玩并选择合适数量的以太进行投资,她可以阻止其他玩家加入该方案(第14行),从而导致最bet365后一个玩家出现在轮中。这种攻击利用了“不可预测状态”漏洞。

  攻击3:同样在这种情况下,攻击者是一个模拟玩家的矿工。假设攻击者设法加入了该方案。为了在一分钟内成为最后一名选手,她可以利用时间戳的漏洞。更具体地说,攻击者设置新块的时间戳,使其至少在一分钟后成为当前块的时间戳。正如我们在讨论“time constraints”漏洞时所讨论的,对时间戳的选择有一个容忍度。如果攻击者设法发布带有延迟时间戳的新块,那么她将是轮中的最后一个玩家。

  Dynamic libraries.

  现在我们考虑一个合约,它可以动态地更新它的一个组件,这是一个集合上的操作库。因此,如果开发了这些操作的更有效的实现,或者修复了错误,则合约可以使用库的新版本。

  契约SetProvider的所有者可以使用函数updateLibrary用一个新的库地址替换库地址。任何用户都可以通过getSet获取库的地址。库集实现了一些基本的集操作。库是特殊的合约,例如不能有可变字段。当用户声明一个接口是一个库时,通过delegate atecall直接调用它的任何函数。标记为存储的参数通过引用传递。

  假设Bob是SetProvider诚实用户的合约。特别是,Bob通过getSetVersion查询库版本:

  现在,假设setProvider的所有者也是一个攻击者。她可以这样攻击Bob,目的是窃取他所有的以太币。在攻击的第一步中,对手发布一个新的库MaliciousSet,然后调用SetProvider的updateLibrary函数使其指向MaliciousSet。

  注意MaliciousSet在第4行执行一个sned操作,将以太币转账给攻击者。由于Bob已经将接口集声明为一个库,所以对version的任何直接调用都将作为delegatecall实现,从而在Bob的环境中执行。因此,这个。第4行发送中的余额实际上是Bob的余额,导致发送将所有以太币发送给对手。之后,函数将正确返回版本号。

  整理了目前以太坊中智能合约的主要漏洞类型结合实例对这些漏洞所可能产生的攻击进行了剖析系统化的总结了智能合约漏洞的产生原因和攻击方式

  本文由bet365官方南京大学软件学院2016级本科生徐光耀翻译转述


bet365官方 bet365

猜你喜欢