说说 QEMU 模式的动态插桩怎么实现的,有什么优缺点
QEMU 模式动态插桩的实现原理
QEMU 本身是一个处理器模拟器,它通过动态二进制翻译(Dynamic Binary Translation, DBT)技术来执行不同架构的指令。这个过程为动态插桩提供了完美的切入点
简单来说,当 QEMU 运行一个目标程序时,它不是逐条解释执行指令,而是会:
- 读取指令块:QEMU 一次性读取一小段(通常是一个基本块)目标程序指令
- 翻译并缓存:它将这些目标指令翻译成宿主机的机器码
- 插入探针(Instrumentation):在翻译过程中,QEMU 会在每个基本块的入口点注入额外的指令。这些额外的指令就是 Fuzzer 用来收集信息(如代码覆盖率)的探针
- 执行翻译后的代码:QEMU 随后执行这段翻译并插桩后的代码
整个过程就像一个即时编译器(JIT)。当一个基本块被执行时,QEMU 会检查其是否已被翻译。如果未翻译,就进行翻译、插桩和缓存;如果已翻译,就直接执行缓存中的代码
AFL-QEMU 就是利用这种机制。它修改了 QEMU 的源码,在翻译层增加了额外的逻辑。每当 QEMU 翻译一个基本块时,AFL-QEMU 就会插入代码,将该基本块的 ID 记录在一个共享内存区域中,从而让 Fuzzer 能够实时获取代码覆盖率信息
优点
- 无需源码:这是最大的优势。QEMU 模式在二进制级别工作,可以对任何闭源的、可执行的程序进行 Fuzzing,这对于分析商业软件、恶意软件以及驱动程序至关重要
- 跨平台:QEMU 能够模拟不同的 CPU 架构(如 ARM、MIPS),这意味着你可以在一个 x86_64 的 Linux 机器上 Fuzz 一个 ARM 架构的程序
- 全系统 Fuzzing:QEMU 可以模拟整个操作系统,包括内核。这使得它可以用于 Fuzzing 驱动程序和内核漏洞
缺点
- 性能开销大:动态二进制翻译本身就会带来显著的性能开销。Fuzzing 速度比源码插桩模式慢得多,通常每秒只能运行几十到几百次,而源码模式可以达到数万次
- 覆盖率信息粗糙:QEMU 通常只能提供基本块级别的覆盖率,但无法像源码插桩那样精确地追踪到每一条指令的执行
- 实现复杂且不稳定:QEMU 模拟器本身就非常复杂,在其中进行插桩会引入更多不确定性,有时会导致模拟过程不稳定或产生非预期的行为
- 不适合处理 I/O 密集型程序:对于那些需要频繁进行磁盘或网络 I/O 的程序,QEMU 的模拟速度会变得更慢,Fuzzing 效率会大大降低