基于覆盖率的模糊测试(已弃用)
- Tier: Ultimate
- Offering: GitLab.com, GitLab Self-Managed, GitLab Dedicated
此功能在 GitLab 18.0 中已弃用, 并计划在 19.0 中移除。这是一个破坏性变更。
入门指南
基于覆盖率的模糊测试会向应用程序的插桩版本发送随机输入, 以尝试引发意外行为。这种行为表明存在您应该解决的 bug。 GitLab 允许您将基于覆盖率的模糊测试添加到您的流水线中。 这有助于您发现其他 QA 流程可能遗漏的 bug 和潜在安全问题。
您应该在 GitLab Secure 中的其他安全扫描器以及您自己的测试流程之外使用模糊测试。 如果您正在使用 GitLab CI/CD, 您可以将基于覆盖率的模糊测试作为 CI/CD 工作流程的一部分运行。
有关概述,请参阅基于覆盖率的模糊测试 - 高级安全测试。
确认基于覆盖率的模糊测试状态
要确认基于覆盖率的模糊测试状态:
- 在左侧边栏,选择 Search or go to 并找到您的项目。
- 选择 Secure > Security configuration。
- 在 Coverage Fuzzing 部分中,状态为:
- Not configured(未配置)
- Enabled(已启用)
- 升级到 GitLab Ultimate 的提示。
启用基于覆盖率的模糊测试
要启用基于覆盖率的模糊测试,请编辑 .gitlab-ci.yml:
-
将
fuzz阶段添加到阶段列表中。 -
如果您的应用程序不是用 Go 编写的,提供 Docker image, 使用匹配的模糊测试引擎。例如:
image: python:latest -
Include
Coverage-Fuzzing.gitlab-ci.yml模板, 该模板作为您的 GitLab 安装的一部分提供。 -
自定义
my_fuzz_target作业以满足您的需求。
基于覆盖率的模糊测试配置示例
stages:
- fuzz
include:
- template: Coverage-Fuzzing.gitlab-ci.yml
my_fuzz_target:
extends: .fuzz_base
script:
# 在这些步骤中构建您的模糊测试目标二进制文件,然后使用 gitlab-cov-fuzz 运行它
# 请参阅我们的示例仓库,了解如何使用任何支持的语言实现此操作
- ./gitlab-cov-fuzz run --regression=$REGRESSION -- <your fuzz target>Coverage-Fuzzing 模板包含隐藏作业 .fuzz_base,
您必须为每个模糊测试目标extend它。
每个模糊测试目标必须有一个单独的作业。例如,
go-fuzzing-example 项目
包含一个作业,为其单个模糊测试目标扩展了 .fuzz_base。
隐藏作业 .fuzz_base 使用了几个 YAML 键,您在自己的作业中不能覆盖它们。
如果将这些键包含在您自己的作业中,您必须复制它们原始的内容:
before_scriptartifactsrules
理解结果
输出
每个模糊测试步骤输出以下产物:
gl-coverage-fuzzing-report.json: 包含基于覆盖率的模糊测试及其结果的详细报告。artifacts.zip: 此文件包含两个目录:corpus: 包含当前作业和所有先前作业生成的所有测试用例。crashes: 包含当前作业找到的所有崩溃事件以及先前作业中未修复的崩溃事件。
您可以从 CI/CD 流水线页面下载 JSON 报告文件。有关更多信息,请参阅 下载产物。
Corpus 注册表
Corpus 注册表是一个 corpus 库。项目中注册表中的 corpus 可用于该项目中的所有作业。 项目范围的注册表是管理 corpus 比每个作业一个 corpus 的默认选项更高效的方法。
Corpus 注册表使用包注册表来存储项目的 corpus。存储在注册表中的 corpus 是隐藏的, 以确保数据完整性。
当您下载 corpus 时,文件名为 artifacts.zip,无论 corpus 最初上传时使用的文件名是什么。
此文件仅包含 corpus,这与您可以从 CI/CD 流水线下载的产物文件不同。
此外,具有 Reporter 或更高权限的项目成员可以使用直接下载链接下载 corpus。
查看 Corpus 注册表详情
要查看 Corpus 注册表详情:
- 在左侧边栏,选择 Search or go to 并找到您的项目。
- 选择 Secure > Security configuration。
- 在 Coverage Fuzzing 部分,选择 Manage corpus。
在 Corpus 注册表中创建 Corpus
要在 Corpus 注册表中创建 corpus,可以:
- 在流水线中创建 corpus
- 上传现有的 corpus 文件
在流水线中创建 Corpus
要在流水线中创建 corpus:
- 在
.gitlab-ci.yml文件中,编辑my_fuzz_target作业。 - 设置以下变量:
- 将
COVFUZZ_USE_REGISTRY设置为true。 - 将
COVFUZZ_CORPUS_NAME设置为命名 corpus。 - 将
COVFUZZ_GITLAB_TOKEN设置为个人访问令牌的值。
- 将
在 my_fuzz_target 作业运行后,corpus 存储在 corpus 注册表中,
名称由 COVFUZZ_CORPUS_NAME 变量提供。corpus 在每次流水线运行时都会更新。
上传 Corpus 文件
要上传现有的 corpus 文件:
- 在左侧边栏,选择 Search or go to 并找到您的项目。
- 选择 Secure > Security configuration。
- 在 Coverage Fuzzing 部分,选择 Manage corpus。
- 选择 New corpus。
- 完成字段。
- 选择 Upload file。
- 选择 Add。
现在您可以在 .gitlab-ci.yml 文件中引用该 corpus。确保在
COVFUZZ_CORPUS_NAME 变量中使用的值与上传的 corpus 文件名称完全匹配。
使用存储在 Corpus 注册表中的 Corpus
要使用存储在 Corpus 注册表中的 corpus,您必须按其名称引用它。 要确认相关 corpus 的名称,请查看 corpus 注册表详情。
先决条件:
- 在项目中启用基于覆盖率的模糊测试。
- 在
.gitlab-ci.yml文件中设置以下变量:- 将
COVFUZZ_USE_REGISTRY设置为true。 - 将
COVFUZZ_CORPUS_NAME设置为 corpus 的名称。 - 将
COVFUZZ_GITLAB_TOKEN设置为个人访问令牌的值。
- 将
基于覆盖率的模糊测试报告
有关 gl-coverage-fuzzing-report.json 文件格式的详细信息,
请阅读schema。
基于覆盖率的模糊测试报告示例:
{
"version": "v1.0.8",
"regression": false,
"exit_code": -1,
"vulnerabilities": [
{
"category": "coverage_fuzzing",
"message": "Heap-buffer-overflow\nREAD 1",
"description": "Heap-buffer-overflow\nREAD 1",
"severity": "Critical",
"stacktrace_snippet": "INFO: Seed: 3415817494\nINFO: Loaded 1 modules (7 inline 8-bit counters): 7 [0x10eee2470, 0x10eee2477), \nINFO: Loaded 1 PC tables (7 PCs): 7 [0x10eee2478,0x10eee24e8), \nINFO: 5 files found in corpus\nINFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytes\nINFO: seed corpus: files: 5 min: 1b max: 4b total: 14b rss: 26Mb\n#6\tINITED cov: 7 ft: 7 corp: 5/14b exec/s: 0 rss: 26Mb\n=================================================================\n==43405==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000001573 at pc 0x00010eea205a bp 0x7ffee0d5e090 sp 0x7ffee0d5e088\nREAD of size 1 at 0x602000001573 thread T0\n #0 0x10eea2059 in FuzzMe(unsigned char const*, unsigned long) fuzz_me.cc:9\n #1 0x10eea20ba in LLVMFuzzerTestOneInput fuzz_me.cc:13\n #2 0x10eebe020 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) FuzzerLoop.cpp:556\n #3 0x10eebd765 in fuzzer::Fuzzer::RunOne(unsigned char const*, unsigned long, bool, fuzzer::InputInfo*, bool*) FuzzerLoop.cpp:470\n #4 0x10eebf966 in fuzzer::Fuzzer::MutateAndTestOne() FuzzerLoop.cpp:698\n #5 0x10eec0665 in fuzzer::Fuzzer::Loop(std::__1::vector\u003cfuzzer::SizedFile, fuzzer::fuzzer_allocator\u003cfuzzer::SizedFile\u003e \u003e\u0026) FuzzerLoop.cpp:830\n #6 0x10eead0cd in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) FuzzerDriver.cpp:829\n #7 0x10eedaf82 in main FuzzerMain.cpp:19\n #8 0x7fff684fecc8 in start+0x0 (libdyld.dylib:x86_64+0x1acc8)\n\n0x602000001573 is located 0 bytes to the right of 3-byte region [0x602000001570,0x602000001573)\nallocated by thread T0 here:\n #0 0x10ef92cfd in wrap__Znam+0x7d (libclang_rt.asan_osx_dynamic.dylib:x86_64+0x50cfd)\n #1 0x10eebdf31 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) FuzzerLoop.cpp:541\n #2 0x10eebd765 in fuzzer::Fuzzer::RunOne(unsigned char const*, unsigned long, bool, fuzzer::InputInfo*, bool*) FuzzerLoop.cpp:470\n #3 0x10eebf966 in fuzzer::Fuzzer::MutateAndTestOne() FuzzerLoop.cpp:698\n #4 0x10eec0665 in fuzzer::Fuzzer::Loop(std::__1::vector\u003cfuzzer::SizedFile, fuzzer::fuzzer_allocator\u003cfuzzer::SizedFile\u003e \u003e\u0026) FuzzerLoop.cpp:830\n #5 0x10eead0cd in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) FuzzerDriver.cpp:829\n #6 0x10eedaf82 in main FuzzerMain.cpp:19\n #7 0x7fff684fecc8 in start+0x0 (libdyld.dylib:x86_64+0x1acc8)\n\nSUMMARY: AddressSanitizer: heap-buffer-overflow fuzz_me.cc:9 in FuzzMe(unsigned char const*, unsigned long)\nShadow bytes around the buggy address:\n 0x1c0400000250: fa fa fd fa fa fa fd fa fa fa fd fa fa fa fd fa\n 0x1c0400000260: fa fa fd fa fa fa fd fa fa fa fd fa fa fa fd fa\n 0x1c0400000270: fa fa fd fa fa fa fd fa fa fa fd fa fa fa fd fa\n 0x1c0400000280: fa fa fd fa fa fa fd fa fa fa fd fa fa fa fd fa\n 0x1c0400000290: fa fa fd fa fa fa fd fa fa fa fd fa fa fa fd fa\n=\u003e0x1c04000002a0: fa fa fd fa fa fa fd fa fa fa fd fa fa fa[03]fa\n 0x1c04000002b0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa\n 0x1c04000002c0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa\n 0x1c04000002d0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa\n 0x1c04000002e0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa\n 0x1c04000002f0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa\nShadow byte legend (one shadow byte represents 8 application bytes):\n Addressable: 00\n Partially addressable: 01 02 03 04 05 06 07 \n Heap left redzone: fa\n Freed heap region: fd\n Stack left redzone: f1\n Stack mid redzone: f2\n Stack right redzone: f3\n Stack after return: f5\n Stack use after scope: f8\n Global redzone: f9\n Global init order: f6\n Poisoned by user: f7\n Container overflow: fc\n Array cookie: ac\n Intra object redzone: bb\n ASan internal: fe\n Left alloca redzone: ca\n Right alloca redzone: cb\n Shadow gap: cc\n==43405==ABORTING\nMS: 1 EraseBytes-; base unit: de3a753d4f1def197604865d76dba888d6aefc71\n0x46,0x55,0x5a,\nFUZ\nartifact_prefix='./crashes/'; Test unit written to ./crashes/crash-0eb8e4ed029b774d80f2b66408203801cb982a60\nBase64: RlVa\nstat::number_of_executed_units: 122\nstat::average_exec_per_sec: 0\nstat::new_units_added: 0\nstat::slowest_unit_time_sec: 0\nstat::peak_rss_mb: 28",
"scanner": {
"id": "libFuzzer",
"name": "libFuzzer"
},
"location": {
"crash_address": "0x602000001573",
"crash_state": "FuzzMe\nstart\nstart+0x0\n\n",
"crash_type": "Heap-buffer-overflow\nREAD 1"
},
"tool": "libFuzzer"
}
]
}与漏洞交互
发现漏洞后,您可以解决它。 合并请求小部件列出了该漏洞,并包含一个用于下载模糊测试产物的按钮。 通过选择检测到的漏洞之一,您可以查看其详细信息。
您还可以从Security Dashboard查看该漏洞, 它显示了您组、项目和流水线中所有安全漏洞的概览。
选择漏洞会打开一个模态框,提供有关该漏洞的额外信息:
- Status: 漏洞的状态。与任何类型的漏洞一样,基于覆盖率的模糊测试漏洞可以是 Detected(已检测)、Confirmed(已确认)、Dismissed(已忽略)或 Resolved(已解决)。
- Project: 存在漏洞的项目。
- Crash type: 代码中崩溃或弱点的类型。这通常映射到 CWE。
- Crash state: 堆栈跟踪的标准化版本,包含崩溃的最后三个函数(没有随机地址)。
- Stack trace snippet: 堆栈跟踪的最后几行,显示崩溃的详细信息。
- Identifier: 漏洞的标识符。这映射到 CVE 或 CWE。
- Severity: 漏洞的严重性。可以是 Critical、High、Medium、Low、Info 或 Unknown。
- Scanner: 检测漏洞的扫描器(例如,Coverage Fuzzing)。
- Scanner Provider: 执行扫描的引擎。对于基于覆盖率的模糊测试,这可以是 Supported fuzzing engines and languages 中列出的任何引擎。
优化
使用以下自定义选项来优化基于覆盖率的模糊测试以适应您的项目。
可用的 CI/CD 变量
使用以下变量在您的 CI/CD 流水线中配置基于覆盖率的模糊测试。
所有 GitLab 安全扫描工具的自定义都应在合并请求中进行测试,然后再将这些更改合并到默认分支。 否则可能会产生意外结果,包括大量误报。
| CI/CD 变量 | 描述 |
|---|---|
COVFUZZ_ADDITIONAL_ARGS |
传递给 gitlab-cov-fuzz 的参数。用于自定义底层模糊测试引擎的行为。阅读模糊测试引擎的文档以获取完整的参数列表。 |
COVFUZZ_BRANCH |
运行长时间运行的模糊测试作业的分支。在所有其他分支上,仅运行模糊测试回归测试。默认:仓库的默认分支。 |
COVFUZZ_SEED_CORPUS |
种子 corpus 目录的路径。默认:空。 |
COVFUZZ_URL_PREFIX |
为离线环境使用而克隆的 gitlab-cov-fuzz 仓库的路径。仅在使用离线环境时才应更改此值。默认:https://gitlab.com/gitlab-org/security-products/analyzers/gitlab-cov-fuzz/-/raw。 |
COVFUZZ_USE_REGISTRY |
设置为 true 以将 corpus 存储在 GitLab corpus 注册表中。如果将此变量设置为 true,则需要 COVFUZZ_CORPUS_NAME 和 COVFUZZ_GITLAB_TOKEN 变量。默认:false。 |
COVFUZZ_CORPUS_NAME |
作业中要使用的 corpus 的名称。 |
COVFUZZ_GITLAB_TOKEN |
使用具有 API 读写权限的个人访问令牌或项目访问令牌配置的环境变量。 |
种子 corpus
种子 corpus 中的文件必须手动更新。它们不会被基于覆盖率的模糊测试作业更新或覆盖。
基于覆盖率的模糊测试流程
模糊测试流程:
- 编译目标应用程序。
- 运行插桩应用程序,使用
gitlab-cov-fuzz工具。 - 解析和分析模糊测试器输出的异常信息。
- 从以下位置下载 corpus:
- 先前的流水线。
- 如果
COVFUZZ_USE_REGISTRY设置为true,则从 corpus 注册表。
- 从先前流水线下载崩溃事件。
- 将解析的崩溃事件和数据输出到
gl-coverage-fuzzing-report.json文件。 - 更新 corpus,要么:
- 在作业的流水线中。
- 如果
COVFUZZ_USE_REGISTRY设置为true,则在 corpus 注册表中。
基于覆盖率的模糊测试的结果在 CI/CD 流水线中可用。
推广
当您在单个项目中熟悉使用基于覆盖率的模糊测试后,您可以利用以下高级功能,包括在离线环境中启用测试。
支持的模糊测试引擎和语言
您可以使用以下模糊测试引擎来测试指定的语言。
| 语言 | 模糊测试引擎 | 示例 |
|---|---|---|
| C/C++ | libFuzzer | c-cpp-example |
| Go | go-fuzz (libFuzzer support) | go-fuzzing-example |
| Swift | libFuzzer | swift-fuzzing-example |
| Rust | cargo-fuzz (libFuzzer support) | rust-fuzzing-example |
| Java (仅 Maven)1 | Javafuzz (推荐) | javafuzz-fuzzing-example |
| Java | JQF (不推荐) | jqf-fuzzing-example |
| JavaScript | jsfuzz |
jsfuzz-fuzzing-example |
| Python | pythonfuzz |
pythonfuzz-fuzzing-example |
| AFL (任何支持 AFL 的语言) | AFL | afl-fuzzing-example |
- 对 Gradle 的支持计划在 issue 409764 中实现。
基于覆盖率的模糊测试持续时间
基于覆盖率的模糊测试可用的时间段为:
- 10分钟持续时间(默认):推荐用于默认分支。
- 60分钟持续时间:推荐用于开发分支和合并请求。较长的持续时间提供更全面的覆盖。
在
COVFUZZ_ADDITIONAL_ARGS变量中设置值--regression=true。
有关完整示例,请阅读 Go 基于覆盖率的模糊测试示例。
持续的基于覆盖率的模糊测试
也可以运行更长时间的基于覆盖率的模糊测试作业,而不阻塞您的主流水线。 此配置使用 GitLab 父子流水线。
此场景中的建议工作流程是在主分支或开发分支上运行长时间、异步的模糊测试作业, 并在所有其他分支和 MR 上运行短时间的同步模糊测试作业。 这平衡了完成每次提交流水线快速完成的需求,同时也给模糊测试器大量时间来充分探索和测试应用程序。 长时间运行的模糊测试作业通常对于基于覆盖率的模糊测试器来说是必要的, 以便在您的代码库中找到更深层次的 bug。
以下是此工作流程的 .gitlab-ci.yml 文件摘录。
完整示例请参阅 Go 模糊测试示例仓库:
sync_fuzzing:
variables:
COVFUZZ_ADDITIONAL_ARGS: '-max_total_time=300'
trigger:
include: .covfuzz-ci.yml
strategy: depend
rules:
- if: $CI_COMMIT_BRANCH != 'continuous_fuzzing' && $CI_PIPELINE_SOURCE != 'merge_request_event'
async_fuzzing:
variables:
COVFUZZ_ADDITIONAL_ARGS: '-max_total_time=3600'
trigger:
include: .covfuzz-ci.yml
rules:
- if: $CI_COMMIT_BRANCH == 'continuous_fuzzing' && $CI_PIPELINE_SOURCE != 'merge_request_event'这创建了两个作业:
sync_fuzzing: 在阻塞配置中运行所有模糊测试目标一段时间。这会找到简单的 bug, 让您可以确信您的 MR 不会引入新的 bug 或导致旧的 bug 重新出现。async_fuzzing: 在您的分支上运行,在不阻塞您的开发周期和 MR 的情况下, 找到代码中的深层 bug。
covfuzz-ci.yml 与原始同步示例中的相同。
FIPS 启用的二进制文件
从 GitLab 15.0 开始,
基于覆盖率的模糊测试二进制文件在 Linux x86 上使用 golang-fips 编译,
并使用 OpenSSL 作为加密后端。有关更多详细信息,请参阅 GitLab 的 FIPS 合规性。
离线环境
要在离线环境中使用基于覆盖率的模糊测试:
-
将
gitlab-cov-fuzz克隆到您的离线 GitLab 实例可以访问的私有仓库中。 -
对于每个模糊测试步骤,将
COVFUZZ_URL_PREFIX设置为${NEW_URL_GITLAB_COV_FUZ}/-/raw, 其中NEW_URL_GITLAB_COV_FUZ是您在第一步中设置的私有gitlab-cov-fuzz克隆的 URL。
故障排除
错误 Unable to extract corpus folder from artifacts zip file
如果您看到此错误消息,并且 COVFUZZ_USE_REGISTRY 设置为 true,
请确保上传的 corpus 文件解压到一个名为 corpus 的文件夹中。
错误 400 Bad request - Duplicate package is not allowed
如果您在运行模糊测试作业时看到此错误消息,并且 COVFUZZ_USE_REGISTRY 设置为 true,
请确保允许重复。有关更多详细信息,请参阅
重复的 Generic packages。