如何使用Fuzzing测试发现c++代码中的安全漏洞? (libFuzzer入门)

8次阅读

c++kquote>libFuzzer 要求实现 extern “C” int LLVMFuzzerTestOneInput(const uint8_t*, size_t)函数,需校验输入边界、禁用 exit/abort、依赖 ASan 检测内存错误,编译须加 -fsanitize=address,fuzzer,复现崩溃必须用 crash-xxx 文件。

如何使用 Fuzzing 测试发现 c ++ 代码中的安全漏洞?(libFuzzer 入门)

libFuzzer 要求你提供一个 FuzzTarget 函数

libFuzzer 不是黑盒扫描器,它需要你把待测逻辑封装成一个接受 const uint8_t*size_t 的 C++ 函数。这个函数就是模糊测试的入口,libFuzzer 会不断传入随机 字节 流来触发边界行为。

常见错误是直接把原始输入(比如 JSON 字符串)硬 编码 进函数,或者忘了处理空指针、零长度等边界情况——这会导致崩溃被误报为漏洞,或漏掉真实问题。

  • 函数签名必须是 extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size)
  • 不要在函数内调用 exit()abort() 或抛未捕获异常(除非你明确想让 libFuzzer 捕获该崩溃)
  • 如果被测代码内部有内存分配 / 释放逻辑,确保 Data 内容不会越界读取(例如用 Size 做长度校验后再构造 std::string_viewstd::vector
  • 避免依赖全局状态或文件系统;每次调用应尽可能独立

编译时必须启用 -fsanitize=address,fuzzer 且禁用优化

libFuzzer 本身不检测内存错误,它依赖 ASan(AddressSanitizer)等 sanitizer 来发现堆溢出、UAF、 溢出等问题。没有 sanitizer,多数 C++ 安全漏洞根本不会暴露为崩溃。

典型错误是只加了 -fsanitize=fuzzer 却漏掉 addressundefined,结果跑半天只看到“timeout”或“done”,却没触发任何 crash。

立即学习C++ 免费学习笔记(深入)”;

  • 推荐完整编译命令:clang++ -g -O1 -fsanitize=address,fuzzer -fno-omit-frame-pointer fuzz_target.cpp -o fuzz_target
  • -O1 是安全阈值;-O2 及以上可能让 sanitizer 失效或掩盖崩溃路径
  • 链接阶段不能漏掉 -fsanitize=fuzzer,否则会报错 undefined reference to __fuzzing_entry_point
  • 若被测代码含 C++ 异常,建议加 -fsanitize=undefined 捕获未定义行为(如 signed integer overflow

崩溃复现必须用原始 crash-xxx 文件,不能靠日志猜

libFuzzer 发现崩溃后会生成类似 crash-123abc…… 的二进制文件。这些文件内容就是触发崩溃的最小输入字节序列。靠日志里“heap-use-after-free on address 0x……”这类信息去手动构造输入,99% 会失败。

原因在于:崩溃路径高度依赖输入中特定字节的位置、长度和数值组合,微小改动就绕过触发点。

  • 复现命令:./fuzz_target ./crash-123abc……
  • 调试命令:gdb --args ./fuzz_target ./crash-123abc……,然后 r 即可精准停在崩溃点
  • 如果 crash-xxx 文件在另一台机器上无法复现,大概率是编译环境不一致(ASan 版本、libc++ vs libstdc++、甚至 clang 版本差异)
  • 别删 corpus 目录——它存着 libFuzzer 积累的有效输入,重启 fuzz 时加 -runs=0 可快速验证已有用例是否仍触发崩溃

自定义字典和 -max_len 对发现 C++ 解析类漏洞很关键

纯随机字节对解析器(JSON/XML/ 自定义协议)效率极低。libFuzzer 支持通过 -dict 提供关键词字典,大幅提升覆盖关键字、结构标记、转义序列的概率。

-max_len 控制输入最大长度,设得太小(如默认 4096)可能错过长字段导致的堆膨胀或整数截断漏洞;设得太大(如 1MB)又会让单次执行过慢,拖垮整体 fuzz 效率。

  • 字典示例(json.dict):
    "{" "}" "[" "]" ":" "," "null" "true" "false" """
  • 启动命令:./fuzz_target -dict=json.dict -max_len=65536 corpus_dir/
  • 对 C++ string/stringstream 解析逻辑,建议从 -max_len=1024 开始逐步加大,观察执行时间是否稳定在 100ms 内
  • 若目标函数内部有 memcpy(dst, src, n)n 来自输入,务必检查 n 是否受 Size 约束——这是整数溢出 + 越界写的高发区

实际跑起来你会发现,最难的不是写 LLVMFuzzerTestOneInput,而是让被测逻辑足够“干净”:没有全局锁、没有随机数依赖、没有外部网络调用。很多 C++ 项目卡在这一步,不是 工具 不会用,是代码本身没为可测试性设计。

text=ZqhQzanResources