以下解读均为本人个人见解,如有曲解或造成不必要的麻烦,欢迎联系我 nexisato0810@gmail.com 进行修改。正文内容中的“作者”均为paper作者本人,我的观点会由“我”或在括号“()”内显式注明。

【Paper作者】:Loi Luu, Duc-Hiep Chu, Hrishi Olickel, Prateek Saxena, Aquinas Hobor. 来自NUS团队

【一句话总结】:针对以太坊智能合约的漏洞提供了一种开源检测工具作改进建议.

【Introduction】

​ 2009年,中本聪向全世界引入了比特币,自此加密货币得到的广泛的发展与应用. 以比特币为代表的加密货币具有去中心化的特性,由网络中所有的节点(即用户)通过运行共识协议维护数据的安全可靠性. 在paper提出的2016年,智能合约成为了比特币的发展方向.

​ 智能合约就是**一段运行在区块链上的程序,并由共识协议保证其正确执行. ** 目前智能合约在应用层面上主要以 solidity 语言编写,可以规定交易过程中的任何规则(比如,合约可以规定某事件自动触发转账). 因此,智能合约可以在各种交易场景中得到广泛的应用.

​ 智能合约由160bit的地址标识,在区块链网络中,用户通过将交易信息发送到合约地址以调用当前加密货币的智能合约. 当要发送一笔新的交易时,这笔交易的所有参与方都会将区块链的当前状态信息、交易的有效载荷信息,作为输入,发送到这个地址,执行合约代码,通过参与共识协议,使得代码的输出与合约所规定的下一个状态相对应.

​ 尽管以太坊等智能合约平台预先定义好共识协议,要求所有用户必须遵循,但它在完全开放的网络环境中运行的,因而很容易遭到特定攻击者的恶意攻击,如修改交易信息或时间戳. 所以,依赖于智能合约的区块链平台,需要在底层语义上进行完善以预防这些潜在的操纵. 由于智能合约是不可逆、不可更改的,因此若不以巨大的代价逆转区块链,现有的缺陷就无法修补,从此可以看到在部署智能合约之前评估其安全性能的必要性.

​ 本paper以以太坊为例,记录了几个安全漏洞,并给出了相应的实际案例,作者认为这些漏洞出现的原因在于合约的编写者对底层语义的理解和合约的实际语义之间存在着语义鸿沟. 此外,作者在数万条智能合约的基础上研究它们对现实生活实际的影响,向我们展示如何利用这些漏洞窃取用户的硬币虚拟货币.

​ 这项工作着重强调了 与/或 缺失这一微小bug可能导致合约开发者产生了错误的安全感. 为避免直接修改现有的智能合约或以太坊的共识协议,作者提供了 Oyente 开源检测工具,帮助开发者在部署智能合约前检测潜在的漏洞. 作者使用这个工具对以太坊网络前1460000个区块链节点中的19366 条智能合约进行评估,发现8833条存在潜在的漏洞,关系到价值3千万美元的虚拟货币,且能够检测到不显著的 TheDAO 漏洞.

​ 总结一下,paper的主要工作有如下几点:

  • 记录了以太坊智能合约中几种新的bug(截止到paper发出的2016年);
  • 提出了关于修改以太坊智能合约的底层语义的一些建议;
  • 设计了 OYENTE 工具用于分析智能合约的漏洞;
  • 在真实的以太坊网络中测试了OYENTE 的效果,表现良好.

【智能合约简介】

​ 在分析paper的方法之前,先看一下什么是共识协议和智能合约

共识协议

​ 所有去中心化的加密货币,在网络中的每个节点都会保存一个分类账副本,这个分类账就是一个区块链. 每个区块则保存了一系列的交易信息、上个区块的地址等,如下图所示

image-20210716185312629

​ 保存了这个分类账副本的用户节点也可以叫做矿工, 每个矿工都可以向区块链中添加包含自己一系列交易信息的区块,高层的共识机制保证了这些分类账的同步,如工作量证明机制. 只有通过工作量证明的矿工,才可以向网络中广播自己的区块,如果有效,则在每个用户节点上都会添加这位矿工的区块,达到同步.

以太坊的智能合约

