AI驱动模糊测试:用大语言模型自动生成Fuzzing Harness
1. 项目概述当模糊测试遇上大语言模型最近在搞自动化安全测试特别是模糊测试这块发现一个挺有意思的项目叫google/oss-fuzz-gen。这项目名一出来懂行的朋友估计眼睛就亮了Google、OSS-Fuzz、Gen生成这几个词凑一块基本就锁定了它的核心——用生成式AI来搞模糊测试。简单来说oss-fuzz-gen是 Google 开源的一个研究项目它试图解决模糊测试领域一个老大难问题如何自动、高效地生成高质量的模糊测试驱动Fuzzing Harness。传统的模糊测试无论是 AFL、libFuzzer 还是 OSS-Fuzz 平台本身都高度依赖一个写好的“驱动”程序。这个驱动就像个“翻译官”负责把模糊器随机生成的、乱七八糟的输入数据转换成目标函数能“听懂”的调用。写这个驱动是个技术活得懂目标库的API、数据结构、调用顺序费时费力还容易出错直接制约了模糊测试的覆盖率和自动化程度。oss-fuzz-fuzz-gen的思路很直接既然大语言模型LLM在代码生成和理解上已经这么猛了能不能让它来当这个“翻译官”的自动生成器项目基于 Google 自家的 Gemini 系列模型训练了一个专门针对 C/C 库生成模糊测试驱动的模型。你给它一个目标库的头文件.h或者一些示例代码它就能尝试理解这个库的接口然后生成一个可以直接编译、链接到 libFuzzer 进行模糊测试的驱动源码。这玩意儿要是真能成熟落地意义可就大了。对于开源项目维护者意味着接入 OSS-Fuzz 这类持续模糊测试平台的门槛会大大降低安全漏洞的发现可以更早、更自动化。对于安全研究员相当于多了一个不知疲倦的“初级模糊测试工程师”能快速对大量目标进行初步的漏洞挖掘。当然它目前还是个研究项目离“开箱即用、全自动搞定”还有距离但其中展现的思路、方法和遇到的挑战对我们理解AI在软件安全领域的应用边界非常有价值。2. 核心原理与技术栈拆解要理解oss-fuzz-gen怎么工作得先拆开看看它的“技术栈”。这不像调用一个现成的API那么简单背后是一套组合拳。2.1 核心组件从代码理解到驱动生成项目的核心是一个基于 Transformer 架构的代码生成模型。但它不是通用的代码补全模型比如 Codex而是经过了特定任务的精调Fine-tuning。这个“特定任务”就是给定库代码的上下文生成一个有效的 libFuzzer 风格的模糊测试驱动。它的输入通常包括目标库的头文件内容这是最主要的上下文模型需要从中提取函数签名、数据结构定义、宏、枚举等。可选的示例代码或文档片段帮助模型理解这些API的典型用法和调用顺序。任务描述Prompt明确指示模型“生成一个libFuzzer测试驱动”。输出则是一个完整的.c或.cc文件包含LLVMFuzzerTestOneInput函数这是 libFuzzer 的标准入口点。对目标库头文件的引用#include。解析输入数据const uint8_t *data, size_t size并构造调用参数的逻辑。调用一个或多个目标库API的代码。必要的错误处理和资源清理如果生成了的话。2.2 关键技术创新点代码上下文表征如何让模型“读懂”C/C头文件是个挑战。项目很可能采用了类似 Tree-sitter 的解析器先将代码转化为抽象语法树AST再结合令牌Token序列形成丰富的代码表征。模型不仅要理解语法还要理解语义比如某个参数的类型是FILE*那在驱动里可能需要用fmemopen或临时文件来模拟。约束引导的生成纯粹靠概率生成代码很容易产出语法错误或逻辑荒谬的驱动。oss-fuzz-gen必然引入了约束。比如语法约束确保生成的C/C代码能通过基础编译检查。API使用约束基于头文件信息确保函数调用时参数类型匹配。模糊测试特定约束生成的驱动必须包含LLVMFuzzerTestOneInput函数并且要尝试去“消费”输入数据不能直接忽略data和size。基于真实数据集的训练模型的训练数据不是凭空造的很可能来源于 OSS-Fuzz 项目中成千上万个已经存在的、人工编写的优质模糊测试驱动。模型从这些真实、有效的样本中学习“一个好的模糊测试驱动长什么样”、“针对某种类型的API应该如何构造输入”。这是它区别于通用代码生成模型的核心。与编译工具链的集成生成驱动不是终点还要能编译、能运行。项目需要集成 Clang 编译器对生成的代码进行即时编译和链接测试。如果编译失败这个失败的信号错误信息可以反馈回去用于改进模型或筛选结果形成一个“生成-编译-验证”的闭环。2.3 技术栈推测根据项目性质和 Google 的技术背景其技术栈可能包含模型框架大概率基于 JAX 或 TensorFlow使用 Google 内部的 Gemini 模型作为基座。代码处理libClang 或 Tree-sitter 用于解析C/C代码提取结构化信息。训练基础设施Google 的 TPU 集群用于处理海量的代码数据。评估管道与 OSS-Fuzz 基础设施集成自动在隔离的沙箱环境中运行生成的驱动评估其代码覆盖率和崩溃发现能力。注意以上部分细节是结合模糊测试和AI代码生成领域的常见实践进行的合理推测因为作为研究项目其论文或文档可能不会披露全部工程细节。但理解这个框架对于我们自己尝试类似思路或评估其输出至关重要。3. 实操尝试使用与生成驱动解析虽然oss-fuzz-gen主要供内部研究但我们可以通过其开源代码和示例理解如何使用以及如何评判它生成的驱动。假设我们已经按照项目README配置好了必要的Python环境、模型权重或访问API的权限和编译工具链。3.1 基本使用流程一个典型的使用命令可能类似于python generate_harness.py \ --header_file /path/to/target_lib.h \ --output /path/to/generated_harness.cc \ --model_path /path/to/checkpoint这个过程背后经历了几个阶段上下文收集脚本会读取target_lib.h可能还会在相同目录下搜索相关的.c文件或简单的使用示例一起打包作为模型的输入上下文。提示工程将原始代码和任务指令按照预定义的模板组合成最终的提示Prompt送给模型。这个模板可能强调了“生成libFuzzer驱动”、“使用提供的输入数据”等关键点。模型推理与生成模型进行推理自回归地生成令牌序列直到产出完整的源代码文件。后处理与输出对生成的源代码进行简单的格式化然后写入指定的输出文件。3.2 生成驱动代码深度解析假设我们有一个简单的目标库mylib.h里面就一个函数// mylib.h int decode_buffer(const unsigned char *input, int input_len, char *output, int output_capacity);一个理想的、由oss-fuzz-gen生成的驱动可能如下// generated_harness.cc #include cstddef #include cstdint #include cstdlib #include mylib.h extern C int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { if (size 1) { return 0; // 模糊测试器认为没有进展 } // 使用输入数据的一部分作为输入长度 int input_len static_castint(data[0]) % (size - 1) 1; if (input_len size - 1) { input_len size - 1; } const unsigned char* input data 1; // 为输出分配缓冲区大小基于输入或固定值 int output_capacity input_len * 2; // 启发式策略 if (output_capacity 1) output_capacity 1; char* output static_castchar*(malloc(output_capacity)); if (output nullptr) { return 0; } // 调用目标函数 int result decode_buffer(input, input_len, output, output_capacity); // 清理资源 free(output); // libFuzzer 期望我们忽略返回值除非是特殊的错误 return 0; }我们来拆解这个生成的驱动看看模型的“思考”过程基础框架正确包含了必要的头文件和extern C因为 libFuzzer 是C接口定义了标准的LLVMFuzzerTestOneInput函数签名。这是模糊测试驱动的“宪法”模型必须遵守。输入验证if (size 1) return 0;这是一个非常经典的模糊测试驱动技巧。libFuzzer 可能会提供空输入如果我们的驱动逻辑至少需要1个字节来工作比如这里需要1个字节来定义input_len那么对于空输入就直接返回0告诉模糊器“这次运行没产生新覆盖”避免无意义的执行。数据解析策略这是驱动生成最核心、最体现“智能”的部分。模型需要从data和size这块原始的、无结构的字节数组中构造出目标函数所需的参数。对于input_len模型选择用第一个字节data[0]来动态决定长度并取模限制在合理范围内size - 1。这是一种非常合理的启发式方法既能产生变化的长度又能防止越界访问。对于input指针直接指向data[1]之后的数据。这样data[0]控制长度后续数据作为实际输入内容结构清晰。对于output缓冲区模型知道output是一个输出缓冲区需要预先分配。它采用了input_len * 2的启发式策略这是一个安全且常见的做法因为解码后的数据可能比原始数据长。同时检查了malloc是否成功。函数调用正确调用了decode_buffer传递了所有构造好的参数。资源清理记得free分配的output缓冲区避免了内存泄漏。这对于长时间运行的模糊测试至关重要。返回值正确返回 0。在 libFuzzer 中除非遇到需要特殊处理的错误通常用 -1 表示否则都返回 0。这个例子展示了模型在理解API签名参数类型、用途和模糊测试特定约束方面的能力。它没有生成“万能”的固定值而是设计了一套从随机字节流中“解析”出有意义参数的逻辑。3.3 实操中的关键检查点当你拿到一个生成的驱动时不要急着上大规模模糊测试先做这几步检查编译测试用与目标库相同的编译器和标志进行编译。这是第一道关卡。clang -g -fsanitizefuzzer,address generated_harness.cc -lmylib -o fuzzer_executable如果编译失败查看错误信息。常见的错误包括缺少头文件、类型不匹配、语法错误。这些错误可以反馈给生成流程如果支持的话。链接测试确保能正确链接到目标库-lmylib。最小化功能测试写一个简单的测试程序用固定的、合法的输入调用生成的驱动看目标函数是否被正确调用程序是否正常结束不崩溃。这能发现一些基础的逻辑错误。初始模糊测试运行用 libFuzzer 以极短的运行时间如5秒和极小的语料库启动一次模糊测试。./fuzzer_executable -max_total_time5 ./corpus观察是否有立即崩溃以及代码覆盖率如果安装了gcov或使用-fsanitize-coverage的初始增长情况。如果运行5秒就发现崩溃那这个驱动已经成功了如果覆盖率纹丝不动说明驱动可能没有有效“消费”输入数据。4. 优势、局限与评估指标任何技术都有其适用边界oss-fuzz-gen这类AI生成的模糊测试驱动也不例外。我们需要客观地看待它的能力和不足。4.1 核心优势自动化与规模化这是最大的优势。面对一个拥有数百个API的大型库人工为每个有潜力的函数编写驱动是巨大的工程。AI模型可以批量、自动地生成大量驱动候选极大提升了启动模糊测试的初始速度。减少领域知识依赖编写一个好的模糊测试驱动需要对目标库和模糊测试都有深入理解。AI模型通过从海量现有驱动中学习封装了这部分知识使得即使对某个库不熟悉的安全研究员或开发者也能快速获得一个可用的起点。启发式数据构造如前面的例子所示模型能生成相对智能的数据解析逻辑如用第一个字节控制长度这比简单的随机分割或固定值分配更有效能更快地探索到程序的深层状态。代码风格一致性生成的驱动在代码风格上通常是统一的便于后续的维护和审查。4.2 当前主要局限与挑战API理解深度不足模型主要依赖头文件的语法信息缺乏对API语义的深层理解。例如参数间复杂约束函数A的输出必须是函数B的输入且顺序不能错。模型可能生成颠倒顺序的调用。状态依赖某些函数调用前必须初始化某个全局上下文或者需要在特定状态如“已连接”、“已打开”下调用。模型生成的驱动可能遗漏这些初始化步骤。资源生命周期管理对于返回句柄或需要配对使用的API如create_context/destroy_context模型可能只生成创建调用忘记销毁导致资源泄漏这在长时间模糊测试中会是问题。难以生成复杂输入结构如果目标函数需要一个复杂的、嵌套的数据结构如一个解析JSON或特定协议报文的函数仅从随机字节流构造出合法结构的概率极低。模型生成的驱动可能无法有效触及核心解析逻辑。“正确性”陷阱模型倾向于生成“编译通过且看起来合理”的代码但这不一定是“有效”的模糊测试驱动。一个驱动如果总是用固定的小范围值调用API或者错误处理路径过早返回其模糊测试效果可能为零。反馈循环缺失目前的研究型生成多是“一次性”的。一个理想的系统应该能将模糊测试运行时的反馈如代码覆盖增长点、发现的崩溃重新用于指导模型生成更有效的驱动形成闭环优化。但这在工程上非常复杂。4.3 如何评估生成的驱动不能只看编译是否通过。我们需要一套更细致的评估指标编译与链接成功率基础指标但权重不高。初始代码覆盖率使用一个小的、合法的种子输入集运行驱动测量其对目标库代码的行覆盖率、函数覆盖率。覆盖率越高说明驱动触及的代码路径越多潜力越大。可以使用llvm-cov或gcov来测量。模糊测试效率在固定时间如1小时内使用 libFuzzer 运行该驱动观察唯一崩溃/超时/错误发现数量直接的安全收益。代码覆盖率增长曲线覆盖率是否快速上升并趋于平稳还是几乎不动执行速度每秒能执行多少次LLVMFuzzerTestOneInput调用这会影响模糊测试的吞吐量。驱动代码质量人工审查代码检查是否有明显的资源泄漏、未定义行为、无效的输入消费逻辑等。一个高质量的AI生成驱动应该在“初始代码覆盖率”和“模糊测试效率”上表现良好。它可能不如经验丰富的人类专家写的驱动那么精妙但作为一个“零成本”的起点如果能达到人工驱动70%的效果其投入产出比就已经非常惊人了。5. 实战经验集成到现有流程与调优思路如果你打算在真实项目中尝试或借鉴oss-fuzz-gen的思路以下是一些从实战角度出发的经验和思考。5.1 集成到CI/CD或安全测试流程不要指望AI生成驱动能完全替代人工。更现实的定位是作为“模糊测试驱动生成的第一阶段”或“辅助工具”。一个可行的集成流程如下目标选择针对新引入的、或尚未有模糊测试覆盖的核心C/C库。批量生成使用oss-fuzz-gen或类似工具为其主要公共API批量生成驱动候选。自动化筛选 a.编译过滤自动编译所有生成驱动过滤掉编译失败的。 b.基础运行过滤用一个极小的、合法的种子语料甚至可以是空输入运行每个可执行驱动几秒钟。过滤掉那些立即崩溃可能是驱动本身有bug或覆盖率完全为零的驱动。人工审查与精选对通过筛选的驱动进行快速人工代码审查。重点看资源管理是否正确输入消费逻辑是否合理是否调用了关键的API组合挑选出最有潜力的几个。投入模糊测试集群将精选后的驱动加入到持续的模糊测试任务中如OSS-Fuzz、内部Fuzzing平台进行长时间24小时以上的测试。结果监控与迭代监控这些驱动的崩溃发现情况和覆盖率增长。对于效果好的驱动可以将其代码作为模板保存对于效果差的分析原因是API理解问题还是数据构造问题这些反馈可以用于改进生成模型或提示词。5.2 针对复杂库的调优与提示工程对于复杂的库直接给个头文件可能不够。你需要为模型提供更多“上下文线索”这类似于给AI“喂小灶”。提供使用示例在项目目录中放置一个简单的example_usage.c文件展示如何正确初始化、调用API、清理资源。模型在生成时可以参考这个文件的代码模式和结构。提供API分组信息如果库的API有明显的模块划分如编码、解码、网络、文件可以尝试分模块生成驱动。给模型的提示词可以更具体“为libfoo的编解码模块生成一个模糊测试驱动重点测试encode_string和decode_buffer函数。”定制化提示模板研究oss-fuzz-gen的提示词模板。你可以尝试修改它加入更明确的指令例如“确保在调用process_data之前先调用init_engine。”“注意ctx指针需要最后用free_context释放。”“尝试使用输入数据的前4个字节作为一个32位整数用来控制循环次数。”后处理脚本编写脚本对生成的驱动进行自动化的“修补”。例如如果发现模型总忘记释放某些资源可以写一个脚本在生成代码后自动搜索特定的API调用模式并插入对应的清理代码。5.3 常见问题与排查实录在实际尝试中你可能会遇到以下典型问题问题1生成的驱动编译失败错误是“未定义的引用”。排查这通常是链接问题不是驱动代码本身的问题。检查你的编译命令是否正确链接了目标库-l参数。确保库文件路径在链接器的搜索路径中。心得将编译和链接命令封装在一个脚本或Makefile里避免手动输入错误。先确保你能用一个最简单的、手写的测试程序成功编译链接目标库再用完全相同的编译链接选项去编译生成的驱动。问题2驱动能编译运行但模糊测试覆盖率几乎不增长。排查检查输入消费在LLVMFuzzerTestOneInput函数开头加一句printf(“Size: %zu\n”, size);运行一下看看模糊器提供的size是否总是0或很小。libFuzzer可能会从很小的输入开始。确保你的驱动逻辑能处理极小输入比如直接return 0。检查驱动逻辑是不是驱动里的某个条件判断过于严格导致绝大部分随机输入都被提前return 0了比如if (size ! 100) return 0;。模型有时会生成这种无意义的硬编码检查。检查API调用是否真的执行在调用目标API前后加打印或者用调试器单步跟踪确认函数确实被调用到了。解决如果问题出在驱动逻辑你需要修改生成模型的输入提供更好的示例或者手动修改这个有问题的驱动。一个常见的修复是将过于严格的检查改为概率性的或基于输入数据的动态检查。问题3驱动运行导致内存泄漏长时间模糊测试后内存耗尽。排查使用 AddressSanitizer 来编译和运行驱动。-fsanitizeaddress,fuzzer。运行很短时间ASan就会报告内存泄漏点。解决找到泄漏的资源通常是malloc的内存、fopen的FILE指针、或库分配的句柄在驱动中确保释放。这可能需要你深入理解目标库的API然后手动修补生成的驱动或者为模型提供更明确的资源管理示例。问题4生成的驱动只测试了某个API的皮毛没有组合多个API或进行状态ful测试。分析这是当前AI生成驱动的普遍局限。模型倾向于为单个API生成简单的测试。应对策略不要期望一个驱动解决所有问题。你可以为复杂的、有状态的操作手动编写驱动。尝试让模型生成“序列化”测试在提示词中要求“生成一个驱动它先调用A再调用B使用第一次调用的结果作为第二次调用的输入”。将AI生成的驱动视为“单元模糊测试”再辅以人工编写的“集成模糊测试”。最后保持合理的期望。oss-fuzz-gen代表了一个充满希望的方向但它不是银弹。它的最佳用途是作为“力量倍增器”帮助安全团队和开发者快速建立模糊测试的初始覆盖发现那些明显的、浅层的漏洞从而让人类专家可以更专注于深度的、逻辑复杂的漏洞挖掘。将AI的“广度”与人类的“深度”结合才是提升软件安全性的正道。