最近利用智能合约代码中的错误进行的攻击造成了毁灭性的后果,从而质疑了这项技术的好处。目前,修复错误并及时部署修补过的合约极具挑战性。由于区块链系统的分布式特性,智能合约始终在线,因此即时修补尤为重要。智能合约管理着大量资产,这些资产面临风险,并且在遭受攻击后往往无法恢复。升级智能合约的现有解决方案依赖于手动且容易出错的流程。本文提出了一个名为 EVMPATCH 的框架,可以立即自动修补错误的智能合约。 EVMPATCH 为流行的以太坊区块链提供字节码重写引擎,并透明/自动地将常见的现成合约重写为可升级的合约。 EVMPATCH 的概念验证实现会自动强化易受整数上溢/下溢和访问控制错误影响的智能合约,可以轻松扩展以涵盖更多错误类别。我们对 14,000 个真实世界合约的评估表明,我们的方法成功阻止了针对合约发起的攻击交易,同时保持合约的预期功能完好无损。我们与经验丰富的软件开发人员进行了一项研究,表明 EVMPATCH 是实用的,并将给定的 Solidity 智能合约转换为可升级合约的时间减少了 97.6%,同时确保与原始合约的功能等效。
现代区块链系统中使用智能合约来实现几乎任意(图灵完备)的业务逻辑。它们支持加密货币或代币的自主管理,并有可能通过消除对受信任(可能是恶意的)第三方的需求来彻底改变许多业务应用程序,例如在支付、保险、众筹或供应链的应用程序中。由于它们的易用性和其中一些合约持有的高货币价值(加密货币),智能合约已成为一个有吸引力的攻击目标。智能合约代码中的编程错误可能会产生毁灭性的后果,因为攻击者可以利用这些漏洞窃取加密货币或代币。
最近,区块链社区目睹了多起智能合约错误导致的事件。一个特别臭名昭著的事件是“TheDAO”重入攻击,导致价值超过 5000 万美元的以太币损失。这导致了以太坊区块链的一个备受争议的硬分叉。一些提案展示了如何通过在开发时进行离线分析或通过执行运行时验证来防御可重入漏洞。另一个臭名昭著的事件是奇偶校验钱包攻击。在这种情况下,攻击者将智能合约移动到无法再访问合约持有的货币的状态。由于访问控制错误,这导致总共约 500,000 个 Ether 被困在智能合约中。此类访问控制漏洞的自动检测先前已在自动漏洞利用生成的背景下进行过研究。此外,整数溢出错误构成了智能合约中的主要漏洞类别。当算术运算结果的宽度超过整数类型所能容纳的宽度时,就会出现此类错误。根据托雷斯等人的一项研究。超过 42,000 份合约存在整数错误。它们尤其会影响所谓的 ERC-20 代币合约,这些合约在以太坊中被用来创建子货币。有趣的是,一些已披露的漏洞实际上被利用,导致大量代币和以太币损失。
这些攻击激发了社区对提高智能合约安全性的兴趣。在这方面,过去几年提出了许多解决方案,从设计更好的开发环境到使用更安全的编程语言、形式验证、符号执行和动态运行时分析。我们指出,所有这些解决方案仅旨在证明某种类型漏洞的正确性或不存在性,因此不能用于保护已部署的(遗留)合约。尽管一些合约集成了升级机制(参见第 2 节),但一旦某个特定合约被标记为易受攻击,就不清楚如何自动修补它并测试修补后合约的有效性。尽管在源代码级别手动修补合同似乎是合理的,但补丁可能会意外破坏兼容性并使升级后的合同无法使用。例如,鉴于以太坊的特殊存储布局设计,delegatecall-proxy 模式要求开发人员确保合约的补丁版本与之前部署的版本兼容。即使是像更改源代码中变量顺序这样的小改动也会破坏这种兼容性。这也带来了挑战,即开发人员必须遵守严格的编码标准,并且必须使用完全相同的编译器版本。因此,修补智能合约错误目前是一个耗时、繁琐且容易出错的过程。例如,在修补 Parity 多重签名钱包合约时,引入了一个漏洞。攻击者能够成为新部署的库合约的所有者。这允许攻击者破坏合约并破坏所有依赖于多重签名钱包库合约的合约。结果,大量的以太币现在被锁定在那些被破坏的合约中。最重要的是,修补智能合约错误非常关键。与在 PC 或移动软件中发现的错误相比,从攻击者的角度来看,智能合约错误是独一无二的,因为 (1) 智能合约始终在区块链上在线,(2) 它们通常持有大量资产,以及 (3) ) 攻击者不需要考虑其他环境变量(例如,软件和库版本、网络流量分析、垃圾邮件或网络钓鱼邮件以通过用户操作触发漏洞利用)。
Contributions. 在本文中,我们解决了自动及时修补智能合约的问题,以帮助开发人员立即对报告的智能合约错误采取行动。我们引入了一种新颖的修补框架(在第 3 节中),它具有用于以太坊智能合约的字节码重写器,独立于源编程语言并且适用于未修改的合约代码。我们的框架,称为 EVMPATCH,利用字节码重写引擎来确保补丁的侵入性最小,并且新打补丁的合约与原始合约兼容。特别是,我们的框架会自动重放修补合约上的交易以
EVMPATCH 使用尽力而为的方法来确保引入的补丁不会破坏函数,方法是使用先前发布的合约交易以及开发人员提供的单元测试进行测试。虽然这种差异化测试方法不能提供有关修补合同正确性的正式证明,但它无需正式规范即可工作。我们的实验(见第 5.2.1 节)表明,这种方法在实践中足以识别损坏的补丁。
通过在字节码级别应用补丁,EVMPATCH 独立于使用的编程语言/编译器和编译器版本。也就是说,EVMPATCH 支持任何现成的以太坊智能合约代码。我们采用字节码编写来确保最小侵入性补丁,这些补丁在设计上与合约的存储布局兼容,我们认为源级补丁不容易在我们建议的自动补丁过程中使用。然而,对于任何在二进制或字节码级别工作的方法,我们都必须解决几个技术挑战(第 4 节)。此外,EVMPATCH 会自动将原始合约转换为使用 delegatecall-proxy 模式。因此,EVMPATCH 能够以完全自动化的方式自动部署新修补的合约,而无需任何开发人员干预。
虽然原则上 EVMPATCH 可以支持修补不同类别的漏洞(参见第 4.5 节),但我们的概念验证实现针对两大类访问控制和整数溢出(第 5 节)错误。后者在高价值 ERC-20 合约中被反复利用,而前者在 Parity 钱包攻击中被滥用。
为了在性能、有效性和功能正确性方面评估 EVMPATCH,我们将 EVMPATCH 应用于 14,000 个真实世界的易受攻击合约。为此,我们使用了 EVMPATCH 框架的补丁测试组件,在补丁合约上将所有现有交易重放至原始合约。这使我们能够对几个积极利用的智能合约进行深入调查,例如令牌燃烧和攻击交易历史(公开披露之前和之后)。对于我们在评估中调查的许多合同,我们发现 EVMPATCH 可以阻止在公开披露漏洞后发生的几次攻击。这表明即使这些合约被正式弃用,它们仍然被合法用户使用并被恶意行为者利用。因此,迫切需要 EVMPATCH 提供的工具,它允许智能合约的开发人员有效地修补他们的合约。我们的评估还涵盖了重要的实际方面,例如 gas 和性能开销(即在以太坊中执行交易的成本)。我们所有修补合约的 gas 开销低于每笔交易 0.01 美元,性能开销可以忽略不计。
为了评估 EVMPATCH 的实用性,我们进行了一项复杂的开发人员研究,重点是比较使用和不使用 EVMPATCH 修补和部署可升级合约的可用性(第 5.3 节)。我们的研究表明,开发人员需要 62.5 分钟(中位数)来手动(不使用 EVMPATCH)将一个简单的智能合约(用大约 80 行代码实现通用钱包功能)转换为可升级的智能合约。尽管有相当长的时间,但他们都没有执行正确的转换,导致合约破裂和潜在的易受攻击的合约。因此,这个时间度量必须被视为一个下限,因为正确转换更复杂的合约将花费更多时间。相比之下,开发人员在 1.5 分钟(中位数)内使用 EVMPATCH 执行了相同的任务——减少了97.6%——同时生成正确的可升级合约。
在本节中,我们提供有关以太坊虚拟机 (EVM)、二进制重写和一些常见合约升级策略的背景信息。
EVM & Smart Contracts: 以太坊区块链系统的核心是一个自定义虚拟机,称为以太坊虚拟机 (EVM),它执行以太坊智能合约。 EVM 由具有自定义指令格式的简单的基于堆栈的虚拟机组成。每条指令都表示为一个一字节的操作码。参数在数据堆栈上传递。唯一的例外是 push 指令,它用于将常量压入堆栈。这些常量直接编码到指令字节中。此外,EVM 遵循哈佛架构模型并将代码和数据分离到不同的地址空间。实际上,EVM 具有用于不同目的的不同地址空间:代码地址空间,其中包含智能合约的代码并被认为是不可变的,用于存储全局状态的存储地址空间,以及用于临时数据的内存地址空间。
在以太坊网络中,网络中的每个矿工和每个完整节点都必须执行智能合约,以计算和验证区块前后的状态。以太坊具有一种机制来限制每个智能合约的执行时间并奖励执行智能合约的矿工:所谓的gas。每条 EVM 指令都需要一定的 gas 预算才能执行。交易发送者选择以太币中每单位 gas 的价格,当交易被包含在区块中时,相应的以太币将作为奖励转移给矿工。最小化执行合约所需的 gas 很重要,因为它间接地最小化了在以太坊中运行智能合约的成本。
智能合约是以面向对象的方式开发的,即每个智能合约都有一个定义好的功能接口:合约的 ABI(应用程序二进制接口)。每当一个智能合约调用另一个智能合约时,它都会使用其中一个调用指令,例如 CALL 或 STATICCALL。然后被调用的合约将处理提供的输入并相应地更新自己的状态。一个合约不能直接访问其他合约的状态(即存储区域),必须始终根据 ABI 使用函数调用来检索另一个合约的任何数据。
与常规 CALL 指令相比,DELEGATECALL 指令将在调用者合约的上下文中执行被调用合约的代码。引入该指令是为了实现库合约,即公共功能可以一次部署到区块链,多个合约可以依赖一个库合约。这意味着被调用者,即库合约,可以完全访问调用者的状态(存储)和 Ether 资金。因此,使用 DELEGATECALL 指令的合约必须完全信任被调用者。
Binary Rewriting: 二进制重写是一种众所周知的在编译后检测程序的技术。 二进制重写也被用于改进安全加固技术,如控制流完整性、已编译的二进制文件,以及动态地将安全补丁应用到正在运行的程序。 对于传统架构上的二进制重写,已经开发了两种风格的方法:静态和动态重写。
动态方法即时重写代码,即在代码执行时。 这避免了对大型二进制文件的不精确静态分析。 然而,动态二进制重写需要一个中间层,它在运行时分析和重写代码。 由于 EVM 不支持动态代码生成或修改,因此无法在以太坊中有效地应用这种方法。 相比之下,静态二进制重写适用于以太坊,因为它完全离线工作。 它依靠静态分析来恢复足够的程序信息来准确地重写代码。
Contract Upgrade Strategies: 一旦智能合约部署在区块链上,以太坊就会将其代码视为不可变的。为了解决这个问题,社区提出了部署升级智能合约的策略。最幼稚的方法是将修补后的合约部署在新地址,并将原始合约的状态迁移到该地址。但是,状态迁移是特定于合约的,必须由合约的开发人员手动实现。它要求合约开发者可以访问旧合约的所有内部状态,以及新合约中接受状态转移的程序。为避免状态迁移,开发者还可以使用单独的合约作为数据存储合约,有时也称为永恒存储模式。然而,这会增加额外的 gas 开销,因为每次逻辑合约需要访问数据时,它都必须对数据存储合约执行代价高昂的外部调用。
一种更常见的策略是使用代理模式编写合约,最有利的版本是委托调用代理模式。在这里,一个智能合约被拆分为两个不同的合约,一个用于代码,一个用于数据存储:i)不可变代理合约,持有所有资金和所有内部状态,但不实现任何业务逻辑; ii) 一个逻辑合约,它是完全无状态的并实现了所有实际的业务逻辑,即这个合约包含管理合约动作的实际代码。代理合约是所有用户交易的入口点。它具有不可变的代码,并且其地址在合约的整个生命周期内保持不变。逻辑合约实施规则,这些规则控制智能合约的行为。代理合约使用 DELEGATECALL 指令将所有函数调用转发到注册的逻辑合约。该指令用于授予逻辑合约访问代理合约中存储的所有内部状态和资金的权限。为了升级合约,需要部署一个新的逻辑合约,并在代理合约中更新其地址。然后代理合约将所有未来的交易转发给修补后的逻辑合约。因此,部署升级合约不需要任何数据迁移,因为所有数据都存储在不可变代理合约中。此外,升级过程对用户也是透明的,因为合约地址保持不变。虽然现有的区块链平台没有提供升级智能合约的机制,但使用这种代理模式可以让 EVMPATCH 以可忽略的成本(在 gas 消耗方面)快速升级合约。
在本节中,我们将介绍我们的自动修补框架的设计,以及时修补和强化智能合约。我们的框架在未经修改的智能合约上运行,并且独立于源代码编程语言,因为它不需要源代码。在其核心,我们的框架利用字节码重写器将侵入性最小的补丁应用到 EVM 智能合约。结合基于代理的可升级智能合约,这种字节码重写方法允许开发人员自动引入补丁并将它们部署在区块链上。这种方法的一个主要优点是,当发现新的攻击类型或发现错误的工具改进时,可以在短时间内自动重新检查、修补和重新部署合约,并且开发人员的干预最少。 EVMPATCH 通常在开发人员的机器上执行,并持续运行新的和更新的漏洞检测工具。这还可以包括动态分析工具,用于分析尚未包含在区块中但已可用于以太坊网络的交易。每当其中一个分析工具发现新的漏洞时,EVMPATCH 就会自动修补合约,测试修补后的合约并进行部署。
代理模式使得在以太坊中轻松部署打补丁的智能合约成为可能。但是,它既不生成修补版本,也不对修补合约进行功能测试。 EVMPATCH 提供了一个全面的框架和工具链来自动及时地修补和测试生成的补丁的有效性,从而填补了这一空白。
如表 1 所示,在以太坊中自动生成补丁有两种可能的策略:源代码或 EVM 字节码的静态重写。乍一看,源代码补丁似乎是一种选择,因为开发人员可以访问源代码,他们能够检查源代码更改,甚至可以在自动化方法引入不希望的更改时进行调整。然而,在以太坊中,在应用源代码重写时存在一个主要挑战:需要仔细保留存储布局。否则,修补后的合约将破坏其内存并失败或(更糟)引入危险的错误。即,源代码中的某些更改可能会破坏合约兼容性,即使更改不会破坏合约的逻辑。
为了将事物置于上下文中,静态大小的变量从地址 0 开始连续排列在存储中;并且大小小于 32 B 的连续变量可以打包到单个 32 B 存储槽中。因此,在源代码中重新排序、添加或删除变量的任何更改可能看起来无害,但在内存级别,此类更改将导致变量映射到错误和意外的存储地址。换句话说,变量声明的更改破坏了合约的内部状态,因为遗留合约和修补合约具有不同的存储布局。
相比之下,字节码重写不会受到这种缺陷的影响,因为许多错误类只需要在 EVM 指令级别上进行更改(参见第 5 节),避免任何容易出错的存储布局更改。选择字节码重写的另一个原因是现有的智能合约漏洞检测工具。到目前为止,他们中的大多数都在 EVM 级别上运行,并在 EVM 级别上报告他们的发现。字节码重写方法可以利用这些分析工具的报告直接生成基于 EVM 字节码的补丁。最后,如果使用源代码重写,开发人员对修补合约的有效性进行彻底测试的可能性有限。特别是,针对旧交易(包括封装攻击的交易)检查修补后的合约在字节码级别更可行。也就是说,交易测试自然仍然需要在字节码级别进行分析,以对攻击交易进行逆向工程,以及它们如何针对修补后的合约失败。字节码重写允许开发人员直接将重写的字节码指令与攻击交易进行匹配,从而使取证分析成为可能。鉴于所有这些原因,我们决定选择字节码重写。
我们在图 1 中描绘的框架由以下主要组件组成:(1)由自动分析工具和公开漏洞披露组成的漏洞检测引擎,(2)将补丁应用到合约的字节码重写器,(3)补丁测试机制验证先前交易的补丁,以及 (4) 合约部署组件上传合约的补丁版本。首先,漏洞检测引擎识别漏洞的位置和类型。然后将此信息传递给字节码重写器,后者根据先前定义的补丁模板对合约进行补丁。修补后的合约随后被转发到补丁测试器,后者将所有过去的交易重播到合约。也就是说,我们不仅修补合约,还允许开发人员检索在原始合约和修补合约之间表现出不同行为和结果的交易列表。这些交易可作为对原始合约的潜在攻击的指标。如果列表为空,我们的框架会立即在以太坊区块链上自动部署打补丁的合约。接下来,我们将更详细地描述我们设计的四个主要组件。
Vulnerability Detection. 在能够应用补丁之前,我们的框架需要识别和检测漏洞。 为此,我们的框架利用了现有的漏洞检测工具,例如 . 对于任何现有工具未检测到的漏洞,我们要求开发人员或安全顾问创建漏洞报告。 在我们的系统中,漏洞检测组件负责识别指令的确切地址、漏洞所在的位置以及漏洞的类型。 然后将此信息传递给字节码重写器,后者相应地修补合约。
Bytecode Rewriter. 一般来说,静态二进制重写技术非常适合在以太坊中应用补丁,因为智能合约的代码大小相对较小:通常在大约 10 KiB 的范围内。此外,EVM 智能合约始终静态链接到所有库代码。合约不可能动态地将新代码引入代码地址空间。与在运行时加载动态链接库的传统架构相比,这使得对二进制重写技术的依赖更加简单。但是,一些智能合约仍然使用类似于动态链接库的概念:专用的 EVM 调用指令允许合约切换到不同的代码地址空间。我们通过将我们的字节码重写器应用于合约本身和库合约来解决这个特殊性。
EVM 的基于堆栈的架构在实现补丁时需要特别注意:当新代码插入代码地址时,对智能合约代码地址空间中任何代码或数据的所有基于地址的引用都必须保留或更新空间。此类引用无法从字节码中轻松恢复。为应对这一挑战,EVMPATCH 使用基于蹦床的方法将新的 EVM 指令添加到空代码区域。实现细节将在第 4 节中描述。
为了实施补丁,字节码重写器处理易受攻击合约的字节码以及漏洞报告。重写基于所谓的补丁模板,该模板根据漏洞类型进行选择并调整为与给定的合约配合使用。
Patch Templates. 在 EVMPATCH 中,我们使用基于模板的修补方法:对于每个受支持的漏洞类别,都将补丁模板集成到 EVMPATCH 中。 此补丁模板会自动适应正在补丁的合约。 我们创建通用补丁模板,以便它们可以轻松应用于所有合约。 EVMPATCH 通过替换特定于合约的常量(即代码地址、函数标识符、存储地址)自动使补丁模板适应手头的合约。 常见漏洞(例如整数溢出)的补丁模板作为 EVMPATCH 的一部分提供,EVMPATCH 的典型用户永远不会与补丁模板交互。 但是,可选地,智能合约开发人员还可以检查或调整现有的补丁模板,甚至为 EVMPATCH 尚不支持的漏洞创建额外的补丁模板。
Patch Tester. 由于智能合约直接处理资产(例如 Ether),因此任何修补过程都不得妨碍合约的实际功能,这一点至关重要。因此,任何补丁都必须经过彻底测试。为了解决这个问题,我们引入了一种补丁测试机制,它基于 (1) 记录在区块链上的交易历史 (2) 可选的开发人员提供的单元测试。在这一点上,我们利用任何区块链系统记录智能合约的所有先前执行的事实,即以太坊中的交易。在我们的例子中,补丁测试器重新执行所有现有交易和可选的任何可用单元测试,并验证旧遗留的所有交易和新打补丁的合约的行为是否一致。补丁测试器检测旧合约和新补丁合约之间的任何行为差异,并向开发人员报告具有不同行为的交易列表。也就是说,作为副产品,我们的补丁测试机制可以用作取证攻击检测工具。即,在执行修补过程时,开发人员还将被通知任何先前滥用任何修补漏洞的攻击,然后可以采取相应的行动。如果合约的两个版本的行为方式相同,则可以自动部署打补丁的合约。否则,开发人员必须调查可疑交易列表,然后调用合约部署组件来上传打补丁的合约。可疑交易列表不仅可以作为潜在攻击的指标,还可以揭示修补后的合约在函数上的不正确,即修补后的合约在良性交易中表现出不同的行为。在第 5 节中,我们对现实世界的易受攻击的合约进行了彻底的调查,以证明 EVMPATCH 在不破坏合约的原始功能的情况下成功地应用了补丁。
Contract Deployment. 如第 2 节所述,基于 delegatecall-proxy 的升级方案是启用即时合约修补的选项。 因此,EVMPATCH 集成了这种部署方法,利用代理合约作为所有具有恒定地址的交易的主要入口点。 在第一次部署之前,EVMPATCH 将原始未修改的合约代码转换为使用 delegatecall-proxy 模式。 这是通过部署一个代理合约来完成的,该合约是不可变的,并假定已正确实现。然后使用字节码重写器将原始字节码转换为逻辑合约,只需对原始代码进行少量更改。 然后将逻辑合约与代理合约一起部署。
Patch Deployment. 最后,当合约打好补丁后,补丁测试器组件测试完补丁后,EVMPATCH 就可以部署新打补丁的合约了。 我们的升级方案将新修补的合约代码部署到新地址,并向之前部署的代理合约发出专用交易,将逻辑合约的地址从旧的易受攻击版本切换到新修补的版本。 任何进一步的交易现在都由修补后的逻辑合约处理。
Human Intervention. EVMPATCH 被设计为完全自动化。 但是,在某些情况下,需要开发人员干预如果 。(1) 漏洞报告与 EVMPATCH 尚不支持的错误类相关, (2)补丁测试器报告至少一个交易由于新引入的补丁而失败,并且失败的交易不是已知的攻击交易,(3)补丁测试器报告新引入的补丁没有阻止至少一个已知的攻击事务。
如果不支持错误类,EVMPATCH 会通知开发人员不支持的漏洞类。由于 EVMPATCH 是可扩展的,它很容易允许开发人员提供自定义补丁模板,从而快速适应针对智能合约的新攻击。更具体地说,EVMPATCH 支持自定义补丁模板的多种格式:EVM 指令,一种简单的特定领域语言,类似于 Solidity 表达式并允许开发人员对函数强制执行前置条件(类似于 Solidity 修饰符)。我们在 5.3 节中进行了一项开发人员研究,以证明编写补丁模板是可行的,并且比手动修补合约更成功。
如果补丁测试人员发现新的失败交易,开发人员必须分析是否发现了新的攻击交易或合法交易失败。对于新发现的攻击交易,EVMPATCH 将此交易添加到攻击列表并进行处理。否则,开发人员会调查合法交易失败的原因。正如我们在 § 5.2.2 中的评估所示,这种情况通常是由于漏洞报告不准确而发生的,即错误报告的漏洞而不是错误的补丁。因此,开发人员可以简单地将错误报告的易受攻击的代码位置列入黑名单,以避免在这些位置打补丁。
这些手动干预通常只需要快速代码审查或调试器会话。我们相信即使是经验丰富的 Solidity 开发人员也可以执行这些任务,因为不需要有关底层字节码重写系统的详细知识(另请参阅我们的开发人员研究中的第 5.3 节)。因此,EVMPATCH 将自己定位为一种工具,使更多的开发人员能够安全地编程和操作以太坊智能合约。
在本节中,我们描述了 EVMPATCH 的实现:在第 4.1 节中,我们讨论了以太坊字节码重写的工程挑战。 此后,我们描述了字节码重写器(第 4.2 节)、补丁测试功能(第 4.3 节)和合约部署机制(第 4.4 节)的实现。 我们在第 4.5 节中讨论有关智能合约错误的可能应用来结束本节。
重写 EVM 字节码时必须解决几个独特的挑战:我们需要处理原始 EVM 字节码的静态分析,并解决 Solidity 合约和 EVM 的几个特殊性。
与传统计算机架构类似,EVM 字节码使用地址来引用代码地址空间中的代码和数据常量。 因此,在修改字节码时,重写器必须确保正确调整基于地址的引用。 为此,重写器通常采用两种静态分析技术:控制流图 (CFG) 恢复和后续数据流分析。 后者对于确定哪些指令是代码中使用的任何地址常量的来源是必要的。 对于 EVM 字节码,有两类指令与此上下文相关:代码跳转和常量数据引用。
Code Jumps. EVM 具有两条分支指令:JUMP 和 JUMPI。 两者都从堆栈中获取目标地址。 请注意,同一合约内的函数调用也会利用 JUMP 和 JUMPI。 也就是说,函数内部的局部跳转和调用其他函数之间没有明确的区别。 EVM 还具有专用的调用指令,但这些指令仅用于将控制权转移到完全独立的合约。 因此,它们在重写字节码时不需要修改。
Constant Data References. 利用所谓的CODECOPY指令将数据从代码地址空间复制到内存地址空间。 一个常见的示例用例是大数据常量,例如字符串。 与跳转指令类似,加载内存的地址通过堆栈传递给 CODECOPY 指令。
由于 EVM 的基于堆栈的架构,处理这两种类型的指令都具有挑战性。例如,跳转指令的目标地址总是在堆栈上提供。也就是说,每个分支都是间接的,即不能简单地通过检查跳转指令来查找目标地址。相反,为了解决这些间接跳转,需要部署数据流分析技术来确定将目标地址推送到堆栈的位置和位置。对于这些跳转中的大部分,我们可以通过分析周围的基本块来追溯跳转目标被压入堆栈的位置。例如,当观察指令 PUSH2 0xdb1; JUMP
时,我们可以通过从push指令中检索地址(0xdb1)来恢复跳转目标。
但是,许多合约包含更复杂的代码模式,主要是因为 Solidity 编译器还支持在不使用调用指令的情况下在内部调用函数。回想一下,在 EVM 中,调用指令的执行类似于远程过程调用。为了优化代码大小并促进代码重用,Solidity 编译器引入了一个概念,其中函数被标记为internal
。这些函数不能被其他合约(合约私有)调用,并遵循不同的调用约定。由于内部函数没有专门的返回和调用指令,Solidity 使用跳转指令来模拟两者。因此,不能轻易区分函数返回和正常跳转。这使得 (1) 识别内部功能和 (2) 构建合约的准确控制流图变得具有挑战性。
在重写EVM智能合约时,字节码重写器中需要同时考虑jump指令和codecopy指令。重写智能合约的明显策略是在插入新指令或删除旧指令后修复代码中的所有常量地址以反映新地址。然而,这种策略具有挑战性,因为它需要准确的控制流图恢复和数据流分析,这需要处理 EVM 代码的特殊性,例如内部函数调用。在传统架构的二进制重写的研究领域,已经开发了一种更实用的方法:蹦床概念 。我们在重写器中使用这种方法并避免调整地址。每当我们的重写器必须对基本块执行更改时,例如插入指令,我们的重写器就会用蹦床替换基本块,该蹦床会立即跳转到打补丁的副本。因此,原始代码中的任何跳转目标保持不变,所有数据常量都保留在其原始地址。我们将在下一节中更详细地描述这个过程。
我们在 Python 中实现了一个基于蹦床的重写器,并利用 pyevmasm 库来反汇编和组装原始 EVM 操作码。我们基于蹦床的字节码重写器在基本块级别上工作。当需要检测指令时,将整个基本块复制到合约末尾。然后将补丁应用到这个新副本。原来的基本块被替换成蹦床,即立即跳转到复制的基本块的短指令序列。每当合约跳转到原始地址的基本块时,就会调用蹦床,通过跳转指令将执行重定向到修补后的基本块。为了恢复执行,被检测的基本块的最后一条指令会跳转回原始合约代码。虽然基于蹦床的方法避免了修复任何引用,但它引入了额外的跳转指令。然而,正如我们将展示的,与这些额外跳跃相关的 gas 成本在实践中可以忽略不计(参见第 5 节)。
为了确保正确执行,我们仍然必须至少计算一个部分控制流图,从打补丁的基本块开始。这对于恢复被修补的基本块和通过所谓的下降边缘连接的后续基本块的边界是必要的。并非所有基本块都以显式控制流指令终止:每当基本块以条件跳转指令 (JUMPI) 结束或只是不以控制流指令结束时,就会存在隐式边沿(即落空)在控制流图中到以下地址的指令。
Handling Fall-Through Edge. 为了处理下降沿,必须考虑两种情况。当下降沿所针对的基本块以 JUMPDEST 指令开始时,基本块被标记为 EVM 中常规跳转的合法目标。在这种情况下,我们可以在合约末尾附加一个显式跳转到重写的基本块,并确保在原始合约代码中的下一个基本块的开头继续执行。如果接下来的基本块不以 JUMPDEST 指令开头,EVM 禁止显式跳转到该地址。在控制流图中,这意味着这个基本块只能通过下降边到达。为了处理这种情况,我们的重写器将基本块复制到合约末尾,就在重写后的基本块后面,在重写代码的控制流图中构建另一个下降边。
图 2 显示了我们的重写器如何更改原始合约的控制流图的示例。 ADD 指令被一个额外执行整数溢出检查的检查添加例程替换。我们称 ADD 指令的地址为补丁点。包含补丁点的基本块被替换为蹦床。在这种情况下,它会立即跳转到 0xFFB 处的基本块。这个位于原始合约末尾的基本块是 0xAB 处原始基本块的副本,但应用了补丁。由于基本块现在位于合约末尾,字节码重写器可以在基本块中插入、更改和删除指令,而无需更改位于较高编号地址的代码中的任何地址。我们用 INVALID 指令填充原始基本块的其余部分,以确保基本块与原始基本块具有完全相同的大小。 0xCD 处的基本块通过下降沿连接到前一个基本块。然而,这个基本块以 JUMPDEST 指令开始,因此是一个合法的跳转目标。因此,重写器然后在 0xFFB 处向修补的基本块附加一个跳转,以确保在地址 0xCD 处的原始合约代码中继续执行。
Adapting to EVM. 在实现字节码重写器时,必须考虑 EVM 的一些特殊性。也就是说,EVM 在代码地址空间中强制执行代码和数据的某种分离。 EVM 实现可防止跳转到嵌入到 PUSH 指令中的数据常量。PUSH指令的常量操作数紧跟在PUSH指令操作码的字节之后。这种常量操作数可能会意外地包含 JUMPDEST 指令的字节。然后,常量将成为合法的跳转目标,并且会出现新的非预期指令序列。为避免此类意外指令序列,EVM 实现对代码部分执行线性扫描以查找所有PUSH指令。作为这些PUSH指令一部分的常量随后被标记为数据,因此被标记为无效的跳转目标,即使它们包含等效于 JUMPDEST 指令的字节。但是,由于性能原因,EVM 实现在标记数据时会忽略控制流信息。因此,PUSH指令操作码字节本身可以是某些数据常量的一部分,例如字符串或其他二进制数据。出于这个原因,智能合约编译器在严格大于任何可访问代码的地址处累积所有数据常量,避免生成的代码与编码到代码地址空间中的数据之间发生任何冲突。然而,我们基于蹦床的重写器确实在智能合约的数据常量后面附加了代码。为了避免重写器附加的代码由于前面的PUSH操作码字节而被意外标记为无效的跳转目标,我们小心地在原始合约的数据和新附加的代码之间插入填充。
Applicability of Trampoline Approach. 基于蹦床的重写方法只需要最少的代码分析并且适用于大多数用例。 然而,这种方法面临两个问题。 首先,指令只能在足够大(以字节为单位的大小)以包含蹦床代码的基本块中修补。 然而,一个典型的蹦床需要 4 到 5 个字节,并且执行一些有意义的计算的基本块通常足够大以包含蹦床代码。 其次,由于基本块的复制,代码大小根据打补丁的基本块而增加,从而增加了部署成本。 然而,我们的实验表明,部署期间的开销可以忽略不计(每次部署平均 0.02 美元,参见第 5 节)。
No reliance on accurate control-flow graph. 仅给定 EVM 字节码恢复准确的控制流图是一个具有挑战性和开放性的问题。 然而,我们基于蹦床的方法不需要准确和完整的控制流图。 相反,我们只需要根据需要应用补丁的指令的程序计数器恢复基本块边界。 这样做时,恢复基本块边界是容易处理的,因为 EVM 具有基本块条目的显式标记(即 JUMPDEST 伪指令)。 此外,我们的重写器只需要恢复基本块的末尾以及通过控制流图中的下降边连接的任何后续基本块。
虽然将蹦床插入原始代码并不会改变合约的功能,但补丁模板本身可以执行任意计算,并且可能会违反补丁合约的语义。为了测试修补后的合约,EVMPATCH 使用了差异测试方法。也就是说,我们重新执行合约的所有交易,以确定原始的、易受攻击的代码和新修补的代码的行为是否不同。 EVMPATCH 将过去的交易用于直接从区块链检索的合约。如果合约带有单元测试,EVMPATCH 还会利用单元测试来测试新打补丁的合约。这种差异化测试方法不能保证形式上的正确性
合同。具有少量可用交易的合约容易出现测试覆盖率低的情况。然而,我们的实验(参见第 5.2.1 节)表明,差异测试方法在实践中运行良好,可以证明补丁不会破坏功能。鉴于合约功能的正式规范的可用性,EVMPATCH 还可以利用模型检查器来更严格地验证打补丁的合约。
在差异测试期间,我们首先从区块链中检索易受攻击合约的交易列表。其次,我们重新执行所有这些事务并检索每个事务的执行路径。然后,我们重新执行相同的交易,但将易受攻击的合约代码替换为修补后的合约代码,以获得第二个执行路径。我们使用基于流行的 go-ethereum 客户端的修改过的 Ethereum 客户端,因为原始客户端不支持此功能。最后,我们比较两个执行路径,补丁测试器生成一个交易列表,其中的行为不同。如果没有这样的交易,那么我们假设补丁不会抑制合约的功能并继续部署打补丁的合约。
原始合约和打补丁的合约的执行路径永远不会相等,因为打补丁会改变控制流程并插入指令。因此,我们只检查可能会改变状态的指令,即写入存储区(即 SSTORE)或将执行流程转移到另一个合约(例如 CALL 指令)的指令。然后我们比较所有状态改变指令的顺序、参数和结果,并找到两条执行路径不同的第一条指令。目前,我们假设引入的补丁不会导致任何新的状态改变指令。这个假设适用于引入输入验证代码并在传递无效输入时恢复的补丁。然而,路径差异计算可以适应于了解补丁引入的潜在状态变化。在作为补丁的一部分的代码中失败的报告事务被标记为潜在攻击事务。如果报告的交易由于修补代码中的gas不足而失败,我们会使用增加的gas预算重新运行相同的交易。我们发出警告,因为用户将不得不考虑补丁引入的额外 gas 成本。最后,开发人员必须检查报告的交易以确定给定的交易列表是合法的还是恶意的。作为一个副作用,这使我们的补丁测试器成为易受攻击合约的攻击检测工具,允许开发人员快速找到先前的攻击交易。
如第 3 节所述,EVMPATCH 利用基于delegatecall-proxy(委托调用代理)的升级模式来部署打补丁的合约。为此,EVMPATCH 将智能合约拆分为两个合约:代理合约和逻辑合约。代理合约是主要入口点并存储所有数据。默认情况下,EVMPATCH 使用随 EVMPATCH 提供的代理合约。然而,EVMPATCH 也可以重用现有的可升级合约,例如使用 ZeppelinOS 框架开发的合约 。用户与位于固定地址的代理合约进行交互。为了方便升级过程,代理合约还实现了更新逻辑合约地址的功能。为了防止恶意升级,代理合约还存储了所有者的地址,允许其发布升级。然后升级只需向代理合约发送一笔交易,代理合约将 (1) 检查调用者是否是所有者,以及 (2) 更新逻辑合约的地址。
代理合约从存储中检索新逻辑合约的地址,并将所有调用转发到该合约。在内部,代理合约利用 DELEGATECALL 指令调用逻辑合约。这允许逻辑合约获得对代理合约的存储内存区域的完全访问,从而允许在没有任何额外开销的情况下访问持久数据。
字节码重写器采用补丁模板,该模板被指定为 EVM 汇编语言的简短片段。 然后根据修补合同专门化该模板并重新定位到修补合同的末尾。 这种基于模板的补丁生成方法允许指定多个通用补丁来解决整类漏洞。 下面,我们列出了可以立即从我们的框架中受益的可能的漏洞类。
Improper access control 只需在函数的开头插入一个检查来验证调用者是某个固定地址还是等于合约状态中存储的某个地址,就可以修补对关键函数的不正确访问控制。 在之前的工作中已经研究了处理此漏洞的检测工具
Mishandled exceptions 当合约使用低级调用指令时,可能会发生处理不当的异常,其中返回值不会自动处理,并且合约没有正确检查返回值。 这个问题可以通过在这样的调用指令之后插入一个通用的返回值检查来修补。
Integer bugs 在处理整数算术时,很可能会出现整数错误,因为 Solidity 默认不使用检查算术。 这导致许多潜在的易受攻击的合约被部署,一些合约被主动攻击。 鉴于这些漏洞的普遍性,我们将在下一节讨论如何使用 EVMPATCH 自动修补整数溢出错误。
在下文中,我们通过将 EVMPATCH 应用于访问控制错误和整数错误这两个主要错误类别来证明其有效性。
在本节中,我们报告了 EVMPATCH 在修补两种突出类型的错误方面的评估结果:(1) 访问控制错误,和 (2) 整数错误(上溢/下溢)
Parity MultiSig 钱包是访问控制错误的一个突出例子。该合约实现了一个由多个账户拥有的钱包。钱包合约采取的任何行动都必须得到至少一位所有者的授权。然而,该合约存在一个致命错误,允许任何人成为唯一所有者,因为相应的函数 initWallet、initMultiowned 和 initDayLimit 没有执行任何访问控制检查。
图 3 显示了修补后的源代码,它向函数 initMultiowned 和 initDayLimit(在图 3 中用 ➀ 标记)添加了内部修饰符。该修饰符使这两个函数无法通过已部署合约的外部接口访问。此外,补丁添加了自定义修饰符 only_uninitialized,用于检查合约是否先前已初始化(用 ➁ 标记)。
开发人员最初在部署修补后的合约时引入了一个新漏洞,该漏洞被积极利用。相比之下,由于 EVMPATCH 执行字节码重写,它会立即生成一个安全修补的合约版本,并会以安全的方式自动部署它。
考虑图 4,它显示了 EVMPATCH 使用的域特定语言中的自定义补丁来指定补丁。因此,我们在 initWallet 函数的开头插入一个补丁,用于检查条件 sload(m_numOwners) == 0 是否成立,即合约是否尚未初始化。如果这不成立,合约执行将中止执行 REVERT 指令。请注意,此处需要使用显式 sload 来从存储加载变量,并且该表达式与图 3 中的补丁在逻辑上相反,因为该补丁实质上插入了 Solidity require 语句。此外,需要从公共函数调度程序中删除另外两个可公开访问的函数。图 4 所示的补丁结合了 EVMPATCH 提供的两个现有补丁模板。首先,add require 补丁模板在输入函数之前强制执行一个前提条件。其次,删除公共函数补丁模板从调度器中删除一个公共函数,有效地将该函数标记为内部函数。
Evaluation Results. 我们通过部署 WalletLibrary 合约的修补版本来对抗攻击,验证了修补后的合约不再可被利用。 此外,我们将源级补丁与 EVMPATCH 应用的补丁进行比较。 表 2 显示了结果的概述。 EVMPATCH 仅将合约大小增加 25 B。 initWallet 函数的额外 gas 成本仅为 235 gas,即每笔交易 0.000,06 美元,235.091 美元/ETH,典型的 gas 价格为 1 Gwei。 这表明 EVMPATCH 可以有效地为访问控制错误插入补丁。
由于整数类型的固定位宽,典型的整数类型被绑定到最小和/或最大大小。但是,程序员通常没有足够注意实际整数类型的大小限制,这可能会导致整数错误。幸运的是,几种高级编程语言(Python、Scheme)能够避免整数错误,因为它们利用几乎无限大小的任意精度整数。然而,事实上的智能合约标准编程语言,即 Solidity,并没有嵌入这样的机制。这将处理整数溢出的负担完全留给了开发人员,他们需要手动实施溢出检查或正确利用 SafeMath 库来安全地执行数字运算。虽然很常见,但前者显然容易出错。例如,最近公布了 ERC-20 代币合约中的多个漏洞。这些合约管理以太坊区块链上的子货币,即所谓的代币。此类代币可以处理大量货币,因为它们跟踪每个代币所有者的代币余额并调解代币和以太币的交换。图 5 显示了 BEC 代币合约代码的摘录,它举例说明了此类整数溢出漏洞。在第 6 行计算总量时,使用了未经检查的整数乘法,允许攻击者提供非常大的 _value。因此,amount 变量将被设置为一个小数目。这有效地绕过了第 11 行中的余额检查,允许攻击者将大量代币转移到攻击者控制的帐户。最近,在超过 42,000 个合约中发现了类似的漏洞。
我们开发了补丁模板,用于检测标准 EVM 整数宽度(即无符号 256 位整数)的整数溢出和下溢。对于整数加法、减法和乘法,这些模板添加了受 C 编程语言和 SafeMath Solidity 库中的安全编码规则启发的检查。当检测到违规时,EVMPATCH 发出异常以中止和回滚当前对合约的调用。
为了验证我们的字节码重写器生成的补丁的正确性,我们使用了最先进的整数检测工具 Osiris 进行漏洞检测。在分析了以太坊区块链前 5,000,000 个区块中的 50,535 个独特合约后,Osiris 在 14,107 个合约中至少检测到一个整数溢出漏洞。使用 EVMPATCH,我们能够成功地自动修补几乎所有这些合约。更具体地说,我们无法修补 14107 个调查合约中的 33 个合约,因为检测到的漏洞所在的基本块对于蹦床代码来说太小了。
在这 14107 份合约中,约有 8000 份涉及以太坊网络上的交易。为了生成一个大型且具有代表性的评估数据集,我们从以太坊区块链中提取了发送到这些合约的所有交易,直到区块 7,755,100(2019 年 5 月 13 日),产生了 26,385,532 笔交易。
使用我们的补丁测试器重放这些交易表明,对于 95.5% 的易受攻击合约,EVMPATCH 生成的补丁符合与这些合约相关的所有先前交易。对于其余 4.5% 的调查合约,我们的补丁由于以下原因之一拒绝了交易:(1) 我们成功阻止了恶意交易,(2) 报告的漏洞是误报,不应修补,或 ( 3) 我们无意中改变了合约的功能。
为了仔细审查,我们从那些可以被 EVMPATCH 成功修补的合约中选择了 ERC-20 代币合约,并确认已成功攻击整数溢出/下溢漏洞(见表 3)。出于比较的目的,我们还在 Solidity 源代码级别手动修补这些合约,方法是用改编自 SafeMath 库的函数替换易受攻击的算术运算。然后使用与原始合约中使用的完全相同的 Solidity 编译器版本和优化选项(如 etherscan.io 上报告的)编译手动修补的源代码。
我们将 EVMPATCH 补丁测试器应用于生成的补丁合约版本并验证报告的结果。这使我们能够验证两种修补方法是否会中止相同的攻击事务。此外,我们可以比较gas消耗的开销和代码大小的增加。请注意,在手动修补方法中,我们不会修补 Osiris 检测到的所有潜在漏洞,因为我们跳过对攻击者无法利用的算术运算添加检查,即包含在函数中的易受攻击的算术运算只能由合同的控制者或所有者。我们使用与表 3 中列出的 ERC-20 代币合约相关的 506,607 笔真实交易来验证我们补丁的正确性。
表 3 显示了补丁测试器的事务执行结果。我们验证了中止的交易,并确认除了一笔交易外,所有交易都对应于真正的攻击,这类似于我们在下面详细讨论的代币销毁的特殊情况。除了有效的攻击交易外,重新执行的交易的执行路径与原始交易的执行路径相匹配,确认我们的补丁没有破坏合约的功能。
我们的结果表明,对于 BEC、SMT 和 HXG 合约,使用 EVMPATCH 修补的合约在运行时产生的 gas 开销(83 gas、47 gas 和 120 gas)与在源代码级别(164 gas、108 gas 和 541 gas)打补丁的那些相比更少。这是因为当只添加很少的检查时,Solidity 编译器会生成非最佳代码。特别是,Solidity 利用内部函数调用来调用 SafeMath 整数溢出检查。虽然这减少了代码大小(如果需要在多个地方进行检查),但它总是需要执行额外的指令——从而增加 gas 开销——来调用和从内部函数返回。相比之下,EVMPATCH 内联了安全的数字操作,从而引入了更少的 gas 开销。人们需要指示 Solidity 编译器有选择地启用函数内联,以产生与 EVMPATCH 类似的 gas 成本。
请注意,手动修补的 SCA 令牌的平均 gas 开销为 0 gas。 这是因为只有一个事务会触发 SafeMath 整数溢出检查。 然而,这是一个攻击交易,它被提前中止,使得无法计算gas开销。
对于 UET 和 SCA,我们发现比手动修补版本更高的 gas 开销。 事实上,在修补版本中,UET 平均每笔交易需要 255 个单位的额外 gas。 相比之下,手动修补版本仅增加了 21 个gas。 这是因为我们的字节码重写器保守地修补了 Osiris 在这两个合约中报告的每个潜在漏洞(分别为 12 个和 10 个)。然而,并非所有这些漏洞实际上都是可利用的,因此我们没有在手动修补期间检测它们。
Code Size Increase. 在以太坊区块链中部署合约也会产生与部署合约大小成正比的成本。更具体地说,以太坊每字节收取 200 gas 以将合约代码存储在
区块链。从表 3 中,我们认识到,当修补单个漏洞时,我们的重写器添加的额外代码量与 SafeMath 方法相当。由于我们的方法复制了原始基本块,因此代码大小开销取决于漏洞的具体位置。在 BEC 代币合约的情况下,我们的重写器增加的代码大小小于源级补丁。 Solidity 编译器为包含 SafeMath 库生成的代码多于补丁所严格需要的代码。即使考虑字节码重写的开销,我们观察到 EVMPATCH 生成的补丁比此合约的手动补丁方法小。
但是,如果修补了许多漏洞,EVMPATCH 会增加稍高的开销。自然地,升级合约的大小会随着需要修复的内联漏洞数量而增加。例如,我们的字节码重写器为 UET 合约生成了 12 个补丁,为 SCA 合约生成了 10 个补丁,导致代码大小增加了 1299 B (18.2%) 和 3811 B (17.3%)。在我们数据集中的最坏情况下,这种代码大小的增加导致每次部署 0.18 美元的额外成本可以忽略不计。
我们的补丁模板目前针对修补单个易受攻击的算法进行了优化。在为我们的字节码重写器开发补丁模板时,我们采用了类似于 Solidity 内部函数调用的方法,这将在修补许多整数溢出时减少代码大小开销。
EVMPATCH 平均对我们的 14,107 个合约数据集中的一个合约应用 3.9 个补丁。原始合约的平均代码大小为 8142.7 B (σ 5327.8 B)。使用 EVMPATCH 应用补丁后,平均尺寸增加为 455.9 B (σ 333.5 B)。这相当于应用补丁后 5.6% 的平均代码大小开销。鉴于以太坊向合约创建交易收取每字节 200 gas 的费用,在撰写本文时,它产生的平均开销为 91,180 gas 或 0.02 美元。在我们观察到的最坏情况下,EVMPATCH 在部署时会产生 199,800 个 gas 的开销,在撰写本文时仅相当于 0.04 美元的额外部署成本。这表明应用字节码重写补丁的开销对于合约部署来说是微不足道的,尤其是与可能面临风险的以太币数量相比时。
Costs of Deployment. 新打补丁的合约的部署成本在使用 EVMPATCH 运行智能合约的成本中占主导地位。 但是,另外还需要一个交易来切换逻辑合约的地址。 由于代理模式不需要状态迁移,因此该交易需要恒定数量的gas。 我们在 EVMPATCH 中使用的代理合约在切换交易期间消耗了 43.167 个 gas,即大约 0.01 美元。 目前,除了代理模式之外,状态迁移是最可行的合约升级策略。 之前的工作估计,即使只有 5000 个 ERC-20 持有者,即智能合约用户,在最好的情况下,状态迁移的成本也可能超过 100.00 美元。 因此,与将所有数据迁移到新合约的成本相比,EVMPATCH 0.01 美元的额外成本可以忽略不计。
Detecting Attacks. EVMPATCH 的补丁测试器还允许我们识别任何先前的攻击交易。 在图 6 中,我们还观察到,虽然其他代币合约的漏洞是在第一次攻击后相当合理的时间内报告的,但 UET 早在漏洞披露之前(5 个月)就已被利用。 更令人惊讶的是,尽管在公开披露漏洞后交易量有所下降,但所有合约仍然相当活跃。 尽管所有这些漏洞在撰写本文前一年左右都已被发现,但在公开披露漏洞后,仍有 23,630 笔交易(占评估交易的 4.66%)向这些易受攻击的合约发出,其中包括成功的攻击。 这意味着这些合约的所有者没有正确迁移到修补版本,用户也没有被正确通知这些合约的易受攻击状态。
在我们对易受攻击的合约进行分析时,我们发现了由 Osiris 漏洞报告引起的误报和漏报。 这表明我们的补丁测试是该过程中的一个重要步骤,因为许多分析工具不精确。我们发现在默认配置中,Osiris 经常实现有限的代码覆盖率。 为此,我们对整个分析和对 SMT 求解器的查询使用了不同的超时设置,并结合了多次运行的结果以实现更好的代码覆盖率。 此外,我们发现——与原始 Osiris 论文中的主张相反——并非所有漏洞都能在两个特定情况下被 Osiris 准确检测到。
Hexagon (HXG) Token. 该合约容易受到整数溢出的影响,这允许攻击者转移非常大量的 ERC-20 代币 [26]。 Osiris 报告了两个误报,这是由 Solidity 编译器生成的 EVM 代码引起的。即使 Solidity 源代码中的所有类型都是无符号类型,编译器也会生成有符号加法。在这里,当 -2 被添加到 balanceOf 映射变量时,Osiris 报告可能的整数溢出。当执行带有负值的有符号整数加法时,当结果从负值范围移动到正值范围时,加法自然会溢出,反之亦然。因此,EVMPATCH 为无符号算术运算修补检查加法,该运算将始终溢出。使用我们的补丁测试器,我们观察所有失败的交易并对补丁合约的字节码进行手动分析,以确定根本原因是 Solidity 编译器中的问题,即与简单的无符号减法相比,生成的代码需要额外的指令.
Social Chain (SCA). 我们的结果还显示,在分析 SCA 令牌时,Osiris 存在问题。虽然 Osiris 确实在有问题的 Solidity 源代码行中检测到乘法期间可能的溢出,但它没有检测到同一源代码行中加法可能存在的整数溢出。但是,在实际的攻击交易中,整数溢出发生在未标记的加法操作中。因此,这构成了 Osiris 的假阴性问题。由于 Osiris 未报告易受攻击的添加,因此 EVMPATCH 也不会自动修补。相比之下,对于手动修补的版本,我们考虑了两种算术运算。相关的攻击交易先前被报告为攻击交易。
Summary of Evaluation. 总而言之,我们对整数溢出检测的评估表明,EVMPATCH 可以正确地将补丁应用于智能合约,防止任何整数溢出攻击。 此外,EVMPATCH 在部署和运行期间仅产生可忽略不计的 gas 开销; 尤其是与处于危险之中的以太币相比。 我们的分析表明,即使在受到攻击和公开披露漏洞之后,所分析的易受攻击的智能合约仍在积极使用中。 这激发了对及时修补框架(例如 EVMPATCH)的需求。 最后,基于对 26;385;532 笔交易的广泛而详细的分析,我们证明 EVMPATCH 始终保留合约的原始功能,除了少数情况下,漏洞报告(由第三方工具 Osiris 生成)不准确 或使用了糟糕的编码实践(黑洞地址)。
Developer Background. 为了量化修补智能合约所需的手动工作并评估 EVMPATCH 的实用性,我们与 6 位在使用区块链技术和开发智能合约方面具有不同经验的专业开发人员进行了深入研究。 我们的开发人员认为自己熟悉区块链技术,但不太熟悉开发 Solidity 代码。 之前没有开发人员开发过可升级的合同。 因此,我们可以量化智能合约开发人员学习和应用可升级合约模式所需的工作量。
Methodology. 在整个研究过程中,我们要求开发人员手动执行由 EVMPATCH 自动执行的多项任务:(1)根据静态分析器(OSIRIS)的输出,手动修补三个易受整数溢出漏洞影响的合约,(2)转换合约手动和使用 EVMPATCH 升级合同,以及 (3) 通过编写自定义补丁模板使用 EVMPATCH 修补访问控制错误。这三个任务涵盖了不同的场景,其中 EVMPATCH 对开发人员很有用。前两个任务包括使用 EVMPATCH 以最少的人工干预修补已知的错误类。对于这两个任务,我们假设没有关于修补智能合约的先验知识(见表 5 开发人员如何评价他们之前的智能合约经验)。相比之下,第三个任务包括扩展 EVMPATCH。这需要了解错误类别并执行根本原因分析以正确修补漏洞。与前两项任务相比,这无疑更具挑战性。由于第三个任务涵盖了不同的错误类别,我们认为由于开发人员首先完成了其他两个任务,因此数据没有明显偏差。
对于所有任务,我们测量了开发人员执行任务所需的时间(不包括阅读任务描述所需的时间)。我们要求开发人员评估他们对相关技术的熟悉程度、他们对补丁的信心程度以及在 7-point Likert 量表上执行任务的难度。完整的问卷和开发人员的回答如表 5 所示,记录的时间测量值如表 4 所示。我们在 github 存储库中提供支持文件。
然后,我们执行了手动代码审查和与 EVMPATCH 的交叉检查,以分析开发人员所犯的错误。我们的研究结果表明,手动正确修补智能合约需要付出巨大努力,而 EVMPATCH 可以实现简单、用户友好和高效的修补。时间测量表明,之前没有 EVMPATCH 经验的开发人员能够在几分钟内使用 EVMPATCH 执行复杂的任务。
Patching Integer Overflow Bugs. 我们要求开发人员修复三个合约中的所有整数溢出漏洞:1 个 BEC(CVE-2018-10299,299 行代码)和 2 个 HXG(CVE-2018-11239,102 行代码)和 3 个 SCA(CVE- 2018-10706,404 行代码)。为了提供一组具有代表性的合约,我们选择了三个具有不同复杂性(在代码行方面)的 ERC-20 合约,其中静态分析还包括遗漏的错误和误报(参见第 5.2.2 节)。我们在所有三个合约上运行 OSIRIS,并向开发人员提供分析输出以及 SafeMath Solidity 库的副本。这准确地类似于现实世界的场景,其中区块链开发人员需要根据最近最先进的漏洞分析工具的分析结果快速修补智能合约,并且可以查找在线提供的手动修补教程。所有开发人员都手动正确地修补了所有三个合约的源代码,这证明了他们在区块链开发方面的专业知识。然而,不利的一面是,开发人员平均需要 51.8 分钟(σ = 16.6 分钟)为三个合约创建补丁版本。相比之下,EVMPATCH 将补丁过程完全自动化,并且能够在最多 10 秒内为三个合约生成补丁。
Converting to an Upgradable Contract. 开发人员必须将给定的智能合约转换为可升级的智能合约。我们向开发人员提供了委托调用-代理模式的简短描述,并要求他们将给定的合约转换为两个合约:一个代理合约和一个基于原始合约的逻辑合约。我们没有提供有关如何处理存储布局问题的更多信息,并且我们明确允许使用在线找到的代码。开发人员平均需要 66.3 min才能将合约转换为可升级的合约。没有任何开发人员正确转换为可升级合约,这也反映在开发人员报告的正确性中的中位置信度为 2.5 。我们观察到两个主要错误:(a) 代理合约只支持一组固定的功能,即代理不支持向合约添加功能,以及 (b) 更重要的是,只有六分之一的开发人员正确处理了存储代理和逻辑合约中的冲突,即六个转换后的合约中有五个被设计破坏了。因此,开发人员执行正确转换需要多长时间仍然是开放的。
接下来,我们要求开发人员利用 EVMPATCH 创建和部署可升级的合约。由于 EVMPATCH 不需要任何关于可升级合约的先验知识,开发人员能够在最多 3 分钟内部署正确的可升级合约。此外,使用 EVMPATCH 打补丁可以激发人们对补丁正确性的高度信心——中位数为 7,这是我们量表中的最佳评级。这有力地证实了使用 EVMPATCH 部署代理确实优于手动修补和升级。
Extending EVMPATCH. 开发人员必须为 EVMPATCH 编写自定义补丁模板。我们指导开发人员如何使用 EVMPATCH 以及如何使用 EVMPATCH 的补丁模板语言编写补丁模板(示例见图 4)。此外,我们向开发人员提供了一份扩展错误报告,显示了如何利用访问控制错误。开发人员利用完整的 EVMPATCH 系统,即 EVMPATCH 应用补丁并使用补丁测试器组件验证补丁,该组件从区块链重放过去的交易,并通知开发人员:(a) 补丁阻止了已知的攻击,以及 (b)补丁是否破坏了其他先前合法交易的功能。因此,EVMPATCH 允许开发人员在几分钟内创建一个功能齐全且安全修补的可升级合约。平均而言,开发人员只需要 5.5 分钟(最多 15 分钟)即可创建自定义补丁模板。正如预期的那样,所有开发人员都使用 EVMPATCH 正确修补了给定的合约,因为 EVMPATCH 的补丁测试人员会向开发人员报告错误的补丁。 EVMPATCH 的集成补丁测试器让开发人员对他们的补丁充满信心。平均而言,开发人员报告的置信水平为 6.6 (σ=0.4),其中 7 是最有信心的。此外,没有一个开发人员认为编写这样一个自定义补丁模板特别困难。
Summary. 我们的研究证实了 EVMPATCH 提供了高度的自动化、效率和可用性,从而将开发人员从手动和容易出错的任务中解放出来。 特别是六个开发者中没有一个能够产生正确的可升级合约,主要是由于存储布局难以保存。 我们的研究还证实,使用自定义补丁模板扩展 EVMPATCH 是一项可行的任务,即使对于不了解 EVMPATCH 内部工作原理的开发人员也是如此。
对“TheDAO”合约的臭名昭著的攻击引起了社区的极大关注。从那时起,发现了许多额外的漏洞利用和防御,主要集中在部署合约之前发现漏洞。卢等人。展示了符号执行器 Oyente,它探索合约代码,同时寻找可能的漏洞。从那时起,人们提出了许多其他具有更好精度、性能和覆盖不同漏洞的符号执行工具。此外,还提出了针对 Solidity 和 EVM 字节码的静态分析器。 Ethainter 分析了多交易环境中的信息流分析和数据清理。此外,形式验证和模型检查的方法已应用于智能合约,并且 EVM 和 Solidity 语言的语义已被形式化。然而,只有一小部分先前的工作研究了动态分析和运行时保护。 Sereum 或 ECFChecker 等工具可以检测对易受攻击合约的实时重入攻击。最近的工作进一步探索了用于保护智能合约的模块化动态分析框架。需要修改智能合约执行环境的保护解决方案不太可能集成到生产区块链系统中。
整数溢出在以太坊智能合约的背景下得到了广泛的研究。 Osiris 是符号执行工具 Oyente 的扩展,用于准确检测整数错误。改进的符号执行引擎首先尝试从 Solidity 编译器生成的特定指令中推断整数类型,即符号和位宽。接下来,它会检查可能的整数错误,例如截断、溢出、下溢和错误的类型转换。我们利用 Osiris 的检测功能,因为它可以精确定位整数溢出错误的确切位置。其他工具(例如 TeEther 和 MAIAN)在生成智能合约漏洞时会隐式地发现整数错误。然而,他们没有报告整数溢出的确切位置,因为他们专注于漏洞利用生成。 ZEUS 利用抽象解释和符号模型检查来验证智能合约的安全属性。虽然 ZEUS 可以检测潜在的整数溢出漏洞,但它是在 LLVM 中间级别进行的,并且无法确定相应 EVM 字节码中的确切位置。
最近,使用 SMARTSHIELD 探索了用于修补智能合约的字节码重写。 SMARTSHIELD 需要完整的控制流图 (CFG) 来更新跳转目标和数据引用。如第 4.1 节所述,由于 EVM 的字节码格式,生成高度准确的 CFG 极具挑战性。我们认为这种字节码重写策略不能扩展到更大更复杂的合约。相比之下,EVMPATCH 基于trampoline 的重写策略不需要精确的CFG,在重写复杂合约时弹性要高得多。SMARTSHIELD 实现了自定义字节码分析来检测漏洞,这可能不如专门分析准确。例如,SMARTSHIELD 的分析不会推断整数类型是否有符号,这对于准确的整数溢出检测很重要。 EVMPATCH 是一个灵活的框架,可以集成许多用于检测漏洞的静态分析工具,并且可以以最少的努力利用分析工具的改进。最后也是最重要的是,EVMPATCH 使部署和管理可升级合同的整个生命周期自动化,而 SMARTSHIELD 旨在强化合同预部署。借助 EVMPATCH,智能合约开发人员还可以修补部署合约后发现的漏洞。
以太坊社区探索了几种设计模式,以允许通过手动迁移到新合约的可升级智能合约,并且代理模式是最受欢迎的(参见第 2 节)。 ZeppelinOS 框架通过实现委托调用代理模式来支持可升级的合约。但是,开发人员必须在 Solidity 级别手动确保旧合约和修补合约的兼容性。这可以使用静态分析工具来实现,这些工具执行“可升级性”检查(例如,Slither 检查兼容的存储布局),这依赖于关于存储分配的编译器行为的准确知识。另一方面,EVMPATCH 结合了现有的分析工具,并提供了一种自动方法来修补检测到的漏洞,同时通过设计保持存储布局的一致性。
更新错误的智能合约是区块链技术领域的主要挑战之一。最近的过去表明,由于底层技术的自然设计,攻击者可以快速成功地滥用智能合约错误:始终在线且可用,一个通用且简单的计算引擎,没有任何微妙的软件和配置依赖项,以及(通常)大量可用的加密货币。虽然许多提案都引入了框架来帮助开发人员发现错误,但开发人员和社区如何快速、自动地对已部署合约上的漏洞做出反应仍然是开放的。在这项工作中,我们开发了一个框架,支持基于字节码重写的智能合约错误的自动和即时修补。在评估方面,我们能够证明现实世界中的易受攻击的合约可以在不违反智能合约功能正确性的情况下成功修补。我们的开发人员研究表明,自动修补方法大大减少了修补智能合约所需的时间,并且我们的实施 EVMPATCH 实际上可以集成到智能合约开发人员工作流程中。我们相信自动修补将提高智能合约的可信度和接受度,因为它允许开发人员对报告的漏洞做出快速反应。
作者要感谢审稿人——尤其是我们的牧羊人曹寅芝——的宝贵反馈,以及开发人员抽出时间参与我们的研究。 这项工作由 Deutsche Forschungsgemeinschaft(DFG,德国研究基金会)根据德国卓越战略 - EXC 2092 CASA - 390781972 和 DFG 作为 CRC 1119 CROSSING 项目 S2 的一部分提供部分资金。 这项工作得到了 EUH2020-SU-ICT-03-2018 CyberSec4Europe 项目的部分支持,该项目由欧盟委员会根据第 2 号赠款协议资助。 830929。