通过模糊测试发现错误定价的操作码
模糊测试是一种通过重复执行和变异测试用例来发现漏洞的测试技术,传统上用于检测段错误、缓冲区溢出和其他可通过崩溃检测的内存损坏漏洞。但它还有您可能不知道的其他用途:给定正确的约束条件,我们可以使用它来发现运行时错误和逻辑问题。
这篇博文解释了Trail of Bits如何为Fuel Labs开发模糊测试工具,并用它识别在Fuel VM(Fuel智能合约运行的平台)中收费过少的操作码。通过实施具有精心选择约束条件的类似模糊测试设置,您可以捕获智能合约平台中的关键错误。
我们如何开发模糊测试工具和种子语料库
Fuel VM有一个现有的使用cargo-fuzz和libFuzzer的模糊测试器。然而,它有几个缺点。首先,它不调用内部合约。其次,它速度较慢(约50次执行/秒)。第三,它使用arbitrary crate生成仅由指令向量组成的随机程序。
我们开发了一个模糊测试工具,允许模糊测试器执行调用内部合约的脚本。该工具仍使用cargo-fuzz执行。但是,我们用LibAFL项目提供的shim替换了libFuzzer。LibAFL运行时允许在多核上执行测试用例,并将模糊测试性能提高到八核机器上的约1,000次执行/秒。
在分析Sway编译器的输出后,我们注意到纯数据与编译器输出中的实际指令交错。因此,简单的指令向量不能准确表示Sway编译器的输出。但更糟糕的是,Sway编译器输出不能用作种子语料库。
为了解决这些问题,必须重新设计模糊测试器的输入。模糊测试器的输入现在是一个字节向量,包含脚本汇编、脚本数据和要调用的合约汇编。每个部分都由一个任意选择的64位魔数值(0x00ADBEEF5566CEAA)分隔。由于这种重新设计,编译后的Sway程序可以用作种子语料库的输入(即作为初始测试用例)。我们使用Sway存储库中的示例作为初始输入,以加速模糊测试活动。
遇到的挑战
在我们的审计过程中,我们必须克服许多挑战。这些包括:
-
secp256k1 0.27.0依赖项目前与cargo-fuzz不兼容,因为它自动启用了一种特殊的模糊测试模式,破坏了secp256k1的功能。我们在fuel-crypto/Cargo.toml中应用了以下依赖项声明:
1 2[dependencies] secp256k1 = { version = "0.27", default-features = false, features = ["std"] }图1:更新的依赖项声明
-
LibAFL shim不稳定,尚未成为任何版本的一部分。因此,预计会出现错误,但由于性能改进,仍然值得考虑使用它而不是默认的模糊测试器运行时。
-
我们寻找一种方法将脚本数据的偏移量传递给在模糊测试器中执行的程序。我们决定通过修补fuel-vm来实现。fuel-vm在实际程序执行之前将偏移量写入寄存器0x10。这样,程序可以可靠地访问脚本数据偏移量。此外,种子输入继续按预期执行。在fuel-vm/src/interpreter/executors/main.rs:523中进行了以下更改:
1 2// Write the script data offset to register 0x10 self.registers[0x10] = script_data_offset;图2:将脚本数据偏移量写入寄存器0x10
此外,我们向种子语料库添加了以下测试用例,该测试用例使用此行为。
|
|
图3:使用现在可用的脚本数据偏移量的测试用例
使用模糊测试分析gas使用情况
模糊测试活动创建的语料库可用于分析汇编程序的gas使用情况。预计gas使用量与执行时间密切相关(注意,执行时间是花费的CPU周期量的代理)。
我们对Fuel VM的gas使用情况的分析包括三个步骤:
- 启动模糊测试活动。
- 在语料库上执行
cargo run --bin collect <file/dir>,生成一个gas_statistics.csv文件。 - 使用图4中的Python脚本检查并绘制收集到的数据结果。
- 识别异常值并在语料库中执行测试用例。在执行过程中,收集有关哪些指令被执行以及执行多长时间的数据。
- 通过按指令分组并将其简化为一个表来检查收集到的数据,该表显示哪些指令导致高执行时间。
本节更详细地描述每个步骤。
步骤1:模糊测试
cargo-fuzz工具将在目录corpus/grammar_aware中输出语料库。模糊测试器试图找到增加覆盖率的输入。此外,LibAFL模糊测试器更喜欢产生长执行时间的短输入。这个目标很有趣,因为它可能发现消耗很少gas但花费很长时间执行的操作。
步骤2:收集数据和评估
图4中的Python脚本加载通过调用cargo run --bin collect <file/dir>创建的CSV文件。然后绘制执行时间与gas消耗的关系图。这已经揭示了一些异常值,这些异常值在使用相同gas量的情况下比其他测试用例需要更长的执行时间。
|
|
图4:确定发现的测试输入的gas使用量与执行时间的Python脚本
图5:运行图4中脚本的结果
步骤3:识别和分析异常值
图6中的Python脚本对数据执行线性回归。然后,我们确定哪些测试用例与回归相差超过1,000ms,并将它们存储在inspect变量中。结果出现在图7中。
|
|
图6:对测试数据执行线性回归的Python脚本
图7:运行图6中脚本的结果
最后,我们重新执行应用了特定更改的语料库,以收集有关哪些执行导致长执行时间的数据。更改如下:
- 在函数instruction_inner的开头添加
let start = Instant::now();。 - 在函数末尾添加
println!("{:?}\t{:?}", instruction.opcode(), start.elapsed().as_nanos());。
这些更改导致测试用例的执行打印出每个指令的操作码和执行时间。
|
|
图8:调查每个指令对执行时间的贡献
Fuel的操作码输出如下所示:
|
|
图9:运行图8中脚本的结果
上述评估表明,操作码MCLI、SCWQ、K256、SWWQ和SRWQ可能定价错误。对于SCWQ、SWWQ和K256,结果是预期的,因为我们通过模糊测试已经发现了有问题的行为。这些问题似乎都已解决(参见FuelLabs/fuel-vm#537)。此分析还显示SRWQ可能存在定价问题。我们不确定为什么MCLI出现在我们的分析中。这可能是由于我们数据中的噪声,因为我们没有发现其实现和定价存在立即问题。
经验教训
随着项目的发展,Fuel团队继续在引入新功能的代码或处理不受信任数据的函数上运行模糊测试活动至关重要。我们向Fuel团队提出了以下建议:
- 运行模糊测试器至少72小时(或理想情况下,一周)。虽然目前没有工具确定理想的执行时间,但覆盖率数据提供了关于何时停止模糊测试的良好估计。在执行超过72小时后,我们没有看到模糊测试器有更多有价值的进展。
- 每当发现新问题时暂停模糊测试活动。开发人员应该对它们进行分类、修复,然后恢复模糊测试。这将减少分类和问题去重所需的工作量。
- 对Fuel VM的主要版本进行模糊测试,特别是在重大更改之后。模糊测试应作为开发过程的一部分集成,而不应只是偶尔进行。
一旦模糊测试程序被调整到快速和高效,它应该被适当地集成到开发周期中以捕获错误。我们建议使用CI系统集成模糊测试的以下过程,例如使用ClusterFuzzLite(参见FuelLabs/fuel-vm#727):
- 在初始模糊测试活动之后,保存每个测试生成的语料库。
- 对于每个内部里程碑、新功能或公共版本,从每个测试的当前语料库开始重新运行模糊测试活动至少24小时。
- 使用生成的新输入更新语料库。
请注意,随着时间的推移,语料库将代表数千CPU小时的改进,并且对于在模糊测试期间指导高效代码覆盖率非常宝贵。攻击者也可以使用语料库快速识别易受攻击的代码;可以通过将模糊测试语料库保存在访问控制的存储位置而不是公共存储库中来避免这种额外风险。一些CI系统允许维护者保留缓存以加速构建和测试。如果语料库不是很大,可以包含在此类缓存中。
未来工作
在未来,我们建议Fuel扩展模糊测试工具中使用的断言,特别是对于块的执行。例如,单元测试中的断言可以作为实施在模糊测试期间评估的额外检查的灵感。
此外,我们遇到了程序所需对齐的问题。Fuel VM的程序必须32位对齐。当前的模糊测试器不遵守这种对齐方式,因此很容易产生无效程序,例如,只插入一个字节而不是四个字节。这可以通过使用基于语法的方法或添加遵守对齐的自定义变异来解决。
与其内部执行模糊测试,不如使用oss-fuzz项目,该项目使用Google广泛的测试基础设施执行自动模糊测试活动。oss-fuzz对广泛使用的开源软件免费。我们相信他们会接受Fuel作为另一个项目。
好处是,Google免费提供所有基础设施,并在源代码中的更改引入新问题时通知项目维护者。收到的报告包括基本信息,如最小化测试用例和回溯。
然而,也有一些缺点:如果oss-fuzz发现关键问题,Google员工将首先知道,甚至在Fuel项目自己的开发人员之前。Google政策还要求90天后公开错误报告,这可能符合也可能不符合Fuel的最佳利益。在决定是否请求Google的免费模糊测试资源时,请权衡这些好处和风险。
如果Trail of Bits可以帮助您进行模糊测试,请联系我们!
1 有关模糊驱动开发的更多信息,请参阅Google的Kostya Serebryany在CppCon 2017上的演讲。
如果您喜欢这篇文章,请分享: Twitter LinkedIn GitHub Mastodon Hacker News