周末模糊测试赚取20万美元:Solana rBPF漏洞深度分析

本文详细分析了在Solana rBPF中发现的资源耗尽和.rodata持久性损坏两个高危漏洞,包括漏洞原理、利用方式、PoC构造和报告过程,展示了模糊测试在发现复杂安全漏洞中的强大能力。

资源耗尽漏洞

初始调查

触发崩溃的输入反汇编为以下汇编代码:

1
2
3
4
5
entrypoint:
  r0 = r0 + 255
  if r0 <= 8355838 goto -2
  r9 = r3 >> 3
  call -1

这个特定的指令集会导致内存泄漏。程序执行步骤如下:

  1. 将r0(初始为0)增加255
  2. 如果r0小于等于8355838,跳回前一条指令
  3. 这个循环将执行32767次(总共65534条指令)
  4. 将r9设置为r3 * 2^3(由于r3初始为0,结果将为0)
  5. 调用不存在的函数
  6. 不存在的函数应该触发未知符号错误

有问题的优化

在jit.rs的第420行有一个优化,Solana应用此优化来减少更新指令计数器的频率。他们只在到达块末尾或调用时更新或检查指令计数器。

问题出现在漏洞补丁中,维护者将第1279行移动到第1275行。在原始指令顺序中,调用过程如下:

  1. 调用失败,因为符号未解析
  2. 报告未解析符号,调用report_unresolved_symbol函数
  3. 该函数在堆分配字符串中返回调用的符号名称(或"Unknown")
  4. 更新pc
  5. 验证指令计数,覆盖未解析符号错误并终止执行

由于未解析符号错误被覆盖,对堆分配String的引用丢失且从未释放,导致内存泄漏。

武器化

通过创建恶意ELF文件,可以控制report_unresolved_symbol中的name字符串大小:

1
2
3
$ echo "#define EVIL do_evil_$(printf 'a%.0s' {1..1048576})
void EVIL();
" > evil.h

使用超长的函数名,每次执行可以泄漏1MiB内存而不是仅仅7字节。

持久性.rodata损坏漏洞

初始调查

触发崩溃的输入反汇编为:

1
2
3
4
5
entrypoint:
  or32 r9, -1
  mov32 r1, -1
  stxh [r9+0x1], r0
  exit

JIT以Ok(0)终止,而解释器以访问违规错误终止。访问地址4294967296(0x100000000)对应程序代码区域。

x86的诅咒

问题出现在jit.rs:1490处错误大小的内存操作数。发出的cmp指令是cmp DWORD PTR [rax+0x19], 0x0,但应该使用8位操作数。

x86 cmp操作数大小问题:opcode 0x81只定义为16、32和64位寄存器操作数。如果要使用8位寄存器操作数,需要使用0x80 opcode变体。

概念证明

创建PoC演示只读数据可以被持久修改:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
static const char data[] = "howdy";

extern uint64_t entrypoint(const uint8_t *input) {
  log(data, 5);
  char *overwritten = (char *)data;
  overwritten[0] = 'e';
  // ... 修改数据
  log(data, 5);
  return 0;
}

程序输出显示修改是持久的:

1
2
3
4
5
howdy
evil!
evil!
evil!
...

影响

如果BPF链上程序存在错误的偏移量或用户控制的偏移量,恶意用户可以修改程序的只读数据。在最坏情况下,这可能导致替换资金金额、钱包地址等。

报告和奖励

两个漏洞都报告给Solana,每个漏洞获得10万美元奖励。第一个漏洞被分类为拒绝服务(非RPC),第二个漏洞虽然作者认为应该获得更高奖励,但也被分类为拒绝服务(非RPC)。

Solana在支付方面表现出极大的灵活性,允许将奖金直接捐赠给德州农工大学网络安全俱乐部,而不是强制使用锁定的SOL代币。

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