以太坊 ABI 解析器零尺寸类型漏洞分析与利用
十亿次空乏 - Trail of Bits 博客
什么是以太坊 ABI?
当链上合约交互或链下组件与合约通信时,以太坊使用 ABI 编码来编码请求和响应。该编码不自我描述,相反,编码器和解码器需要提供定义所表示数据类型的模式。与 C 编程语言中平台相关的 ABI 相比,以太坊规定了如何以二进制表示形式在应用程序之间传递数据。尽管规范不是正式的,但它很好地理解了数据交换的方式。
目前,该规范存在于 Solidity 文档中。ABI 定义影响了智能合约语言(如 Solidity 和 Vyper)中使用的类型。
理解漏洞
零尺寸类型(ZST)是在磁盘上存储占用零(或最小)字节,但一旦加载到内存中表示则需要 substantially more 的数据类型。以太坊 ABI 允许零尺寸类型(ZST)。ZST 可以通过强制应用程序分配大量内存来处理少量磁盘或网络表示来导致拒绝服务(DoS)攻击。
考虑以下示例:当解析器遇到 ZST 数组时会发生什么?它应该尝试解析数组声称包含的尽可能多的 ZST。因为每个数组元素占用零字节,定义一个非常大的 ZST 数组是微不足道的。
作为一个具体示例,下图显示了一个 20 字节磁盘的有效负载,它将反序列化为数字 2、1 和 3 的数组。第二个 8 字节磁盘的有效负载将反序列化为 232 个 ZST 元素(如空元组或空数组)。
如果每个 ZST 在解析后占用零字节内存,这将不是问题。实际上,这种情况很少见。通常,每个元素将需要少量但非零的内存来存储,导致表示整个数组的 enormous allocation。这导致拒绝服务攻击。
健壮的解析器设计对于防止崩溃、误解、挂起或 excessive resource usage 等严重问题至关重要。此类问题的根本原因可能在于规范或实现。
在以太坊 ABI 的情况下,我认为规范本身是有缺陷的。它有机会明确禁止零尺寸类型(ZST),但未能这样做。这种 oversight 与最新的 Solidity 和 Vyper 版本形成对比,在这些版本中,定义 ZST(如空元组或数组)是不可能的。
为了确保 maximum safety,文件格式规范必须精心制作,并且它们的实现必须 rigorously fortified 以避免 unforeseen behaviors。
概念验证
让我们深入一些示例,展示几个库中的错误。我们将数据有效负载定义为:
有效负载由两个 32 字节块组成,描述了一个序列化的 ZST 数组。第一个块定义了数组元素的偏移量。第二个块定义了数组的长度。独立于编程语言,我们总是将其称为有效负载。
我们将尝试使用 ABI 模式()[]
和uint32[0][]
使用几个不同的以太坊 ABI 解析库来解码此有效负载。前者是空元组的动态数组,后者是空静态数组的动态数组。动态和静态之间的区别很重要,因为空静态数组占用零字节,而动态数组占用几个字节,因为它序列化数组的长度。
eth_abi (Python)
以下 Python 程序使用官方 eth_abi 库(<4.2.0);程序将首先挂起,然后因内存不足错误而终止。
eth_abi 库仅支持空元组表示;空静态数组未定义。
ethabi (Rust)
ethabi 库(v18.0.0)允许直接从其 CLI 触发错误。
ethers-rs (Rust)
以下 Rust 程序使用 ethers-rs 库和模式uint32[0][]
,通过 Rust 类型Vec<[u32; 0]>
隐式对应。
它容易受到 DoS 问题的影响,因为 ethers-rs 库(v2.0.10)使用 ethabi。
foundry (Rust)
foundry 工具包使用 ethers-rs,这表明 DoS 向量也应该存在那里。结果确实如此!
一种触发错误的方法是直接通过 CLI 解码有效负载,就像在 ethabi 中一样。
另一个更有趣的概念验证是部署以下恶意智能合约。它使用汇编返回与有效负载匹配的数据。
如果合约的返回类型被定义,它可能导致 CLI 工具中的挂起和巨大内存消耗。以下命令在测试网上调用合约。
alloy-rs
如果解码有效负载,alloy-rs(0.4.2)中的 ABI 解析器会遇到与其他库相同的挂起。
ethereumjs-abi
最后,ABI 解析器 ethereumjs-abi(0.6.8)库也容易受到攻击。
其他库
go-ethereum 和 ethers.js 库没有此错误,因为它们隐式不允许 ZST。这些库期望数组的每个元素至少 32 字节长。web3.js 库也不受影响,因为它使用 ethers-js。
漏洞的发现方式
测试这种类型错误的想法是在我偶然发现 borsh-rs 库中的一个问题后产生的。Rust 库尝试在常量时间内解析 ZST 数组,这导致了未定义行为,以缓解 DoS 向量。库的作者最终决定完全禁止 ZST。在另一次审计中,一个自定义 ABI 解析器在解析 ZST 时也有 DoS 向量。看到这两个问题不太可能是巧合,我们调查了其他 ABI 解析库以寻找此错误类别。
如何利用它
此错误是否可利用取决于受影响库的使用方式。在上面的示例中,演示目标是 CLI 工具。
我没有找到一种方法来制作触发此错误并将其部署到主网的智能合约。这主要是因为 Solidity 和 Vyper 程序在其最新版本中不允许 ZST。
然而,使用上述任何库的应用程序都可能容易受到攻击。一个可能易受攻击的应用程序示例是 Etherscan,它解析不受信任的 ABI 声明。此外,任何从合约获取和解码数据的链下软件如果允许用户指定 ABI 类型,则可能容易受到此错误的影响。
模糊测试你的解码器!
解码器中的错误通常很容易通过模糊测试解码例程来捕获,因为输入通常是字节数组,可以直接用作模糊测试器的输入。当然,也有例外,比如最近的 libwebp 0-day(CVE-2023-4863),它没有通过在 OSS-fuzz 中 endless hours of fuzzing 被发现。
在 Trail of Bits 的审计中,我们采用模糊测试来识别错误,并教育客户如何进行自己的模糊测试。我们旨在将我们的模糊测试器贡献给 Google 的 OSS-fuzz 以进行持续测试,从而通过优先考虑关键审计组件来补充手动审查。我们正在更新我们的测试手册,这是一个 exhaustive resource 供开发人员和安全专业人员使用,包括优化模糊测试器配置和自动化分析工具在整个软件开发生命周期中的 specific guidance。
协调披露
作为披露过程的一部分,我们向库作者报告了漏洞。
eth_abi (Python):以太坊拥有的库通过私有 GitHub 咨询修复了错误。错误在版本 v4.2.0 中修复。
ethabi (Rust) 和 alloy-rs:crates 的维护者要求我们在禁运期结束后打开 GitHub 问题。我们在这里和这里创建了相应的问题。
ethereumjs-abi:我们没有收到项目的回复,因此创建了一个 GitHub 问题。
ethers-rs 和 foundry:我们通知了项目关于它们使用 ethabi(Rust)的情况。我们期望它们尽快更新到 ethabi 的修补版本或切换到另一个 ABI 解码实现。一般社区将通过发布 ethabi 和 alloy-rs 的 RustSec 咨询和 eth_abi(Python)的 GitHub 咨询来通知。
披露时间线如下:
2023 年 6 月 30 日:首次联系 ethabi(Rust)、eth_abi(Python)、alloy-rs 和 ethereumjs-abi crates 的维护者。
2023 年 6 月 30 日:alloy-rs 维护者通知应创建 GitHub 问题。
2023 年 6 月 30 日:eth_abi(Python)项目的首次回应和内部分类开始。
2023 年 8 月 2 日:为 eth_abi(Python)在 GitHub 上创建私有安全咨询。
2023 年 8 月 31 日:eth_abi(Python)发布修复,没有公开引用 DoS 向量。我们后来验证了此修复。
2023 年 12 月 29 日:发布此博客文章和在 ethabi、alloy-rs 和 ethereumjs-abi 存储库中的 GitHub 问题。更多精彩内容 请关注我的个人公众号 公众号(办公 AI 智能小助手)公众号二维码
办公AI智能小助手
评论