​ 以太坊可以看作基于交易事务的状态机,经过每次交易 TT ,以太坊网络中的区块链状态就会发生变化:σσ\sigma \to \sigma' . 智能合约是存储在区块链中的“自动代理”,通常承担着区块中“创建订单”的一部分功能. 在交易创建成功后,智能合约以“合约地址”标识,每个合约都持有一部分虚拟货币(以太坊中称为“以太币”). 合约状态主要由两部分组成:私有存储空间余额(即合约持有的虚拟货币的数量). 以太坊智能合约代码采用低级、基于堆栈的字节码语言,称为“以太坊虚拟机(EVM)”代码. 在应用级,用户采用 Solidity 为代表的静态高级语言对合约进行定义,再编译为EVM字节码. 以下列合约代码为例:

image-20210716234617084

​ 该合约规定了如何对解决计算难题的用户节点进行奖赏. 合约的创建者在创建的一笔新的交易中include进来Puzzle的EVM字节码,当这条交易被区块链所接收,矿工们会执行三个步骤:首先,为这个新的合约分配一个全新的调用地址;然后,运行相应的构造函数(如第8行)初始化合约的私有存储空间;最后,执行匿名函数(如第15行,可以理解为main函数). 默认情况下,任何调用这个合约的交易,都会以匿名函数function()作为函数的入口.

​ 我们接着看上面这段代码,发送者的信息、发送给合约的以太币数量和调用交易包含的数据都被保存在msg默认输入变量中,例如,合约的所有者通过一些以太币调用交易ToT_o 更新 rewardreward 变量(即msg.value),在此之前合约应当向这个所有者发送了与这个奖励值相等的以太币数量(即代码19行). 这样,交易ToT_o变为一个新的状态,与此前相比具有不同的rewardreward变量. 类似地,用户也可以在不同的交易负载(即msg.data)提交对Puzzle的解决方案,若某个提交者正确则可以得到合约向它发送的奖励.

​ 以太坊的智能合约使用了gas机制. 具体而言,每条以太坊字节码中的指令都有预先设定的gas值,当用户发起一条交易,调用合约的时候,必须指定其愿意提供多少gas(gasLimit)和每单位gas的价格gasPrice. 当某个矿工将这条交易放入他的提议区块时,会收到实际消耗的gas和gasPrice的乘积的交易费用. 若实际消耗的gas多于gasLimit,那么就会因异常而终止,区块链的状态恢复到交易前,但交易的发送者仍需要向这位矿工支付所有的gasLimit,以对抗可能发生的资源耗尽攻击.

【智能合约存在的漏洞】

交易顺序依赖

​ 考虑如下情形:区块链当前状态 σ\sigma,有一个新区块中包含两条交易 Ti,TjT_i, T_j,这两个区块调用了同一个合约. 在这种情况下,用户便无法得知在交易调用合约时,合约处于哪个状态. 只有挖矿成功的矿工才可以决定交易的顺序,这样合约的状态就取决于矿工如何排列这些交易顺序,这类合约称为 交易顺序依赖(Transaction-Ordering Dependent, TOD) 合约.

​ 有对顺序的依赖,在并发情况下就会出现竞争的问题,即便在正常情况下对合约的并发调用也会产生意想不到的结果;更何况恶意的攻击. 以上文中的Puzzle合约为例,分情况讨论:

  • 考虑存在两个并发的交易请求 Tu,ToT_u, T_o. ToT_o 来自合约的所有者,用于更新reward值,TuT_u 来自用户,用于提交解决方案以获得奖励. 由于两个交易请求被并发地广播出来,所以在矿工的提议区块中会将这两条交易包括进来. 交易的顺序取决于提交者要获得多少奖励. 但如果 ToT_o 先被执行,提交者很可能会得到与期望不一样的reward. 特别地,在卖方频繁更新价格的场景中,由于TOD的问题很可能导致买家需要支付比观测价格更高的费用.
  • 对于恶意攻击者而言,TOD的漏洞可以被恶意牟利. 注意到,在提交者广播TuT_uTuT_u 被放入新区块中存在着一个时间差,这样合约的所有者可以通过监听获取到交易TuT_u,以及对应问题的解决方案. 这样合约的所有者就以更高的 gasPrice(这样可以激励矿工将这条交易放在提议区块中,顺序先于TuT_u)向网络提交答案.

时间戳依赖

​ 时间戳依赖合约以区块的时间戳作为触发器执行某些操作. 以如下theRun为例

image-20210717102402165

​ 该函数第9-10行按照先前区块的哈希值作为随机数种子计算输出,第5-7行基于当前区块的时间戳决定选择哪一个区块.

