想要高质量的代码吗?学习如何使用差分模糊测试!

本文介绍了差分模糊测试(Differential Fuzzing)的概念、使用场景和结构。差分模糊测试通过生成大量随机输入,比较同一算法的不同实现,以发现逻辑错误。文章还提供了一个使用Rust的libfuzzer工具的差分模糊测试示例,并讨论了如何处理不同实现间输入输出格式的差异,以确保测试的有效性。

让我们坦诚地说,谁没有错过测试生活中的极端情况呢? 肯定发生在你身上过,也许你已经在实现了几个月后才意识到(也许当它已经在生产环境中时!)。 有些情况甚至会逃过最有经验的测试人员的眼睛,为了避免向你的经理解释,今天我们介绍一下fuzzing的概念,以及它的一种类型:差分fuzzer。

正如 OWASP 基金会所规定的:

Fuzz 测试或 Fuzzing 是一种黑盒软件测试技术,它包括以自动方式使用格式错误/半格式错误的数据注入来查找实现错误。

在寻找代码中的错误时,Fuzzing 是一种非常有效的技术。 这是通过生成大量随机条目来测试程序来实现的。 由此产生的测试通常能够达到不太常见的情况,通常会被忽视的那种。

如果你有兴趣了解有关 fuzzer 概念的更多信息,你可以观看我们制作的关于该主题的视频,使用 fuzzer 进行黑客攻击fuzzing 工具

然而,这个工具并不局限于单个程序,或者仅仅是寻找最终导致崩溃的错误; 我们还可以比较同一程序的至少两个不同实现的输出,并检查它们是否遵循相同的行为; 这被称为差分 fuzzing。

我们应该何时使用差分 fuzzing?

我们可以使用差分 fuzzing 技术的情况是我们拥有同一算法的两种不同实现。 例如,让我们想想实现 Ethereum Virtual Machine 的所有不同语言。 差分 fuzzer 的简单魔力是使用不同的输入进行大规模测试,所有实现在运行相同的流程或函数时都返回相同的结果。 如果不是这种情况,则至少有一个实现在逻辑上存在错误,并且给出了我们未预期的结果。

差分 fuzzer 的结构

差分 fuzzer 的结构很简单,首先,我们定义我们将使用哪个工具进行 fuzzing,因为我们有大量的工具用于此目的,例如 HonggfuzzCargofuzzAtheris 等等。

无论你选择哪种工具(我们不评判这里的偏好),所有工具都应该为我们提供相同的东西,一系列随机输入,我们将它们注入到要测试的代码中。

有了 fuzzer 提供的输入,我们将其调整为每个实现。 这样,两者最终都应该具有相同的输入,并且我们告诉 fuzzer 如果结果不同则返回错误。 这将为我们提供一个输入列表,其中至少有一个实现在其逻辑中存在错误,从而给出与预期不同的结果。

为此,我们可能需要中间函数来确保两个实现返回的结果具有可比性。

差分 fuzzer 示例

##![no_main]
use libfuzzer_sys::fuzz_target;
use std::io::prelude::*;
use inflate::inflate_bytes;
use libflate::deflate::Decoder;

// 如果 deflate 解码函数的两个不同实现返回不同的结果,则此差分 fuzzer 会 panic
fuzz_target!(|data: &[u8]| {
    let mut libflate_decoded = Decoder::new(data);
    let mut decoded_data = Vec::new();
    let libflate_res = libflate_decoded.read_to_end(&mut decoded_data).is_ok();

    let inflate_decoded = inflate_bytes(data).is_ok();

    if libflate_res != inflate_decoded {
    panic!("differential fuzz failed {}-{}",
            libflate_res, inflate_decoded)
    }

});

在示例中,我们可以看到差分 fuzzer 的示例。 此 fuzzer 是使用 libfuzzer 工具创建的,旨在在 Rust 中使用。 代码的结构很简单,对于你想要使用的所有 fuzzer 工具来说都是一样的。

首先,我们有导入,其中包括我们想要在 fuzzer 中比较的实现。 然后我们有将要运行 fuzzer 的函数,在本例中是 fuzz_target!() 函数。 这个函数为我们提供了一个随机生成的输入,在本例中是变量 data。 使用先前生成的 data,我们运行我们想要测试的代码。 在像 libflate 使用的示例代码这样的情况下,我们需要调整随机 data 以在第一时间被代码接收。 作为最后一步,我们进行差分魔法,也就是说,我们比较不同实现返回的结果。

在本例中,正如我们可以在运行 fuzzer 时在图像中看到的那样,它发现了一个崩溃,因为其中一个实现返回了一个有效结果,而另一个实现返回了一个错误。

输入和输出

根据输入和输出的每个特定情况,我们可能需要提供一些额外的代码。

让我们通过一个简单的例子来理解这意味着什么。 我们可能正在比较一些代码的两种实现,这些代码接收一个 二次方程 和一个答案,并返回答案是否响应方程。 在这种情况下,其中一个实现接收 4 个数字,这些数字对应于方程和答案的索引,另一个实现接收输入作为带有方程的字符串,如“axx+bx+c=d”。


## 实现 1
def check_if_answer(a,b,c,d, answer):
    result = (a * answer^2) + (b * answer) + c

    if result == d: {
        True
    }
    else: {
        False
    }

## 实现 2
def check_if_anwer(equation, answer):

    string_without_x = remove_x_from_string(equation) # 这返回 "a+b+c=d"
    array_of_indexes = split_string(string_without_x) # 这返回 [a,b,c,d]
    [a,b,c,d] =  array_of_indexes
    result = (a * answer^2) + (b * answer) + c

    if result == d: {
        True
    }
    else: {
        False
    }

为了给不同的实现提供“相同”的输入,我们需要调整起始输入,使其对两者都相同。 在输出中,我们也必须这样做。 其中一个实现可能返回 True/ False,而另一个实现返回 0/1,我们需要调整输出,以便相等性按其应有的方式工作。

这适用于常规 fuzzer 和差分 fuzzer。

结论

如果你正在实现一个已经有另一个实现的流程,以确保即使是极端情况也能得到一致的处理,那么差分 fuzzer 是一种非常有价值的工具。 在选择我们想要用于我们项目的解决方案的实现时,也可以使用此工具,通过比较每个实现的有效性。

  • 原文链接: blog.lambdaclass.com/do-...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
lambdaclass
lambdaclass
LambdaClass是一家风险投资工作室,致力于解决与分布式系统、机器学习、编译器和密码学相关的难题。