利用模糊测试发现燃料虚拟机中的操作码定价漏洞

本文详细介绍了Trail of Bits团队如何为Fuel Labs开发模糊测试工具链,通过定制化测试策略发现燃料虚拟机中操作码定价过低的安全漏洞。文章涵盖测试框架重构、性能优化方案、数据分析方法及实际漏洞挖掘案例,为智能合约平台安全测试提供实用指导。

利用模糊测试发现错误定价的操作码

模糊测试作为一种通过重复执行和变异测试用例来发现漏洞的技术,传统上用于检测段错误、缓冲区溢出等可通过崩溃识别的内存破坏漏洞。但它的用途远不止于此:在合适的恒定性条件下,我们还能用它发现运行时错误和逻辑问题。

本文介绍Trail of Bits团队如何为Fuel Labs开发模糊测试工具链,并利用其在Fuel虚拟机(Fuel智能合约运行平台)中发现操作码燃料消耗定价过低的问题。通过实施具有精心选择恒定条件的类似模糊测试方案,您也能在智能合约平台中发现关键漏洞。

测试工具链与种子语料库开发

Fuel虚拟机原本存在基于cargo-fuzz和libFuzzer的模糊测试器,但存在三大缺陷:无法调用内部合约、执行速度较慢(约50次/秒)、仅通过arbitrary库生成随机指令向量构成的程序。

我们开发的测试工具链支持模糊测试器执行调用内部合约的脚本,虽然仍使用cargo-fuzz执行框架,但将libFuzzer替换为LibAFL项目提供的适配层。LibAFL运行时支持多核测试用例执行,在八核机器上将模糊测试性能提升至约1000次/秒。

通过分析Sway编译器输出,我们发现纯数据与实际指令在编译输出中交错存在,简单指令向量无法准确反映编译器输出,更严重的是Sway编译器输出无法作为种子语料库使用。

为解决这些问题,我们重新设计了模糊测试输入格式:输入现在是由脚本汇编代码、脚本数据及被调用合约汇编代码组成的字节向量,各部分通过自定义的64位魔数(0x00ADBEEF5566CEAA)分隔。这一 redesign 使得编译后的Sway程序可作为种子语料库输入。我们使用Sway代码库中的示例作为初始输入以加速测试进程。

技术挑战与解决方案

审计过程中我们攻克了多项技术难题:

  1. 密码库兼容性问题
    secp256k1 0.27.0依赖库与cargo-fuzz不兼容,因其自动启用特殊模糊测试模式导致功能异常。通过在fuel-crypto/Cargo.toml中应用以下依赖声明解决:

    1
    2
    
    [dependencies]
    secp256k1 = { version = "0.27", features = ["static-context"], default-features = false }
    
  2. 测试框架稳定性
    LibAFL适配层尚未稳定发布,存在预期错误,但鉴于性能提升明显,仍值得替代默认模糊测试运行时。

  3. 脚本数据偏移量传递
    通过修改fuel-vm源码,在执行实际程序前将偏移量写入0x10寄存器,确保程序可靠访问脚本数据偏移量。在fuel-vm/src/interpreter/executors/main.rs第523行实施以下变更:

    1
    2
    
    // 将脚本数据偏移量写入寄存器0x10
    registers[0x10] = script_data_offset;
    

同时向种子语料库添加了利用此特性的测试用例:

1
2
; 测试用例:使用现可用的脚本数据偏移量
MOV R1, [0x10]  ; 加载脚本数据偏移量

基于模糊测试的燃气消耗分析

模糊测试活动生成的语料库可用于分析汇编程序的燃气消耗。理论上燃气消耗应与执行时间(即CPU周期数的代理指标)高度相关。

我们的分析流程包含三个步骤:

  1. 启动模糊测试活动
  2. 对语料库执行cargo run --bin collect <文件/目录>,生成gas_statistics.csv文件
  3. 使用图4的Python脚本检查并绘制收集到的数据
  4. 识别异常值并执行语料库中的测试用例,收集各指令执行时长数据
  5. 按指令分组分析数据,生成显示高耗时指令的汇总表

步骤详解

步骤1:模糊测试
cargo-fuzz工具将语料库输出至corpus/grammar_aware目录。模糊测试器会寻找提高代码覆盖率的输入,同时LibAFL偏好执行时间长的简短输入,这有助于发现燃气消耗低但执行时间长的操作。

步骤2:数据收集与评估
图4的Python脚本加载CSV文件后绘制执行时间与燃气消耗关系图(图5),清晰显示存在消耗相同燃气但执行时间更长的异常测试用例。

步骤3:异常值识别与分析
图6的Python脚本对数据进行线性回归分析,识别偏离回归线1000毫秒以上的测试用例存入inspect变量(结果见图7)。通过以下修改重新执行语料库以收集详细指令耗时数据:

  • 在instruction_inner函数开头添加let start = Instant::now();
  • 在函数末尾添加println!("{:?}\t{:?}", instruction.opcode(), start.elapsed().as_nanos());

图8展示了各指令对执行时间的贡献度分析结果,图9显示MCLI、SCWQ、K256、SWWQ和SRWQ操作码可能存在定价问题。其中SCWQ、SWWQ和K256的问题已通过模糊测试发现并修复(见FuelLabs/fuel-vm#537),SRWQ的定价问题尚待确认,MCLI的异常可能源于数据噪声。

经验总结与建议

随着项目发展,Fuel团队需持续对新功能代码或处理不可信数据的函数进行模糊测试。我们提出以下建议:

  1. 测试时长控制
    模糊测试至少运行72小时(理想为一周)。虽然目前缺乏确定最佳执行时间的工具,但代码覆盖率数据可作为停止测试的参考指标——72小时后测试器不再产生有价值进展。

  2. 问题处理流程
    发现新问题时暂停测试活动,开发人员应进行分类修复后恢复测试,减少问题去重工作量。

  3. 测试集成策略
    对Fuel VM主要版本(特别是重大变更后)进行模糊测试,将其纳入开发流程而非偶尔执行。

我们推荐通过CI系统(如ClusterFuzzLite)集成模糊测试的标准化流程:

  • 初始测试后保存每个测试生成的语料库
  • 每个内部里程碑、新功能或公开发布时,基于当前语料库重新运行至少24小时测试
  • 用新生成的输入更新语料库

需注意语料库随时间推移将积累数千CPU小时的优化成果,对指导高效代码覆盖极具价值。但攻击者也可能利用语料库快速定位漏洞,建议将语料库存储在访问受控的位置而非公开仓库。部分CI系统支持维护人员使用缓存加速构建测试,若语料库体积不大可纳入此类缓存。

未来工作方向

建议Fuel团队扩展测试工具链中的断言检查,特别是在区块执行方面。现有单元测试中的断言可作为实现模糊测试期间额外检查的参考。

当前测试器未遵循Fuel VM要求的32位对齐规范,容易产生无效程序(如插入1字节而非4字节)。未来可通过基于语法的方法或添加遵守对齐规范的自定义变异解决此问题。

可考虑使用oss-fuzz项目(谷歌提供的免费开源软件模糊测试基础设施)进行测试。虽然谷歌会免费提供基础设施并在源码变更引入新问题时通知维护人员(报告包含最小化测试用例和回溯信息等关键数据),但需注意:关键漏洞将首先被谷歌员工获知,且90天后漏洞报告必须公开。建议权衡利弊后决定是否使用该资源。

如果Trail of Bits能协助您的模糊测试工作,欢迎联系我们!


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