​ 矿工在挖出一个区块时,要为这个区块打上时间戳,这个时间戳取决于矿工本地系统时间,误差范围900s. 在区块链接收到这一新的区块时,先检查时间戳是否大于上一个区块,在对误差范围进行检验. 在这样一个比较大范围的时间差内,攻击者就可以选择不同区块的时间戳来操纵时间戳依赖的合约的输出结果.

​ 以theRun为例,先前区块的哈希值、Last_payout等变量都是已知的,因此就可以预先计算所有对自己有利的时间戳,最终这个操作区块时间戳的攻击者可以将这个调整这个随机种子的输出为任意值.

未处理异常

​ 在以太坊中,一个合约可以用多种方式调用另外一个合约,如send指令或调用函数,若在被调用方的合约中出现了异常,那么被调用合约就会中止,恢复状态并返回false. 但如果调用方采用如send指令等方式调用,如果不检查被调用方的返回值,这个异常可能不会被发送给调用者,异常无法得到处理,且这种情况很容易出现. 以如下代码为例

image-20210717113850467

​ 这个KoET代码段在第15行没有检查compensation交易的返回值便直接声明了king. 因此如果这个交易没有正常执行的话,当前的king就可能丢失掉trone,并且没有任何的compensation. 其实,在合约向动态的地址发送资金时,经常发生上述类似的问题,因为发送方不知道要分配多少gas,因此合约应当始终检查交易是否正常执行.

​ 在这个场景下,被调用方的异常导致send调用失败,攻击者在调用合约的时候会刻意造成send的失败. EVM调用堆栈的深度限制为1024帧,如果在合约中调用一次,栈帧增加一个. 在上述代码中,攻击者可以在向KoET发送指令前,准备一个合约,故意调用1023次,这样在这个交易到达KoET合约的时候,这个合约的调用栈帧就满了,从而第15行的send指令失败,当前的king就不会得到任何的资金. 这样对攻击者没有任何的直接利益,不过存在一些合约在调用堆栈限制的基础上,采用传销的方式进行获利.

可重入漏洞

​ 在以太坊中,当一个合约调用另一个合约时,会进入中断,等待被调用方执行完成. 如果被调用合约需要使用调用方所处的中间状态时,可能会导致一些问题,而且这种问题在编写的时候并不会被明显地发现. 如下所示

image-20210717120943761

​ 在第11行将提款人的当前余额发送到一个用于提款的合约的地址,这个提款人的余额由内部变量userBalance表示,但是userBalance在调用后才会变为0,智能合约在转账的时候会调用收款方的fallback函数,如果它调用发款方的函数,就会产生循环调用转账的现象,直到合约的以太币清0或者gas耗尽.

【如何设计更好的智能合约】

以太坊的操作语义

​ 作者对以太坊的操作语义进行了“形式化”,以太坊中的用户账户状态为为 σ\sigma ,经过交易 TT 映射后转化为 σ\sigma'. 矿工们形成区块链并验证区块的过程如下所示:

image-20210717151541083

​ 其中 <BC,σ><BC, \sigma> 表示区块链及其当前的状态,Γ\Gamma 表示即将发生的交易的集合. 只有被选取到的 leaderleader 才能运行成功一次 PROPOSEPROPOSE 阶段,对于其他的矿工只需要在leaderleader 广播区块之后运行 ACCEPTACCEPT 规则进行状态转移即可.

​ 其次,作者定义了以太坊虚拟机的执行状态 μ\mu, 调用记录堆栈定义为:

image-20210717153444533

​ 其中 AA 表示一个调用堆栈, ϵ\epsilon 表示空的调用记录栈,<e>exc<e>_{exc} 表示被抛出的异常,四元数组的含义为:M:M : 合约代码数组;$pc : $ 下一条指令执行的地址;$l : $ 辅助存储空间;s:s: 操作数堆栈.

image-20210717154020675

​ 作者将以太坊的一笔交易定义为三元组 <id,v,l><id, v, l> ,分别代表要调用的合约、需要向合约中充值的资金、用于捕获输入参数值的数组. 上图第一条语义表示交易的正常执行过程,第二条表示交易触发异常而中止的语义. 交易的执行需要满足两个性质:原子性一致性.

​ 此外,作者将EVM提炼成指令语言 EtherLiteEtherLite ,在Solidity等高级语言进行编译后会插入如下集合中的某些指令:

image-20210717161849425

设计更好语义的一些建议

