Tutorials

了解 Go 中的模糊测试

Read this post in other languages:

本文由外部贡献者撰写。

Hrittik Roy

Hrittik 是一位作者和软件工程师,专门研究云原生生态系统。 他曾参与过许多大型项目,在云计算的技术和商业方面都有经验。 他经常在会议上发言,并写过许多关于软件开发和分布式系统的文章。 在他的空闲时间,他喜欢散步。

LinkedIn Twitter

 

 

 

 

试图为所有可能的用户与您软件的交互手动创建测试用例即使不是不可能的,也仍令人望而却步。 模糊测试,或称 “Fuzzing”,是一种自动化的软件测试,可以帮助发现潜在的错误和安全漏洞。

该测试涉及将随机数据(或 “Fuzz”)注入被测软件中。 这种测试框架可以帮助发现可能导致崩溃或其他安全问题的未定义行为。 虽然不可能用模糊测试找到所有的错误,但它可以成为发现和修复许多常见类型问题的有效方法。 它经常被用来测试那些处理来自不受信任来源的输入或可能有意外输入的程序。

模糊测试通常是自动化的,可用于测试功能和安全缺陷。 功能性模糊测试包括向程序输入无效数据以检查意外行为。 安全性模糊测试包括向程序输入恶意数据以发现安全漏洞。

Go有一个很棒的软件包生态系统,比如开源的模糊测试软件包 gofuzzgo-fuzz。它们使用简单,可以集成到您的测试工作流程中。 然而,从 Go 1.18 开始,语言中出现了原生的模糊测试支持,这意味着您不再需要导入任何外部包。 本篇文章将介绍模糊测试的优缺点,如何在 Go 中实现模糊测试,以及不同的模糊测试技术。

模糊测试的用例

模糊测试可用于任何有未受信任的输入源的地方,无论是在开放的互联网上还是在安全敏感的地方。 模糊测试可用于测试软件、文件、网络策略、应用程序和库。

例如,假设您有一个需要用户姓名作为输入的应用程序,而您必须测试该应用程序是否有无效数据。 您可以用模糊法创建一个测试案例,使用带有特殊字符或数值的输入,这可能导致崩溃或导致内存或缓冲区溢出。

其他用例包括:

  • 服务器-客户端检查:在这种情况下,客户端和服务器都被模糊化以发现漏洞。
  • 数据包检查:对网络 Sniffer 识别数据包检查设备漏洞的能力进行测试。
  • 测试 API,对接口中的每个参数和方法进行测试,以确定弱点。

这个列表只是成千上万的用例中的一个子集。 任何消耗复杂输入的东西都可以从模糊测试中受益。

如何在 Go 中进行模糊测试

让我们来看看如何对一个简单的应用程序进行模糊测试。 要学习本教程,您需要具备 Go 的应用知识并在本地安装最新稳定版本的 Go。 您还需要一个普通的或集成的终端,一个支持模糊测试的环境(AMD64 或 ARM64 架构),并在您的计算机上安装 GoLand

函数

您可以对模块、简单的函数或任何您认为需要模糊测试的东西使用模糊测试。 在本教程中,您将使用一个简单的函数来比较两个字节的切片,如果切片不相等则返回 false:

func Equal(a []byte, b []byte) bool {  
   // 检查切片的长度是否相同    
   if len(a) != len(b) {  
      return false  
   }  
   for i := range a {  
      // 检查同一索引中的元素是否相同  
      if a[i] != b[i] {  
         return false  
      }  
   }  
   return true  
}

生成测试用例

GoLand 提供了非常强大的工具,允许您自动生成测试。 它还提供了一种智能方式,根据您要实现的目标来生成测试。 您可以使用该功能来创建强大的测试用例,其步骤就像创建一个函数、文件或包,然后点击按钮来生成测试一样简单。

首先,按 Alt+Insert⌘N),选择 Test for function 来生成单元测试用例。 或者,您可以右键点击函数名称,选择 Generate(生成):

然后,点击 Test for function 来生成测试用例:

在 GoLand 生成第一个单元测试后,如果需要,您可以添加您自己的测试。 您也可以跳过智能测试生成,自己创建所有的测试,尽管这个选项可能不太有效。

func TestEqual(t *testing.T) {
    type args struct {
        a []byte
        b []byte
    }
    tests := []struct {
        name string
        args args
        want bool
    }{
        {
            name: "Slices have the same length",
            args: args{a: []byte{102, 97, 108, 99}, b: []byte{102, 97, 108, 99}},
            want: true,
        },
        {
            name: "Slices don’t have the  same length",
            args: args{a: []byte{102, 97, 99}, b: []byte{102, 97, 108, 99}},
            want: false,
        },
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            if got := Equal(tt.args.a, tt.args.b); got != tt.want {
                t.Errorf("Equal() = %v, want %v", got, tt.want)
            }
        })
    }
}

一旦测试被创建,有两种方法可以运行它们。 您可以在集成终端中使用 go test 命令,它将产生一个成功或失败的状态:

hrittik@hrittik:~/go-fuzz-testing$ go test
PASS
ok      github.com/hrittikhere/go-fuzz-testing  0.003s

另外,您也可以使用 GoLand 来运行测试,点击间距的绿色三角形图标。 这种方法的好处是,它还为您提供了调试和记录(在 Linux 机器上可用)功能。如果您想找到并理解一个失败的测试,这很有帮助。

当您使用 GoLand 进行测试时,结果会显示在一个专门的窗格中,其中有关于所有运行的测试的细节。

生成模糊测试

现在您的代码已经通过了单元测试,让我们来创建模糊测试。 您的测试应该包含在您创建的文件中,对于模糊测试,您应该保持以下规则。

  • 您的函数名需要以 Fuzz 开头。
  • 您应该接受 *testing.F 类型到您的函数。
  • 您应该只针对一个函数。

