文章路径
参考链接1
参考链接2
参考链接3
Cryptocurrencies record transactions in a decentralized data structure called a blockchain. Two of the most popular cryptocurrencies, Bitcoin and Ethereum, support the feature to encode rules or scripts for processing transactions. This feature has evolved to give practical shape to the ideas of smart contracts, or full-fledged programs that are run on blockchains. Recently, Ethereum’s smart contract system has seen steady adoption, supporting tens of thousands of contracts, holding millions dollars worth of virtual coins.
In this paper, we investigate the security of running smart contracts based on Ethereum in an open distributed network like those of cryptocurrencies. We introduce several new security problems in which an adversary can manipulate smart contract execution to gain profit. These bugs suggest subtle gaps in the understanding of the distributed semantics of the underlying platform. As a refinement, we propose ways to enhance the operational semantics of Ethereum to make contracts less vulnerable. For developers writing contracts for the existing Ethereum system, we build a symbolic execution tool called Oyente to find potential security bugs. Among 19, 366 existing Ethereum contracts, Oyente flags 8, 833 of them as vulnerable, including the TheDAO bug which led to a 60 million US dollar loss in June 2016. We also discuss the severity of other attacks for several case studies which have source code available and confirm the attacks (which target only our accounts) in the main Ethereum network.
加密货币在称为区块链的分散数据结构中记录交易。最受欢迎的两种加密货币:比特币和以太坊,都支持对规则或脚本进行编码以处理交易。该功能已经发展到可以切实地塑造智能合约或在区块链上运行的全功能程序的想法。最近,以太坊的智能合约系统得到了稳定的采用,支持成千上万份合约,持有价值数百万美元的虚拟币。
在本文中,我们研究了在类似加密货币的开放式分布式网络中基于以太坊运行智能合约的安全性。我们介绍了几个新的安全性问题,在这些问题中,对手可以操纵智能合约的执行以获取利润。这些错误表明在理解底层平台的分布式语义方面存在细微的差距。作为改进,我们提出了增强以太坊操作语义的方法,以减少合同的脆弱性。对于为现有的以太坊系统写智能合约的开发人员,我们构建了一个名为Oyente的符号执行工具来发现潜在的安全漏洞。在19366份现有以太坊合同中,Oyente标记其中8833份为易受攻击的合同,其中包括TheDAO错误,该错误导致2016年6月损失了6000万美元。我们还将在几个案例研究中讨论其他攻击的严重性,这些案例研究都提供了源代码,并确认了主要以太坊网络中的攻击(仅针对我们的帐户)。
智能合约(或简称合约)是存储在区块链中的“自治代理”,编码为将合约引入区块链的“创造”交易的一部分。成功创建后,智能合约将通过合约地址进行标识; 每个合约都持有一定数量的虚拟币(以太币),具有自己的私有存储,并与其预定义的可执行代码相关联。
合约状态包含两个主要部分:(1)私有存储;(2)Ether币数量,即余额。
以太坊合约的代码是基于底层的堆栈字节码语言,称为以太坊虚拟机(EVM)代码。 用户使用高级编程语言(例如Solidity [16](类似于JavaScript的语言))定义合同,然后将其编译为EVM代码。 为了在地址 γ处调用合约,用户将交易发送到合约地址,交易包括:(1)给合约支付执行费用(以太币);(2)调用输入数据
图3举例:
首先,为新合同准备一个唯一的地址。 然后,通过运行构造函数(即第8行中的Puzzle()函数)来分配和初始化合同的私有存储。 最后,对应于匿名函数(从第15行开始)的可执行EVM代码部分与合同相关联。
为了确保公平地补偿计算工作量,以太坊向矿工支付了与所需计算成比例的费用。具体来说,以太坊字节码中的每条指令都有预先指定的气体量。当用户发送交易以调用合同时,她必须指定愿意为执行提供多少天然气gas(称为gasLimit)以及每个gas单位的价格(称为gasPrice)。随后,将交易包含在其提议的区块中的矿工随后会收到交易费用,该费用对应于执行实际的gas量乘以gasPrice。如果某些执行需要比gasLimit更多的gas,则执行将终止,并发生异常,状态σ会恢复为初始状态,就好像未执行该执行一样。在这种中止的情况下,作为对付资源枯竭攻击的对策,发送方仍然必须将全部gasLimit支付给矿工[6]。
交易顺序依赖性:一个块,包含了一组交易,在每个时期区块链状态都会更新好几次。假设这样一个场景:有两个交易同时调用同一个合约,当用户执行个人调用时,她们不确定调用的是哪一状态的智能合约。
因为只有开采该区块的矿工才能决定这些交易的顺序,从而决定订单更新的顺序。结果,合同的最终状态取决于矿工如何矿工如何命令调用它的事务。我们称此类合约为交易顺序依赖(或TOD)合约。
对于读者而言,可能并不明显,为什么依赖交易订单对智能合约会造成问题。 原因是双重的。 首先,如果存在并发调用,即使对合同的良性调用也可能对用户产生意外结果。 其次,恶意用户可以利用TOD合同获得更多利润,甚至窃取用户的钱。 我们通过使用图3中的Puzzle合同解释以下两种情况。
很多合约的执行逻辑是和当前block的时间戳有关的。而一个block的时间戳是由矿工(挖矿时的系统)决定的,并且允许有。但是,这里时间可以允许有900秒的偏移
合同可能遇到的下一个安全问题是使用块时间戳作为触发条件来执行一些关键操作,例如汇款。我们称此类合同为时间戳相关合同。
时间戳相关合同的一个很好的例子是图5中的theRun合同,该合同使用本地随机数生成器来确定谁赢得了头奖[22]。从技术上讲,TheRun使用某个先前区块的哈希作为随机种子来选择获胜者(第9-10行)。根据当前块时间戳(第5-7行)确定块的选择。
让我们回想一下,在挖掘一个块时,矿工必须设置该块的时间戳(图2)。通常,时间戳记设置为矿工本地系统的当前时间。但是,矿工可以将此值改变大约900秒,同时仍让其他矿工接受该块[23]。具体来说,在接收到一个新块并检查其他有效性检查之后,矿工将检查块时间戳是否大于时间戳。上一个区块的时间,距离本地系统上的时间戳记不超过900秒。因此,对手可以选择不同的组时间戳来操纵与时间戳相关的合同的结果。
矿工可以将块时间戳设置为特定值,这会影响与时间戳相关的条件的值,并有利于矿工。例如,在theRun合约中,已知前一个区块的哈希值和区块编号,还知道其他合约变量(如last payout),这些变量有助于生成随机种子。因此,矿工可以预先计算并选择时间戳,以便函数随机产生有利于他的结果。结果,对手可能会将随机种子的结果完全偏向任何值,从而将头奖奖励给他喜欢的任何玩家。因此,Run易受任何能够操纵阻止时间戳的对手的攻击。
尽管Run运行使用时间戳作为不安全的确定性随机种子,但其他协定使用块时间戳作为全局时间戳并执行一些与时间相关的计算。我们在第6节中展示了一个示例,该示例为此目的使用时间戳,并且容易受到操纵。不幸的是,还有许多其他与时间相关的合同。正如我们在第6节中显示的那样,在前19个366个合同中,有83个依赖于块时间戳将以太坊转移到不同的地址。
在以太坊中,合约有多种方法可以调用另一个合约,例如,通过send指令或直接调用合约的函数(例如aContract.someFunction())。如果被调用方合同中引发了异常(例如,电量不足,超出了调用堆栈限制),则被调用方合同终止,恢复其状态并返回false。但是,根据调用的方式,被调用方合同中的异常可能会传播也可能不会传播给调用者。例如,如果调用是通过send指令进行的,则调用者合同应显式检查返回值以验证调用是否已正确执行。这种不一致的异常传播策略导致许多情况下,异常处理不当。如我们稍后在第6节中所述,通过send调用其他合同后,27.9%的合同不检查返回值。我们通过图6中的示例讨论了不验证合同调用返回值的威胁,这是真实合同的代码片段[12]。
图6中的KingOfTheEtherThrone(简称KoET)合同允许用户通过支付当前国王所需的一定数量的以太币来宣称自己是“以太王”。国王从他支付给前一个国王的价格和另一个人支付给他钱成为下一个继任者之间的差价赚取收益。当用户索取王位时,合同将赔偿金发送给割让的国王(第15行),并将用户分配为新国王(第18-20行)。
在分配新国王之前,KoET合同不会在第15行中检查补偿交易的结果。因此,如果由于某种原因补偿交易未能正确完成,则现任国王将失去王位而没有任何补偿。实际上,发生了此类问题并导致KoET终止[12]。 [12]中报告的原因是,当前国王的地址Ak不是正常地址,而是合同地址。将交易(或呼叫)发送到Ak时,将执行一些代码,因此比正常地址的交易需要更多的gas。但是,KoET不知道Ak内部预先执行什么操作来决定在send指令中提供多少气体。因此,Ak用尽了gas并引发异常。结果,Ak的状态(和余额)保持不变,补偿金返还给KoET,现任国王失去了王位而没有任何补偿。
当合同将钱汇到动态地址时,通常会出现上述问题,因为发件人不知道要为交易分配多少gas。在执行后续逻辑之前,合同应始终检查交易是否成功。被调用方合同可以抛出任何类型的异常(例如,被零除,数组索引超出范围等)。
在这种情况下,收件人(或被调用方)似乎有故障,导致send操作失败。但是,如下所示,调用主叫方合同的恶意用户可能会故意导致send操作失败,无论被叫方做什么。
故意超过调用堆栈的深度限制。
以太坊虚拟机的实现将调用堆栈的深度限制为1024帧。 如果合同通过send或call指令调用另一个协议,则调用堆栈的深度会增加一。 这打开了一个攻击媒介,故意使KoET合同第15行中的发送指令失败。 具体来说,攻击者可以准备合同以将其自身调用1023次,然后再向KoET发送交易以要求从当前国王那里获得王位。 因此,攻击者确保KoET的调用堆栈深度达到1024,从而导致第15行中的发送指令失败。 结果,现任国王将不会获得任何付款。 先前的报告[24]中已确定了该调用栈问题,但在当前的以太坊协议中仍未解决。
利用调用堆栈限制获得收益。
在先前对GET的攻击中,攻击者除了使其他用户失去其应享的权利外,没有获得任何直接的收益。 但是,在许多其他示例中,攻击者也可以利用直接受益。 在以太坊中,许多合同实施验证了庞氏骗局(或金字塔)计划[22,25,26]。 这些合同从其他人的后续投资中向投资者支付投资利息。 攻击者可以投资自己的钱,使得给先前投资者付款的支付操作失败,因此他可以及早获得利息。 我们将在第6节中讨论其中一种合同。具体而言,我们表明5 411个合同(占27.9%)容易受到故意超过 call-depth限制的攻击。 我们还通过以太坊最受欢迎的合约之一进行攻击(对其他人没有伤害,但对我们自己的帐户无害),以证实我们的发现。
重入是著名的漏洞,最近TheDao黑客[19]攻击者利用该重入漏洞在攻击发生时窃取了360万以太币或6000万美元。为了完整起见,我们将在此处讨论该漏洞,稍后我们将在第5节中介绍如何在我们的工具Oyente中实施重入检测。
在以太坊中,当一个合约调用另一个合约时,当前执行将等待调用(call)结束。当调用的接收者利用调用者者所处的中间状态时,这可能会导致问题。如果不考虑被调用方可能存在的恶意行为,则在编写合同时可能不会立即察觉。
攻击示例。我们可以在sendBalance中看到这种攻击的示例(图7)。 sendBalance调用的合同可以简单地再次调用它,因为该合同的余额尚未设置为零。在图7中,第15行将提取者的当前余额(如内部变量userBalances所示)发送到希望提取其余额的合同地址。但是,变量userBalances只会在调用后清零,这意味着记录用户余额的合同的持久存储尚未更改。第15行中的呼叫的被调用方合同使用其默认函数,可以调用withdrawBalance,直到该合约以太币耗尽(或耗尽可用gas),以先到者为准。
重入攻击详细举例1
重入攻击详细举例2
我们在第4.1节中为Ethereum形式化了一个“轻量级”语义,然后在第4.2节中以这种形式为基础,为第3节中确定的安全问题提供解决方案。尽管它是轻量级的,但我们的形式主义严格地捕捉了Ethereum的有趣特性,揭示了它语义上的微妙之处,这使我们能够更精确地陈述我们提出的解决方案。
Formation and Validation of a Block.块的形成和验证
Transaction Execution:
一个交易可以激活一份合约的代码执行。 在以太坊中,这种执行可以获得三种类型的空间来存储数据:
(1)后进先出堆栈s;
(2)辅助存储器l,无限扩展的数组;
(3)合同的长期存储地址str,它是给定合同地址id的σ[id]的一部分。 与堆栈和辅助存储器不同(后者在计算结束后会复位),而它作为σ的一部分长期存储。
尽管以太坊中的交易是一个复杂的结构,并指定了多个字段,但我们将其抽象为一个三元⟨id,v,l⟩,其中id是要调用的合约的标识符,v是 存入合同的值,l是捕获输入参数值的数据数组。 因此,可以使用图9中的规则对事务执行进行建模:第一个规则描述了成功终止(或“正常停止”)的执行,而第二个规则描述了异常终止的执行。
请注意,事务的执行旨在遵循“交易语义”,其两个重要属性是:(1)原子性,要求每个交易“全部或全部”。 换句话说,如果事务的一部分失败,那么整个事务都会失败,并且状态保持不变; (2)一致性,确保任何交易都会使系统从一种有效状态进入另一种有效状态。 当我们讨论EVM指令的操作语义时,我们将在本节的后面部分展示如何违反这些属性。
EVM执行指令
push指令接受一个参数v∈value,它可以是数字常量z、代码标签λ、内存地址α或压缩/接收地址γ,并将其添加到“操作数堆栈”的顶部。
pop指令删除(忘记)操作数堆栈的顶部元素。
op指令表示所有算术和逻辑等操作,弹出其参数,执行操作,并推送结果。
条件分支bne是标准的“不等于零的分支”。它从操作数堆栈的顶部弹出两个元素z和λ;如果z为非零,则程序计数器设置为λ,否则程序计数器递增。加载和存储指令分别以原始方式读取和写入内存。但是,这里有两种类型的加载和存储,处理上面提到的两种类型的内存。
mload和mstore分别处理辅助存储器l,sload和sstore分别评估和更新合约存储str,即契约的状态。
现在让我们讨论更多以太坊启发的有趣指令。 关键指令是call和return,其操作语义在表1中提供。每一行都描述了执行可以从配置μ移到配置μ’的条件。 第一列表示该规则捕获的指令形式。 如果要执行的指令与该格式匹配并且满足第二列中的所有(附带)条件,则可以从与第三列中的模式匹配的配置到与最后一列中的模式匹配的配置进行步骤 。
call指令大致类似于远程过程调用。 放置在操作数堆栈上的参数是:
destination、amount of Ether to transfer、以及两个值st和sz(用于“起始地址”和“大小”)以指定一片内存,其中包含附加的特定于函数的参数 。类似地,操作数堆栈中的后两个值为返回值指定一个位置。 当调用返回或发生异常时,它们将暴露(在规则中)。 与没有固定最大大小的操作数堆栈不同,调用堆栈的最大大小为1024。如果调用堆栈已满,则远程调用将导致异常(call的第二条规则)。 当远程调用返回时,在操作数堆栈上放置一个特殊标志,其中1表示成功调用(第二条返回规则),0表示未指定的异常(规则EXC)。
有两点需要注意。 首先,被调用方的异常不限于(调用)堆栈溢出。 这可能是由于各种原因造成的,例如gas耗尽,被零除等。第二,异常不会自动传播。 希望这样做的合同编写者必须显式检查0,然后引发新的异常,通常是跳转到无效的标签。 对于Solidity中的某些高级命令,编译器将插入执行这些步骤的代码段。
安全问题:回顾第3.3节中讨论的安全问题,特别是异常处理不当时。问题的根本原因在于异常如何影响最终状态的不一致性,这取决于合同方法是作为事务调用的,还是通过调用指令调用的。在前一种情况下,图9中的rule TX exception将中止执行;而在后一种情况下,表1中的row exc 将标志0推入调用方的操作数堆栈。异常在被调用者处发生并转换为标志0(并且执行正常继续)的方式确实破坏了原子性属性。换句话说,以太坊事务在语义上没有原子性。这可能会导致严重的后果,特别是考虑到以太坊中的资金转账大多是使用call指令完成的。
suicide指令将所有剩余的以太币转移给接收者γ,然后终止合同。 尽管与转移以太币的call有些类似,但它不使用call 堆栈。
create指令创建一个新的合约账户,并从操作数堆栈中获取三个参数:1)要放入新合约中的以太币的数量,2)以及两个值以指定一个内存片,其中包含新合约的字节码。 它分三个步骤进行:
1.创建一个新地址并为新合同分配存储空间。 指定量的以太币也将被存入合同。
2.初始化合同的存储。
3.将关联的代码主体存入合同。
如果合同创建成功,则将新合同的地址压入操作数堆栈; 否则,将标记值0推送。 上述三个步骤依赖于某些辅助程序,我们将不会尝试用我们的体系来捕获这些辅助程序。注意:(1)在执行初始化代码时,新创建的地址存在,但没有内部主体代码; (2)如果初始化以异常结束,则状态留给“僵尸”帐户,并且任何剩余余额将永远锁定在该帐户中。 换句话说,不成功的合同创建可能导致存在于系统中的无效合同,从而破坏“交易语义”的一致性属性。 这个问题可能不会直接导致某些安全攻击,这在以太坊当前的设计中显然是不可取的。
getstate是一个抽象指令,其中与3.2中的安全性问题相关的具体实例将获取当前块的时间戳。 getstate指令通常将某些“特殊”值压入堆栈:特别是current timestamp,current timestamp,current timestamp,current balance以及this contract’s own address。 请注意,其中一些值应被视为常量(例如current timestamp, block id),而其他值将随着交易的执行而同时更新(remaining gas, current balance)。
回想一下,TOD契约是易受攻击的,因为用户不确定在执行事务时契约将处于哪个状态。这似乎是不可避免的,因为矿工可以在事务之间设置任意顺序(规则建议)。 为了消除TOD问题,即使选定交易之间存在固有的不确定性顺序,我们也必须保证合同代码的调用会返回预期的输出或失败。
保证条件:我们的交易执行新规则如图10所示。交易T现在另外指定了保护条件g; 当前状态σ需要满足g才能执行T。 如果不满足g,则通过新规则TX-stale删除事务。 对于不提供g的交易,我们考虑g≡true。 该解决方案保证了发送方获得了预期的输出或交易失败。 该解决方案也向后兼容,因为我们不需要更改现有合同代码:旧交易可以简单地将默认保护条件设为true。
为了说明这一点,让我们重新查看第3.1节中的“Puzzle”合约。 用户提交交易Tu以要求奖励的用户应指定条件g≡(奖励== R),其中R是合同中存储的当前奖励。 如果首先执行所有者的交易,则g无效,用户的交易Tu将中止,这意味着所有者将无法获得解决方案。 请注意,Puzzle只是分散交易市场或市场中更为严肃的一类合同的一个示例(请参阅第3.1节)。 通过我们的解决方案,买家可以轻松避免支付比发出购买订单时观察到的价格高得多的价格。
请注意,“受保护的交易”类似于大多数现代处理器支持的“比较和交换”(CAS)指令。 CAS是关键的标准多线程同步原语,“受保护的事务”为以太网提供了等效的功能。
允许合同访问块时间戳本质上是一项冗余功能,使合同容易受到对手的操纵。通常,块时间戳用于两个目的:1)用作确定性随机种子(例如,在Run合同中)和2)用作分布式网络中的全局时间戳(在[25、26、28]中)。使用块时间戳作为随机种子是不明智的,因为它的熵低且时间戳易于操作。有一些方法可以在区块链上获得更好的随机种子[29,30]。
合同不应使用易于操作的时间戳,而应使用区块索引(以太坊中每12秒大约创建一个新区块)来模拟全局时间。块索引始终增加(增加1),从而消除了攻击者偏向于访问时间的合同执行输出的任何灵活性。
实际的解决方法是将现有的时间戳概念转换为块号。可以通过返回TIMESTAMP指令的块ID并转换相关的表达式来实现更改。
该实现只需要更改4.1节中的getstate指令。
一个简单的解决方案是在合同调用另一个合同时检查返回值。当前,Solidity编译器会插入一个代码段以执行异常转发,但通过send或call进行调用时除外(从Solidity的角度来看)它们被认为是低级指令。这种半途而废的解决方案仍然使“原子性”破裂。
更好的解决方案是在EVM级别上自动将异常从被调用方传送到调用方。这很容易实现,但需要所有客户端进行升级。我们还可以提供适当的异常处理机制,例如,通过具有明确的throw 和 catch EVM指令。如果在被调用方中(隐式或显式)引发了异常并且未正确处理异常,则可以恢复调用方的状态。这种方法已经在许多流行的编程语言(包括C ++,Java和Python)中使用。请注意,当合同所有者/编写者恶意并且故意在合同中植入错误时,添加throw 和 catch指令将无济于事。
上一节中提出的我们的解决方案确实要求网络中的所有客户端都进行升级,因此冒着看不到实际部署的风险。作为部署前的缓解措施,我们提供了一种称为Oyente的工具,以帮助:(1)开发人员编写更好的合同; (2)用户避免调用有问题的合同。重要的是,其他分析也可以实现为独立的插件,而不会干扰我们现有的功能。例如,Oyente的一种直接扩展是对合同的最坏情况用气量进行更精确的估算。
我们的分析工具基于符号执行[31]。符号执行将程序变量的值表示为输入符号值的符号表达式。每个符号路径都有一个路径条件,该条件是对符号输入的公式,该符号输入是通过累积这些输入必须满足的约束才能建立的,以便执行该路径。如果路径条件不令人满意,则该路径是不可行的。否则,该路径是可行的。
我们选择符号执行是因为它可以静态地逐个路径推理程序。一方面,这优于动态测试,因为动态测试会导致程序逐个输入。对于以太坊来说,动态测试甚至需要更多的精力来模拟执行环境。例如,要检测事务顺序的依赖性,我们必须比较不同执行路径的交错结果。考虑到区块链行为的不确定性和复杂性,很难通过动态测试来解决这个问题。
另一方面,通过一次推理一条路径,与使用静态污点分析或常规数据流分析的传统方法相比,符号执行可以实现更好的精度(或更少的误报)。在那些方法中,抽象程序状态经常被合并,承认在真正的执行中永远不会发生的状态,并最终导致高误报率。
图11描绘了Oyente的体系结构概述。 它需要两个输入,包括要分析的合约的字节码和当前的以太坊全局状态。 它回答合同是否存在任何安全问题(例如,TOD,时间戳依赖,错误处理的异常),并向用户输出“有问题的”符号路径。
我们工具的一个副产品是合同字节码的控制流图(CFG)。 我们计划在将来Oyente将能够充当交互式调试器,因此我们会将CFG和有问题的路径输入到Graph Visualizer中。
字节码可在区块链上公开获得,Oyente解释EVM指令集以忠实地将指令映射到约束(即位级精度)。 以太全局状态提供合约变量的初始化(或当前)值,从而实现更精确的分析。 所有其他变量,包括值,消息调用的数据,均视为输入符号值。
Oyente遵循模块化设计。 它由四个主要组件组成,分别是CFGBuilder,Explorer,CoreAnalysis和Validator。 CFGBuilder构造合同的控制流图,其中节点是基本执行块,边表示这些块之间的执行跳转。 Explorer是我们的主要模块,象征性地执行合同。 然后,Explorer的输出将馈送到CoreAnalysis,在此我们实现针对第3节中标识的漏洞的逻辑。最后,在向用户报告之前,Validator会过滤掉一些误报。
我们用大约4,000行代码在Python中实现了Oyente。 目前,我们使用Z3 [33]作为求解器来确定可满足性。 Oyente忠实地模拟以太坊虚拟机(EVM)代码,该代码以其语言包含64种不同的指令。 Oyente能够检测到第3节中讨论的所有三个安全问题。我们在下面描述每个组件。
CFG Builder。
CFGBuilder会构建一个skeletal CFG ,其中包含所有基本块作为节点,以及一些表示跳跃的边,可以通过本地研究相应的源节点来确定目标的跳跃。 但是,某些边缘无法在此阶段静态确定,因此在后续阶段的符号执行期间会动态构建它们。
Explorer。
我们的Explorer从skeletal CFG的入口节点开始。 在任何时候,Explorer可能正在执行许多符号状态。 Explorer的核心是解释器循环,该循环获取要运行的状态,然后在该状态的上下文中象征性地执行一条指令。 该循环继续进行,直到没有剩余状态或达到用户定义的超时为止。
条件跳转(JUMPI)采用布尔表达式(分支条件),并根据条件是true还是false来更改状态的程序计数器。Explorer 查询Z3以确定分支条件在当前路径上是可证明是正确的还是错误的; 如果是这样,程序计数器将更新为适当的目标地址。 否则,两个分支都是可能的:我们然后以“深度优先搜索”方式探索两个路径,并相应地更新程序计数器和每个路径的路径条件。 可能会将更多边缘添加到skeletal CFG。
在探索阶段结束时,我们产生了一组符号路径。 每条路径都与路径约束和后续阶段分析所需的辅助数据相关联。 尤其是使用约束求解器Z3,可以帮助我们从考虑中消除可证明的不可行痕迹。
Core Analysis。
CoreAnalysis包含用于检测合同的子组件,这些合同是TOD,与时间戳相关或异常处理的异常。Explorer仅收集表现出不同的以太流的路径。 因此,根据当交易顺序改变时发出的以太币是否不同来检测合约是否为TOD。 同样,如果要发送的条件包括块时间戳,我们将检查合同是否与时间戳相关。 我们描述了我们如何执行以下分析。
我们通过定量和定性分析来衡量Oyente的功效。 我们在以太坊的前1459999个区块中的所有合约上运行Oyente。 我们的目标是三重的。 首先,我们旨在衡量真实的以太坊合同中第3节中讨论的安全漏洞的普遍性。 其次,我们强调,我们的设计和实现选择受现实生活中智能合约的特征驱动,而Oyente具有足够的能力来处理它们。 最后,我们提供了一些案例研究,这些案例表明了许多合同开发人员对以太坊的微妙语义的误解。
截至2016年5月5日,我们从区块链上收集了19,366份智能合约。这些合约目前持有30,6864枚以太币,即在撰写本文时的总金额为3,000万美元。 合同中的余额差异很大:大多数合同没有任何以太币(例如,余额为零),其中10%拥有至少1个以太币,而最高的余额(2401557以太币)则占总余额的38.9%。 所有合同。 平均而言,一份合约具有318.5个以太币,相当于4523美元。 这表明攻击者有充分的动机来针对和利用智能合约中的漏洞来获取利润。
以太坊合约从简单到相当复杂。 图13显示了合同中的指令数量在18到23609之间,平均数为2505,中位数为838。图13中显示了单个合同中使用的不同指令的数量。为了我处理这些合同,Oyente需要正确处理63条指令的逻辑。 我们选择在EVM字节码而不是源代码(例如,Solidity [16])上构建Oyente,因为只有少量合同在公共存储库上公开提供了源代码[34,35]。 Oyente发现总数为366213条可行的执行路径,这些时间在Amazon EC2上花费了大约3000个小时的总分析时间。
实验装置。 我们在基准测试中以19,366个合同运行Oyente。 所有实验都是在4个Amazon EC2 m4.10xlarge实例上进行的,每个实例都具有40个具有160 GB内存的Amazon vCPU,并运行64位Ubuntu 14.04。 我们使用Z3 v4.4.1作为约束求解器[33]。 我们将符号执行(例如,Explorer组件)的超时设置为每份合约30分钟。 每个Z3请求的超时设置为1秒。
性能。 Oyente平均需要350秒来分析合同。 267个合同需要30分钟以上的时间进行分析。 Oyente探索的路径数范围为1到4613,平均每个合约为19,中位数为6。我们观察到,运行时间几乎取决于探索的路径数,即合约的复杂度。
图12报告了我们的结果。 Oyente标记了8,833个具有至少一个第3节中讨论的安全性问题的合同,其中1,682个是不同的(通过直接比较字节码)。 其中,我们只能收集175个合同的源代码,以确认该工具的正确性; 我们会手动检查误报。 在所有有来源合同中,Oyente的假阳性率很低,为6.4%,即175个案例中只有10个案例。
Mishandled exceptions。
(Oyente)标记的5 411张合同处理不当,占基准的27.9%。 在这5,411份合同中,有1,385份与众不同,有116份合同提供了源代码。 通过人工分析,我们验证了所有这116个合同都是真实的肯定。 为了确认,我们确定是否存在任何外部调用(SEND,CALL指令),并且没有随后进行任何故障检查。 通过验证返回值是否为非零来实现这些失败检查。
下面的观察解释了这一问题的普遍性。 在公共区块链的前1,459,999个区块中,处理了180、394个跨合同呼叫。 对于每个合同调用,可能会有对其他合同的其他调用,从而增加了调用堆栈的深度。 这些可能是由于函数或库调用,外部帐户交易或嵌套的递归合同调用引起的。 我们在图14中绘制了这些合同调用的调用堆栈深度,该图显示了其中大多数都涉及一定程度的嵌套(例如,调用其他合同)。 此外,在良性运行中,所有合同发票都不会超过调用堆栈深度50,该深度远低于调用堆栈深度的限制1,024。这解释了为什么在良性调用中异常通常是意外的并且未处理。
Transaction-ordering dependence。
TOD合同相对较少,有3,056张合同,占我们基准合同的15.7%。 在这些合同中,有135个不同的合同,其中32个具有可用的源代码。 我们手动验证其中9个为误报,而23个为真的。 为了确认,我们寻找以太币的不同流动,其中结果取决于输入交易的顺序。
稍后将讨论几种可以利用这种依赖性的真正肯定的情况。 作为误报的一个示例,图15显示了一种情况,其中有两个分开的以太流,但是其执行顺序不会改变合约的结果。 如果未满足某些要求,则第一个流程(在第4行中发送)将以太币返回给发送方,而第一个流程(第7-17行)则向先前的参与者付款。 Oyente意识到以太币的两种流动,并把合同标记为潜在的bug。 但是,传入交易执行这些流程的顺序不会影响其中的预期付款和接收者。 该工具可能涉及更多耗时的分析,以在将来解决此类情况。 合同的最终状态保持不变。
Timestamp dependence。
Oyente在我们的基准报告中报告了83个与时间戳相关的合同,其中52个是不同的。 其中只有7个具有可用的源代码,我们对这些源代码进行了手动验证以防误报。 为了确认这一点,我们检查时间戳是否包含在以太流的路径条件中,以便对块时间戳的操纵将导致与合同不同的支付或接收者。
Reentrancy Handling
再入处理。 Oyente报告了340个此漏洞的实例,其中186个与众不同。 其中只有2个具有可用的源代码,其中一个是著名的TheDAO合同[19],
另一个已知来源合同具有可重入漏洞,但是由于该漏洞使用SEND指令而不是CALL来调用另一个帐户,因此该漏洞无法利用。 此处的主要区别在于,CALL在进行通话时会将所有剩余的gas发送给被叫方,而SEND会限制该数量。 这种简单的更改将限制可能的重入攻击造成的损害。 我们在许多公开的可重入示例中进一步测试了Oyente [36] [37],Oyente确认了所有可能重复执行呼叫的情况。
我们调查了Oyente标记的几个合约,以展示其如何帮助分析以太坊智能合约。
我们已经发现脆弱的合同在6.2节中报告了不同程度的损害严重性。 例如,在错误处理的异常类别中,由Oyente标记的具有最高余额(最高,以太坊1,099.5)的PonziGovernMental [26]合同。 合同的运作方式如下。
•合同接受用户的投资。 新投资支付给以前的投资者,并增加了头奖。
•在没有新投资的12小时后,最后的投资者和合同所有者分担头奖。
处理最后一步的代码在图17中。第7行和第9行使用send指令发送Ether,如3.3节中所述,它可能无法正确执行。 合同不检查操作是否成功,从而容易受到攻击。
Stealing Ether from investors.
从投资者那里窃取以太币。 让我们考虑一下图17中的代码片段,它具有一个细微的错误。 第11行似乎无害,因为它在所有债权人均已清偿后将所有剩余金额偿还给所有者。 但是,如果所有者以精心构建的1023呼叫堆栈大小来调用该合同,则任何发送指令都不会成功,从而导致以太没有中断。 第二次调用该合同将导致所有者获得合同的全部余额,并迫使以前的投资者损失投资于该合同的所有以太币。 其他合同中也存在类似的攻击(参见[22,25])。
Manipulation of contract outcome.
操纵合同结果。 PonziGovernMental是时间戳相关合同的另一个示例。 在第5行中,PonziGovernMental使用当前块时间戳确定当前时间。 如果用户在自上次存款以来已接近12个小时时调用PonziGovernMental,则矿工可以设置下一个区块时间戳,以使第5行中的条件有效或无效。 因此,矿工可以通过选择一个比当前时间提前的时间戳值来强制PonziGovernMental合同提前完成本轮工作。 另外,矿工可以通过选择较小的时间戳来将回合延长12个小时。 因此,在合同中拥有权益的矿工将时间戳记设置为有利于其结果的值。 该问题也存在于其他合同中(见[28])。
Lock or Sabotage Others’ Funds.
锁定或破坏他人的资金。 攻击者还可以阻止他人获得其合法付款。 此类攻击的一个示例是EtherID,它是以太坊中最活跃的合约之一,截至撰写时,该交易共有57,738个接收/发送交易[38]。 EtherID充当以太坊网络的名称注册商,允许用户创建,购买和出售任何ID(例如代币)。 处理用户请求购买注册ID的代码在图18中。该代码检查买方是否有足够的以太币(第2行),将付款发送给现有所有者(第4行),并更改ID的所有权( 第5-10行)。 如第3.3节所述,第4行中的发送指令可能会失败。 结果,ID所有者可能无法收到付款,而仍必须将其ID的所有权转让给买方。 所有者无法在以后要求付款。 Ether值永远锁定在合约中。
验证上述对公共区块链的攻击是可行的,但是出于道德原因,我们不对用户可能会损失资金的合同[22,25,26]进行攻击确认。 相反,我们对攻击严重程度较低的EtherID合同执行验证。 更重要的是,EtherID允许我们定位自己的帐户,其他帐户在实验中不受影响。
我们通过创建我们自己的ID并自行购买来验证EtherID的问题。 我们显示,当注册者使用合同钱包或购买者恶意并且执行call-stack溢出攻击时,我们的ID的注册簿未收到预期的付款。
我们的两个ID是dummywallet和foowallet,分别由两个地址0x33dc532ec9b61ee7d8adf558ff248542c2a2a62e和0x62ec11a7fb5e35bd9e243eb7f867a303e0dfe08b注册。 购买任何一个ID的价格为0.01以太网。 地址0x33dc532 …是合同地址,在收到任何付款时会执行一些计算(因此会花费gas)。
然后,我们从不同的地址发送两个交易以购买两个ID。 第一交易购买dummywallet。 但是,0x33dc532 …是一个合同地址,该地址用于在收到任何付款后立即燃烧所有提供的天然气,而无需执行任何其他操作。 因此,EtherID的第4行中的send函数到0x33dc532 …将失败。 结果,0x33dc532 …出售它的ID而没有收到任何付款。 资金0.01以太币将永远保存在合约EtherID中。
合同发送的第二笔交易7调用了1023次,然后向EtherID发送购买请求以购买foowallet。 进行此类攻击的代码段如图16所示。当EtherID在第4行中执行send时,call 堆栈已具有1024帧,因此无论使用多少gas,send操作都会失败。 因此,地址0x62ec11a7 …并未收到应付款0.01的以太币。
manipulation | n. 操纵;操作;处理;篡改 |
---|---|
guard against | 防止;提防 |
explicitly | adv. 明确地;明白地 |
irreversible | adj. 不可逆的;不能取消的;不能翻转的 |
patch | vt. 修补;解决;掩饰 |
invoke | vt. 调用 |
compile | 汇编,编译 |
unanimously | adv. 全体一致地 |
compensation | n. 补偿;报酬 |
expend | v. 花费;耗尽 |
subsequently | adv. 随后,其后;后来 |
multiply | 乘 |
countermeasure | 对策 |
scenario | 场景 |
inconsistent | 不一致的 |
propagation | 传播 |
malicious | 恶意的 |
entitlement | n. 权利;津贴 |
semantics | 语意 |
denote | vt. 表示,指示 |
identical | n. 完全相同的事物 |
numeric | 数值的 |
constant | 常量 |
positives | 误报 |