这篇文章最初发布在以太坊博客.
通过以太坊漏洞赏金计划,我们收到了一份关于新的实验性 ABI 编码器(称为 ABIEncoderV2)中存在漏洞的报告。经过调查,我们发现该组件存在几种不同变体。本公告的第一部分详细解释了此错误。新的 ABI 编码器仍然标记为实验性,但我们认为这值得发布公告,因为它已经在主网上使用。
此外,在过去的两周内,优化器中发现了两项低影响错误,其中一项已在 Solidity v0.5.6 中修复。这两个错误都是随着版本 0.5.5 引入的。有关详细信息,请参阅本公告的第二部分。
该 0.5.7 版本 包含对本博文中解释的所有错误的修复。
这里提到的所有错误都应该在触及相关代码路径的测试中轻松显现,至少在以零值和非零值的组合运行测试时。
感谢 Melonport 团队(Travis Jacobs & Jenna Zenk)和 Melon 委员会(Nick Munoz-McDonald, Martin Lundfall, Matt di Ferrante & Adam Kolar)通过以太坊漏洞赏金计划报告此问题!
谁应该关注
如果您部署了使用实验性 ABI 编码器 V2 的合约,那么这些合约可能会受到影响。这意味着只有在源代码中使用以下指令的合约才会受到影响
pragma experimental ABIEncoderV2;
此外,还有许多触发错误的要求。有关更多信息,请参阅下面更详细的技术细节。
据我们所知,主网上大约有 2500 个合约使用实验性 ABIEncoderV2。尚不清楚其中有多少合约包含此错误。
如何检查合约是否存在漏洞
此错误只有在满足以下所有条件时才会显现
- 将包含数组或结构体的存储数据直接发送到外部函数调用、abi.encode 或事件数据,而没有事先分配到局部(内存)变量,并且
- 存在一个数组,该数组包含大小小于 32 字节的元素,或者一个结构体,该结构体包含共享存储槽的元素或类型为 bytesNN 的成员,其长度小于 32 字节。
除此之外,在以下情况下,您的代码不会受到影响
- 如果您所有的结构体或数组只使用 uint256 或 int256 类型
- 如果您只使用整数类型(可能更短),并且一次最多编码一个数组
- 如果您只返回此类数据,并且不在 abi.encode、外部调用或事件数据中使用它。
如果您有一个满足这些条件的合约,并且想要验证该合约是否确实存在漏洞,您可以通过security@ethereum.org.
如何在未来防止此类缺陷
为了谨慎起见,实验性 ABI 编码器只有在显式启用时才可用,以便用户可以在将其视为稳定之前与它交互并测试它,而无需对其过分信任。
我们尽最大努力确保高品质,并且最近开始在 OSS-Fuzz 上对某些部分进行“语义”模糊测试(我们之前对编译器进行了崩溃模糊测试,但那并没有测试编译器的正确性)。
对于开发人员来说,使用漏洞检测器之类的工具很难检测出 Solidity 编译器中的错误,因为在源代码或 AST 表示上运行的工具无法检测到仅在已编译字节码中引入的缺陷。
防止此类缺陷的最佳方法是对您的合约进行严格的端到端测试(验证所有代码路径),因为编译器中的错误很可能不是“静默”的,而是表现为无效数据。
可能的后果
自然,任何错误都可能产生不同的后果,具体取决于程序控制流,但我们预计这更有可能导致故障而不是可利用性。
该错误在触发时,会在某些情况下向其他合约的函数调用发送损坏的参数。
时间线
2019-03-16:
- 通过漏洞赏金计划报告,关于在将直接从存储器中的布尔值数组读取到 ABI 编码器时导致的损坏。
2019-03-16 至 2019-03-21
- 调查根本原因,分析受影响的合约。发现使用实验性编码器编译的合约数量出乎意料地多,其中许多都没有经过验证的源代码。
- 对错误的调查发现了更多触发错误的方法,例如使用结构体。此外,在同一例程中发现了一个数组溢出错误。
- 检查了 Github 上的少数合约,发现没有一个受到影响。
- 对 ABI 编码器进行了错误修复。
2019-03-20:
- 决定公开信息。
- 原因:不可能及时检测到所有易受攻击的合约并联系所有作者,而且阻止主网上进一步传播易受攻击的合约将是一件好事。
2019-03-26:
- 新的编译器版本,版本 0.5.7。
- 发布本文。
技术细节
背景
合约 ABI 是一种规范,用于描述如何从外部(Dapp)或在合约之间交互时交换数据。它支持各种类型的数据,包括简单的数字、字节和字符串值,以及更复杂的数据类型,包括数组和结构体。
当合约收到输入数据时,它必须解码该数据(由“ABI 解码器”完成),并且在返回数据或将数据发送到另一个合约之前,它必须对其进行编码(由“ABI 编码器”完成)。Solidity 编译器为合约中定义的每个函数(以及 abi.encode 和 abi.decode)生成这两部分代码。在 Solidity 编译器中,生成编码器和解码器的子系统称为“ABI 编码器”。
在 2017 年年中,Solidity 团队开始开发一个名为“ABI 编码器 V2”的新实现,目标是拥有一个更灵活、更安全、性能更高且可审计的代码生成器。此实验性代码生成器在显式启用时,从 2017 年底的 0.4.19 版本开始提供给用户。
缺陷
实验性 ABI 编码器无法正确处理小于 32 字节的非整数类型。这适用于 bytesNN 类型、bool、enum 以及其他类型,当这些类型是数组或结构体的组成部分并直接从存储器中编码时。这意味着这些存储引用必须直接在 abi.encode(...) 中使用,作为外部函数调用的参数或事件数据中的参数,而没有事先分配到局部变量。使用 return 不会触发此错误。类型 bytesNN 和 bool 会导致数据损坏,而 enum 可能会导致无效的 revert。
此外,即使基类型为整数类型,元素长度小于 32 字节的数组也可能无法得到正确处理。如果编码的元素数量不是每个槽位可以容纳的元素数量的倍数,则以上述方式编码此类数组可能会导致编码中的其他数据被覆盖。如果在编码中没有数组后面的数据(请注意,动态大小数组始终在具有静态大小内容的静态大小数组之后进行编码),或者如果只编码了一个数组,则不会覆盖其他数据。
两个不相关的错误
与上面解释的 ABI 编码器问题无关,在优化器中发现了两个错误。这两个错误都是随着 0.5.5 版本(发布于 3 月 5 日)引入的。它们不太可能出现在编译器生成的代码中,除非使用内联汇编。
这两个错误是在最近将 Solidity 添加到 OSS-Fuzz 中发现的,OSS-Fuzz 是一个用于查找各种项目中的差异或问题的安全工具包。对于 Solidity,我们包含了多个不同的模糊测试器,用于测试编译器的不同方面。
- 优化器将类似于 ((x << a) << b)) 的操作码序列(其中 a 和 b 是编译时常量)转换为 (x << (a + b)),但没有正确处理加法中的溢出。
- 优化器在使用常量 31 作为第二个参数时,无法正确处理 byte 操作码。这可能发生在对 bytesNN 类型进行索引访问时,其编译时常量值(不是索引)为 31,或者在内联汇编中使用 byte 操作码时。