对比一下 QEMU 模式的 Fuzzing 和源码模式的 Fuzzing
1. 源码模式 Fuzzing
源码模式 Fuzzing(也称为插桩式 Fuzzing)是在编译时对目标程序进行修改,插入额外的代码(即“插桩”)。这些桩点会在程序运行时收集代码覆盖率等信息,并反馈给 Fuzzer,指导其生成更有效的输入
工作原理
- 编译插桩:Fuzzer 使用专门的编译器前端(如 AFL-Clang 或 LLVM-sanitizer)来编译目标程序的源码
- 插入探针:编译器会在每个基本块(Basic Block)的开头插入一个探针。当程序执行到一个新的基本块时,探针会向 Fuzzer 反馈这个信息
- 反馈循环:Fuzzer 根据这些覆盖率信息,判断哪些输入探索了新的代码路径。它会保留这些有价值的输入,并对其进行变异,以期能找到更深层次的代码逻辑
优点
- 高效:插桩非常轻量级,几乎不会引入额外的性能开销。Fuzzer 能够以极高的速度运行和测试样本,每秒可达数千甚至数万次
- 精确的代码覆盖率:由于插桩在编译时完成,Fuzzer 能够获得非常精确的、基本块级别的代码覆盖率,这使得它能够更有效地探索代码路径
- 直接定位崩溃点:由于 Fuzzer 能够知道输入触发了哪段代码,一旦发生崩溃,它能迅速定位到崩溃发生的基本块
缺点
- 需要源码:这是最大的局限性。如果目标程序是闭源的,你就无法使用这种方法
- 编译复杂:对于复杂的项目,编译过程可能会很复杂,需要处理各种依赖和编译选项
2. QEMU 模式 Fuzzing
QEMU 模式 Fuzzing(也称为黑盒或二进制 Fuzzing)是在二进制级别进行插桩和监控。它使用 QEMU 模拟器来运行目标程序,并通过修改 QEMU 的代码来收集代码覆盖率信息。
工作原理
- 二进制插桩:Fuzzer 启动一个修改过的 QEMU 用户模式模拟器
- 动态翻译:QEMU 在运行目标程序时,会动态地将目标程序的机器码翻译成宿主机的机器码。在翻译过程中,Fuzzer 的插桩逻辑会被嵌入到生成的代码中
- 收集覆盖率:当翻译后的代码执行时,插桩逻辑会收集代码覆盖率信息,并反馈给 Fuzzer。
优点
- 无需源码:这是 QEMU 模式最大的优势。它能够 Fuzz 任何闭源的、可执行的二进制文件,这在分析恶意软件或商业软件时至关重要
- 全系统覆盖:除了用户态程序,QEMU 还可以模拟整个系统。这意味着你可以用它来 Fuzz 驱动程序或内核
缺点
- 性能开销大:由于 QEMU 是一个模拟器,它会引入大量的性能开销。Fuzzing 速度比源码模式慢得多,通常每秒只能运行几十到几百次
- 覆盖率信息粗糙:QEMU 模式通常只能提供基本块级别的覆盖率,但可能无法像源码模式那样精确地追踪到每一条指令的执行
- 不稳定性:由于 QEMU 本身的复杂性,Fuzzing 过程中可能会出现一些不稳定的情况
特性 | 源码模式 Fuzzing | QEMU 模式 Fuzzing |
---|---|---|
是否需要源码 | 是 | 否 |
性能 | 极高(每秒数千次) | 较低(每秒数十次) |
代码覆盖率 | 非常精确(基本块级别) | 较精确(基本块级别) |
适用场景 | 开源项目、内部代码审计 | 闭源软件、恶意软件分析、驱动程序 Fuzzing |
代表工具 | AFL++、LibFuzzer | AFL-QEMU |