受保护的交易(TOD)

​ 要减轻TOD的问题,我们需要保证合约代码的调用要么返回预期的输出,要么执行失败,即便在交易中的存在不确定的交易记录, 在此前交易语义设计的基础上,类似于现代处理器中的CAS指令,作者新增加了一个保护规则 gg,只有交易状态满足规则 gg 的时候交易才会被执行,若不满足则被 TXSTALETX-STALE 删除.

image-20210717164639124

确定性时间戳

​ 实际上,允许合约访问区块时间戳本身就是冗余的,这反而使得合约更容易受到攻击者的操纵. 区块时间戳一般有两个作用,作为确定性的随机种子;或分布式网络中的全局时间戳. 合约不应当使用易于操作的时间戳,而应当使用区块的索引,通过创建新区块的方式模拟全球的时间,这样攻击者合约就无法利用区块时间戳自由输出.

更合理地处理异常

​ 一个合理的解决方案是在合约调用另一个合约时,检查返回值. 这种解决方案是在Solidity编译器中插入一个新的代码片段转发异常,不过这使得交易的原子属性也被破坏. 最好的解决方案还是从EVM级别入手,将异常从调用者转发给被调用者,但是这需要所有用户升级. 其实在被调用方可以采取一些适当的异常处理机制,比如在被调用方的通过显式的 throw或者catch EVM等指令,若抛出的异常未能被正常处理,可以恢复调用者的状态. 不过对于刻意的攻击者来说,他们仍然可以在合约中插入漏洞,throw等操作对这种情况仍然没有帮助.

【OYENTE Tool】

​ 先前的解决方案其实都是要求客户端进行升级的,对此作者团队提供了一个开源工具OYENTE,功能主要有:辅助开发者编写合约;避免用户调用有问题的合约. 某些扩展的分析功能可以作为独立的插件模块,比如估计合约在最坏情况下消耗多少gas.

OYENTE基于符号执行的,它将程序变量的值转化为符号表达式作为输入. 每条符号路径需要遵循一系列路径条件才可行. 符号执行方式可以静态地逐路径推理程序,相比于需要输入的动态测试不需要模拟执行环境. 另一方面,相比于静态污点分析或者数据流分析的传统方法,符号执行可以通过推理路径实现更高的检测精度.

​ 总体架构的设计如下所示:

image-20210717173728032
  • 输入:待分析合约的字节码和以太坊当前的全局状态,
  • 输出:存在问题的符号路径

OYENTE采用模块化设计,由Python语言实现,使用Z3作为求解器,可以模拟EVM代码的执行环境,包括四个主要部分:

  • CFGBuilder:上半部分的控制流图(Control Flow Graph, CFG) 构造器,用于支持Oyente可视化交互,对CFG和有问题的符号路径进行可视化. CFG的节点表示执行代码的区块,边表示区块之间的执行跳转.
  • Explorer:是用于符号执行的主要模块,核心是一个解释器循环,它通过获取区块运行的状态,再在这个状态的上下文中执行单个指令,直到没有剩余状态或超时.
  • CoreAnalysisExplorer的输出传递给CoreAnalysis用于定位合约中的漏洞,由各个不同的子模块检测相应的智能合约漏洞.
  • Validator:过滤掉一些误报,用于验证CoreAnalysis的追踪记录.

【方案评估】

​ 作者的团队在以太坊前 1459999 个区块中的所有合约运行了Oyente用于定性和定量分析,目的有三个:测量前文所提安全漏洞存在的普遍性;强调Oyente的设计由现实智能合约的特征驱动;提供某些合约开发者忽视以太坊的底层语义差距的案例.

​ 作者团队收集了在这些区块中收集了19366条合约(实验时间2016.5.5),总共余额为3068654以太币,在论文撰写的当时价值约3kw美元. 这些合约的余额差距很大,绝大多数合约没有任何以太币,有些合约的以太币余额高达240w,平均每条合约持币318.5,价值4523美元. 此外,各个合约的复杂度也不同,下图展示了这些合约中的指令数量

image-20210717221611031

​ 由于很少有合约提供Solidity源代码,团队选择在EVM字节码上运行Oyente检测工具,分析了共 366213 条可执行路径,在Amazon EC2上分析约3000小时.

定性分析

**实验环境:**4个Amazon EC2云服务器实例,每个实例包含40个Amazon vCPU和160GB内存,OS为Ubuntu 14.04,Z3 v4,4,1作为求解器,Oyente的每个组件的运行时限30min,Z3的请求超时限制在1s.