您可以使用以下代码:

func FuzzEqual(f *testing.F) {  
// 只要测试一个函数
   f.Fuzz(func(t *testing.T, a []byte, b []byte) {  
// a、b 的值将被自动生成并传递
      Equal(a, b)  
// 如果两个自动生成的值都匹配,则 Equal 匹配。如果不匹配,则返回 False。
   })  
}

在此处,ab 是模糊参数,它们将与 Equal 进行比较,得出测试结果。 要运行测试,您可以再次点击侧边栏,打开 Run 选项卡,或者点击测试函数,按 Ctrl+Shift+F10⌃⇧R)。

由于模糊测试不是一种标准的测试方法,您要选择第二个选项,它有 -fuzz 标志,以在您的 IDE 上启动模糊测试。

另外,您也可以使用 go test -fuzz .,不过这种方法不提供调试功能。

一旦启动,模糊测试将继续进行,直到您手动停止测试。 在几次执行之后,按右侧面板上的方形红色停止按钮,停止您的测试。

在下面的截图中,停止后,测试被评估并报告为通过,这意味着测试的所有迭代都是成功的。

就注释而言,”elapsed“ 是测试运行的时间,”new interesting” 是添加到语料库中提供独特结果的输入数量,“execs” 是运行的单个测试的数量。

如果您的测试失败了, 您可以使用 Test Runner 来跟踪您的测试执行,找到失败的具体测试案例,然后进行修改来修复您的代码。

如果您想看看 GoLand 中的模糊测试失败时会发生什么,请使用这个例子。 如果测试失败,失败的种子语料库条目将被写入文件并置于 testdata 文件夹的软件包目录中。 此文件的路径也将作为可点击链接出现在控制台中。 如果您点击此链接,文件将在 IDE 中打开,文件顶部将显示绿色三角形图标。 点击此图标将运行 go test 并显示失败的种子语料库条目。

如果需要,您还可以通过 Test Runner 工具条将测试结果导出为 HTML、XML 或自定义 XSL 模板。

进阶技术

在上一节中,您查看了一个简单的模糊测试,但这在大多数生产情况下是不够的。 对于需要更彻底测试的场景,您可以使用以下技术。

差分模糊测试

差分模糊测试是一种模糊技术,它比较程序的两个不同实现的结果,每个实现接收不同的输入,以检测只在特定条件下发生的错误。

差分模糊测试的最大优势之一是,它可以帮助您发现使用其他模糊测试时可能隐藏的错误。 这方面的一个明显的例子是能够发现新的 HTTP 请求 “Smuggling” (偷渡)技术

主要的缺点是,设置两个不同的程序实现可能很困难。 也很难确定什么算作它们之间的 “difference” (差异)。 此外,如果实现方式不一样,比如一个是调试版,另一个是发布版,那么这种技术可能就没有效果。

往返模糊测试

往返模糊测试的前提是,如果存在两个操作相反的函数,则可以通过将主要函数的输出作为次要函数的输入,然后将结果与主要输入或原始数据进行比较来测试数据的准确性和完整性。

这种测试技术的好处是,它可以帮助发现孤立的输入可能错过的错误,比如在货币转换的情况下,浮点错误可能不明显。 然而,如果两个函数不是完全反向的,它也可以引入新的错误,就像舍入误差经常出现的情况。 一般来说,当两个函数简单且被充分理解时,往返模糊测试是最有效的。

往返模糊测试的缺点是,由于您必须对数据进行两次转换,它可能很难设置,而为复杂的系统进行架构需要时间。 此外,不同的数据格式必须与两个函数兼容。

模糊测试的优势

模糊测试的易用性和简单实现使其成为一种必要的测试技术。 它加速了软件开发的生命周期,使错误更容易修复,并减少最终产品中的错误。

这是因为,与其他测试技术(如集成)不同,模糊测试可以在开发的早期进行,只要目标代码的任何部分被编写出来就可以了。 模糊测试允许您针对个别功能,而不管整个代码库的状态如何。 它的特点还在于能够同时测试应用程序的多个组件,这在传统测试技术中是不可能的。

最后,对于现代库来说,它非常容易实现,并能捕获大量的错误和安全缺陷,将其打造为您手中的核心测试技术。

模糊测试的缺点

模糊测试是一种寻找软件缺陷或漏洞的强大技术,但它有一些局限性。 它不能替代传统的测试技术,如单元测试、集成测试和回归测试,应该作为这些技术的补充,而不是替代。 同样,必须继续进行安全更新、渗透测试和遵守安全标准。 模糊测试不是软件安全的尚方宝剑。

另一个缺点是,模糊测试只能找到导致崩溃或其他可检测故障的缺陷。

最后,模糊测试只是告诉您有一个错误,仍需测试人员或开发者负责调试并找到根本原因。 这可以通过使用 GoLand 来简化,它提供了可以帮助您的调试器会话、断点和核心转储。

虽然这不一定是一个缺点,但您也应该记住,模糊测试占用大量的内存和 CPU,所以不建议在软件交付管道中运行模糊测试。

结论

在本文中,您了解了有关模糊测试的更多信息。 模糊测试是一种非常有用的测试技术,很容易上手。 这种测试方法在整个行业中使用,并帮助 Google 在数千个工具和应用程序中捕获了超过一万五千个错误

模糊测试是一个非常有效和低成本的测试过程。 GoLand模糊测试的内置支持使其实施更加简单。

接下来呢? 有关模糊测试的更多信息,请访问 Go Security 博客

 

本博文英文原作者:

Sue

Sergey Kozlovskiy

image description

Discover more