最近利用智能合约代码中的错误进行的攻击造成了严重后果,修复错误并及时部署补丁合约具有很大的挑战性。即时修补尤为重要,因为由于区块链系统的分布式特性,智能合约始终在线,它们还管理着相当数量的资产。这些资产正处于危险之中,并且通常在攻击后无法收回。现有的升级智能合约的解决方案取决于手动过程。本文提出了一个名为EVMPATCH的工具(https://github.com/uni-due-syssec/evmpatch-developer-study ),该工具可立即自动修补错误的智能合约。 EVMPATCH具有用于流行的以太坊区块链的字节码重写引擎,并且透明/自动地将常见的现成合约重写为可升级合约。
EVMPATCH的概念验证实现会自动加固智能合约,这些合约容易受到整数上溢/下溢和访问控制错误的影响,但可以轻松扩展以涵盖更多错误类。对14,000个现实合约的评估表明,本文工具成功地阻止了对合约发起的攻击交易,同时保持了合约的预期功能不变。研究者与经验丰富的软件开发人员进行了一项研究,结果表明EVMPATCH是实用的,并且可以将从给定的Solidity智能合约转换为可升级合约的时间减少97.6%,同时还可以确保与原始合约的功能等效。
0x01 Introduction
在现代区块链系统中使用智能合约可以实现几乎任意(图灵完备)的业务逻辑。它们可以实现对加密货币或Token的自治管理,并有可能通过消除对可信赖的(可能是恶意的)第三方的需求(例如在支付,保险,众筹或供应链中的应用)来彻底改变许多业务应用程序。由于它们的易用性和某些合约持有的高货币价值(加密货币),智能合约已成为攻击的诱人目标。智能合约代码中的编程错误可能会造成毁灭性后果,因为攻击者可以利用这些错误窃取加密货币或Token。
最近,由于智能合约错误,一个特别臭名昭著的事件是“ TheDAO”重入攻击,造成价值超过5000万美元的以太损失。这导致了以太坊区块链的备受争议的硬分叉。先前工作展示了如何通过在开发时进行离线分析或通过执行运行时验证来防御重入漏洞。另一个臭名昭著的事件是Parity钱包攻击,在这种情况下攻击者将智能合约移动到无法再使用该合约所持有货币的状态。由于访问控制错误,总共有约500,000个以太币锁在智能合约中。先前已经在自动利用漏洞产生的背景下研究了这种访问控制漏洞的自动检测,此外整数溢出错误构成了智能合约中的主要漏洞类别。当算术运算的结果宽度大于整数类型可容纳的宽度时,会发生此类错误,它们特别影响所谓的ERC-20代币合约,该合约在以太坊中被用来创建代币。所披露的几个漏洞导致大量代币和以太币损失。
这些攻击激发了社区对增强智能合约安全性的兴趣。在这方面,最近几年提出了许多解决方案,从设计更好的开发环境到使用更安全的编程语言,形式验证,符号执行和动态运行时分析。所有这些解决方案仅旨在证明某种类型的漏洞的正确性或不存在性,因此不能用于保护已经部署的(旧的)合约。尽管某些合约集成了升级机制,但是一旦将特定合约标记为易受攻击,尚不清楚如何自动对其进行修补并测试所修补合约的有效性。即使在源代码级别手动修补合约似乎是合理的,但修补程序可能会意外地破坏兼容性,并使升级后的合约不可用。例如,考虑到以太坊的特殊存储布局设计,委托调用代理模式要求开发人员确保合约的补丁版本与以前部署的版本兼容。即使是很小的更改,例如更改源代码中变量的顺序,也可能破坏这种兼容性。这另外带来了挑战,即开发人员必须遵守严格的编码标准,并且必须使用相同的确切编译器版本。结果,修补智能合约错误目前是一个耗时,麻烦且容易出错的过程。
例如,在修补Parity multisig wallet合约的同时,引入了一个漏洞。攻击者得以成为新部署的库合约的所有者。这使攻击者可以销毁合约并破坏依赖于multisig wallet库合约的所有合约。结果,大量的以太币现在被锁定在那些违约的合约中。最重要的是,修补智能合约错误非常重要。与在PC或移动软件中发现的错误相反,从攻击者的角度来看,智能合约错误是独特的,因为(1)智能合约始终在区块链上在线;(2)它们通常拥有大量资产;(3)攻击者无需考虑其他环境变量(例如,软件和库版本,网络流量分析,垃圾邮件或网络钓鱼邮件即可通过用户操作来触发利用)。
0x02 Design of EVMPatch
在本节中将介绍自动修补工具框架的设计,以及时修补和强化智能合约。本文提出的框架在未修改的智能合约上运行,并且与源代码编程语言无关,因为它不需要源代码。在其核心部分,框架利用字节码重写器将最小程度的侵入式补丁应用于EVM智能合约。结合基于代理的可升级智能合约,这种字节码重写方法使开发人员可以自动引入补丁并将其部署在区块链上。这种方法的一个主要优点是,当发现新的攻击类型或改进了漏洞查找工具时,可以在短时间内以最少的开发人员干预自动重新检查,修补和重新部署合约。 EVMPATCH通常在开发人员的计算机上执行,并持续运行新的和更新的漏洞检测工具。这也可以包括动态分析工具,该工具可以分析尚未包含在区块中但已经可供以太坊网络使用的交易。每当分析工具之一发现新漏洞时,EVMPATCH都会自动修补合约,测试修补后的合约并进行部署。
A.设计选择
代理模式使在以太坊中轻松部署修补的智能合约成为可能。但是,它既不会生成补丁版本,也不会在补丁合约上进行功能测试。 EVMPATCH通过提供全面的框架和工具链来自动,及时地修补和测试生成的修补程序的有效性,从而填补了这一空白。
如上表所示,有两种在以太坊中自动生成补丁的可能策略:源代码或EVM字节码的静态重写。乍看之下,源代码修补似乎是一种选择,因为开发人员可以访问源代码,他们可以检查源代码更改,甚至可以在自动方法引入不希望的更改的情况下进行调整。但是,在以太坊中,应用源代码重写存在一个主要挑战:一个需要仔细保留存储布局。否则,修补后的合约将破坏其内存并失败,或者(更糟)引入危险的错误。即,即使更改不会破坏合约的逻辑,源代码中的某些更改也可能破坏合约的兼容性。
为了将它们放在上下文中,静态大小的变量从地址0开始连续放置在存储中;大小小于32B的连续变量可以打包到单个32B存储插槽(storage slot)中。结果,对源代码中的变量进行重新排序,添加或删除的任何更改可能看起来都是无害的,但是在内存级别,此类更改将导致变量映射到错误的和意外的存储地址。换句话说,变量声明中的更改会破坏合约的内部状态,因为旧版合约和修补后的合约具有不同的存储布局。
相反,字节码重写不受此缺陷的困扰,因为许多错误类仅需要在EVM指令级别上进行更改,从而避免了易于出错的存储布局更改。选择字节码重写的另一个原因是现有的智能合约漏洞检测工具。截至目前,他们中的大多数在EVM级别上运行,并在EVM级别上报告他们的发现。字节码重写方法可以利用这些分析工具的报告来直接生成基于EVM字节码的补丁。最后,如果使用源代码重写,则开发人员对修补合约的有效性进行彻底测试的可能性有限。
特别是在字节码级别上,针对旧事务(包括封装了攻击的事务)检查修补的合约更为可行。也就是说,交易测试自然仍然需要在字节码级别上进行分析,以对攻击交易进行逆向工程,以及它们如何针对修补后的合约失败。字节码重写允许开发人员直接将重写的字节码指令与攻击交易相匹配,从而使取证分析变得可行。由于所有这些原因,决定选择字节码重写。
B.框架设计
上图中描述的框架包括以下主要组件:(1)漏洞检测引擎,包括自动分析工具和公开漏洞披露;(2)字节码重写器,将补丁应用到合约;(3)补丁测试机制验证先前交易的补丁,以及(4)合约部署组件上载合约的补丁版本。首先,漏洞检测引擎会识别漏洞的位置和类型。然后,此信息将传递到字节码重写器,后者根据先前定义的补丁模板对合约进行补丁。此后,已打补丁的合约将被转发到补丁测试器,该测试器将过去的所有交易重播到该合约。也就是说不仅修补合约,而且允许开发人员检索在原始合约和修补合约之间表现出不同行为和结果的交易清单。这些交易可作为潜在攻击原始合约的指标。如果列表为空,框架会立即自动将修补后的合约部署在以太坊区块链上。接下来,将对设计的四个主要组成部分进行更详细的描述。
漏洞检测:在能够应用补丁之前,框架需要识别和检测漏洞,为此框架利用了现有的漏洞检测工具。对于任何现有工具未检测到的漏洞,要求开发人员或安全顾问创建漏洞报告。在系统中,漏洞检测组件负责标识指令的确切地址,漏洞所在的位置以及漏洞的类型。然后,此信息将传递到字节码重写器,由后者对合约进行相应的修补。
字节码重写器:通常,静态二进制重写技术非常适合在以太坊中应用补丁,因为智能合约的代码大小相对较小:通常在10KB左右。此外,EVM智能合约始终静态链接到所有库代码。合约不可能将新代码动态地引入代码地址空间。与传统的体系结构(在运行时加载动态链接的库)相比,这使得对二进制重写技术的依赖更加简单。但是,某些智能合约仍然使用类似于动态链接库的概念:专用的EVM调用指令允许合约切换到不同的代码地址空间。本文通过将字节码重写器应用于合约本身和库合约来解决这种特殊性。
EVM的基于堆栈的体系结构在实施修补程序时需要特别注意:在将新代码插入代码地址时,必须保留或更新智能合约的代码地址空间中对任何代码或数据的所有基于地址的引用空间,这样的引用不能轻易地从字节码中恢复。为了应对这一挑战,EVMPATCH利用基于 trampoline的方法将新的EVM指令添加到空白代码区域。要实施补丁,字节码重写器将处理易受攻击合约的字节码以及漏洞报告。重写基于所谓的补丁模板,该补丁模板根据漏洞类型进行选择并进行调整以与给定的合约一起使用。
补丁模板:在EVMPATCH中,采用了基于模板的补丁程序方法:对于每种受支持的漏洞类别,补丁程序模板都集成到了EVMPATCH中。此修补程序模板会自动适应要修补的合约。通过创建通用补丁模板,以便可以轻松地将其应用于所有合约。 EVMPATCH通过替换特定于合约的常量(即代码地址,函数标识符,存储地址),使补丁模板自动适应当前合约。 EVMPATCH附带了针对常见漏洞(例如整数溢出)的补丁程序模板,EVMPATCH的典型用户将永远不会与补丁程序模板进行交互。但是,可选地,智能合约开发人员还可以检查或改编现有的补丁程序模板,甚至为EVMPATCH尚不支持的漏洞创建其他补丁程序模板。
补丁测试器:由于智能合约直接处理资产(例如以太币),因此至关重要的是,任何修补过程都不会妨碍合约的实际功能。因此,任何补丁都必须进行彻底测试。为了解决此问题,引入了一种补丁测试机制,该机制基于(1)基于记录在区块链上的交易历史记录和(2)可选的开发人员提供的单元测试。任何区块链系统都记录了智能合约的所有先前执行,即以太坊中的交易。在案例中,补丁测试器重新执行所有现有交易,并可选地执行任何可用的单元测试,并验证旧遗留和新补丁合约的所有交易是否表现一致。
补丁测试器检测到旧的旧版合约与新补丁的合约之间的任何行为差异,并将具有不同行为的交易列表报告给开发人员。就是说,补丁程序测试机制可以用作攻击取证检测工具。即在执行修补过程时,还将通知开发人员任何先前的攻击,这些攻击滥用了任何已修补的漏洞,然后可以采取相应措施。如果两个合约版本的行为相同,可以自动部署修补的合约。否则,开发人员必须调查可疑交易列表,然后调用合约部署组件以上传已修补的合约。可疑交易列表不仅可以作为潜在攻击的指标,而且可以揭示修补后的合约在功能上不正确,即修补后的合约在良性交易上显示出不同的行为。
合约部署:基于委托调用代理的升级方案是启用即时合约修补的选择选项。因此,EVMPATCH使用代理合约作为具有恒定地址的所有事务的主要入口点,集成了此部署方法。在第一次部署之前,EVMPATCH会转换原始的未修改合约代码以利用委托调用代理模式。这是通过部署代理合约来完成的,该代理合约是不可变的,并假设已正确实施3。然后使用字节码重写器将原始字节码转换为逻辑协定,而只需对原始码进行少量更改即可。然后将逻辑合约与代理合约一起部署。
修补程序部署:最终在对合约进行修补后,并且在由修补程序测试器组件测试了修补程序之后,EVMPATCH可以部署新修补的合约。本研究升级方案将新修补的合约代码部署到新地址,并向先前部署的代理合约发出专用交易,这会将逻辑合约的地址从旧的易受攻击版本切换到新修补的版本。现在,任何进一步的事务都由修补的逻辑合约处理。
人为干预:EVMPATCH设计为完全自动化的。但是,在某些情况下,如果(1)漏洞报告与EVMPATCH尚不支持的错误类相关,或者(2)补丁测试程序报告至少一个事务因以下原因而失败,则需要开发人员干预:新引入的补丁程序,失败的事务不是已知的攻击事务,(3)补丁测试程序报告新引入的补丁程序未阻止至少一个已知的攻击事务。
如果不支持错误类,则EVMPATCH会通知开发人员不支持的漏洞类。由于EVMPATCH是可扩展的,因此它很容易允许开发人员提供自定义补丁模板,从而可以快速适应针对智能合约的新攻击。更具体地说,EVMPATCH支持自定义补丁模板的多种格式:EVM指令,一种简单的特定于域的语言,类似于Solidity表达式,并允许开发人员对函数强制执行先决条件(类似于Solidity修饰符)。
如果补丁测试程序发现新的失败交易,则开发人员必须分析是否发现了新的攻击交易或合法交易失败。对于新发现的攻击事务,EVMPATCH将此事务添加到攻击列表并继续。否则,开发人员将调查合法交易失败的原因。如评估所示,此类情况通常是由于漏洞报告不准确(即错误报告的漏洞而不是补丁错误)而发生的。因此,开发人员可以简单地将错误报告的易受攻击的代码位置列入黑名单,以避免在这些位置进行修补。
这些手动干预通常只需要快速的代码检查或调试器会话。即使是具有中等经验的Solidity开发人员也可以执行这些任务,因为不需要有关底层字节码重写系统的详细知识。因此,EVMPATCH将自身定位为一种工具,使更多开发者可以安全地编程和操作以太坊智能合约。
0x03 EVMPatch Implementation
在本节中描述了EVM PATCH的实现,讨论了以太坊中字节码重写的工程挑战。将描述字节码重写器、补丁测试功能、合约部署机制的实现,以及有关智能合约错误的可能应用。
A.字节码重写的挑战
重写EVM字节码时,必须解决几个独特的挑战:需要处理原始EVM字节码的静态分析,并处理Solidity合约和EVM的若干特殊性。
与传统的计算机体系结构相似,EVM字节码使用地址来引用代码地址空间中的代码和数据常量。因此,在修改字节码时,重写器必须确保正确调整了基于地址的引用。为此,重写器通常采用两种静态分析技术:控制流图(CFG)恢复和后续数据流分析。后者对于确定哪些指令是代码中使用的任何地址常量的来源很有必要。对于EVM字节码,在此上下文中涉及两类指令:代码跳转和常量数据引用。
代码跳转:EVM具有两个分支指令:JUMP和JUMPI。两者都从堆栈中获取目标地址。请注意,同一合约内的函数调用也利用JUMP和JUMPI。也就是说,函数内部的局部跳转与调用其他函数之间没有明显的区别。 EVM还具有专用的呼叫指令,但是这些指令仅用于将控制权转移到完全独立的合约中。因此,它们在重写字节码时不需要修改。
常量数据引用:所谓的CODECOPY指令用于将数据从代码地址空间复制到内存地址空间。一个常见的示例用例是大数据常量,例如字符串。与跳转指令类似,从中加载内存的地址通过堆栈传递到CODECOPY指令。
由于EVM基于堆栈的体系结构,因此处理两种类型的指令都具有挑战性。例如,跳转指令的目标地址总是提供在堆栈上。也就是说,每个分支都是间接的,即不能仅通过检查跳转指令来查找目标地址。相反,要解决这些间接跳转,需要部署数据流分析技术来确定将目标地址压入堆栈的位置和位置。对于大多数此类跳转,可以分析周围的基本块,以追溯跳转目标在堆栈上所处的位置。例如,当遵守指令PUSH2 0xdb1; JUMP时,可以通过从push指令中检索地址(0xdb1)来恢复跳转目标。
但是,许多协定包含更复杂的代码模式,这主要是因为Solidity编译器还支持内部调用函数而无需利用调用指令。回想一下,在EVM中,呼叫指令的执行与远程过程呼叫的执行类似。为了优化代码大小并促进代码重用,Solidity编译器引入了一个概念,其中将函数标记为内部。这些函数不能被其他合约(专用于合约)调用,并遵循不同的调用约定。由于内部函数没有专用的返回和调用指令,因此Solidity会利用跳转指令来模拟两者。因此,无法轻易地区分函数返回和正常跳转。这给识别内部函数和立准确的合约控制流图带来了挑战。
重写EVM智能合约时,字节码重写器中的跳转指令和代码复制指令都需要考虑。重写智能合约的显而易见的策略是在插入新指令或删除旧指令后,修复代码中的所有常量地址以反映新地址。但是,此策略具有挑战性,因为它需要精确的控制流图恢复和数据流分析,这需要处理EVM代码的特殊性,例如内部函数调用。
在传统建筑的二进制重写研究领域,已经开发出了一种更加实用的方法:所谓的 trampoline概念。本研究在重写器中使用此方法,并避免调整地址。每当重写器必须对基本块进行更改(例如,插入指令)时,重写器就会用 trampoline替换该基本块,并立即跳到补丁的副本。因此,原始代码中的任何跳转目标均保持不变,并且所有数据常量均保持在其原始地址。将在下一节中更详细地描述此过程。
B.实现字节码重写器
使用Python实现了一个基于 trampoline的重写器,并利用pyevmasm5库反汇编和组装了原始EVM操作码。基于 trampoline的字节码重写器可在基本块级别上运行。当需要执行指令时,整个基本块都将复制到合约末尾。然后将该修补程序应用于此新副本。原来的基本块被 trampoline所取代,即一条简短的指令序列,立即跳转到复制的基本块。每当合约在其原始地址处跳转到基本块时,就会调用 trampoline,通过一条跳转指令将执行重定向到修补的基本块。为了恢复执行,已插入的基本块的最终指令发出了跳回到原始合约代码的指令。虽然基于 trampoline的方法可以避免修正任何参考文献,但它会引入其他跳转指令。但是正如将要展示的,与这些额外的跳跃相关的gas成本在实践中可以忽略不计。
为了确保正确执行,仍然必须从打补丁的基本块开始至少计算部分控制流图。这对于恢复已修补的基本块以及通过所谓的Fall-Through Edge连接的以下基本块的边界是必要的。并非所有基本块都以显式控制流指令终止:每当基本块以条件跳转指令(JUMPI)结尾或仅不以控制流指令结尾时,就会存在隐式Edge(即Fall-Through)在控制流图中找到位于以下地址的指令。
处理Fall-Through Edge:要处理Fall-Through Edge,必须考虑两种情况。当以Fall-Through Edge为目标的基本块以JUMPDEST指令开始时,该基本块被标记为EVM中常规跳转的合法目标。在这种情况下,可以在合约结尾处将显式跳转附加到重写的基本块上,并确保执行在原始合约代码中的下一个基本块的开头继续执行。如果以下基本块不是以JUMPDEST指令开头,则EVM禁止显式跳转到该地址。在控制流图中,这意味着只能通过Fall-Through Edge才能到达该基本块。为了处理这种情况,重写器将基本块复制到合约的末尾,恰好在重写的基本块后面,从而在重写代码的控制流图中构造了另一个Fall-Through Edge。
上图显示了重写器如何更改原始合约的控制流图的示例。用已检查的添加例程代替ADD指令,该例程还执行整数溢出检查。将ADD指令的地址称为补丁点。包含跳接点的基本块被 trampoline替换。在这种情况下,它立即跳到0xFFB的基本块。该基本块位于原始合约的末尾,是原始基本块在0xAB处的副本,但已应用了补丁。由于基本块现在位于合约的末尾,因此字节码重写器可以在基本块中插入,更改和删除指令,而无需更改位于高编号地址的代码中的任何地址。使用INVALID指令填充原始基本块的其余部分,以确保基本块的大小与原始基本块的大小完全相同。 0xCD处的基本块通过下降沿连接到先前的基本块。但是,此基本块以JUMPDEST指令开头,因此是合法的跳转目标。因此,重写器随后将跳转添加到已修补的基本块的0xFFB处,以确保执行以原始合约的代码在地址0xCD处继续执行。
适用于EVM:EVM具有一些在实现字节码重写器时必须考虑的特殊性。即,EVM强制在代码地址空间中对代码和数据进行某种分隔。 EVM实现可防止跳转到PUSH指令中嵌入的数据常量。Push指令的常量操作数紧跟在推送指令操作码的字节之后。这样的常量操作数可能会意外地包含JUMPDEST指令的字节。然后,该常数将成为合法的跳转目标,并且将出现新的意外指令序列。为避免此类意外的指令序列,EVM实现对代码段执行线性扫描以查找所有Push指令。然后,将这些Push指令中的常量标记为数据,并因此将其标记为无效的跳转目标,即使它们包含的字节等于JUMPDEST指令也是如此。
但是,由于性能原因,EVM实现在标记数据时会忽略控制流信息。这样,推送指令操作码字节本身可以是某些数据常量(例如字符串或其他二进制数据)的一部分。因此,智能合约编译器将所有数据常量累积在严格大于任何可到达代码的地址上,从而避免了生成的代码与编码到代码地址空间中的数据之间的任何冲突。但是,基于 trampoline的重写器确实在智能合约的数据常量后面追加了代码。为避免重写程序附加的代码由于在前的推操作码字节而被意外标记为无效的跳转目标,谨慎地在原始合约的数据和新附加的代码之间插入填充。
Trampoline方法适用性:基于 trampoline的重写方法仅需要最少的代码分析,并且适用于大多数用例。但是,这种方法面临两个问题。首先,只能将指令修补在足够大(就字节大小而言)以包含 trampoline代码的基本块中。但是,典型的 trampoline需要4到5个字节,并且执行一些有意义的计算的基本块通常足够大以包含 trampoline代码。其次,由于复制了基本块,因此代码大小根据所修补的基本块而增加,从而增加了部署成本。但是实验表明,部署期间的开销可以忽略不计。
不依赖精确控制流程图:仅给出EVM字节码来恢复准确的控制流程图是一个具有挑战性和开放性的问题。但是,基于 trampoline的方法不需要准确而完整的控制流程图。相反,仅需要在给定指令的程序计数器的情况下恢复基本块边界(需要在其中应用补丁)。这样做时,恢复基本块边界是很容易的,因为EVM具有用于基本块的显式标记(即JUMPDEST伪指令)。此外,重写器仅需要恢复基本块的末尾以及通过控制流图中的Fall-Through Edge连接的任何后续基本块。
C.补丁测试
虽然将trampoline插入原始代码不会更改合约的功能,但是补丁模板本身可以执行任意计算,并且可能会违反补丁合约的语义。为了测试修补后的合约,EVMPATCH使用了差异测试方法。也就是说,重新执行合约的所有交易,以确定原始易受攻击的代码和新修补的代码的行为是否不同。 EVMPATCH利用直接从区块链检索的合约的过去交易行为。如果合约包含单元测试,则EVMPATCH还将利用单元测试来测试新修补的合约。这种差异测试方法不能保证合约的形式正确性。可用事务数量少的合约容易导致测试覆盖率低。但是实验表明,差异测试方法在实践中足够有效,可以证明补丁程序不会破坏功能。考虑到合约功能的正式规范的可用性,EVMPATCH还可以利用模型检查器来更严格地验证补丁合约。
在差异测试期间,首先从区块链检索到易受攻击合约的交易列表。其次重新执行所有这些事务,并检索每个事务的执行跟踪。然后重新执行相同的事务,但是用修补的合约代码替换易受攻击合约的代码,以获得第二条执行跟踪。使用基于流行的以太坊客户端6的经过修改的以太坊客户端,因为原始客户端不支持此功能。最后将执行跟踪进行比较,补丁测试器会生成一个行为不同的事务列表。如果没有此类交易,则假定补丁不会抑制合约的功能,并继续部署补丁的合约。
由于打补丁会更改控制流并插入指令,原始合约和打补丁合约的执行轨迹永远不会相等。因此,仅检查可能改变状态的指令,即写入存储区(即SSTORE)或将执行流转移到其他合约的指令(例如CALL指令)。然后比较所有状态更改指令的顺序,参数和结果,并找到两条执行轨迹不同的第一条指令。当前,假设引入的补丁程序不会导致任何新的状态更改指令。此假设适用于引入输入验证代码并在传递无效输入时还原的补丁。但是,跟踪差异计算可以调整为了解补丁程序引入的潜在状态变化。
在代码中失败的报告事务(作为补丁程序的一部分)被标记为潜在攻击事务。如果报告的交易由于补丁代码中的用尽gas而失败,将以增加的gas预算重新运行同一笔交易。工具发出警告,因为用户将不得不考虑该补丁引入的额外gas成本。最后,开发人员必须检查报告的交易,以确定给定的交易清单是合法的还是恶意的。副作用是,这使补丁程序测试器成为易受攻击合约的攻击检测工具,使开发人员可以快速找到以前的攻击交易。
D.补丁合约部署
如前所述,EVMPATCH利用基于委托调用代理的升级模式来部署补丁合约。为此,EVMPATCH将智能合约分为两个合约:代理合约和逻辑合约。代理合约是主要入口点,并存储所有数据。默认情况下,EVMPATCH使用EVMPATCH随附的代理合约。但是,EVMPATCH也可以重用现有的可升级合约,例如使用ZeppelinOS框架开发的合约。用户与位于固定地址的代理合约进行交互。为了促进升级过程,代理合约还实现了更新逻辑合约的地址的功能。为了防止恶意升级,代理合约还存储了所有者的地址,该所有者被允许发布升级。然后,升级仅包括向代理合约发送一个交易,这将(1)检查调用方是否是所有者,并且(2)更新逻辑合约的地址。
代理合约从存储中检索新逻辑合约的地址,并将所有调用转发到该合约。在内部,代理合约利用DELEGATECALL指令调用逻辑合约。这允许逻辑合约获得对代理合约的存储区域的完全访问权限,从而无需任何额外开销即可访问持久性数据。
E.可能应用
字节码重写器采用一个补丁模板,该模板被指定为EVM汇编语言的简短代码段。然后,该模板根据已修补合约进行专用化,并重新定位到已修补合约的末尾。这种基于模板的补丁程序生成方法允许指定多个通用补丁程序来解决整个漏洞类别。在下文中列出了可以从框架中立即受益的可能的漏洞类别。只需在函数的开头插入一个检查,以确认调用方是某个固定地址或等于合约状态中存储的某个地址,就可以修补对关键函数的不当访问控制。在先前的工作中已经研究了用于处理此漏洞的检测工具。
当合约使用低级调用指令时,错误处理的异常可能发生,其中返回值不会自动处理,并且合约未正确检查返回值。可以通过在此类调用指令后插入通用返回值检查来解决此问题。
在处理整数算术时,很可能会出现整数错误,因为默认情况下,Solidity不使用检查的算术。这导致部署了许多潜在的易受攻击的合约,并且有一些受到积极攻击。鉴于这些漏洞的普遍性,将在下一节中讨论如何使用EVMPATCH自动修补整数溢出错误。接下来,通过将EVMPATCH应用于访问控制错误和整数错误这两个主要的错误类别,证明了EVMPATCH的有效性。
0x04 Evaluation of EVMPATCH
在本节中报告EVMPATCH在修补两种主要类型的错误时的评估结果:(1)访问控制错误,以及(2)整数错误(上溢/下溢)。
A.修补访问控制错误
Parity MultiSig Wallet是访问控制错误的一个突出示例。该合约实现了一个由多个帐户拥有的钱包。钱包合约采取的任何行动都必须至少由其中一位所有者授权。但是,该合约存在一个致命错误,该错误使任何人都可以成为唯一所有者,因为相应的函数initWallet,initMultiowned和initDayLimit没有执行任何访问控制检查。
上图显示了修补后的源代码,该源代码将内部修饰符添加到函数initMultiowned和initDayLimit(在图中用mark标记)。此修改器使这两个函数无法通过已部署合约的外部接口访问。此外,该修补程序添加了自定义修饰符only_uninitialized,该修饰符用于检查协定是否先前已初始化(标记为➁)。
开发人员最初在部署补丁合约时引入了一个新漏洞,该漏洞已被积极利用。相反,由于EVMPATCH执行字节码重写,因此它将立即生成合约的安全补丁版本,并以安全的方式自动部署它。
考虑下图,该图显示了由EVMPATCH使用的特定于域的语言的自定义补丁,用于指定补丁。在initWallet函数的开头插入一个补丁,以检查条件sload(m_numOwners)== 0是否成立,即合约是否尚未初始化。如果不成立,则合约执行将通过REVERT指令中止。请注意,这里需要使用显式的sload从存储中加载变量,并且表达式在逻辑上与上图中的补丁相反,因为该补丁实际上插入了Solidity require语句。此外,还需要从公共职能调度程序中删除其他两个可公共访问的职能。下图中显示的补丁结合了EVMPATCH提供的两个现有补丁模板。首先,添加需求补丁模板会在输入函数之前强制执行先决条件。其次,删除公共函数补丁模板从调度程序中删除公共函数,从而有效地将该函数标记为内部函数。
https://p1.ssl.qhimg.com/t01251178746843ac3e.png
评估结果:通过部署针对攻击的WalletLibrary合约的补丁版本,验证了补丁合约不再可利用。此外,将源代码级别的补丁程序与EVMPATCH应用的补丁程序进行了比较。下表显示了结果概述。 EVMPATCH仅将合约规模增加了25 B。initWallet函数的额外气体成本仅为235 gas,即每笔交易0.000,06 USD的价格为235.091 USD / ETH,典型的gas价格为1 Gwei。这表明EVMPATCH可以有效地插入修补程序以解决访问控制错误。
B.修补整数错误
由于整数类型的固定位宽,典型的整数类型绑定到最小和/或最大大小。但是,程序员通常对实际整数类型的大小限制没有给予足够的重视,这可能会导致整数错误。幸运的是,几种高级编程语言(Python,Scheme)能够避免整数错误,因为它们利用了几乎无限大小的任意精度整数。但是,用于智能合约的事实上的标准编程语言(即Solidity)没有嵌入这种机制。这就给开发人员留下了完全处理整数溢出的负担,他们需要手动执行溢出检查或正确利用SafeMath库安全地执行数字运算。尽管很常见,但前者显然容易出错。例如,最近揭露了ERC-20代币合约中的多个漏洞。这些合约在以太坊区块链上管理所谓的Token。这样的Token可以处理大量货币,因为它们跟踪每个Token所有者的Token余额并介导Token和以太币的交换。
下图显示了BECToken合约的代码摘录,该代码例证了此类整数溢出漏洞。在第6行中计算总量时,将使用未经检查的整数乘法,从而使攻击者可以提供非常大的_value。结果,数量变量将被设置为少量。这有效地绕过了第11行中的余额检查,使攻击者可以将大量Token转移到攻击者控制的帐户中。最近,在超过42,000个合约中发现了类似的漏洞。
本研究开发了补丁模板,用于检测标准EVM整数宽度(即无符号256位整数)的整数上溢和下溢。对于整数加法,减法和乘法,这些模板添加了受C编程语言和SafeMath Solidity库中的安全编码规则启发的检查。当检测到违规时,EVMPATCH会发出异常以中止并将当前调用回滚到合约。
(1)评估结果
为了验证字节码重写器生成的补丁的正确性,使用了最新的整数检测工具Osiris进行漏洞检测。在分析了以太坊区块链的前5,000,000个区块中的50,535个唯一合约之后,Osiris在14,107个合约中检测到至少一个整数溢出漏洞。使用EVMPATCH,能够成功地自动修补几乎所有这些合约。更具体地说,无法在14107个被调查的合约中修补的有33个,因为检测到的漏洞所在的基本区块对于trampoline代码来说太小了。
在这14107个合约中,约有8000个涉及以太坊网络上的交易。为了生成庞大且具有代表性的评估数据集,从以太坊区块链中提取了发送至这些合约的所有交易,直至区块7,755,100(2019年5月13日),共产生26,385,532笔交易。
使用补丁测试器重播这些交易表明,在所有易受攻击的合约中,有95.5%的合约是EVMPATCH生成的补丁符合与这些合约相关的所有先前交易。对于其余4.5%的被调查合约,补丁程序由于以下原因之一拒绝了交易:(1)成功停止了恶意交易,(2)报告的漏洞为误报且不应进行补丁程序,或( 3)无意中更改了合约的功能。
为了进行仔细检查,从那些可以被EVMPATCH成功修补的,具有已被成功攻击的已确认整数上溢/下溢漏洞的合约中选择了ERC-20Token合约(请参见下表)。为了进行比较,还通过用SafeMath库改编的函数替换了易受攻击的算术运算,在Solidity源代码级别上手动修补了这些合约。然后使用原始合约中使用的完全相同的Solidity编译器版本和优化选项来编译手动修补的源代码(如etherscan.io所述)。
将EVMPATCH补丁测试器应用于生成的补丁合约版本,并验证了报告的结果。这能够验证两种修补方法是否都中止了相同的攻击事务。另外,可以比较gas消耗的开销和代码大小的增加。请注意在手动修补方法中,不会修补Osiris检测到的所有潜在漏洞,因为跳过了对攻击者无法利用的那些算术运算的检查,即仅包含在函数中的漏洞算术运算只能被攻击者调用。使用上表中列出的与ERC-20代币合约相关的总数506,607个实际交易来验证补丁的正确性。
在被识别为攻击的交易中,发现了一个针对HXGToken的特定交易。事务确实确实触发了整数溢出,但是HXGToken通过将它们转移到黑洞地址0x0来销毁一些Token。销毁的Token无法恢复,黑洞地址的余额不影响合约的行为。在分析合约时,Osiris不了解此黑洞地址的语义,并报告可能的整数溢出。然后,EVMPATCH保守地修补Osiris报告的整数溢出错误,这会导致一个合法交易失败。这种模式可被视为不良的编码实践,因为它浪费了不必要的时间来存储黑洞地址的余额。
Gas费用:修补程序引入的其他代码可能会导致交易失败,并显示错误消息。尽管补丁通常不会显着增加gas消耗,但是当交易的发送方提供非常紧张的gas预算时,仍会发生这种行为。当由于修补程序异常而导致重新执行带有修补代码的事务而导致早期失败时,将无法准确地将修补合约与原始合约的行为进行比较。为了解决这个问题,在EVM中禁用了gas统计。在上表中报告了交易执行过程中的额外gas量。排除了那些不执行包含易受攻击代码的功能的交易,这些交易不受补丁影响,因此与本文度量无关。
结果表明,与BEC,SMT和HXG合约相比,用EVMPATCH修补的合约在运行时产生的gas开销(83gas,47gas和120gas)要比在源代码级别上修补的合约(164gas,108gas和541gas)。这是由于以下事实:仅添加很少的检查时,Solidity编译器会生成非最佳代码。特别是,Solidity利用内部函数调用来调用SafeMath整数溢出检查。尽管这样可以减小代码大小(如果需要在多个位置进行检查),但它总是需要执行其他指令(从而增加了开销)来调用内部函数并从中返回。相反,EVMPATCH内联了安全的数字运算,从而减少了gas费用。需要指示Solidity编译器有选择地启用函数内联,以产生与EVMPATCH相似的gas成本。
请注意,对于手动修补的SCAToken,平均gas开销为0。这是因为只有一个事务触发了SafeMath整数溢出检查。但是,这是一次攻击交易,并且会提前中止,因此无法进行gas费用计算。对于UET和SCA,发现比手动修补版本的gas开销更高。实际上,修补版本中的每笔交易,UET平均需要255个单位的额外gas。相比之下,手动修补版本仅添加21gas。这是由于字节码重写器保守地修补了Osiris在这两个合约(分别为12和10)中报告的每个潜在漏洞。但是,实际上并非所有漏洞都可以被利用,因此没有在手动修补过程中对其进行检测。
代码大小增加:在以太坊区块链中部署合约也会产生与部署的合约规模成比例的成本。更具体地说,以太坊每字节收取200 gas的费用以将合约代码存储在区块链上。从上表中认识到,当修补一个漏洞时,重写器添加的额外代码量与SafeMath方法相当。由于方法复制了原始基本块,因此代码大小开销取决于漏洞的特定位置。对于BECToken合约,重写器将代码大小增加到小于源代码级别补丁的大小。 Solidity编译器生成的包含SafeMath库的代码比补丁所必需的要多。即使考虑了字节码重写的开销,对于该合约,EVMPATCH生成的修补程序比手动修补方法小。
但是,如果修补了许多漏洞,则EVMPATCH会增加稍高的开销。自然,升级后的合约的大小会随着由于内联而修复的漏洞数量的增加而增加。例如,字节码重写器为UET合约生成了12个补丁,为SCA合约生成了10个补丁,从而使代码大小增加了1299B(18.2%)和3811B(17.3%)。在数据集中最坏的情况下,代码大小的这种增加导致每次部署的额外成本微不足道,为0.18美元。补丁程序模板目前已针对补丁一个易受攻击的算法进行了优化。在为字节码重写器开发补丁模板时,直接采用类似于Solidity内部函数调用的方法,可以在修补许多整数溢出时减少代码大小的开销。
EVMPATCH在14,107个合约数据集中平均对一个合约应用了3.9个补丁。原始合约的平均代码大小为8142.7 B(σ5327.8B,σ=31.3min,最快的33min和最慢的110min)。使用EVMPATCH应用补丁后,平均大小增加了455.9 B(σ333.5B)。应用补丁后,这等于5.6%的平均代码大小开销。鉴于以太坊向合约创建交易收取每字节200gas的费用,在撰写本文时,它产生的平均管理费用为91,180gas或0.02美元。在观察到的最坏情况下,EVMPATCH在部署时会产生199,800gas的间接费用,在撰写本文时,这仅相当于大约0.04美元的额外部署成本。这表明,通过字节码重写应用补丁的开销对于合约部署而言可以忽略不计,尤其是与可能危及的以太坊数量相比。
部署成本:新修补的合约的部署成本在使用EVMPATCH运营智能合约的成本中占主导地位。但是,此外,还需要进行一笔交易来切换逻辑合约的地址。由于代理模式不需要状态迁移,因此此事务需要恒定量gas。在EVMPATCH中使用的代理合约在转换交易期间消耗了43.167gas,即约0.01美元。当前,除了代理模式以外,迁移状态是最可行的合约升级策略。先前的工作估计,即使只有5000个ERC-20持有者,即智能合约用户,在最佳情况下,状态迁移的费用也可能超过100美元。因此,与将所有数据迁移到新合约的成本相比,EVMPATCH的0.01美元的额外成本微不足道。
检测攻击:EVMPATCH的补丁程序测试器还能够识别任何先前的攻击交易。在上图中观察到,尽管在第一次攻击后的相当合理的时间内就报告了其他Token合约的漏洞,但在漏洞披露之前很久(5个月)就已经利用了UET。更令人惊讶的是,尽管公开披露漏洞后所有交易量都减少了,但所有合约仍然相当活跃。尽管在撰写本文之前的一年左右就已发现了所有这些漏洞,但在公开披露这些漏洞(包括成功的攻击)之后,仍然存在23,630笔交易(这些漏洞占被评估交易的4.66%)发布给这些脆弱的合约。这意味着这些合约的所有者没有正确迁移到补丁程序版本,也没有正确通知用户这些合约的易受攻击状态。
(2)误报/漏报分析
在对易受攻击的合约进行分析的过程中,发现了由Osiris的漏洞报告引起的误报和误报。这表明补丁程序测试是该过程中的重要一步,因为许多分析工具都不精确。发现在默认配置中,Osiris通常会达到有限的代码覆盖率。为此,在整个分析和对SMT求解器的查询中都使用了不同的超时设置,并结合了多次运行的结果以实现更好的代码覆盖率。此外发现,与原始Osiris论文中的主张相反,在两种特定情况下,Osiris不能准确地检测到所有漏洞。
Hexagon(HXG)Token:该合约容易受到整数溢出的攻击,这使得攻击者可以转移非常大量的ERC-20Token。 Osiris报告了两个误报,这是由Solidity编译器生成的EVM代码引起的。即使在Solidity源代码中所有类型都是无符号类型,编译器也会生成一个有符号加法。在此处,当将−2添加到balanceOf映射变量时,Osiris报告可能的整数溢出。当使用负值执行带符号整数加法时,当结果从负值范围移至正值范围时,加法运算自然会溢出,反之亦然。这样,EVMPATCH会为无符号算术运算修补已检查的加法,该运算将始终溢出。使用补丁程序测试器观察所有失败的事务,并对补丁合约的字节码进行手动分析,以确定根本原因是Solidity编译器中的问题,即与简单的无符号减法相比,生成的代码需要附加指令。
Social Chain(SCA):结果还显示,在分析SCAToken时,Osiris存在问题。虽然Osiris确实在有问题的Solidity源代码行中的乘法运算过程中检测到了可能的溢出,但它并未在同一源代码行中检测到加法时可能出现的整数溢出。但是,在实际的攻击事务中,整数溢出发生在未标记的加法运算期间。因此,这构成了Osiris的误报性问题。由于Osiris未报告易受攻击的添加,因此EVMPATCH也不会自动对其进行修补。相反,对于手动打补丁的版本,将两种算术运算都考虑在内。相关的攻击交易先前被报告为攻击交易。
评估摘要:总而言之,对整数溢出检测的评估表明,EVMPATCH可以正确地将补丁应用于智能合约,从而防止任何整数溢出攻击。此外,在部署和运行期间,EVMPATCH仅产生可忽略不计的瓦斯费用;特别是与处于危险之中的以太相比。分析表明即使在受到攻击且存在漏洞之后,被分析的易受攻击的智能合约仍在积极使用中公开披露。这激发了对及时修补框架工作(例如EVMPATCH)的需求。最后,基于对26,385,532笔交易的广泛而详细的分析,证明了EVMPATCH始终保留合约的原始功能,除了少数情况下(由第三方工具Osiris生成的漏洞报告)不准确或不良。使用了编码实践(黑洞地址)。
C.开发者研究
开发人员背景:为了量化修补智能合约和评估EVM PATCH的有效性所需的手动工作,与6位专业开发人员进行了深入研究,这些开发人员在使用区块链技术和开发智能合约方面具有不同的经验。开发人员认为自己熟悉区块链技术,但对开发Solidity代码不是很熟悉。以前,没有开发者开发过可升级的合约。这样可以量化智能合约开发者学习和应用可升级合约模式所需的工作量。
方法:在整个研究过程中,要求开发人员手动执行由EVMPATCH自动执行的多个任务:(1)在给定静态分析器(OSIRIS)输出的情况下,手动修补由于整数溢出错误而易受攻击的三个合约,(2)使用EVMPATCH手动将合约转换为可升级合约,以及(3)通过编写自定义补丁模板,使用EVMPATCH修补访问控制错误。这三个任务涵盖了不同的场景ios,其中EVMPATCH对开发人员可能有用。前两个任务涉及如何使用EVMPATCH来以最少的人工干预来修补已知的错误类。对于这两个任务,假设没有打补丁智能合约的先验知识。相反,第三项任务是扩展EVMPATCH。这需要了解错误类并执行根本原因分析以正确修补漏洞。与前两个任务相比,这无疑更具挑战性。由于第三项任务涵盖了不同的错误类别,因此认为由于开发人员首先完成了其他两项任务,因此数据没有明显的偏差。
对于所有任务,本研究测量了开发人员执行任务所需的时间(不包括阅读任务说明所需要的时间)。要求开发人员对他们对相关技术的熟悉程度,对补丁的置信度以及在7点Likert量表上执行任务的难度进行评分。记录的时间度量如上表所示,在github存储库中提供了支持文件。然后,使用EVMPATCH进行了手动代码审查和交叉检查,以分析开发人员所犯的错误。研究结果表明,需要手动进行正确的智能合约修补,而EVMPATCH可以实现简单,用户友好和高效的修补。时间测量表明,没有EVMPATCH经验的开发人员能够在几分钟内利用EVMPATCH执行复杂的任务。
修补整数溢出错误:要求开发人员以三份合约解决所有整数溢出漏洞:(1)BEC(CVE-2018-10299,299行代码),(2)HXG(CVE-2018-11239,102行代码)和(3)SCA(CVE-2018-10706,404行代码)。为了提供一组具有代表性的合约,选择了三个ERC-20合约,这些合约具有不同的复杂性(根据代码行),并且其中的静态分析还包括遗漏的错误和错误的警报。在所有三个合约上都运行OSIRIS,并为开发人员提供了分析结果以及SafeMath Solidity库的副本。
这与现实非常相似,在这种情况下,区块链开发人员需要根据最新的最新漏洞分析工具的分析结果快速修补智能合约,并可以在线查找手动修补教程。所有开发人员都手动正确地修补了所有三个合约的源代码,这证明了他们在区块链开发方面的专业知识。但是,不利的一面是,开发人员平均花了51.8分钟(σ= 16.6分钟)来为这三个合约创建补丁版本。相比之下,EVMPATCH完全自动执行修补过程,并且能够在最多10秒内为三个合约生成修补程序。
转换为可升级合约:开发人员必须将给定的智能合约转换为可升级的智能合约。本研究为开发人员提供了对委托调用代理模式的简短描述,并要求他们将给定的合约转换为两个合约:一个代理合约和一个基于原始合约的逻辑合约。没有提供有关如何处理存储布局问题的更多信息,明确允许使用在线找到的代码。开发人员平均需要66.3分钟才能将合约转换为可升级的合约。没有开发人员执行将其正确转换为可升级合约的过程,这也反映在开发人员报告的正确性的中位数置信度为2.5。发现了两个主要错误:(a)代理合约仅支持一组固定函数,即代理不支持向合约中添加函数,以及(b)更重要的是,六分之一的开发人员正确处理了存储代理合约和逻辑合约中的冲突,即六个转换合约中的五个被设计破坏。因此,它保持开放状态,开发人员需要花费多长时间才能执行正确的转换。
接下来,要求开发人员利用EVMPATCH来创建和部署可升级合约。由于EVMPATCH不需要有关可升级合约的任何先验知识,因此开发人员最多可以在3分钟内部署正确的可升级合约。此外,使用EVMPATCH进行修补可提高对修补程序正确性的信心(中位数为7,这是范围内的最佳评级)。这充分证实了使用EVM PATCH部署代理确实优于手动修补和升级。
扩展EVMPATCH:开发人员必须为EVMPATCH编写自定义补丁模板。研究者指导开发人员如何使用EVMPATCH以及如何使用EVMPATCH的补丁模板语言编写补丁模板。此外向开发人员提供了扩展的错误报告,该报告显示了如何利用访问控制错误。开发人员利用了完整的EVMPATCH系统,即EVMPATCH应用了补丁并使用补丁测试器组件验证了补丁,该组件重放了来自区块链的过去交易,并通知开发人员是否:(a)补丁可以防止已知的攻击,以及(b)该修补程序是否破坏了其他先前合法交易中的功能。这样,EVMPATCH允许开发人员在几分钟之内创建一个功能完整且可安全修补的可升级合约。平均而言,开发人员只需要5.5分钟(最多15分钟)即可创建自定义补丁模板。
不出所料,所有开发人员都使用EVMPATCH正确地修补了给定的合约,因为EVMPATCH的补丁测试器会向开发人员报告错误的补丁。 EVMPATCH的集成补丁测试器使开发人员对其补丁程序充满信心。平均而言,开发人员报告的置信度为6.6(σ= 0.4),其中7为最高置信度。此外,没有开发人员认为编写这样的自定义补丁模板特别困难。
总结:本文研究证实了EVMPATCH具有高度的自动化,效率和可用性,从而使开发人员摆脱了手动和容易出错的任务。特别是,这六个开发人员中,没有一个能够产生正确的可升级合约,主要是由于难以保存存储布局。研究还证实,即使对于不了解EVMPATCH内部工作原理的开发人员,使用自定义补丁模板扩展EVMPATCH是一项可行的任务。
0x05 Conclusion
更新错误的智能合约是区块链技术领域的主要挑战之一。由于基础技术的设计,攻击者可以快速成功滥用智能合约错误:始终在线且可用。尽管许多建议引入了可帮助开发人员查找错误的框架,但对于开发人员和社区如何快速,自动地对已部署合约中的漏洞作出反应,这仍然是开放的。在这项工作中开发了一个框架,该框架支持基于字节码重写的智能合约错误的自动即时修补。
在评估方面能够证明在不违反智能合约功能正确性的情况下,可以成功修补现实世界中的易受攻击合约。开发人员研究表明,自动修补方法可以大大减少修补智能合约所需的时间,并且实现EVMPATCH实际上可以集成到智能合约开发人员的工作流程中。自动修补程序将使开发人员能够对所报告的漏洞做出快速反应,从而提高智能合约的可信度和接受度。