性能分析: 平均每个合约分析350s,每个合约的符号执行路径从1到4613不等,平均19条,中位数为6,运行时间取决于路径的数量,即合约运行的复杂度.

实验结果:

image-20210717222925567

​ 对于发现到的每个安全问题,Oyente都会报告是哪种类型的漏洞,统计图如上图所示, 误报率为 6.4%6.4 \%.

​ 可以看到,未处理异常 在所有漏洞中出现的次数最多,基于观测的主要原因在于每个合约可能会调用函数、库、或嵌套合约等,从而增加了调用堆栈的深度. 下图统计了这些合约运行时调用堆栈的深度,表明绝大部分合约调用并不会超过这个深度,因此在良性的调用中,异常通常为未处理的.

image-20210717225136415

​ TOD合约中有32份具有源代码,其中9份被判定为FP,其中一份源代码如下:

image-20210717225629689

​ 以太币有两个独立的执行流,但是它们执行的顺序并不影响合约的输出. Oyente虽然检测到了两条执行流,并判为潜在的漏洞合约,但实际上这两条交易并不需要规定严格的顺序,因此需要消耗更多的时间进一步分析以避免误报.

​ 特别的,在检测到的可重入处理中,有1份可用的合约源码存在不易被发现 TheDAO 漏洞,还有1份虽然存在可重入漏洞,但由于采用了 SEND调用方式,并不需要向被调用方发送 gas ,因此一定程度上降低了遭受到可重入攻击的危害.

定量分析

​ 以PonziGovernmentMental为例

image-20210717231152261

​ 该合约是被Oyente标记为未处理异常的合约中以太币余额最多的一份. 主要功能逻辑为:接收用户的投资,这笔资金发送到先前投资者和 jackpotjackpot 中;若超过12小时没有受到新的投资,则最后一个投资者和合约的所有者共享 jackpotjackpot. 合约中采用了 sendsend 指令发送以太币,因此无法检查发送操作是否成功,容易遭到攻击,主要有下列三种

  • 攻击者从投资者手中窃取以太币:以第11行为例,目的在于债权人还清账后向合约所有者发送资金. 此时如果所有者故意使得调用栈深度调整为1023,那么send指令不会成功,再进行一次调用将导致合约的所有者收到全部的余额,使得投资者对合约的投资失效.
  • 操纵合约的输出:此外,这还是一条时间戳依赖的合约, 如第5行,合约通过区块的时间戳决定当前时间, 因此矿工可以通过设置提议区块的时间戳获取到对自己有利的输出.
  • 锁定或破坏其他人的资金EtherIDEtherID 是以太坊中比较活跃的一个合约,它作为以太坊网络的ID注册商,允许用户习性购买注册 EtherID . 代码中也采用了 send 的方式向所有者付款,其次再更改 ID 所有权. 但是未处理异常的情况下,ID 的所有者可能接收不到付款,却仍需要向买家转移所有权,这样以太币就被所在了合约中. 团队在以太坊网络中用自己的账户进行了相应的攻击测试. 它们试图创建自己的 EtherID,实验发现当买家恶意调用合约使得调用堆栈溢出时,卖方在没有收到相应的付款的情况下将ID出售,以太币却永远贮存在了合约中.

【后记】

​ 这篇paper的篇幅比较长,在我没有智能合约相关研究背景的情况下阅读起来有些困难. 总结一下本文的思路:在区块链的兴盛背景下,引入了使用智能合约以太坊. 首先解释了加密货币和智能合约的基本原理,然后引入了智能合约的四种潜在的漏洞,针对这些漏洞对智能合约的开发者提供了一些EVM级的语义修改建议,并提出了Oyente检测工具用于分析合约是否具有某种潜在类型的漏洞,并通过在以太坊网络环境中实际部署测试验证了它的有效性.

​ 区块链近年来的研究方向聚焦于智能合约,以我目前而言接触到paper研究方向为智能合约的漏洞检测. 本paper由于发表于2016年,神经网络相关的研究方法还没有被广泛应用,因此在EVM级进行了检测,但更细致的实现机制仍然需要通过研究Oyente的源码进行分析. 目前流行的智能合约漏洞检测手段考虑采用图卷积的方式,通过建图分析其不同函数之间的调用关系,对异常函数直接打上标签,进一步可以得到更细粒度的诱因.