ASP.NET Core 模糊测试实战指南:基于 SharpFuzz 与 libFuzzer 构建与运行 Fuzzing Targets
【免费下载链接】aspnetcoreASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.项目地址: https://gitcode.com/GitHub_Trending/as/aspnetcore
ASP.NET Core 仓库维护了一套独立的模糊测试(Fuzzing)基础设施,它位于 src/Fuzzing 目录下:一方面为各库提供以IFuzzer接口实现的模糊测试目标(target),另一方面提供从这些 target 生成 OneFuzz 云端部署包、以及本地运行与覆盖率采集的配套工具。本篇文章将完整讲解该体系的原理、本地运行步骤、崩溃复现与覆盖率分析流程,并手把手演示如何新增一个 fuzzing target,帮助你掌握为 ASP.NET Core 各类解析器、头信息处理与传输层代码构建自动化安全测试的完整链路。
项目定位与技术选型
与仓库里其他组件不同,src/Fuzzing不是被引用的运行时库,而是一套“对 ASP.NET Core 库做模糊测试”的工具项目。README 明确说明了它的两个职责:
- 提供针对各种 ASP.NET Core 库的fuzzing targets;
- 提供从这些 target 生成 OneFuzz 部署(deployment)的支撑代码。
技术栈上有两条主线,从源码中可以逐一对上:
- SharpFuzz负责对被测程序集进行插桩(instrumentation)。仓库在 AspNetCoreFuzzing.csproj 中直接引用
SharpFuzz包,并在部署准备阶段调用名为sharpfuzz的 CLI 工具对目标程序集做二进制改写(详见 Program.cs 的InstrumentAssemblies方法)。 - libFuzzer负责运行时驱动:它不断生成随机/变异的输入字节流,喂给插桩后的代码,并利用插桩反馈进行覆盖引导(coverage-guided)的进化。运行时通过微软维护的
libfuzzer-dotnet.exe启动宿主进程,再由宿主把输入交给 .NET target。README 指出该工程是仿照 dotnet/runtime 仓库中运行时模糊测试基础设施搭建的。
仓库中持续模糊测试(continuous fuzzing)运行在 Windows 环境上,因此 README 中所有本地命令均以 Windows.cmd/.ps1/.bat脚本给出,文章后续命令默认同样适用于 Windows 环境。
源码结构与部署产物
src/Fuzzing下的文件布局非常精简:
src/Fuzzing/ ├── README.md # 使用说明(本文主题文档) └── AspNetCoreFuzzing/ # 自包含的 fuzzing 宿主工程 ├── AspNetCoreFuzzing.csproj # 自包含发布、引用 SharpFuzz 与目标库 ├── IFuzzer.cs # fuzzing target 抽象接口 ├── Program.cs # 宿主入口:反射发现、插桩、OneFuzz 部署 ├── Assert.cs # 供 fuzzer 内部使用的轻量断言 ├── run.bat # 一键调用 prepare-onefuzz 生成 deployment ├── collect-coverage.ps1 # 用 fuzz 语料采集被测库的代码覆盖率 └── Fuzzers/ └── MultipartReaderFuzzer.cs # 现有 target 的参照实现关键点是AspNetCoreFuzzing.csproj中SelfContained=true、默认RuntimeIdentifier为win-x64(源码)。这意味着构建后除了AspNetCoreFuzzing.exe宿主,还会复制一份运行所需的所有依赖库到输出目录——这正是插桩和后续 OneFuzz 打包的前提:插桩工具需要就地改写目标 DLL,而云端机器上不再需要额外还原依赖。
本地运行前的准备工作
按 README,两步即可准备好环境。
第一步:激活仓库内的 .NET 本地环境。在仓库根目录执行:
.\activate.ps1仓库采用“本地 dotnet”机制(脚本会定位根目录下.dotnet目录,覆盖率脚本中也能看到它的使用,见 collect-coverage.ps1),因此后续命令都应在这个已激活的 shell 中执行。
第二步:安装 SharpFuzz 命令行工具,用于对程序集做插桩:
dotnet tool install --global SharpFuzz.CommandLine安装后,sharpfuzz <path-to-dll>命令即可对单个程序集执行插桩,宿主程序在部署阶段会以子进程方式调用它(Program.cs)。
注意:宿主工程 AspNetCoreFuzzing.csproj 目前引用了
Microsoft.Net.Http.Headers、Microsoft.AspNetCore.WebUtilities、Microsoft.AspNetCore.Http.*、Microsoft.AspNetCore.Components.Endpoints、Microsoft.AspNetCore.Server.Kestrel.Core等目标库,新增 target 时若涉及新库需要在此补充引用。
构建并启动一次模糊测试
构建宿主工程
进入工程目录并构建:
cd src/Fuzzing/AspNetCoreFuzzing dotnet build由于是自包含发布,构建产物中会同时出现AspNetCoreFuzzing.exe与全部依赖库副本。
生成 deployment 目录
run.bat会在artifacts\bin\AspNetCoreFuzzing下递归搜索宿主 exe,并以prepare-onefuzz deployment参数调用它(run.bat)。该命令会为每个 fuzzing target创建一个独立目录,完成以下工作(对应 Program.cs 的PrepareFuzzer):
- 将发布目录中所有文件复制到该 target 的目录;
- 按
IFuzzer.Dictionary/IFuzzer.Corpus声明复制字典与种子语料; - 用 SharpFuzz 对
TargetAssemblies指定的程序集插桩; - 写入
OneFuzzConfig.json(云端配置)与local-run.bat(本地运行脚本)。
在反复迭代代码时,需要每次先重新构建再运行:
dotnet build && run.bat细节:插桩并非盲目重复执行。
InstrumentAssemblies会把当前 DLL 与上次记录的.original副本做字节比较,仅当程序集发生变化时才重新调用 sharpfuzz,避免无谓重插桩(Program.cs)。在 Windows 上,它还会设置SHARPFUZZ_INSTRUMENT_MIXED_MODE_ASSEMBLIES=1环境变量,以支持 mixed-mode 程序集插桩。
启动本地模糊测试
prepare-onefuzz在每个 target 目录里生成的local-run.bat本质是一条libfuzzer-dotnet.exe --target_path=AspNetCoreFuzzing.exe --target_arg=<FuzzerName>启动命令(Program.cs),并会把附加参数原样透传给 libFuzzer。以现有MultipartReaderFuzzer为例:
deployment\MultipartReaderFuzzer\local-run.bat默认情况下 libFuzzer 会无限期运行,不断生成并测试随机输入;按Ctrl+C即可停止。当它发现有趣输入或崩溃时,会把文件写入当前目录:
crash-<hash>—— 导致 fuzzer 崩溃的输入,这代表一个真实的 Bug;timeout-<hash>—— 导致单次执行超过超时上限而挂起的输入,这可能是 Bug(如死循环、极端慢路径)。
常用 libFuzzer 选项
libFuzzer 暴露了大量运行时选项,README 摘录了最有用的几个:
| Option | 说明 |
|---|---|
-max_total_time=600 | 600 秒(10 分钟)后停止 |
-timeout=30 | 将任何单次输入执行 >30 秒视为超时 |
-jobs=5 | 并行运行 5 个 fuzzer 实例 |
-max_len=1024 | 限制生成输入的最大字节数为 1024 |
例如:对multipart-inputs语料目录跑 10 分钟、并行多实例:
deployment\MultipartReaderFuzzer\local-run.bat multipart-inputs -timeout=30 -max_total_time=600 -jobs=5注意这里multipart-inputs是用户提供的语料目录,libFuzzer 会把新发现的输入回写到语料中,用于不断回归与覆盖演化。
崩溃与超时的调查复现
libFuzzer 落盘的crash-*/timeout-*文件都是二进制输入样本。宿主程序支持“指定 fuzzer + 输入文件或目录”两种模式做确定性复现:
cd src/Fuzzing/AspNetCoreFuzzing :: 复现单个崩溃文件 dotnet run -- MultipartReaderFuzzer C:\path\to\crash-abc123 :: 复现某目录下全部崩溃/超时文件 dotnet run -- MultipartReaderFuzzer C:\path\to\directory-with-crash-files\宿主收到文件/目录参数后会跳过 libFuzzer 循环,直接逐个读取输入并调用fuzzer.FuzzTarget(...)(Program.cs),非常适合做回归验证。由于工程自包含,也可不经dotnet run直接运行 exe:
artifacts\bin\AspNetCoreFuzzing\Debug\net11.0\win-x64\AspNetCoreFuzzing.exe MultipartReaderFuzzer crash-abc123(产物路径中的net11.0/win-x64随仓库当前目标框架与运行时标识而定。)
需要交互式调试时,在对应 fuzzer 的FuzzTarget方法内打断点,再用调试器启动即可:
dotnet run --no-build -- MultipartReaderFuzzer crash-abc123如何列出所有可用的 fuzzers
target 是通过反射自动发现的(见后文),因此直接以无参数方式运行宿主即可打印全部可用列表:
dotnet run --no-build用真实语料生成覆盖率报告
模糊测试的价值不止于找崩溃,还在于评估代码路径的覆盖程度。仓库提供了 collect-coverage.ps1,思路是:先用 fuzzer 跑一段时间产出语料,再以这些输入驱动目标代码并采集被测程序集的覆盖率。
mkdir multipart-inputs deployment\MultipartReaderFuzzer\local-run.bat multipart-inputs .\collect-coverage.ps1 MultipartReaderFuzzer multipart-inputs脚本内部会依次完成:检查/安装coverlet.console与dotnet-reportgenerator-globaltool→ 构建工程 → 通过dotnet run -- <Fuzzer> --get-instrumented-assemblies读取需要统计的程序集 → 用 coverlet 以这些输入为测试数据采集覆盖率(OpenCover 格式)→ 用 ReportGenerator 生成 HTML。报告位于:
.\coverage-report\html\index.html其中--get-instrumented-assemblies是宿主内置的隐藏参数,会把某 fuzzer 声明的TargetAssemblies(自动补.dll后缀)逐行输出(Program.cs 与 Program.cs)。
创建新的 Fuzzing Target
IFuzzer 接口契约
新增一个 target 只需实现 IFuzzer.cs 定义的内部接口:
internal interface IFuzzer { // 友好名称,默认取类名,用于命令行与目录命名 string Name => GetType().Name; // 需要被插桩的程序集列表,即被测代码所在程序集 string[] TargetAssemblies { get; } // 可选:用于引导 fuzzer 的字典文件名 string? Dictionary => null; // 可选:作为初始语料的目录名 string? Corpus => null; // 每个测试输入都会调用的入口,需尽量覆盖目标程序集中的代码路径 void FuzzTarget(ReadOnlySpan<byte> bytes); }一个最小示例:MediaType 解析永不抛异常
README 给出的经典例子是验证MediaTypeHeaderValue.TryParse在任何畸形输入下都不抛异常:
internal sealed class MediaTypeFuzzer : IFuzzer { public string[] TargetAssemblies => ["Microsoft.Net.Http.Headers"]; public void FuzzTarget(ReadOnlySpan<byte> bytes) { string input = Encoding.UTF8.GetString(bytes); _ = MediaTypeHeaderValue.TryParse(input, out _); } }两个核心成员的含义:
TargetAssemblies:被测代码所在、需要被 SharpFuzz 插桩的程序集。宿主在部署阶段会就地改写这些 DLL,为其注入覆盖率探针;若某 target 不声明任何目标程序集,部署时会直接报错“Specify at least one target”。FuzzTarget:fuzzer 为每个测试输入执行的逻辑,必须真正触达目标程序集中的代码路径,否则插桩与覆盖引导都失去意义。方法签名接收ReadOnlySpan<byte>,保证 libFuzzer 的字节流可以零拷贝进入 target。
反射发现机制
所有IFuzzer实现都是自动注册的:宿主在启动时扫描自身程序集,筛选出所有非抽象、实现了IFuzzer的类并实例化,再按名称排序(Program.cs)。这意味着:
- 新 target 无需改任何注册表/清单代码,加类即可被本地运行与 CI 持续模糊测试识别;
- 宿主会为每个 target 自动生成
deployment\<FuzzerName>\local-run.bat与OneFuzzConfig.json。
提交新 fuzzer 时的额外注意点
CI 流水线 .azure/pipelines/fuzzing/deploy-to-onefuzz.yml 中的条目是自动生成的:每次执行run.bat(即prepare-onefuzz)时,宿主都会重写 YAML 中# ONEFUZZ_TASK_WORKAROUND_START与# ONEFUZZ_TASK_WORKAROUND_END标记之间的内容,为每个 fuzzer 生成独立的onefuzz-task@0步骤(Program.cs)。因此提交新 fuzzer 时必须连同更新后的 YAML 一起提交。
说明:这个“逐个生成任务”的做法源自一个已知限制——OneFuzz 尚不能在同一部署中处理多个共享相似程序集/PBD 的任务,仓库通过为每个 fuzzer 独立成步来规避。YAML 采用 CRLF 行尾由宿主统一处理。
真实案例纵深:MultipartReaderFuzzer
仓库当前唯一的正式 target MultipartReaderFuzzer.cs 是理解编写思路的最佳范本。它模糊测试的是MultipartReader——负责按 RFC 2046 从 HTTP 请求体解析 multipart 表单数据的解析器,文件上传功能直接暴露给不可信网络数据,安全风险面大,是典型的模糊测试对象。
其FuzzTarget实现值得拆解:
public string[] TargetAssemblies => ["Microsoft.AspNetCore.WebUtilities"]; public void FuzzTarget(ReadOnlySpan<byte> bytes) { if (bytes.Length < 4) return; // 用前两个字节推导 boundary 长度(1-32 字符) int boundaryLength = (bytes[0] % 32) + 1; ... // 从输入中切出 boundary 串,并清洗空字符与空白 var boundary = Encoding.ASCII.GetString(bytes.Slice(headerOffset, boundaryLength)); boundary = boundary.Replace('\0', 'x'); if (string.IsNullOrWhiteSpace(boundary)) boundary = "boundary"; var body = bytes[(headerOffset + boundaryLength)..]; TestMultipartReader(body.ToArray(), boundary, async: false).GetAwaiter().GetResult(); TestMultipartReader(body.ToArray(), boundary, async: true).GetAwaiter().GetResult(); }设计上它把“随机字节”结构化为“boundary 头 + multipart body”,既维持了输入的高自由度,又保证足够比例的输入能穿过前置解析逻辑,从而高效触达深层代码。其TestMultipartReader设置了贴近真实使用习惯的限额(HeadersCountLimit = 32、HeadersLengthLimit = 8KB、BodyLengthLimit = 64KB),逐段读取所有 section 并同步、异步两条路径都跑一遍,最大化覆盖读取逻辑;同时显式吞掉InvalidDataException/IOException/InvalidOperationException/ArgumentException这类畸形输入下的“预期异常”,让非预期异常才能浮出表面成为崩溃信号。
这与 README 中MediaTypeFuzzer的“TryParse 永不抛”命题是同一思路的两种写法:fuzzer 的价值正是把“库应该优雅处理畸形输入”这类不变量,变成机器可执行的自动断言。
用真实输入做回归验证
宿主程序的第二个参数支持单文件或目录,这让 fuzzing 产物可以直接变成回归测试集:
cd src/Fuzzing/AspNetCoreFuzzing :: 对整个目录中的样本逐个执行 dotnet run -- MultipartReaderFuzzer inputs :: 对单个样本文件执行 dotnet run -- MultipartReaderFuzzer inputs/sample.bin实操建议(与 README 的表述一致):把有价值的输入或历史崩溃文件保存下来,在修复之后重新跑一遍,确认问题真正闭环。若传入的是目录,宿主会枚举其中所有文件逐个执行;若是文件则只执行该文件(Program.cs)。
补充:host 进程对输入的硬性约束与 OneFuzz 配置
8 字节对齐约束
在真正的 libFuzzer 循环中,宿主会在把字节交给 target 前校验输入内存地址的 8 字节对齐,不满足时直接抛出ArgumentOutOfRangeException(Program.cs)。注释说明了原因:部分 fuzzer 假定输入至少 2 字节对齐。因此新编写 target 时无需自行对齐,但应意识到该约束由宿主统一保障。
字典与语料的配套校验
若 target 声明了Dictionary或Corpus,宿主会要求发布目录的Dictionaries/、Corpora/中存在对应文件/目录,否则部署直接失败(Program.cs)。字典会以-dict=参数传给 libFuzzer,语料则作为初始种子目录;在本地脚本中,种子语料会放在用户追加参数之后拼接,从而保证用户另行指定的语料目录能优先接收新输入(Program.cs)。Dictionaries文件夹在工程文件中通过None Include="Dictionaries\*"随构建复制(AspNetCoreFuzzing.csproj)。
云端部署的下载校验
prepare-onefuzz首次运行时需要下载libfuzzer-dotnet.exe,宿主会对下载文件做SHA-512 哈希比对,不匹配则拒绝使用(Program.cs),避免供应链上的中间人替换风险。
生成的OneFuzzConfig.json中还包含云端行为编排:每次任务超时 120 秒、单输入超时 60 秒、崩溃会自动创建 ADO 工作项并配置去重字段与状态流转规则(Program.cs),非 CI 环境(未设置TF_BUILD)提交的任务名会追加-local后缀以便区分。
小结
ASP.NET Core 的 fuzzing 体系遵循一条清晰的流水线:IFuzzer定义被测逻辑 → 反射自动发现 → SharpFuzz 对目标程序集插桩 → libFuzzer/libfuzzer-dotnet 覆盖引导执行 → 崩溃/超时样本落盘 → 复现与覆盖率回归。在此基础上,新增一个 target 的成本被压缩到“写一个类”的粒度,而本地调试、覆盖率分析与 OneFuzz 云端任务生成全部由 Program.cs 自动完成。无论是想为某段解析逻辑建立持续的安全保障,还是希望沉淀一套可复用的“畸形输入即回归用例”的工作流,都可以以 MultipartReaderFuzzer.cs 为模板快速起步。
【免费下载链接】aspnetcoreASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.项目地址: https://gitcode.com/GitHub_Trending/as/aspnetcore
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考